Search documentation of Plushie

# Plushie.Command (Plushie v0.7.2)

Copy Markdown View Source

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` (see `Command.Text`)
- Scroll: `scroll_to/3`, `snap_to/3`, `snap_to_end/1`, `scroll_by/3` (see `Command.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` (see `Command.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` (see `Command.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` (see `Command.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/2` delivers `%Plushie.Event.AsyncEvent{tag: tag, result: result}`. `stream/2` delivers `%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 through `update/2`. The `type` field identifies the query kind, `tag` holds the stringified event tag, and `value` holds the result payload. For example, `system_theme(:my_tag)` delivers `%SystemEvent{type: :system_theme, tag: "my_tag", value: "dark"}`.
- Platform effects: `Plushie.Effect` functions deliver `%Plushie.Event.EffectEvent{tag: tag, result: result}`. The `tag` matches the atom you provided when creating the effect command. Timeouts deliver the same struct with `result: {:error, :timeout}`. See `Plushie.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

event_tag()

Tag atom used to identify async results in `update/2`.

t()

A command to be dispatched by the runtime.

widget_id()

Stable string identifier for a widget node in the UI tree.

window_id()

Stable string identifier for a window node in the UI tree.

### Functions

__using__(opts)

Standalone DSL for declaring typed command functions.

advance_frame(timestamp)

Advance the animation clock by one frame in headless/test mode.

allow_automatic_tabbing(enabled)

Sets whether the system can automatically organize windows into tabs.

announce(text, politeness \\ :polite)

Triggers a screen reader announcement without a visible widget.

batch(commands)

Issue multiple commands. Commands in the batch execute sequentially in list order, with state threaded through each.

cancel(event_tag)

Cancel a running async or stream command by its tag.

clear_images()

See `Plushie.Command.Image.clear_images/0`.

close_window(window_id)

See `Plushie.Command.Window.close_window/1`.

create_image(handle, data)

See `Plushie.Command.Image.create_image/2`.

create_image_rgba(handle, width, height, pixels)

See `Plushie.Command.Image.create_image_rgba/4`.

delete_image(handle)

See `Plushie.Command.Image.delete_image/1`.

disable_mouse_passthrough(window_id)

See `Plushie.Command.Window.disable_mouse_passthrough/1`.

dispatch(value, msg_fn)

Wraps an already-resolved value in a command. The runtime immediately dispatches `msg_fn.(value)` through `update/2` without spawning a task.

drag_resize_window(window_id, direction)

See `Plushie.Command.Window.drag_resize_window/2`.

drag_window(window_id)

See `Plushie.Command.Window.drag_window/1`.

enable_mouse_passthrough(window_id)

See `Plushie.Command.Window.enable_mouse_passthrough/1`.

exit()

Exit the application.

find_focused(tag)

Queries which widget currently has focus.

focus(widget_id)

Focus the widget or canvas element identified by `widget_id`.

focus_next()

Move focus to the next focusable widget.

focus_next_within(scope)

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.

focus_previous()

Move focus to the previous focusable widget.

focus_previous_within(scope)

Move focus to the previous focusable widget within the subtree rooted at `scope`. See `focus_next_within/1` for semantics.

focus_window(window_id)

See `Plushie.Command.Window.focus_window/1`.

is_maximized(window_id, tag)

See `Plushie.Command.WindowQuery.is_maximized/2`.

is_minimized(window_id, tag)

See `Plushie.Command.WindowQuery.is_minimized/2`.

list_images(tag)

See `Plushie.Command.Image.list_images/1`.

load_font(family, data)

Loads a font at runtime from binary data.

maximize_window(window_id, maximized \\ true)

See `Plushie.Command.Window.maximize_window/2`.

minimize_window(window_id, minimized \\ true)

See `Plushie.Command.Window.minimize_window/2`.

monitor_size(window_id, tag)

See `Plushie.Command.WindowQuery.monitor_size/2`.

move_cursor_to(widget_id, value)

See `Plushie.Command.Text.move_cursor_to/2`.

move_cursor_to_end(widget_id)

See `Plushie.Command.Text.move_cursor_to_end/1`.

move_cursor_to_front(widget_id)

See `Plushie.Command.Text.move_cursor_to_front/1`.

move_window(window_id, x, y)

See `Plushie.Command.Window.move_window/3`.

none()

A no-op command. Returned implicitly when `update/2` returns a bare model.

pane_close(pane_grid_id, pane_id)

Close a pane in the pane grid.

pane_maximize(pane_grid_id, pane_id)

Maximize a pane in the pane grid.

pane_restore(pane_grid_id)

Restore all panes from maximized state.

pane_split(pane_grid_id, pane_id, axis, new_pane_id)

Split a pane in the pane grid along the given axis.

pane_swap(pane_grid_id, pane_a, pane_b)

Swap two panes in the pane grid.

raw_id(window_id, tag)

See `Plushie.Command.WindowQuery.raw_id/2`.

request_attention(window_id, urgency \\ nil)

See `Plushie.Command.Window.request_attention/2`.

resize_window(window_id, width, height)

See `Plushie.Command.Window.resize_window/3`.

scale_factor(window_id, tag)

See `Plushie.Command.WindowQuery.scale_factor/2`.

screenshot(window_id, tag)

See `Plushie.Command.Window.screenshot/2`.

scroll_by(widget_id, x, y)

See `Plushie.Command.Scroll.scroll_by/3`.

scroll_to(widget_id, x, y)

See `Plushie.Command.Scroll.scroll_to/3`.

select_all(widget_id)

See `Plushie.Command.Text.select_all/1`.

select_range(widget_id, start_pos, end_pos)

See `Plushie.Command.Text.select_range/3`.

send_after(delay_ms, event)

Send `event` through `update/2` after `delay_ms` milliseconds.

set_icon(window_id, rgba_data, width, height)

See `Plushie.Command.Window.set_icon/4`.

set_max_size(window_id, width, height)

See `Plushie.Command.Window.set_max_size/3`.

set_min_size(window_id, width, height)

See `Plushie.Command.Window.set_min_size/3`.

set_resizable(window_id, resizable)

See `Plushie.Command.Window.set_resizable/2`.

set_resize_increments(window_id, width, height)

See `Plushie.Command.Window.set_resize_increments/3`.

set_window_level(window_id, level)

See `Plushie.Command.Window.set_window_level/2`.

set_window_mode(window_id, mode)

See `Plushie.Command.Window.set_window_mode/2`.

show_system_menu(window_id)

See `Plushie.Command.Window.show_system_menu/1`.

snap_to(widget_id, x, y)

See `Plushie.Command.Scroll.snap_to/3`.

snap_to_end(widget_id)

See `Plushie.Command.Scroll.snap_to_end/1`.

stream(fun, event_tag)

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}`.

