Connection-level helper that wires an ExRatatui.App to a
Phoenix.LiveView (or Phoenix.LiveComponent) over a freshly
constructed ExRatatui.CellSession.
Implements the ExRatatui.Transport behaviour as a marker — Phoenix
doesn't supervise transports the way ExRatatui.SSH.Daemon does, so
there's no child_spec/1. Each LiveView mount calls start_link/1
with target: self(), holds onto the returned %{server, cell_session}
references in its assigns, and pattern-matches on the rendered
%CellSession.Diff{} payloads in handle_info/2.
Wire protocol
Outbound (server → LiveView):
{:phoenix_ex_ratatui, :render, %ExRatatui.CellSession.Diff{}}Inbound (LiveView → server) — same as every byte-stream transport:
{:ex_ratatui_event, %ExRatatui.Event.t{}}
{:ex_ratatui_resize, w, h}Use push_event/2 and resize/3 rather than sending those messages
by hand — both also handle the CellSession.resize/3 step that must
precede a resize message.
Example mount
def mount(_params, _session, socket) do
if connected?(socket) do
{:ok, refs} =
PhoenixExRatatui.Transport.start_link(
mod: MyApp,
width: 80,
height: 24,
target: self()
)
{:ok, assign(socket, tui: refs)}
else
{:ok, assign(socket, tui: nil)}
end
end
def handle_info({:phoenix_ex_ratatui, :render, diff}, socket) do
{:noreply, push_event(socket, "render", encode_for_client(diff))}
endThe server is started linked to the caller. When the LiveView process
exits (browser disconnect, navigation, crash), the server's
terminate/2 runs deterministically: closes the CellSession, calls
the user terminate/2, emits transport-disconnect telemetry. We do
not rely on Phoenix.LiveView.terminate/2 (which only fires when the
socket is trap_exit-aware, and we don't recommend that).
Summary
Types
References returned from start_link/1. Hold onto all three —
the :server pid for push_event/2 and stop/2, the
:cell_session for resize/3 (which must resize the session
before notifying the server), and :mod for telemetry / debug
contexts where the App module that's being driven matters.
Functions
Sends a decoded terminal event to the runtime server. Wrapper around
the {:ex_ratatui_event, _} mailbox protocol so callers don't have
to know the message tag.
Resizes the underlying CellSession and notifies the server so the
next render uses the new dimensions.
Constructs an ExRatatui.CellSession at width x height and starts
an ExRatatui.Server driving mod against it. The server ships
rendered cell diffs to target as
{:phoenix_ex_ratatui, :render, %CellSession.Diff{}} messages.
Stops the runtime server cleanly. The CellSession is closed by the
server's terminate/2; no separate cleanup is needed.
Types
@type refs() :: %{ server: pid(), cell_session: ExRatatui.CellSession.t(), mod: module() }
References returned from start_link/1. Hold onto all three —
the :server pid for push_event/2 and stop/2, the
:cell_session for resize/3 (which must resize the session
before notifying the server), and :mod for telemetry / debug
contexts where the App module that's being driven matters.
Functions
@spec push_event(pid() | refs(), ExRatatui.Event.t()) :: :ok
Sends a decoded terminal event to the runtime server. Wrapper around
the {:ex_ratatui_event, _} mailbox protocol so callers don't have
to know the message tag.
Used by the LiveView's client-side hook to forward keypresses, mouse
clicks, and synthetic events from the browser into the App's
handle_event/2.
@spec resize(refs(), pos_integer(), pos_integer()) :: :ok | {:error, term()}
Resizes the underlying CellSession and notifies the server so the
next render uses the new dimensions.
The two operations happen in this order on purpose: the server's
resize handler updates the cached size before the next render,
but it does not touch the session itself — that's the transport's
job. Calling resize/3 here gets it right; sending
{:ex_ratatui_resize, _, _} directly to the server would leave the
session stuck at the old size and the next take_cells_diff/1
would emit a stale full payload.
Returns {:error, reason} if CellSession.resize/3 fails (e.g. the
session was already closed); the server is not notified in that case.
Constructs an ExRatatui.CellSession at width x height and starts
an ExRatatui.Server driving mod against it. The server ships
rendered cell diffs to target as
{:phoenix_ex_ratatui, :render, %CellSession.Diff{}} messages.
Options
:mod(required) — module implementingExRatatui.App.:width(required) — initial terminal width in cells. Must be>= 1.:height(required) — initial terminal height in cells. Must be>= 1.:target(required) —pid/0the runtime server should be linked to (typicallyself()from a LiveView mount). Used by the default writer if:writeris not given.:writer(optional) — a 1-arity function called with each rendered%CellSession.Diff{}. Defaults to a function that sends{:phoenix_ex_ratatui, :render, diff}to:target.PhoenixExRatatui.LiveComponentoverrides this to callPhoenix.LiveView.send_update/3instead, since LiveComponents have nohandle_info/2and must receive updates viaupdate/2.:intent_writer(optional) — a 1-arity function called with each runtime intent (e.g.{:navigate, "/path"}). Defaults to a function that sends{:phoenix_ex_ratatui, :intent, intent}to:target.PhoenixExRatatui.LiveComponentoverrides this to route intents throughPhoenix.LiveView.send_update/3for the same reason as:writer.- Any other option — passed through verbatim to
mod.mount/1. Use this to thread per-connection context (current user, params, LiveView socket id) into the App without a global registry.
Return shape
{:ok, %{server: server_pid, cell_session: %CellSession{}}}Returns whatever error tuple mod.mount/1 produced if mount fails;
the CellSession is closed defensively before propagating.
The server is linked to the calling process. When the LiveView
exits, the server's terminate/2 runs and closes the session.
Stops the runtime server cleanly. The CellSession is closed by the
server's terminate/2; no separate cleanup is needed.
Idempotent in the practical sense: calling stop/2 after the server
has already exited is harmless (the call returns immediately). For
graceful shutdown from a LiveView's terminate/3, prefer relying on
the link — letting the LiveView exit naturally tears down the server
via the standard EXIT path.