Hex.pm Docs CI License

Run ExRatatui apps inside Livebook notebooks.

KinoExRatatui Demo

KinoExRatatui is a byte-stream transport that pipes the runtime's rendered ANSI through xterm.js and forwards keypresses and resize events back. Implemented as a Kino.JS.Live widget on top of ExRatatui.Transport.ByteStream.

Features

  • Same App, same surface — any module implementing ExRatatui.App runs unchanged.
  • Responsive sizing — xterm.js's FitAddon derives cell dimensions and reports resize events; the App sees them as %ExRatatui.Event.Resize{} in handle_event/2.
  • Static framesKino.ExRatatui.frame/2 renders a one-shot [{widget, rect}, ...] list and ships the bytes to xterm.js. Useful for documentation, side-by-side comparisons via Kino.Layout.grid/1, screenshots, etc.
  • Inline images — the bundled @xterm/addon-image registers Sixel and iTerm2 inline-image parsers, so ExRatatui.Widgets.Image renders PNG / JPEG / GIF / WebP / BMP end-to-end in Livebook. Build images with ExRatatui.Image.new/2 and place them in the widget tree like any other widget; pass protocol: :sixel or protocol: :iterm2 at construction time.
  • Themeable — pass :theme, :font_family, :font_size, :height, :cursor_blink, :scrollback, and :stopped_message to new/2 (or the static-friendly subset to frame/2) to override the defaults per cell. The :theme map is the full xterm.js ITheme — 16 ANSI colors, selection, cursor accents, the lot. Use the :dark / :light / :livebook atom shorthands to pick a bundled palette; :livebook follows the user's prefers-color-scheme and live-switches.
  • Global defaultsKino.ExRatatui.configure/1 writes display defaults to the :kino_ex_ratatui Application environment. Per-instance opts still win key-by-key. See the Configuration guide.
  • Accessible stopped state — when the runtime exits the widget renders a role="status" aria-live="polite" DOM overlay over the xterm container. Screen readers announce it; sighted users see a clean italic message instead of the frozen final frame. Customise the text with :stopped_message.
  • Zero browser-side state on cell re-eval — re-running the cell tears the runtime down and starts a fresh one, matching every other Kino.JS.Live widget.
  • Telemetry[:kino_ex_ratatui, :transport, :connect | :disconnect], [:kino_ex_ratatui, :render, :frame], [:kino_ex_ratatui, :input, :forward], and [:kino_ex_ratatui, :resize] events sit one layer above ex_ratatui's own runtime/render telemetry. See the Telemetry guide for the full event catalogue and a Telemetry.Metrics example.

Examples

Five notebook examples live under examples/ — open them in Livebook and run the cells. See the catalog for a one-liner per notebook and a recommended starting point.

Ecosystem

Installation

Add kino_ex_ratatui to the Livebook setup cell (or the project's mix.exs):

Mix.install([
  {:kino_ex_ratatui, "~> 0.2"}
])

Prerequisites

  • Elixir 1.17+
  • Livebook 0.19+

Quick Start

defmodule Counter do
  use ExRatatui.App

  alias ExRatatui.Event.Key
  alias ExRatatui.Layout.Rect
  alias ExRatatui.Widgets.{Block, Paragraph}

  def mount(_), do: {:ok, %{n: 0}}

  def render(state, frame) do
    [
      {%Paragraph{
         text: "Count: #{state.n}\n\n+ increment   - decrement   q quit",
         block: %Block{title: "counter"}
       },
       %Rect{x: 0, y: 0, width: frame.width, height: frame.height}}
    ]
  end

  def handle_event(%Key{code: "+"}, s), do: {:noreply, %{s | n: s.n + 1}}
  def handle_event(%Key{code: "-"}, s), do: {:noreply, %{s | n: s.n - 1}}
  def handle_event(%Key{code: "q"}, s), do: {:stop, s}
  def handle_event(_, s),                do: {:noreply, s}
end

Kino.ExRatatui.new(Counter)

Static frames

alias ExRatatui.Layout.Rect
alias ExRatatui.Widgets.{Block, Paragraph}

Kino.ExRatatui.frame(
  [
    {%Paragraph{
       text: "Hello from a static frame!",
       block: %Block{title: "demo"}
     },
     %Rect{x: 0, y: 0, width: 40, height: 5}}
  ],
  cols: 40,
  rows: 5
)

frame/2 renders the widget list once via ExRatatui.Session, ships the resulting ANSI to xterm.js, and stops. No event loop, no runtime server.

How it works

KinoExRatatui implements ExRatatui.Transport as a byte-stream transport — the same shape as the built-in SSH transport. The wiring:

xterm.js (iframe)            Kino.ExRatatui (Kino.JS.Live)         ExRatatui.Server
                     
onData(bytes)         >    handle_event("input", _)        >   {:ex_ratatui_event, _}
ResizeObserver        >    handle_event("resize", _)       >   {:ex_ratatui_resize, _, _}
xterm.write(bytes)    <    broadcast_event("ansi", _)      <   writer_fn.(bytes)

The runtime server starts lazily on the first "resize" event so the ExRatatui.Session opens at the exact dimensions xterm.js's FitAddon settled on. From there, input bytes round-trip through ExRatatui.Transport.ByteStream.forward_input/3 (which absorbs synthesized Event.Resize events and dispatches everything else as {:ex_ratatui_event, _}). When the App returns {:stop, _}, the live widget catches the runtime's :DOWN and broadcasts a stop state message.

Guides

GuideDescription
ConfigurationGlobal display defaults via configure/1, the theme atom shorthands, and the merge order
Telemetry:telemetry events for transport, render, input, and resize — logging and Telemetry.Metrics

Contributing

See CONTRIBUTING.md for development setup and guidelines.

KinoExRatatui is built on ExRatatui, a general-purpose terminal UI library for Elixir. Contributions to its underlying rendering, widgets, or layout engine are very welcome too.

License

MIT — see LICENSE.