{"id":50634763,"url":"https://github.com/mcass19/phoenix_ex_ratatui","last_synced_at":"2026-06-09T03:01:12.758Z","repository":{"id":362028134,"uuid":"1256939155","full_name":"mcass19/phoenix_ex_ratatui","owner":"mcass19","description":"Run ExRatatui apps inside Phoenix LiveView","archived":false,"fork":false,"pushed_at":"2026-06-05T11:16:19.000Z","size":1572,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-07T01:18:08.120Z","etag":null,"topics":["elixir","liveview","phoenix","ratatui","rust","terminal-user-interface","tui"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mcass19.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-02T08:06:40.000Z","updated_at":"2026-06-05T11:16:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mcass19/phoenix_ex_ratatui","commit_stats":null,"previous_names":["mcass19/phoenix_ex_ratatui"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/mcass19/phoenix_ex_ratatui","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcass19%2Fphoenix_ex_ratatui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcass19%2Fphoenix_ex_ratatui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcass19%2Fphoenix_ex_ratatui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcass19%2Fphoenix_ex_ratatui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mcass19","download_url":"https://codeload.github.com/mcass19/phoenix_ex_ratatui/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcass19%2Fphoenix_ex_ratatui/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34044919,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-08T02:00:07.615Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elixir","liveview","phoenix","ratatui","rust","terminal-user-interface","tui"],"created_at":"2026-06-07T01:04:05.603Z","updated_at":"2026-06-08T02:00:55.545Z","avatar_url":"https://github.com/mcass19.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PhoenixExRatatui\n\n[![Hex.pm](https://img.shields.io/hexpm/v/phoenix_ex_ratatui.svg)](https://hex.pm/packages/phoenix_ex_ratatui)\n[![Docs](https://img.shields.io/badge/hex-docs-blue)](https://hexdocs.pm/phoenix_ex_ratatui)\n[![CI](https://github.com/mcass19/phoenix_ex_ratatui/actions/workflows/ci.yml/badge.svg)](https://github.com/mcass19/phoenix_ex_ratatui/actions/workflows/ci.yml)\n[![License](https://img.shields.io/hexpm/l/phoenix_ex_ratatui.svg)](https://github.com/mcass19/phoenix_ex_ratatui/blob/main/LICENSE)\n\nRun [ExRatatui](https://github.com/mcass19/ex_ratatui) apps inside a [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view).\n\n![PhoenixExRatatui Demo](https://raw.githubusercontent.com/mcass19/phoenix_ex_ratatui/main/.github/demo.gif)\n\n`PhoenixExRatatui` is a thin transport that pipes the runtime's rendered **cell buffer** to the browser, where a small JS hook paints cells directly into the DOM as `\u003cspan\u003e` elements. No terminal emulator, no ANSI on the wire — just structured cell deltas over the LiveView socket.\n\n## Features\n\n- **Two unified-module APIs** — `use PhoenixExRatatui.LiveView` for a full-page TUI route, `use PhoenixExRatatui.LiveComponent` to embed a TUI inside an existing LiveView. The same module is both the Phoenix component and the `ExRatatui.App` driving it; a hidden `Module.Runtime` proxy bridges the two `handle_info/2` arities.\n- **Callback and reducer runtimes** — `runtime: :reducer` opts into command/subscription-driven apps (`tui_init/1` + `tui_update/2` + `tui_subscriptions/1`); the default `:callbacks` runtime uses `tui_mount/1` + `tui_handle_event/2` + `tui_handle_info/2`.\n- **Cell-diff rendering over the socket** — the rendered cell buffer ships as a structured `%{width, height, ops}` payload of `\u003cspan\u003e`-cell deltas. Arrays not objects, to roughly halve the wire size on full frames.\n- **Tiny, dependency-free JS hook** — ~5KB minified (vs. xterm.js's ~250KB). Measures the cell box, paints diffs by direct `cells[row][col]` lookup, forwards `keydown` as input events, and re-reports size via `ResizeObserver`.\n- **Inter-page navigation via runtime intents** — return `{:navigate, \"/path\"}`, `:patch`, or `:redirect` (internal or external) from any handler; the macro dispatches through `push_navigate/2` and friends.\n- **Auto-focus on full-page TUIs** — keystrokes flow without clicking the grid first. Embedded components deliberately don't steal focus.\n- **`:telemetry` integration** — transport connect/disconnect spans, a per-frame render span, and input-forward events, layered above the events `ex_ratatui` already emits.\n- **Full color and modifiers** — named, RGB, and 256-color indexed; bold, italic, underline, and more, inherited straight from ExRatatui.\n\n## Examples\n\nThe [`examples/demo/`](https://github.com/mcass19/phoenix_ex_ratatui/tree/main/examples/demo) Phoenix app showcases the unified LV and LC side-by-side:\n\n| View | Route | Demonstrates |\n|------|-------|--------------|\n| Home | `/` | Full-page LiveView, reducer runtime, navigation intents |\n| Chat | `/chat` | Full-page LiveView, callbacks runtime, Markdown/Textarea/Throbber/slash-command popup/scrollback |\n| Admin | `/admin` | An embedded reducer-runtime `LiveComponent` with a live Gauge/Table system monitor |\n\nRun it with `mix deps.get \u0026\u0026 mix phx.server` from inside `examples/demo/`.\n\n## Ecosystem\n\n- [ex_ratatui](https://github.com/mcass19/ex_ratatui) — The core terminal UI library this builds on.\n- [kino_ex_ratatui](https://github.com/mcass19/kino_ex_ratatui) — Run TUIs inside [Livebook](https://livebook.dev) notebooks.\n\n## Installation\n\nAdd `phoenix_ex_ratatui` to the deps in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:phoenix_ex_ratatui, \"~\u003e 0.1\"}\n  ]\nend\n```\n\nThen fetch:\n\n```sh\nmix deps.get\n```\n\n### Prerequisites\n\n- Elixir 1.17+\n- Phoenix LiveView 1.1+\n\n### Wiring the JS hook\n\nThe hook is resolved as a normal npm module. Add it to `assets/package.json` alongside Phoenix's own JS deps:\n\n```json\n{\n  \"dependencies\": {\n    \"phoenix\": \"file:../deps/phoenix\",\n    \"phoenix_html\": \"file:../deps/phoenix_html\",\n    \"phoenix_live_view\": \"file:../deps/phoenix_live_view\",\n    \"phoenix_ex_ratatui\": \"file:../deps/phoenix_ex_ratatui\"\n  }\n}\n```\n\nRun `npm install` (or `cd assets \u0026\u0026 npm install`), then import the hook in `assets/js/app.js`:\n\n```js\nimport { Socket } from \"phoenix\"\nimport { LiveSocket } from \"phoenix_live_view\"\nimport { PhoenixExRatatuiHook } from \"phoenix_ex_ratatui\"\n\nconst liveSocket = new LiveSocket(\"/live\", Socket, {\n  hooks: { PhoenixExRatatuiHook }\n})\n```\n\nThe hook sets sensible defaults on the container (monospace font, `white-space: pre`, `line-height: 1`) only when they aren't already supplied, so the grid stays themeable with CSS.\n\n## Quick Start\n\nBoth shapes are **unified modules** — the same module is both a Phoenix LiveView/LiveComponent and the `ExRatatui.App` driving it. The macro auto-generates a hidden `Module.Runtime` proxy that conforms to `ExRatatui.App` by delegating to the `tui_*` callbacks.\n\n### Full-page TUI route\n\n```elixir\ndefmodule MyAppWeb.MyTuiLive do\n  use PhoenixExRatatui.LiveView\n\n  def tui_mount(_opts), do: {:ok, %{count: 0}}\n\n  def tui_render(state, frame) do\n    alias ExRatatui.Layout.Rect\n    alias ExRatatui.Widgets.Paragraph\n    [{%Paragraph{text: \"Count: #{state.count}\"},\n      %Rect{x: 0, y: 0, width: frame.width, height: frame.height}}]\n  end\n\n  def tui_handle_event(%ExRatatui.Event.Key{code: \"+\"}, state),\n    do: {:noreply, %{state | count: state.count + 1}}\n\n  def tui_handle_event(%ExRatatui.Event.Key{code: \"q\"}, state),\n    do: {:stop, state}\n\n  def tui_handle_event(_event, state), do: {:noreply, state}\nend\n\n# In the router (no special macro):\nlive \"/tui\", MyAppWeb.MyTuiLive\n```\n\n### Embedded LiveComponent\n\n```elixir\ndefmodule MyAppWeb.AdminCounterPanel do\n  use PhoenixExRatatui.LiveComponent\n\n  def tui_mount(_opts), do: {:ok, %{n: 0}}\n  def tui_render(state, frame), do: # ...\n  def tui_handle_event(_event, state), do: {:noreply, state}\nend\n\ndefmodule MyAppWeb.AdminLive do\n  use Phoenix.LiveView\n\n  def render(assigns) do\n    ~H\"\"\"\n    \u003ch1\u003eAdmin Dashboard\u003c/h1\u003e\n    \u003c.live_component module={MyAppWeb.AdminCounterPanel} id=\"admin-tui\" /\u003e\n    \u003cp\u003eOther admin content\u003c/p\u003e\n    \"\"\"\n  end\nend\n```\n\n## How It Works\n\n```\n┌─────────────────┐   tui_* callbacks   ┌──────────────────────┐\n│  Your module    │ ◀────────────────── │  Module.Runtime      │  (hidden proxy,\n│  (LiveView/LC)  │                     │  conforms to App     │   generated by macro)\n└────────┬────────┘                     └──────────┬───────────┘\n         │                                         │\n         │ PhoenixExRatatui.Transport              │ ExRatatui.Server\n         ▼                                         ▼\n   CellSession  ──── %CellSession.Diff{} ────▶  Renderer.Html\n                                                   │\n                          push_event(\"phx_ex_ratatui:render\", payload)\n                                                   ▼\n                                       JS hook paints \u003cspan\u003e cells\n   browser keydown ──── \"phx_ex_ratatui:input\" ────▶ back into the runtime\n```\n\nA `CellSession` plus a linked ExRatatui.Server drive the module. On each render the server hands a `%CellSession.Diff{}` to the transport, which forwards it to the LiveView; `PhoenixExRatatui.Renderer.Html` encodes it to a JSON-friendly payload and `push_event/3`s it to the browser. The hook paints the deltas and forwards keystrokes back as `phx_ex_ratatui:input` events. Because the `Server` is linked to the LiveView process, teardown is deterministic — when the LiveView exits, the session closes and disconnect telemetry fires.\n\n## Inter-page navigation via runtime intents\n\nA TUI can navigate to another route by emitting a runtime intent from any handler:\n\n```elixir\ndef tui_handle_event(%Key{code: \"enter\"}, state) do\n  {:noreply, state, intents: [{:navigate, \"/dashboard\"}]}\nend\n\ndef tui_handle_event(%Key{code: \"q\"}, state) do\n  {:noreply, state, intents: [{:redirect, \"/login\"}]}\nend\n```\n\nRecognised intent shapes:\n\n| Intent | Effect |\n|---|---|\n| `{:navigate, \"/path\"}` | `Phoenix.LiveView.push_navigate/2` |\n| `{:patch, \"/path\"}` | `Phoenix.LiveView.push_patch/2` |\n| `{:redirect, \"/path\"}` | `Phoenix.LiveView.redirect/2` (internal) |\n| `{:redirect, [external: \"https://…\"]}` | `redirect/2` to an external URL |\n\nUnrecognised intents are dropped (logged at warning) so a TUI stays portable across consumers — return whatever the runtime understands and the LV ignores the rest.\n\nFor the embeddable `LiveComponent`, intents bubble up to the parent LV via `send/2` (Phoenix LV forbids redirects from inside `LiveComponent.update/2`). Add this clause to the parent LV:\n\n```elixir\ndef handle_info({:phoenix_ex_ratatui, :intent, intent}, socket) do\n  {:noreply, PhoenixExRatatui.LiveView.dispatch_intent(socket, intent)}\nend\n```\n\n## Threading socket data into the App\n\nLiveView assigns and TUI state live in different processes. The `tui_mount_opts/1` callback is the bridge — it receives the LiveView socket and returns the keyword list passed as `opts` to `tui_mount/1`:\n\n```elixir\ndefmodule MyAppWeb.AdminTui do\n  use PhoenixExRatatui.LiveView\n\n  @impl Phoenix.LiveView\n  def mount(_params, session, socket) do\n    {:ok, socket} = super(nil, nil, socket)\n    {:ok, assign(socket, :user_id, session[\"user_id\"])}\n  end\n\n  def tui_mount_opts(socket), do: [user_id: socket.assigns.user_id]\n\n  def tui_mount(opts), do: {:ok, %{user_id: opts[:user_id]}}\nend\n```\n\n## Guides\n\n| Guide | Description |\n|-------|-------------|\n| [Getting Started](guides/getting_started.md) | Extended walkthrough of both the full-page and embedded APIs, the JS hook wiring, and the typical project structure |\n| [Telemetry](guides/telemetry.md) | `:telemetry` events for transport, render, input, and intents — logging and `Telemetry.Metrics` |\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.\n\nPhoenixExRatatui is built on [ExRatatui](https://github.com/mcass19/ex_ratatui), a general-purpose terminal UI library for Elixir. Contributions to its underlying rendering, widgets, or layout engine are very welcome too.\n\n## License\n\nMIT — see [LICENSE](https://github.com/mcass19/phoenix_ex_ratatui/blob/main/LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmcass19%2Fphoenix_ex_ratatui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmcass19%2Fphoenix_ex_ratatui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmcass19%2Fphoenix_ex_ratatui/lists"}