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

Macro that turns the calling module into a full-page TUI route — the
same module is both a `Phoenix.LiveView` and the `ExRatatui.App` that
drives it.

## Quick start

    # In the router (no special macro needed):
    live "/tui", MyAppWeb.MyTuiLive

    # The live module:
    defmodule MyAppWeb.MyTuiLive do
      use PhoenixExRatatui.LiveView
      alias ExRatatui.Layout.Rect
      alias ExRatatui.Widgets.Paragraph

      def tui_mount(_opts), do: {:ok, %{count: 0}}

      def tui_render(state, frame) do
        [{%Paragraph{text: "Count: #{state.count}"},
          %Rect{x: 0, y: 0, width: frame.width, height: frame.height}}]
      end

      def tui_handle_event(%ExRatatui.Event.Key{code: "+"}, state),
        do: {:noreply, %{state | count: state.count + 1}}

      def tui_handle_event(%ExRatatui.Event.Key{code: "q"}, state),
        do: {:stop, state}

      def tui_handle_event(_event, state), do: {:noreply, state}
    end

## How it works

`use PhoenixExRatatui.LiveView` injects a full `Phoenix.LiveView`
implementation (mount/3, render/1, handle_event/3, handle_info/2)
AND, via `@after_compile`, generates a sibling
`MyAppWeb.MyTuiLive.Runtime` module that implements `ExRatatui.App`
by delegating to the `tui_*` callbacks on the calling module.

The runtime proxy exists because `c:Phoenix.LiveView.handle_info/2`
(msg, socket) and `c:ExRatatui.App.handle_info/2` (msg, state) collide
on arity. Splitting the App into a hidden submodule lets both
behaviours live side-by-side without renaming Phoenix LV callbacks.

The proxy needs no attention — define the `tui_*` callbacks and the
macro handles the rest.

## TUI callbacks (override as needed)

  * `tui_mount(opts)` — return `{:ok, state}`, `{:ok, state,
    runtime_opts}`, or `{:error, reason}`. `opts` is the keyword
    list returned by `tui_mount_opts/1`.
  * `tui_render(state, frame)` — return a list of `{widget, rect}`
    tuples. Default: `[]`.
  * `tui_handle_event(event, state)` — return `{:noreply, state}` or
    `{:stop, state}`. Default: `{:noreply, state}`.
  * `tui_handle_info(msg, state)` — same shape; for messages sent to
    the runtime server (PubSub, `send/2`). Default: `{:noreply, state}`.
  * `tui_terminate(reason, state)` — cleanup. Default: `:ok`.
  * `tui_mount_opts(socket)` — return the keyword list passed as
    `opts` to `tui_mount/1`. Use this to thread per-connection
    context (current user, params) from `Phoenix.LiveView` assigns
    into the App. Default: `[]`.

## Threading socket data into the App

`tui_mount_opts/1` is the bridge:

    defmodule MyAppWeb.AdminTui do
      use PhoenixExRatatui.LiveView

      @impl Phoenix.LiveView
      def mount(_params, session, socket) do
        {:ok, socket} = super(nil, nil, socket)
        {:ok, Phoenix.Component.assign(socket, :user_id, session["user_id"])}
      end

      def tui_mount_opts(socket), do: [user_id: socket.assigns.user_id]

      def tui_mount(opts), do: {:ok, %{user_id: opts[:user_id], n: 0}}
    end

## Lifecycle

- **HTTP mount** (not `connected?`) — empty assigns, render hook
  container, no Transport. Per `phoenix-thinking`'s no-work-in-mount
  rule: `mount/3` runs twice (HTTP request + WebSocket handshake).
- **WebSocket mount** — `Process.flag(:trap_exit, true)` so a
  mount-failing TUI returns `{:error, _}` cleanly from
  `Transport.start_link/1` instead of killing the LV (which would
  trigger an infinite client reconnect loop). Also attaches the
  library's `:handle_event` / `:handle_info` lifecycle hooks (see
  `__attach_hooks__/3`).
- **First `phx_ex_ratatui:resize`** — call `tui_mount_opts/1`, start
  the Transport at `{cols, rows}` driving the generated `Runtime`
  proxy.
- **Subsequent resizes** — `Transport.resize/3` updates the
  `CellSession` and notifies the runtime.
- **Hook input** — the `:handle_event` hook decodes it into an
  `%ExRatatui.Event.Key{}` and forwards it via `Transport.push_event/2`.
