Plushie.Command (Plushie v0.6.0)

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

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: get_window_size/2, get_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 data holds the result payload. For example, get_system_theme(:my_tag) delivers %SystemEvent{type: :system_theme, tag: "my_tag", data: "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.async(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.

t()

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

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.

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

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.

Clears all in-memory images.

Close the window identified by window_id.

Creates an in-memory image from encoded PNG/JPEG bytes.

Creates an in-memory image from raw RGBA pixel data.

Deletes an in-memory image by handle name.

Disable mouse passthrough on a window.

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

Start drag-resizing the window from the given edge/corner direction.

Start dragging the window.

Enable mouse passthrough on a window (clicks pass through to windows below).

Exit the application.

Queries which widget currently has focus.

Focus the widget identified by widget_id.

Focus a specific interactive element within a canvas. Supports "window#canvas".

Move focus to the next focusable widget.

Move focus to the previous focusable widget.

Give focus to a window.

Query the current window mode (windowed, fullscreen, hidden).

Query the window's current scale factor (DPI scaling).

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

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

Query the position of a window.

Query the size of a window.

Query whether a window is maximized.

Query whether a window is minimized.

Lists all in-memory image handles.

Loads a font at runtime from binary data.

Maximize or restore a window.

Minimize or restore a window.

Query the monitor size for the display containing a window.

Move the text cursor to a specific position. Supports "window#path".

Move the text cursor to the end of the input. Supports "window#path".

Move the text cursor to the front of the input. Supports "window#path".

Move a window to the given position.

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.

Query the raw platform window ID (e.g. X11 window ID, HWND).

Request user attention for a window. Urgency can be :informational or :critical.

Resize a window to the given dimensions.

Take a screenshot of a window. Result arrives as a tagged event.

Scroll the widget by a relative offset. Supports "window#path".

Scroll the widget identified by widget_id to offset.

Select all text in the widget identified by widget_id.

Select a range of text in the input. Supports "window#path".

Send event through update/2 after delay_ms milliseconds.

Sets the window icon from raw RGBA pixel data.

Set the maximum size of a window.

Set the minimum size of a window.

Set whether a window is resizable.

Sets the resize increment size for a window.

Set window stacking level (:normal, :always_on_top, :always_on_bottom).

Set window mode (windowed, fullscreen, etc.).

Show the system menu for a window.

Snap the scrollable widget to an absolute offset. Supports "window#path".

Snap the scrollable widget to the end of its content. Supports "window#path".

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

Toggle window decorations (title bar, borders).

Toggle window maximized state.

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

Updates an existing in-memory image with new encoded PNG/JPEG bytes.

Updates an existing in-memory image with new raw RGBA pixel data.

Send a command to a native widget.

Send a batch of widget commands (processed in one cycle).

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

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)

@spec announce(text :: String.t()) :: %Plushie.Command{payload: term(), type: term()}

Triggers a screen reader announcement without a visible widget.

The text is immediately announced by assistive technology as a live-region assertion. Useful for status updates, error messages, and other dynamic content that should be announced but doesn't need to be visually displayed.

Example

Command.announce("File saved successfully")
Command.announce("3 search results found")

async(fun, event_tag)