system_info(tag)

Query system information (OS, CPU, memory, graphics).

system_theme(tag)

Query the current system theme (light/dark mode).

task(fun, event_tag)

Run `fun` asynchronously in a Task. When it returns, the runtime dispatches `%Plushie.Event.AsyncEvent{tag: event_tag, result: result}` through `update/2`.

toggle_decorations(window_id)

See `Plushie.Command.Window.toggle_decorations/1`.

toggle_maximize(window_id)

See `Plushie.Command.Window.toggle_maximize/1`.

tree_hash(tag)

Computes a SHA-256 hash of the renderer's current tree state.

update_image(handle, data)

See `Plushie.Command.Image.update_image/2`.

update_image_rgba(handle, width, height, pixels)

See `Plushie.Command.Image.update_image_rgba/4`.

widget_batch(commands)

Send a batch of widget commands processed atomically in one cycle.

widget_command(id, family, value \\ nil)

Send a command to a widget by ID.

window_mode(window_id, tag)

See `Plushie.Command.WindowQuery.window_mode/2`.

window_position(window_id, tag)

See `Plushie.Command.WindowQuery.window_position/2`.

window_size(window_id, tag)

See `Plushie.Command.WindowQuery.window_size/2`.

## Types

# event_tag()

    @type event_tag() :: atom()

