KinoExRatatui

Hex.pmDocsCILicense

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

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

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.