Plushie.Bridge (Plushie v0.6.0)

Copy Markdown View Source

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.

Summary

Functions

Returns a specification to start this module under a supervisor.

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

Captures a renderer screenshot and returns the raw response map.

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

Sends an effect request to the renderer.

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

Sends an interact request to the renderer.

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

Registers an effect stub with the renderer.

Sends application-level settings to the renderer.

Sends an encoded snapshot of tree to the renderer.

Subscribes to a renderer-side event source.

Sends a system-wide operation to the renderer.

Sends a system-wide query to the renderer.

Removes a previously registered effect stub.

Unsubscribes from a renderer-side event source.

Sends a single widget command to the renderer.

Sends a batch of widget commands to the renderer.

Sends a widget operation to the renderer.

Sends a window lifecycle operation to the renderer.

Starts the bridge linked to the calling process.

Stops the bridge GenServer.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

restart_renderer(bridge)

@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(bridge, name, opts \\ [], timeout \\ 30000)

@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(bridge, timestamp)

@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(bridge, id, kind, payload)

@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(bridge, op, payload)

@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(bridge, id, action, selector, payload \\ %{})

@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(bridge, ops)

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

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

send_register_effect_stub(bridge, kind, response)

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

Registers an effect stub with the renderer.

send_settings(bridge, settings)

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

Sends application-level settings to the renderer.

send_snapshot(bridge, tree)

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

Sends an encoded snapshot of tree to the renderer.

send_subscribe(bridge, kind, tag, max_rate \\ nil, window_id \\ nil)

@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(bridge, op, settings \\ %{})

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

Sends a system-wide operation to the renderer.

send_system_query(bridge, op, settings \\ %{})

@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(bridge, kind)

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

Removes a previously registered effect stub.

send_unsubscribe(bridge, kind, tag \\ nil)

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

Unsubscribes from a renderer-side event source.

send_widget_command(bridge, node_id, op, payload)

@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(bridge, commands)

@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(bridge, op, payload)

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

Sends a widget operation to the renderer.

send_window_op(bridge, op, window_id, settings \\ %{})

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

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

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

Stops the bridge GenServer.