Tag atom used to identify async results in `update/2`.

# t()

    @type t() :: %Plushie.Command{payload: map(), type: atom()}

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.

# widget_id()

    @type widget_id() :: String.t()

Stable string identifier for a widget node in the UI tree.

# window_id()

    @type window_id() :: String.t()

Stable string identifier for a window node in the UI tree.

## Functions

# __using__(opts)

(macro)

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]
    end

Generates:

    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 ...

# advance_frame(timestamp)

    @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)

# allow_automatic_tabbing(enabled)

    @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

# announce(text, politeness \\ :polite)

    @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)

# batch(commands)

    @spec batch(commands :: t() | [t()]) :: %Plushie.Command{
      payload: term(),
      type: term()
    }

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(event_tag)

    @spec cancel(event_tag :: atom()) :: %Plushie.Command{payload: term(), type: term()}

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)

# clear_images()

See `Plushie.Command.Image.clear_images/0`.

# close_window(window_id)

See `Plushie.Command.Window.close_window/1`.

# create_image(handle, data)

See `Plushie.Command.Image.create_image/2`.

# create_image_rgba(handle, width, height, pixels)

See `Plushie.Command.Image.create_image_rgba/4`.

# delete_image(handle)

See `Plushie.Command.Image.delete_image/1`.

# disable_mouse_passthrough(window_id)

See `Plushie.Command.Window.disable_mouse_passthrough/1`.

# dispatch(value, msg_fn)

    @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.

# drag_resize_window(window_id, direction)

See `Plushie.Command.Window.drag_resize_window/2`.

# drag_window(window_id)

See `Plushie.Command.Window.drag_window/1`.

# enable_mouse_passthrough(window_id)

See `Plushie.Command.Window.enable_mouse_passthrough/1`.

# exit()

    @spec exit() :: %Plushie.Command{payload: term(), type: term()}

Exit the application.

# find_focused(tag)

    @spec find_focused(tag :: atom()) :: %Plushie.Command{payload: term(), type: term()}

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(widget_id)

    @spec focus(widget_id :: widget_id()) :: %Plushie.Command{
      payload: term(),
      type: term()
    }

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"`.

# focus_next()

    @spec focus_next() :: %Plushie.Command{payload: term(), type: term()}

Move focus to the next focusable widget.

# focus_next_within(scope)

    @spec focus_next_within(scope :: widget_id()) :: %Plushie.Command{
      payload: term(),
      type: term()
    }

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")

# focus_previous()

    @spec focus_previous() :: %Plushie.Command{payload: term(), type: term()}

Move focus to the previous focusable widget.

# focus_previous_within(scope)

    @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.

# focus_window(window_id)

See `Plushie.Command.Window.focus_window/1`.

# is_maximized(window_id, tag)

See `Plushie.Command.WindowQuery.is_maximized/2`.

# is_minimized(window_id, tag)

See `Plushie.Command.WindowQuery.is_minimized/2`.

# list_images(tag)

See `Plushie.Command.Image.list_images/1`.

# load_font(family, data)

    @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)

# maximize_window(window_id, maximized \\ true)

See `Plushie.Command.Window.maximize_window/2`.

# minimize_window(window_id, minimized \\ true)

See `Plushie.Command.Window.minimize_window/2`.

# monitor_size(window_id, tag)

See `Plushie.Command.WindowQuery.monitor_size/2`.

# move_cursor_to(widget_id, value)

See `Plushie.Command.Text.move_cursor_to/2`.

# move_cursor_to_end(widget_id)

See `Plushie.Command.Text.move_cursor_to_end/1`.

# move_cursor_to_front(widget_id)

See `Plushie.Command.Text.move_cursor_to_front/1`.

# move_window(window_id, x, y)

See `Plushie.Command.Window.move_window/3`.

# none()

    @spec none() :: %Plushie.Command{payload: term(), type: term()}