- **Runtime emits a frame** — the `:handle_info` hook encodes it via
  `PhoenixExRatatui.Renderer.Html.encode_diff/1` and pushes a
  `phx_ex_ratatui:render` event.
- **Runtime exits** (App returned `{:stop, _}`, or crash) — the
  `:handle_info` hook gets the server's EXIT signal, nulls out `:tui`,
  sets `:tui_ended` so the user sees a refresh prompt instead of a
  frozen-cells display. EXITs from any other process pass through to
  the user's own `handle_info/2`.

## Options

  * `:container_id` — DOM id for the hook container. Defaults to
    `"phoenix-ex-ratatui"`. Override when embedding multiple TUI
    pages on the same router so the JS hook's `getElementById`
    queries don't collide.
  * `:runtime` — `:callbacks` (default) or `:reducer`. Selects the
    `ExRatatui.App` runtime style used by the generated proxy.
    Reducer-runtime modules implement `tui_init/1`, `tui_render/2`,
    `tui_update/2` (with `{:event, _}` / `{:info, _}` wrapped
    messages), and optionally `tui_subscriptions/1`, instead of the
    callbacks-runtime `tui_mount/1` / `tui_handle_event/2` /
    `tui_handle_info/2` quartet. See
    [ExRatatui.App](`ExRatatui.App`) for the runtime distinction.

## Defining your own LiveView callbacks

`mount/3` and `render/1` are `defoverridable` — wrap either and call
`super(...)`:

    def mount(params, session, socket) do
      {:ok, socket} = super(params, session, socket)
      {:ok, assign(socket, :title, "My TUI")}
    end

`handle_event/3` and `handle_info/2` are **not** injected, so define
them normally. The library consumes its own browser events
(`phx_ex_ratatui:*`) and internal messages (`{:phoenix_ex_ratatui, …}`,
the runtime server's `{:EXIT, …}`) through `Phoenix.LiveView` lifecycle
hooks attached in `mount/3`. Those hooks run first and pass everything
else through, so your own `phx-click` handlers, PubSub, and timers
coexist with the TUI — no `super`, no special callback names:

    def handle_info({:new_message, msg}, socket) do
      # the TUI's render messages are consumed by the hook before this
      # runs; here you only see your own messages
      {:noreply, assign(socket, :latest, msg)}
    end

For mixed pages where the TUI is one of several pieces of UI, reach for
`PhoenixExRatatui.LiveComponent` instead.

# `__using__`
*macro* 

Generates the unified LiveView + App module. See the moduledoc.

# `decode_input`

```elixir
@spec decode_input(map()) :: ExRatatui.Event.t()
```

Decodes the `phx_ex_ratatui:input` payload the JS hook sends into an
`t:ExRatatui.Event.t/0`.

Modifiers stay as the string list `ExRatatui.Event.Key` uses
(`"ctrl"`, `"shift"`, `"alt"`, `"meta"`), so an App matching on
`%ExRatatui.Event.Key{modifiers: ["ctrl"]}` behaves the same here as
it does over SSH or in a terminal. Keeping them as strings (rather
than atoms) also means untrusted client input never grows the atom
table.

## Examples

    iex> event = PhoenixExRatatui.LiveView.decode_input(%{"kind" => "key", "code" => "a", "modifiers" => ["ctrl"]})
    iex> {event.code, event.modifiers, event.kind}
    {"a", ["ctrl"], "press"}

# `dispatch_intent`

```elixir
@spec dispatch_intent(Phoenix.LiveView.Socket.t(), term()) ::
  Phoenix.LiveView.Socket.t()
```

Dispatches a runtime intent against a LiveView socket.

Intents are emitted by an `ExRatatui.App` from `tui_handle_event/2`
or `tui_handle_info/2` via the third element of a `{:noreply, state,
intents: [...]}` (or `{:stop, ...}`) transition. They flow through
ExRatatui.Server's `intent_writer_fn` to this LV, where this
helper maps the intent shape to the equivalent Phoenix LV action.

Recognised intents:

  * `{:navigate, path}` — `Phoenix.LiveView.push_navigate(socket, to: path)`
  * `{:patch, path}` — `Phoenix.LiveView.push_patch(socket, to: path)`
  * `{:redirect, path}` — `Phoenix.LiveView.redirect(socket, to: path)`
  * `{:redirect, [external: url]}` — external redirect

Unrecognised intents are dropped and logged at warning level. This
keeps a TUI app forward-compatible: a future intent the consumer
doesn't know how to handle yet won't crash the LV.

Public so `PhoenixExRatatui.LiveComponent` can reuse the same
dispatch table.

---

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