docs/reference/commands.md

# Commands and Effects

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:

```elixir
# 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](app-lifecycle.md#return-value-validation) 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 |

```elixir
{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

```elixir
{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

```elixir
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 `: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](testing.md).

## DIY patterns

The runtime is a GenServer. You can bypass the command system and send
messages directly:

```elixir
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](../guides/11-async-and-effects.md) -
  effects, async, streaming, and multi-window
- [App Lifecycle reference](app-lifecycle.md) - return value
  validation and update cycle
- [Testing reference](testing.md) - effect stubs