Commands are pure data returned from update/2 and init/1. The
runtime executes them after the update cycle completes. They are how
your app triggers side effects: background work, focus changes, window
operations, platform effects, and more.
See Plushie.Command for full API docs and Plushie.Effect for
platform effect functions.
Returning commands
update/2 and init/1 support three return forms:
# Bare model (no commands)
def update(model, _event), do: model
# Model + single command
def update(model, %WidgetEvent{type: :click, id: "save"}) do
{model, Command.focus("editor")}
end
# Model + command list
def update(model, %WidgetEvent{type: :click, id: "export"}) do
{model, [
Effect.file_save(:export, title: "Export"),
Effect.notification(:notify, "Exporting", "Saving to file...")
]}
endInvalid return shapes (e.g. {model, :ok} or 3-element tuples) raise
ArgumentError immediately. See
App Lifecycle for details.
Command categories
All functions live in Plushie.Command unless noted otherwise.
Control flow
| Function | Purpose |
|---|---|
none/0 | No-op command (useful in conditional pipelines) |
done/2 | Lift a value into the command pipeline. Calls mapper.(value) and queues the result for update/2 in the next message cycle, no task spawned. |
batch/1 | Execute a list of commands sequentially |
exit/0 | Shut down the app |
batch/1 executes commands in order via Enum.reduce. Each command
threads through the runtime state sequentially. Use it to combine
multiple side effects from a single update/2 clause.
Async
| Function | Purpose |
|---|---|
async/2 | Run a function in a background Task. Result delivered as %AsyncEvent{tag: tag, result: result}. |
stream/2 | Run a function with an emit callback. Each emit.(value) delivers %StreamEvent{tag: tag, value: value}. Final return delivered as %AsyncEvent{}. |
cancel/1 | Kill an in-flight async or stream task by tag. |
send_after/2 | One-shot delayed event. Sends event through update/2 after delay_ms milliseconds. If a timer with the same event is already pending, the old one is cancelled. |
send_after/2 is a one-shot timer (fires once). For recurring timers,
use Plushie.Subscription.every/2 instead.
Focus
| Function | Purpose |
|---|---|
focus/1 | Set focus on a widget by scoped ID path |
focus_element/2 | Set focus on a canvas element within a canvas |
focus_next/0 | Move focus to the next focusable widget |
focus_previous/0 | Move focus to the previous focusable widget |
Text
| Function | Purpose |
|---|---|
select_all/1 | Select all text in a text input/editor |
move_cursor_to_front/1 | Move cursor to start |
move_cursor_to_end/1 | Move cursor to end |
move_cursor_to/2 | Move cursor to a specific position |
select_range/3 | Select a text range |
Scroll
| Function | Purpose |
|---|---|
scroll_to/2 | Scroll to absolute position |
snap_to/3 | Snap scroll to a position |
snap_to_end/1 | Snap scroll to the end |
scroll_by/3 | Scroll by a relative offset |
Window operations
| Function | Purpose |
|---|---|
close_window/1 | Close a window |
resize_window/3 | Set window size |
move_window/3 | Set window position |
maximize_window/2 | Maximize/unmaximize |
minimize_window/2 | Minimize/unminimize |
set_window_mode/2 | Set fullscreen/windowed mode |
toggle_maximize/1 | Toggle maximized state |
toggle_decorations/1 | Toggle window decorations |
gain_focus/1 | Bring window to front |
set_window_level/2 | Set window z-level |
drag_window/1 | Begin window drag |
drag_resize_window/2 | Begin window resize drag |
screenshot/2 | Capture window screenshot |
Screenshot results arrive as a raw {:screenshot_response, data} tuple
in update/2 (not a SystemEvent). The data map contains name,
hash, width, height, and optionally rgba binary image data.
Window queries
| Function | Purpose |
|---|---|
get_window_size/2 | Query window dimensions |
get_window_position/2 | Query window position |
is_maximized/2 | Query maximized state |
is_minimized/2 | Query minimized state |
get_mode/2 | Query fullscreen/windowed mode |
get_scale_factor/2 | Query DPI scale factor |
raw_id/2 | Query platform window handle |
monitor_size/2 | Query monitor dimensions |
Results arrive as %SystemEvent{type: type, tag: tag, data: data}.
The tag matches the atom you provided. Data maps use atom keys:
%{width: 800, height: 600}.
System
| Function | Purpose |
|---|---|
get_system_theme/1 | Query OS light/dark preference |
get_system_info/1 | Query system information |
allow_automatic_tabbing/1 | macOS automatic tab management |
announce/1 | Screen reader announcement |
announce/1 triggers a live-region assertion for assistive technology.
The text is immediately spoken by the screen reader without requiring
a visible widget. Use for status updates, error notifications, and
dynamic content.
Other
| Function | Purpose |
|---|---|
load_font/1 | Load a font from binary data at runtime |
tree_hash/1 | Query structural hash of the UI tree |
find_focused/1 | Query which widget has focus |
advance_frame/1 | Manual animation frame advance (test/headless mode) |
widget_command/3 | Command to a native Rust widget |
pane_split/4 | Split a pane in a pane grid |
pane_close/2 | Close a pane |
Platform effects
All functions live in Plushie.Effect. Each takes an atom tag as
its first argument and returns a Plushie.Command struct. Results
arrive as %Plushie.Event.EffectEvent{tag: tag, result: result} in
update/2.
File dialogs
| Function | Purpose |
|---|---|
file_open/2 | Single file picker |
file_open_multiple/2 | Multi-file picker |
file_save/2 | Save dialog |
directory_select/2 | Single directory picker |
directory_select_multiple/2 | Multi-directory picker |
{model, Effect.file_open(:import, title: "Import", filters: [{"Elixir", "*.ex"}])}
def update(model, %EffectEvent{tag: :import, result: {:ok, %{path: path}}}), do: ...
def update(model, %EffectEvent{tag: :import, result: :cancelled}), do: modelClipboard
| Function | Purpose |
|---|---|
clipboard_read/1 | Read plain text |
clipboard_write/2 | Write plain text |
clipboard_read_html/1 | Read HTML content |
clipboard_write_html/3 | Write HTML content (with optional alt text) |
clipboard_clear/1 | Clear clipboard |
clipboard_read_primary/1 | Read primary selection (Linux) |
clipboard_write_primary/2 | Write primary selection (Linux) |
Notifications
{model, Effect.notification(:saved, "Exported", "File saved to #{path}")}Options: :icon, :timeout (auto-dismiss ms), :urgency
(:low, :normal, :critical), :sound.
Async mechanics
One task per tag. Starting
async/2orstream/2with a tag that is already in-flight kills the previous task. Use unique tags for concurrent work.Nonce-based stale rejection. Each task gets a nonce at creation. Results from killed tasks carry a stale nonce and are silently discarded.
Crashes become errors. If the task process crashes (exception, exit), the result is
{:error, {:crashed, reason}}.Renderer restarts. In-flight async tasks survive a renderer restart (they run in the BEAM, not the renderer). Results may be stale if the work depended on renderer state.
Streaming
cmd = Command.stream(fn emit ->
for chunk <- fetch_chunks() do
emit.(%{progress: chunk.index, data: chunk.data})
end
:done
end, :import)Each emit.() delivers a %StreamEvent{tag: :import, value: value}.
The function's final return value is delivered directly as
%AsyncEvent{tag: :import, result: :done}. Wrap in {:ok, ...}
yourself if you want to follow the async convention.
Effect lifecycle
Tag-based matching. Every effect takes an atom tag. The tag returns in
%EffectEvent{tag: tag}for direct pattern matching.One effect per tag. Starting a new effect with a tag that has a pending request discards the previous one.
Default timeouts: file dialogs 120s, clipboard/notifications 5s. Override with
:timeoutoption.Timeout delivery.
result: {:error, :timeout}.Cancellation. User dismissing a dialog delivers
result: :cancelled(not an error).Effect stubs.
register_effect_stub(:file_open, {:ok, %{path: "..."}})intercepts effects by kind in tests. See the Testing reference.
DIY patterns
The runtime is a GenServer. You can bypass the command system and send messages directly:
def update(model, %WidgetEvent{type: :click, id: "fetch"}) do
pid = self()
spawn(fn ->
result = MyApp.HTTP.get!("/api/data")
send(pid, {:fetched, result})
end)
model
end
def update(model, {:fetched, result}) do
%{model | data: result}
endThis is useful for integrating with existing OTP infrastructure --
supervisors, GenServers, Phoenix PubSub. Messages arrive as events in
the next update cycle. The tradeoff: you lose tag-based cancellation
and stale-result rejection that async/2 provides.
See also
Plushie.Command- full module docs with specsPlushie.Effect- platform effect functions- Async and Effects guide - effects, async, streaming, and multi-window
- App Lifecycle reference - return value validation and update cycle
- Testing reference - effect stubs