Search documentation of Plushie

# Commands and Effects

Copy Markdown View Source

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...")
      ]}
    end

Invalid 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 or canvas element by scoped ID path
`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/3` | 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
`focus_window/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, value: 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/2` | Load a font from binary data at runtime (family name required)
`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` | Send a command to any widget by ID, family, and optional value
`widget_batch/1` | Send a batch of widget commands processed atomically
`pane_split/4` | Split a pane in a pane grid
`pane_close/2` | Close a pane

## Custom command modules

`use Plushie.Command` declares typed command functions in any module. The same `command` macro used in native widgets generates functions that return `%Command{}` structs.

    defmodule MyApp.Commands.Gauge do
      use Plushie.Command

      command :set_value, value: :float
      command :reset
      command :set_range, fields: [min: :float, max: :float]
    end

This generates `set_value/2`, `reset/1`, and `set_range/3` with type guards and `Plushie.Type.encode_value/1` encoding. See the custom widgets guide for the full command declaration syntax.

The built-in `Command.Text` and `Command.Scroll` modules use this DSL internally.

## 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: model

### Clipboard

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/2` or `stream/2` with 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, value: 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 `:timeout` option.

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

This 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 specs
- `Plushie.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

← Previous Page Canvas
Next Page → Configuration