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.
| Option | Type | Default | Notes |
|---|---|---|---|
:theme | map() or :dark / :light / :livebook | catppuccin-style dark theme | A 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_family | String.t() | "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace" | CSS font-family value. Anything xterm.js can render. |
:font_size | pos_integer() | 13 | Cell font size in px. |
:height | String.t() | "400px" | CSS height applied to the xterm container. Accepts any valid CSS length ("600px", "60vh", …). |
:cursor_blink | boolean() | true | xterm.js cursor blink. |
:scrollback | non_neg_integer() | 1000 | xterm.js scrollback line limit. |
:stopped_message | String.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
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
@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.
@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 to80.:rows— terminal height in cells. Defaults to24.:theme— see Display options. Same defaults asnew/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
@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
)