A no-op command. Returned implicitly when `update/2` returns a bare model.

# pane_close(pane_grid_id, pane_id)

    @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.

# pane_maximize(pane_grid_id, pane_id)

    @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.

# pane_restore(pane_grid_id)

    @spec pane_restore(pane_grid_id :: widget_id()) :: %Plushie.Command{
      payload: term(),
      type: term()
    }

Restore all panes from maximized state.

# pane_split(pane_grid_id, pane_id, axis, new_pane_id)

    @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.

# pane_swap(pane_grid_id, pane_a, pane_b)

    @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.

# raw_id(window_id, tag)

See `Plushie.Command.WindowQuery.raw_id/2`.

# request_attention(window_id, urgency \\ nil)

See `Plushie.Command.Window.request_attention/2`.

# resize_window(window_id, width, height)

See `Plushie.Command.Window.resize_window/3`.

# scale_factor(window_id, tag)

See `Plushie.Command.WindowQuery.scale_factor/2`.

# screenshot(window_id, tag)

See `Plushie.Command.Window.screenshot/2`.

# scroll_by(widget_id, x, y)

See `Plushie.Command.Scroll.scroll_by/3`.

# scroll_to(widget_id, x, y)

See `Plushie.Command.Scroll.scroll_to/3`.

# select_all(widget_id)

See `Plushie.Command.Text.select_all/1`.

# select_range(widget_id, start_pos, end_pos)

See `Plushie.Command.Text.select_range/3`.

# send_after(delay_ms, event)

    @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.

# set_icon(window_id, rgba_data, width, height)

See `Plushie.Command.Window.set_icon/4`.

# set_max_size(window_id, width, height)

See `Plushie.Command.Window.set_max_size/3`.

# set_min_size(window_id, width, height)

See `Plushie.Command.Window.set_min_size/3`.

# set_resizable(window_id, resizable)

See `Plushie.Command.Window.set_resizable/2`.

# set_resize_increments(window_id, width, height)

See `Plushie.Command.Window.set_resize_increments/3`.

# set_window_level(window_id, level)

See `Plushie.Command.Window.set_window_level/2`.

# set_window_mode(window_id, mode)

See `Plushie.Command.Window.set_window_mode/2`.

# show_system_menu(window_id)

See `Plushie.Command.Window.show_system_menu/1`.

# snap_to(widget_id, x, y)

See `Plushie.Command.Scroll.snap_to/3`.

# snap_to_end(widget_id)

See `Plushie.Command.Scroll.snap_to_end/1`.

# stream(fun, event_tag)

    @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)

# system_info(tag)

    @spec system_info(tag :: event_tag()) :: %Plushie.Command{
      payload: term(),
      type: term()
    }

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

# system_theme(tag)

    @spec system_theme(tag :: event_tag()) :: %Plushie.Command{
      payload: term(),
      type: term()
    }

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

# task(fun, event_tag)

    @spec task(fun :: fun(), event_tag :: atom()) :: %Plushie.Command{
      payload: term(),
      type: term()
    }

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.

# toggle_decorations(window_id)

See `Plushie.Command.Window.toggle_decorations/1`.

# toggle_maximize(window_id)

See `Plushie.Command.Window.toggle_maximize/1`.

# tree_hash(tag)

    @spec tree_hash(tag :: atom()) :: %Plushie.Command{payload: term(), type: term()}

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" => "..."}}`.

# update_image(handle, data)

See `Plushie.Command.Image.update_image/2`.

# update_image_rgba(handle, width, height, pixels)

See `Plushie.Command.Image.update_image_rgba/4`.

# widget_batch(commands)

    @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.

# widget_command(id, family, value \\ nil)

    @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.

# window_mode(window_id, tag)

See `Plushie.Command.WindowQuery.window_mode/2`.

# window_position(window_id, tag)

See `Plushie.Command.WindowQuery.window_position/2`.

# window_size(window_id, tag)

See `Plushie.Command.WindowQuery.window_size/2`.