Commands describe side effects that update/2 wants the runtime to perform.
They are plain data: inspectable, testable, serializable. The runtime
interprets them after update/2 returns. Nothing executes inside update.
Categories
- Basic:
none/0,done/2,async/2,stream/2,cancel/1,send_after/2,exit/0 - Focus:
focus/1(widget command),focus_next/0,focus_previous/0(widget ops) - Text:
select_all/1,move_cursor_to_front/1,move_cursor_to_end/1,move_cursor_to/2,select_range/3(seeCommand.Text) - Scroll:
scroll_to/3,snap_to/3,snap_to_end/1,scroll_by/3(seeCommand.Scroll) - Window ops:
close_window/1,resize_window/3,move_window/3,maximize_window/2,minimize_window/2,set_window_mode/2,toggle_maximize/1,toggle_decorations/1,focus_window/1,set_window_level/2,drag_window/1,drag_resize_window/2,request_attention/2,screenshot/2,set_resizable/2,set_min_size/3,set_max_size/3,enable_mouse_passthrough/1,disable_mouse_passthrough/1,show_system_menu/1,set_icon/4,set_resize_increments/3(seeCommand.Window) - Window queries:
window_size/2,window_position/2,is_maximized/2,is_minimized/2,window_mode/2,scale_factor/2,raw_id/2,monitor_size/2(seeCommand.WindowQuery) - System ops:
allow_automatic_tabbing/1 - System queries:
system_theme/1,system_info/1 - PaneGrid ops:
pane_split/4,pane_close/2,pane_swap/3,pane_maximize/2,pane_restore/1 - Image ops:
create_image/2,create_image_rgba/4,update_image/2,update_image_rgba/4,delete_image/1,list_images/1,clear_images/0(seeCommand.Image) - Queries:
tree_hash/1,find_focused/1 - Font:
load_font/2 - Accessibility:
announce/1 - Widget commands:
widget_command/3,widget_batch/1 - Test/Headless:
advance_frame/1 - Batch:
batch/1
Result delivery
Commands deliver results back to update/2 through three mechanisms:
- Async/Stream:
async/2delivers%Plushie.Event.AsyncEvent{tag: tag, result: result}.stream/2delivers%Plushie.Event.StreamEvent{tag: tag, value: value}for each chunk. - Window and system queries:
window_size/2,window_mode/2, etc. deliver%Plushie.Event.SystemEvent{}structs throughupdate/2. Thetypefield identifies the query kind,tagholds the stringified event tag, andvalueholds the result payload. For example,system_theme(:my_tag)delivers%SystemEvent{type: :system_theme, tag: "my_tag", value: "dark"}. - Platform effects:
Plushie.Effectfunctions deliver%Plushie.Event.EffectEvent{tag: tag, result: result}. Thetagmatches the atom you provided when creating the effect command. Timeouts deliver the same struct withresult: {:error, :timeout}. SeePlushie.Effect.
Usage
def update(model, %Plushie.Event.WidgetEvent{type: :click, id: "save"}) do
cmd = Plushie.Command.task(fn -> save(model) end, :save_result)
{model, cmd}
end
def update(model, %Plushie.Event.AsyncEvent{tag: :save_result, result: :ok}), do: %{model | saved: true}Multiple commands can be issued at once via batch/1:
cmd = Plushie.Command.batch([
Plushie.Command.focus("name_input"),
Plushie.Command.send_after(5000, :auto_save)
])
Summary
Types
Tag atom used to identify async results in update/2.
A command to be dispatched by the runtime.
Stable string identifier for a widget node in the UI tree.
Stable string identifier for a window node in the UI tree.
Functions
Standalone DSL for declaring typed command functions.
Advance the animation clock by one frame in headless/test mode.
Sets whether the system can automatically organize windows into tabs.
Triggers a screen reader announcement without a visible widget.
Issue multiple commands. Commands in the batch execute sequentially in list order, with state threaded through each.
Cancel a running async or stream command by its tag.
Wraps an already-resolved value in a command. The runtime immediately
dispatches msg_fn.(value) through update/2 without spawning a task.
Exit the application.
Queries which widget currently has focus.
Focus the widget or canvas element identified by widget_id.
Move focus to the next focusable widget.
Move focus to the next focusable widget within the subtree rooted at
scope. Only widgets that are descendants of the scope widget are
considered; focus wraps at the subtree boundary.
Move focus to the previous focusable widget.
Move focus to the previous focusable widget within the subtree rooted
at scope. See focus_next_within/1 for semantics.
Loads a font at runtime from binary data.
A no-op command. Returned implicitly when update/2 returns a bare model.
Close a pane in the pane grid.
Maximize a pane in the pane grid.
Restore all panes from maximized state.
Split a pane in the pane grid along the given axis.
Swap two panes in the pane grid.
Send event through update/2 after delay_ms milliseconds.
Run fun as a streaming async task. The function receives an emit callback
that sends intermediate results to update/2 as
%Plushie.Event.StreamEvent{tag: event_tag, value: value}. The function's final
return value is delivered as %Plushie.Event.AsyncEvent{tag: event_tag, result: result}.
Query system information (OS, CPU, memory, graphics).
Query the current system theme (light/dark mode).
Run fun asynchronously in a Task. When it returns, the runtime dispatches
%Plushie.Event.AsyncEvent{tag: event_tag, result: result} through update/2.
Computes a SHA-256 hash of the renderer's current tree state.
Send a batch of widget commands processed atomically in one cycle.
Send a command to a widget by ID.
Types
@type event_tag() :: atom()
Tag atom used to identify async results in update/2.
A command to be dispatched by the runtime.
Always a %Command{} struct. batch/1 wraps multiple commands into a
single struct with type: :batch. The runtime normalizes bare lists
internally, but the public type is always a struct.
@type widget_id() :: String.t()
Stable string identifier for a widget node in the UI tree.
@type window_id() :: String.t()
Stable string identifier for a window node in the UI tree.
Functions
Standalone DSL for declaring typed command functions.
Use this in any module to generate command builder functions from
typed declarations. The same command macro used in native widgets
works here, producing functions that return %Command{} structs.
Example
defmodule Plushie.Command.Text do
use Plushie.Command
command :select_all
command :move_cursor_to, value: :integer
command :select_range, fields: [start_pos: :integer, end_pos: :integer]
endGenerates:
def select_all(widget_id) when is_binary(widget_id)
def move_cursor_to(widget_id, value) when is_binary(widget_id) and is_integer(value)
def select_range(widget_id, start_pos, end_pos) when is_binary(widget_id) and ...
@spec advance_frame(timestamp :: non_neg_integer()) :: %Plushie.Command{ payload: term(), type: term() }
Advance the animation clock by one frame in headless/test mode.
Sends an advance_frame message to the renderer with the given
timestamp (monotonic milliseconds). If on_animation_frame is
subscribed, the renderer emits an animation_frame event back.
This is a test/headless-only command. In normal daemon mode the renderer drives animation frames from the display vsync.
Example
Plushie.Command.advance_frame(16)
@spec allow_automatic_tabbing(enabled :: boolean()) :: %Plushie.Command{ payload: term(), type: term() }
Sets whether the system can automatically organize windows into tabs.
This is a macOS-specific setting. On other platforms it is a no-op. See: https://developer.apple.com/documentation/appkit/nswindow/1646657-allowsautomaticwindowtabbing
@spec announce(text :: String.t(), politeness :: :polite | :assertive) :: %Plushie.Command{ payload: term(), type: term() }
Triggers a screen reader announcement without a visible widget.
The text is announced by assistive technology as a live-region
update. The default politeness is :polite which is the correct
choice for most toast-style feedback (saves, confirmations,
counts). Pass :assertive to interrupt the user's current
announcement for urgent context.
Example
Command.announce("File saved successfully")
Command.announce("3 search results found", :polite)
Command.announce("Connection lost", :assertive)
Issue multiple commands. Commands in the batch execute sequentially in list order, with state threaded through each.
Accepts a single command, a list of commands, or a nested list; anything
List.wrap/1 can normalize.
Cancel a running async or stream command by its tag.
If the task has already completed, this is a no-op. The runtime tracks running tasks by their event tag and terminates the associated process.
Example
Command.cancel(:file_import)
@spec dispatch(value :: term(), msg_fn :: (term() -> term())) :: %Plushie.Command{ payload: term(), type: term() }
Wraps an already-resolved value in a command. The runtime immediately
dispatches msg_fn.(value) through update/2 without spawning a task.
Useful for lifting a pure value into the command pipeline.
The mapper receives only the value, not the model. Do not capture the
current model in the closure: it will be stale by the time the event
is processed. The mapper should produce an event struct that update/2
handles by reading the current model at that point.
Exit the application.
Queries which widget currently has focus.
The result arrives in update/2 as
%SystemEvent{type: :find_focused, tag: tag, value: %{"focused" => "..." | nil}}.
Note: if no widget is focused, the "focused" field may be nil.
Focus the widget or canvas element identified by widget_id.
Supports scoped paths for canvas elements: "canvas/element"
focuses the element within the canvas. Supports window-qualified
paths: "main#email" or "main#canvas/element".
Move focus to the next focusable widget.
Move focus to the next focusable widget within the subtree rooted at
scope. Only widgets that are descendants of the scope widget are
considered; focus wraps at the subtree boundary.
Useful for menus, pane grids, and any other keyboard container that wants a bounded Tab cycle without leaking focus to siblings.
Example
Command.focus_next_within("main#menu")
Move focus to the previous focusable widget.
@spec focus_previous_within(scope :: widget_id()) :: %Plushie.Command{ payload: term(), type: term() }
Move focus to the previous focusable widget within the subtree rooted
at scope. See focus_next_within/1 for semantics.
@spec load_font(family :: String.t(), data :: binary()) :: %Plushie.Command{ payload: term(), type: term() }
Loads a font at runtime from binary data.
The font data should be the raw bytes of a TrueType (.ttf) or OpenType
(.otf) font file. Once loaded, the font can be referenced by family
in widget font props.
Example
font_data = File.read!("path/to/CustomFont.ttf")
Plushie.Command.load_font("CustomFont", font_data)
A no-op command. Returned implicitly when update/2 returns a bare model.
@spec pane_close(pane_grid_id :: widget_id(), pane_id :: String.t()) :: %Plushie.Command{ payload: term(), type: term() }
Close a pane in the pane grid.
@spec pane_maximize(pane_grid_id :: widget_id(), pane_id :: String.t()) :: %Plushie.Command{ payload: term(), type: term() }
Maximize a pane in the pane grid.
@spec pane_restore(pane_grid_id :: widget_id()) :: %Plushie.Command{ payload: term(), type: term() }
Restore all panes from maximized state.
@spec pane_split( pane_grid_id :: widget_id(), pane_id :: String.t(), axis :: atom() | String.t(), new_pane_id :: String.t() ) :: %Plushie.Command{payload: term(), type: term()}
Split a pane in the pane grid along the given axis.
@spec pane_swap( pane_grid_id :: widget_id(), pane_a :: String.t(), pane_b :: String.t() ) :: %Plushie.Command{payload: term(), type: term()}
Swap two panes in the pane grid.
@spec send_after(delay_ms :: non_neg_integer(), event :: term()) :: %Plushie.Command{ payload: term(), type: term() }
Send event through update/2 after delay_ms milliseconds.
If a timer with the same event term is already pending, the previous
timer is canceled and replaced. This prevents duplicate deliveries
when send_after is called repeatedly for the same event.
@spec stream(fun :: ((term() -> :ok) -> term()), event_tag :: atom()) :: %Plushie.Command{ payload: term(), type: term() }
Run fun as a streaming async task. The function receives an emit callback
that sends intermediate results to update/2 as
%Plushie.Event.StreamEvent{tag: event_tag, value: value}. The function's final
return value is delivered as %Plushie.Event.AsyncEvent{tag: event_tag, result: result}.
Only one task per tag can be active. If a task with the same tag is already running, it is killed and replaced. Use unique tags if you need concurrent streams.
This is sugar over spawning a process manually. You can achieve the same
thing with bare Task and send/2 if you prefer direct Elixir patterns.
Example
Command.stream(fn emit ->
for chunk <- File.stream!("big.csv") do
emit.({:chunk, process(chunk)})
end
:done
end, :file_import)
Query system information (OS, CPU, memory, graphics).
The result arrives in update/2 as
%Plushie.Event.SystemEvent{type: :system_info, tag: tag, value: info} where tag
is the stringified event tag and info is a map with keys:
"system_name", "system_kernel", "system_version",
"system_short_version", "cpu_brand", "cpu_cores", "memory_total",
"memory_used", "graphics_backend", "graphics_adapter".
System info is always available (the sysinfo iced feature is enabled
unconditionally).
Example
def update(model, %Plushie.Event.WidgetEvent{type: :click, id: "sys_info"}) do
{model, Plushie.Command.system_info(:sys_info)}
end
def update(model, %Plushie.Event.SystemEvent{type: :system_info, tag: "sys_info", value: info}) do
%{model | system: info}
end
Query the current system theme (light/dark mode).
The result arrives in update/2 as
%Plushie.Event.SystemEvent{type: :system_theme, tag: tag, value: mode} where
tag is the stringified event tag and mode is "light", "dark", or
"none" (when no system preference is detected). Returns "none" on
Linux systems without a desktop environment. Apps should provide a theme
fallback.
Example
def update(model, %Plushie.Event.WidgetEvent{type: :click, id: "check_theme"}) do
{model, Plushie.Command.system_theme(:theme_result)}
end
def update(model, %Plushie.Event.SystemEvent{type: :system_theme, tag: "theme_result", value: mode}) do
%{model | theme_mode: mode}
end
Run fun asynchronously in a Task. When it returns, the runtime dispatches
%Plushie.Event.AsyncEvent{tag: event_tag, result: result} through update/2.
Only one task per tag can be active. If a task with the same tag is already running, it is killed and replaced. Use unique tags if you need concurrent tasks.
Computes a SHA-256 hash of the renderer's current tree state.
The result arrives in update/2 as
%SystemEvent{type: :tree_hash, tag: tag, value: %{"hash" => "..."}}.
@spec widget_batch(commands :: [{String.t(), String.t(), term()}]) :: %Plushie.Command{ payload: term(), type: term() }
Send a batch of widget commands processed atomically in one cycle.
Each command in the list is a {id, family, value} tuple. All commands
are applied before any resulting events are emitted.
@spec widget_command(id :: String.t(), family :: String.t(), value :: term()) :: %Plushie.Command{ payload: term(), type: term() }
Send a command to a widget by ID.
Commands use the unified wire format matching events:
{"type": "command", "id": "gauge", "family": "set_value", "value": 72.0}The value defaults to nil for commands with no payload (e.g. reset).
The family string identifies the operation. For native widgets, it
maps to the Rust widget's handle_widget_op dispatch. For built-in
widgets, the renderer handles it directly.