# `Plushie.Bridge`
[🔗](https://github.com/plushie-ui/plushie-elixir/blob/v0.6.0/lib/plushie/bridge.ex#L1)

Bridge to the plushie renderer process.

Manages the connection to the renderer, buffers partial JSONL lines (JSON
mode) or receives length-prefixed frames (MessagePack mode), and forwards
decoded events to the runtime process.

## Transport modes

Controlled by the `:transport` option:

- `:spawn` (default) -- spawns the renderer binary as a child process
  using an Erlang Port. The Port handles stdio framing automatically.

- `:stdio` -- reads/writes the BEAM's own stdin/stdout. Used when the
  renderer spawns the Elixir process (e.g. `plushie --exec`).

- `{:iostream, pid}` -- sends and receives protocol messages via an
  external process (the iostream adapter). Used for custom transports
  like SSH channels, TCP sockets, or WebSockets where the adapter
  process handles the underlying I/O and framing.

  The iostream adapter must:
  1. Receive `{:iostream_bridge, bridge_pid}` on startup (Bridge sends
     this during init).
  2. Send `{:iostream_data, binary}` to the bridge when protocol data
     arrives (one complete protocol message per delivery).
  3. Handle `{:iostream_send, iodata}` messages from the bridge by
     writing the data to the underlying transport.
  4. Send `{:iostream_closed, reason}` when the transport is closed.

## Wire formats

Controlled by the `:format` option:

- `:json` -- JSONL over stdio. Opt-in for debugging and observability.
  The Port is opened with `{:line, 65_536}`, which causes the driver to
  deliver data as `{port, {:data, {:eol, line}}}` for complete lines and
  `{port, {:data, {:noeol, chunk}}}` for partial lines that exceed the
  line buffer. Partial chunks are accumulated in `buffer` and flushed when
  the matching `:eol` chunk arrives.

- `:msgpack` (default) -- MessagePack over stdio with 4-byte big-endian
  length-prefixed framing. The Port is opened with `{:packet, 4}`, which
  causes the Erlang Port driver to handle framing automatically in both
  directions. Complete frames arrive as `{port, {:data, binary}}`.

On unexpected exit the bridge applies exponential back-off and attempts
to restart the renderer up to `max_restarts` times. If the limit is
exhausted the GenServer stops with `{:max_restarts_reached, reason}`.

During restart the runtime rebuilds renderer-owned state by re-sending
settings, a full snapshot, subscriptions, and windows. Transient commands
that cannot be rebuilt from that state are held until the runtime finishes
resync, then sent in order.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `restart_renderer`

```elixir
@spec restart_renderer(bridge :: GenServer.server()) :: :ok
```

Restarts the renderer process intentionally (e.g. after a Rust rebuild).

Unlike crash recovery, this does not count against the restart limit
and does not use exponential backoff. The existing renderer is closed
cleanly before opening the new one. The runtime receives
`:renderer_restarted` and re-syncs as usual.

# `screenshot`

```elixir
@spec screenshot(
  bridge :: GenServer.server(),
  name :: String.t(),
  opts :: keyword(),
  timeout :: timeout()
) :: map()
```

Captures a renderer screenshot and returns the raw response map.

Width and height are optional positive integers. The call blocks until the
renderer replies with `screenshot_response`.

# `send_advance_frame`

```elixir
@spec send_advance_frame(bridge :: GenServer.server(), timestamp :: non_neg_integer()) ::
  :ok
```

Sends an advance_frame message to the renderer (headless/test mode).

# `send_effect`

```elixir
@spec send_effect(
  bridge :: GenServer.server(),
  id :: String.t(),
  kind :: String.t(),
  payload :: map()
) :: :ok
```

Sends an effect request to the renderer.

# `send_image_op`

```elixir
@spec send_image_op(bridge :: GenServer.server(), op :: String.t(), payload :: map()) ::
  :ok
```

Sends an image operation (create/update/delete) to the renderer.

# `send_interact`

```elixir
@spec send_interact(
  bridge :: GenServer.server(),
  id :: String.t(),
  action :: String.t(),
  selector :: map(),
  payload :: map()
) :: :ok
```

Sends an interact request to the renderer.

The renderer will process the interaction against its widget tree and
respond with `interact_step` / `interact_response` messages. These are
forwarded to the runtime as `{:interact_step, id, events}` and
`{:interact_response, id, events}`.

## Parameters

- `id` -- unique request identifier, used to correlate responses.
- `action` -- the interaction verb. One of: `"click"`, `"toggle"`,
  `"select"`, `"type_text"`, `"submit"`, `"press"`, `"release"`,
  `"type_key"`, `"slide"`, `"paste"`, `"scroll"`, `"move_to"`,
  `"sort"`, `"canvas_press"`, `"canvas_release"`, `"canvas_move"`,
  `"pane_focus_cycle"`.
- `selector` -- a map identifying the target widget. Keys are
  optional and include `"by"` (e.g. `"id"`, `"text"`, `"role"`,
  `"label"`, `"focused"`) and `"value"` (the lookup value). An
  empty map targets the focused widget or the window root.
- `payload` -- action-specific data. Examples:
  - `%{text: "hello"}` for `"type_text"` / `"paste"`
  - `%{value: "option"}` for `"select"`
  - `%{value: 0.5}` for `"slide"`
  - `%{key: "Enter", modifiers: %{}}` for `"press"` / `"release"` / `"type_key"`
  - `%{x: 10, y: 20, button: "left"}` for `"canvas_press"` / `"canvas_release"`
  - `%{x: 10, y: 20}` for `"canvas_move"` / `"move_to"`
  - `%{delta_x: 0, delta_y: -3}` for `"scroll"`
  - `%{column: "name", direction: "asc"}` for `"sort"`
  - `%{}` for `"click"`, `"toggle"`, `"submit"`, `"pane_focus_cycle"`

# `send_patch`

```elixir
@spec send_patch(bridge :: GenServer.server(), ops :: [map()]) :: :ok
```

Sends a patch (list of diff ops) to the renderer.

# `send_register_effect_stub`

```elixir
@spec send_register_effect_stub(
  bridge :: GenServer.server(),
  kind :: String.t(),
  response :: term()
) :: :ok
```

Registers an effect stub with the renderer.

# `send_settings`

```elixir
@spec send_settings(bridge :: GenServer.server(), settings :: map()) :: :ok
```

Sends application-level settings to the renderer.

# `send_snapshot`

```elixir
@spec send_snapshot(bridge :: GenServer.server(), tree :: map()) :: :ok
```

Sends an encoded snapshot of `tree` to the renderer.

# `send_subscribe`

```elixir
@spec send_subscribe(
  bridge :: GenServer.server(),
  kind :: String.t(),
  tag :: String.t(),
  max_rate :: non_neg_integer() | nil,
  window_id :: String.t() | nil
) :: :ok
```

Subscribes to a renderer-side event source.

# `send_system_op`

```elixir
@spec send_system_op(
  bridge :: GenServer.server(),
  op :: String.t(),
  settings :: map()
) :: :ok
```

Sends a system-wide operation to the renderer.

# `send_system_query`

```elixir
@spec send_system_query(
  bridge :: GenServer.server(),
  op :: String.t(),
  settings :: map()
) :: :ok
```

Sends a system-wide query to the renderer.

# `send_unregister_effect_stub`

```elixir
@spec send_unregister_effect_stub(bridge :: GenServer.server(), kind :: String.t()) ::
  :ok
```

Removes a previously registered effect stub.

# `send_unsubscribe`

```elixir
@spec send_unsubscribe(
  bridge :: GenServer.server(),
  kind :: String.t(),
  tag :: String.t() | nil
) :: :ok
```

Unsubscribes from a renderer-side event source.

# `send_widget_command`

```elixir
@spec send_widget_command(
  bridge :: GenServer.server(),
  node_id :: String.t(),
  op :: String.t(),
  payload :: map()
) :: :ok
```

Sends a single widget command to the renderer.

# `send_widget_commands`

```elixir
@spec send_widget_commands(
  bridge :: GenServer.server(),
  commands :: [{String.t(), String.t(), map()}]
) :: :ok
```

Sends a batch of widget commands to the renderer.

# `send_widget_op`

```elixir
@spec send_widget_op(bridge :: GenServer.server(), op :: String.t(), payload :: map()) ::
  :ok
```

Sends a widget operation to the renderer.

# `send_window_op`

```elixir
@spec send_window_op(
  bridge :: GenServer.server(),
  op :: String.t(),
  window_id :: String.t(),
  settings :: map()
) :: :ok
```

Sends a window lifecycle operation to the renderer.

# `start_link`

```elixir
@spec start_link(opts :: keyword()) :: GenServer.on_start()
```

Starts the bridge linked to the calling process.

Required opts:
  - `:runtime`       - pid to receive `{:renderer_event, event}` messages

Required for `:spawn` transport (default):
  - `:renderer_path` - filesystem path to the plushie binary

Optional opts:
  - `:name`          - registration name passed to `GenServer.start_link/3`
  - `:transport`     - `:spawn` (default, spawns renderer as child process),
                       `:stdio` (reads/writes the BEAM's own stdin/stdout),
                       or `{:iostream, pid}` (custom transport via iostream adapter)
  - `:format`        - wire format, `:msgpack` (default) or `:json`
  - `:log_level`     - renderer log level (`:off`, `:error`, `:warning`, `:info`, `:debug`).
                       Default: `:error`. Ignored when `RUST_LOG` is set in the environment.
  - `:renderer_args` - extra CLI args prepended to the renderer command (e.g. `["--headless"]`)
  - `:max_restarts`  - max restart attempts before giving up (default: 5)
  - `:restart_delay` - base delay in ms for exponential back-off (default: 100)

# `stop`

```elixir
@spec stop(bridge :: GenServer.server()) :: :ok
```

Stops the bridge GenServer.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
