Kino.ExRatatui (KinoExRatatui v0.2.1)

Copy Markdown View Source

Run an ExRatatui.App inside a Livebook notebook via xterm.js.

Kino.ExRatatui is a byte-stream transport that pipes the runtime server's rendered ANSI through an xterm.js iframe and forwards keypresses + resize events back.

Example

defmodule Counter do
  use ExRatatui.App
  alias ExRatatui.Event.Key
  alias ExRatatui.Layout.Rect
  alias ExRatatui.Widgets.Paragraph

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

  def render(state, frame) do
    [{%Paragraph{text: "Count: #{state.n}"},
      %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: "q"}, s), do: {:stop, s}
  def handle_event(_, s),                do: {:noreply, s}
end

Kino.ExRatatui.new(Counter)

Mount options

The second argument to new/2 is a keyword list. Any key not listed under Display options below is forwarded verbatim to ExRatatui.App.mount/1. Use it for per-instance configuration your App reads from its mount opts.

The keys :mod, :name, and :transport are reserved by the runtime and silently overwritten.

Display options

Reserved opts that configure the xterm.js iframe rather than the App. All are optional; defaults preserve the current widget look. Unknown values raise ArgumentError at the call site.

OptionTypeDefaultNotes
:thememap() or :dark / :light / :livebookcatppuccin-style dark themeA map is forwarded to xterm.js's Terminal({theme: ...}) verbatim — accepts the full xterm.js ITheme object (:background, :foreground, :cursor, :cursorAccent, :selectionBackground, the 16-color ANSI palette, …). Atom keys are JSON-encoded as strings; use the camelCase xterm.js expects (cursorAccent, not cursor_accent). The atom shorthands resolve in the JS hook: :dark and :light pick a bundled palette, :livebook follows the user's prefers-color-scheme and live-switches when it changes.
:font_familyString.t()"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"CSS font-family value. Anything xterm.js can render.
:font_sizepos_integer()13Cell font size in px.
:heightString.t()"400px"CSS height applied to the xterm container. Accepts any valid CSS length ("600px", "60vh", …).
:cursor_blinkboolean()truexterm.js cursor blink.
:scrollbacknon_neg_integer()1000xterm.js scrollback line limit.
:stopped_messageString.t()"App stopped — re-evaluate the cell to start a new run."Shown in the iframe after the runtime exits ({:stop, _} or mount/1 failure).

Example:

Kino.ExRatatui.new(Counter,
  # mount opts → App.mount/1
  start: 10,
  # display opts → xterm.js
  theme: %{background: "#0d1117", foreground: "#c9d1d9"},
  font_size: 14,
  height: "600px"
)

Global configuration

Set defaults that apply to every new/2 and frame/2 call in the current Livebook (or your application's runtime) with configure/1:

# In a setup cell, or your application's start/2:
Kino.ExRatatui.configure(
  theme: :livebook,
  font_family: "JetBrains Mono, ui-monospace, monospace",
  font_size: 14
)

Merge order is per-instance opts > configure/1 > module defaults, key by key. Setting theme: globally and then passing theme: %{background: "#000"} per-instance overrides the theme alone — the global font_size still wins for that call. See configure/1 for the full list and the Configuration guide for recipes.

Lifecycle

Each new/2 call spawns a fresh Kino.JS.Live server. The runtime server and ExRatatui.Session are created lazily on the first "resize" event from the iframe so we always start at the correct cell dimensions reported by xterm.js's FitAddon. Re-evaluating the cell (or closing the notebook) tears the runtime server down and starts a new one — no state is preserved across re-evals.

How it plugs into ExRatatui

Implements ExRatatui.Transport as a byte-stream transport, using ExRatatui.Transport.start_server/1 to boot the runtime and ExRatatui.Transport.ByteStream to pump input + resize events. See the Custom Transports guide for the reference shape.

Inline images

The bundled JS hook loads @xterm/addon-image, so ExRatatui.Widgets.Image renders end-to-end in Livebook over Sixel and iTerm2 inline-image protocols. Build images with ExRatatui.Image.new/2 and place them in the widget tree like any other widget — no kino-side configuration is needed. The protocol is picked explicitly at construction time; pass :sixel or :iterm2 (xterm.js does not implement the Kitty graphics protocol). See the Images guide for the full API.

Summary

Functions

Sets global defaults applied to every subsequent new/2 and frame/2 call in the current runtime.

Renders a one-shot static frame of widgets and returns a non-interactive Kino.JS widget that paints it once.

Builds a new live kino that hosts mod (an ExRatatui.App).

Functions

configure(opts)

@spec configure(keyword()) :: :ok

Sets global defaults applied to every subsequent new/2 and frame/2 call in the current runtime.

Accepts the same keys as the Display options on new/2. Each value is validated immediately (the same way it would be on new/2) — bad shapes raise ArgumentError.

Per-instance opts on new/2 / frame/2 still win key-by-key. Calling configure/1 again merges into the existing config rather than replacing it, so you can split related settings across cells.

Returns :ok.

Examples

# Reactively follow Livebook's light/dark mode and bump the font.
Kino.ExRatatui.configure(theme: :livebook, font_size: 14)

# Per-instance overrides still apply. This call uses the configured
# font_size: 14 but a custom theme.
Kino.ExRatatui.new(Counter, theme: %{background: "#000"})

Stored under the :kino_ex_ratatui Application environment, so the same value is also reachable via Application.get_all_env(:kino_ex_ratatui) if you're orchestrating from a release config/runtime.exs or any other Config-driven setup.

frame(widgets, opts \\ [])

@spec frame(
  [{ExRatatui.widget(), ExRatatui.Layout.Rect.t()}],
  keyword()
) :: Kino.JS.t()

Renders a one-shot static frame of widgets and returns a non-interactive Kino.JS widget that paints it once.

Useful for documentation, screenshots in notebooks, or Kino.Layout.grid([frame_a, frame_b, frame_c]) side-by-side comparisons. There is no event loop, no resize handling, and no runtime server — just an ExRatatui.Session rendered once and written to xterm.js.

Options

  • :cols — terminal width in cells. Defaults to 80.
  • :rows — terminal height in cells. Defaults to 24.
  • :theme — see Display options. Same defaults as new/2.
  • :font_family — same.
  • :font_size — same.

Live-only display opts (:height, :cursor_blink, :scrollback, :stopped_message) are not accepted here and will raise ArgumentError — they have nothing to apply against in a static one-shot frame.

Examples

iex> alias ExRatatui.Layout.Rect
iex> alias ExRatatui.Widgets.{Block, Paragraph}
iex> kino = 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
...> )
iex> kino.module
Kino.ExRatatui

new(mod, opts \\ [])

@spec new(
  module(),
  keyword()
) :: Kino.JS.Live.t()

Builds a new live kino that hosts mod (an ExRatatui.App).

opts is split into Display options consumed by the widget itself and Mount options forwarded verbatim to ExRatatui.App.mount/1. Reserved display keys never reach the App.

Examples

# Plain
Kino.ExRatatui.new(Counter)

# Mount opt forwarded to mount/1
Kino.ExRatatui.new(Counter, start: 10)

# Display opt + mount opt
Kino.ExRatatui.new(Counter,
  start: 10,
  theme: %{background: "#0d1117", foreground: "#c9d1d9"},
  font_size: 14
)