# `PhoenixExRatatui.Transport`
[🔗](https://github.com/mcass19/phoenix_ex_ratatui/blob/v0.2.0/lib/phoenix_ex_ratatui/transport.ex#L1)

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

The 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 `c:Phoenix.LiveView.terminate/2` (which only fires when the
socket is `trap_exit`-aware, and we don't recommend that).

# `refs`

```elixir
@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.

# `push_event`

```elixir
@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`.

# `resize`

```elixir
@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.

# `start_link`

```elixir
@spec start_link(keyword()) :: {:ok, refs()} | {:error, term()}
```

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 implementing `ExRatatui.App`.
  * `:width` (required) — initial terminal width in cells. Must be `>= 1`.
  * `:height` (required) — initial terminal height in cells. Must be `>= 1`.
  * `:target` (required) — `t:pid/0` the runtime server should be
    linked to (typically `self()` from a LiveView mount). Used by
    the default writer if `:writer` is 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.LiveComponent` overrides this to call
    `Phoenix.LiveView.send_update/3` instead, since LiveComponents
    have no `handle_info/2` and must receive updates via
    `update/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.LiveComponent` overrides this to
    route intents through `Phoenix.LiveView.send_update/3` for 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{}, mod: module}}

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.

# `stop`

```elixir
@spec stop(refs(), term()) :: :ok
```

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.

---

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