@spec async(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.

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

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

Clears all in-memory images.

close_window(window_id)

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

Close the window identified by window_id.

create_image(handle, data)

@spec create_image(handle :: String.t(), data :: binary()) :: %Plushie.Command{
  payload: term(),
  type: term()
}

Creates an in-memory image from encoded PNG/JPEG bytes.

The raw binary is stored as-is in the command payload. The protocol layer handles format-specific encoding (native binary for msgpack, base64 for JSON).

create_image(handle, width, height, pixels)

@spec create_image(
  handle :: String.t(),
  width :: pos_integer(),
  height :: pos_integer(),
  pixels :: binary()
) :: %Plushie.Command{payload: term(), type: term()}

Creates an in-memory image from raw RGBA pixel data.

The raw binary is stored as-is in the command payload. The protocol layer handles format-specific encoding (native binary for msgpack, base64 for JSON).

delete_image(handle)

@spec delete_image(handle :: String.t()) :: %Plushie.Command{
  payload: term(),
  type: term()
}

Deletes an in-memory image by handle name.

disable_mouse_passthrough(window_id)

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

Disable mouse passthrough on a window.

done(value, msg_fn)

@spec done(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.

drag_resize_window(window_id, direction)

@spec drag_resize_window(window_id :: window_id(), direction :: atom() | String.t()) ::
  %Plushie.Command{payload: term(), type: term()}

Start drag-resizing the window from the given edge/corner direction.

drag_window(window_id)

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

Start dragging the window.

enable_mouse_passthrough(window_id)

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

Enable mouse passthrough on a window (clicks pass through to windows below).

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, data: %{"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 identified by widget_id.

Supports window-qualified paths: "main#email" targets widget "email" in window "main".

focus_element(canvas_id, element_id)

@spec focus_element(canvas_id :: widget_id(), element_id :: String.t()) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Focus a specific interactive element within a canvas. Supports "window#canvas".

focus_next()

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

Move focus to the next focusable widget.

focus_previous()

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

Move focus to the previous focusable widget.

gain_focus(window_id)

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

Give focus to a window.

get_mode(window_id, tag)

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

Query the current window mode (windowed, fullscreen, hidden).

Result arrives as %Plushie.Event.SystemEvent{tag: tag, data: mode}.

get_scale_factor(window_id, tag)

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

Query the window's current scale factor (DPI scaling).

Result arrives as %Plushie.Event.SystemEvent{tag: tag, data: factor}.

get_system_info(tag)

@spec get_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, data: 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.get_system_info(:sys_info)}
end

def update(model, %Plushie.Event.SystemEvent{type: :system_info, tag: "sys_info", data: info}) do
  %{model | system: info}
end

get_system_theme(tag)

@spec get_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, data: 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.get_system_theme(:theme_result)}
end

def update(model, %Plushie.Event.SystemEvent{type: :system_theme, tag: "theme_result", data: mode}) do
  %{model | theme_mode: mode}
end

get_window_position(window_id, tag)

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

Query the position of a window.

Result arrives as %Plushie.Event.SystemEvent{tag: tag, data: data} where data is %{x: x, y: y} or nil if unavailable.

get_window_size(window_id, tag)

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

Query the size of a window.

Result arrives as %Plushie.Event.SystemEvent{tag: tag, data: data} where data is %{width: width, height: height}.

is_maximized(window_id, tag)

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

Query whether a window is maximized.

Result arrives as %Plushie.Event.SystemEvent{tag: tag, data: boolean}.

is_minimized(window_id, tag)

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

Query whether a window is minimized.

Result arrives as %Plushie.Event.SystemEvent{tag: tag, data: boolean}.

list_images(tag)

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

Lists all in-memory image handles.

The result arrives in update/2 as %SystemEvent{type: :image_list, tag: tag, data: %{"handles" => [...]}}.

load_font(data)

@spec load_font(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 name in widget font props.

Example

font_data = File.read!("path/to/CustomFont.ttf")
Plushie.Command.load_font(font_data)

maximize_window(window_id, maximized \\ true)

@spec maximize_window(window_id :: window_id(), maximized :: boolean()) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Maximize or restore a window.

minimize_window(window_id, minimized \\ true)

@spec minimize_window(window_id :: window_id(), minimized :: boolean()) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Minimize or restore a window.

monitor_size(window_id, tag)

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

Query the monitor size for the display containing a window.

Result arrives as %Plushie.Event.SystemEvent{tag: tag, data: data} where data is %{width: width, height: height} or nil if the monitor cannot be determined.

move_cursor_to(widget_id, position)

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

Move the text cursor to a specific position. Supports "window#path".

move_cursor_to_end(widget_id)

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

Move the text cursor to the end of the input. Supports "window#path".

move_cursor_to_front(widget_id)

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

Move the text cursor to the front of the input. Supports "window#path".

move_window(window_id, x, y)

@spec move_window(window_id :: window_id(), x :: number(), y :: number()) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Move a window to the given position.

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 :: term()) :: %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 :: term()) ::
  %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 :: term(),
  axis :: atom() | String.t(),
  new_pane_id :: term()
) :: %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 :: term(), pane_b :: term()) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Swap two panes in the pane grid.

raw_id(window_id, tag)

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

Query the raw platform window ID (e.g. X11 window ID, HWND).

Result arrives as %Plushie.Event.SystemEvent{tag: tag, data: platform_id}.

request_user_attention(window_id, urgency \\ nil)

@spec request_user_attention(window_id :: window_id(), urgency :: atom() | nil) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Request user attention for a window. Urgency can be :informational or :critical.

resize_window(window_id, width, height)

@spec resize_window(window_id :: window_id(), width :: number(), height :: number()) ::
  %Plushie.Command{payload: term(), type: term()}

Resize a window to the given dimensions.

screenshot(window_id, tag)

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

Take a screenshot of a window. Result arrives as a tagged event.

scroll_by(widget_id, x \\ 0.0, y \\ 0.0)

@spec scroll_by(widget_id :: widget_id(), x :: float(), y :: float()) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Scroll the widget by a relative offset. Supports "window#path".

scroll_to(widget_id, offset)

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

Scroll the widget identified by widget_id to offset.

Supports window-qualified paths: "main#list".

select_all(widget_id)

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

Select all text in the widget identified by widget_id.

Supports window-qualified paths: "main#email".

select_range(widget_id, start_pos, end_pos)

@spec select_range(
  widget_id :: widget_id(),
  start_pos :: non_neg_integer(),
  end_pos :: non_neg_integer()
) :: %Plushie.Command{payload: term(), type: term()}

Select a range of text in the input. Supports "window#path".

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)

@spec set_icon(
  window_id :: window_id(),
  rgba_data :: binary(),
  width :: pos_integer(),
  height :: pos_integer()
) :: %Plushie.Command{payload: term(), type: term()}

Sets the window icon from raw RGBA pixel data.

The rgba_data must be a binary of width * height * 4 bytes (one byte each for R, G, B, A per pixel, row-major). The raw binary is stored as-is in the command payload. The protocol layer handles format-specific encoding (native binary for msgpack via Msgpax.Bin, base64 for JSON).

Example

icon_data = File.read!("icon_32x32.rgba")
Plushie.Command.set_icon("main", icon_data, 32, 32)

set_max_size(window_id, width, height)

@spec set_max_size(window_id :: window_id(), width :: number(), height :: number()) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Set the maximum size of a window.

set_min_size(window_id, width, height)

@spec set_min_size(window_id :: window_id(), width :: number(), height :: number()) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Set the minimum size of a window.

set_resizable(window_id, resizable)

@spec set_resizable(window_id :: window_id(), resizable :: boolean()) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Set whether a window is resizable.

set_resize_increments(window_id, width, height)

@spec set_resize_increments(
  window_id :: window_id(),
  width :: number() | nil,
  height :: number() | nil
) :: %Plushie.Command{payload: term(), type: term()}

Sets the resize increment size for a window.

When set, the window will only resize in multiples of the given width and height. Pass nil for both to clear the constraint. Useful for terminal emulators and grid-aligned apps.

set_window_level(window_id, level)

@spec set_window_level(window_id :: window_id(), level :: atom() | String.t()) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Set window stacking level (:normal, :always_on_top, :always_on_bottom).

On Wayland, window stacking is compositor-controlled and this command may be silently ignored.

set_window_mode(window_id, mode)

@spec set_window_mode(window_id :: window_id(), mode :: atom() | String.t()) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Set window mode (windowed, fullscreen, etc.).

show_system_menu(window_id)

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

Show the system menu for a window.

snap_to(widget_id, x \\ 0.0, y \\ 0.0)

@spec snap_to(widget_id :: widget_id(), x :: float(), y :: float()) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Snap the scrollable widget to an absolute offset. Supports "window#path".

snap_to_end(widget_id)

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

Snap the scrollable widget to the end of its content. Supports "window#path".

stream(fun, event_tag)

@spec stream(fun :: (fun() -> 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)

toggle_decorations(window_id)

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

Toggle window decorations (title bar, borders).

toggle_maximize(window_id)

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

Toggle window maximized state.

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, data: %{"hash" => "..."}}.

update_image(handle, data)

@spec update_image(handle :: String.t(), data :: binary()) :: %Plushie.Command{
  payload: term(),
  type: term()
}

Updates an existing in-memory image with new encoded PNG/JPEG bytes.

The raw binary is stored as-is in the command payload. The protocol layer handles format-specific encoding (native binary for msgpack, base64 for JSON).

update_image(handle, width, height, pixels)

@spec update_image(
  handle :: String.t(),
  width :: pos_integer(),
  height :: pos_integer(),
  pixels :: binary()
) :: %Plushie.Command{payload: term(), type: term()}

Updates an existing in-memory image with new raw RGBA pixel data.

The raw binary is stored as-is in the command payload. The protocol layer handles format-specific encoding (native binary for msgpack, base64 for JSON).

widget_command(node_id, op, payload \\ %{})

@spec widget_command(node_id :: String.t(), op :: String.t(), payload :: map()) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Send a command to a native widget.

Widget commands bypass the normal tree update / diff / patch cycle and are delivered directly to the target native widget on the Rust side.

widget_commands(commands)

@spec widget_commands(commands :: [{String.t(), String.t(), map()}]) ::
  %Plushie.Command{
    payload: term(),
    type: term()
  }

Send a batch of widget commands (processed in one cycle).

Each command in the list is a {node_id, op, payload} tuple.