All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
0.2.1 - 2026-05-20
Added
Inline image rendering via
@xterm/addon-image. The bundled JS hook now loads@xterm/addon-image0.9.0 in both the live (new/2) and static (frame/2) init paths, registering Sixel (DCS q ...) and iTerm2 inline-image (ESC ] 1337 ; File=... ^G) parser handlers on the xterm.js terminal. Without it those escape sequences were silently swallowed andExRatatui.Widgets.Image(introduced in ex_ratatui 0.10) rendered nothing in Livebook. Construct images withExRatatui.Image.new/2passingprotocol: :sixelorprotocol: :iterm2— xterm.js does not implement the Kitty graphics protocol. Bundle grows ~60 KB minified; no Elixir API change, no per-instance opt change. Moduledoc gained an Inline images section pointing at the ex_ratatui Images guide.examples/new_widgets.livemd— tour of the three widgets introduced in ex_ratatui 0.10. One consolidated Livebook notebook coveringExRatatui.Widgets.Image(a static frame with the new image addon plus an interactive App that cycles:sixel/:iterm2at runtime),ExRatatui.Widgets.CodeBlock(syntect-highlighted snippet with:solarized_dark, line numbers, and emphasis on lines 3..5), andExRatatui.Widgets.BigText(slide-deck banner showing:fulland:quadrantdensity variants). Closes with aKino.Layout.grid/2side-by-side comparison of all three. Catalogue entry added toexamples/README.md.Mix.installpins~> 0.3so the notebook picks up the addon-image bundle as soon as 0.3.0 is published.
0.2.0 - 2026-05-07
Added
- Accessible stopped-state DOM overlay. When the runtime server exits (
{:stop, _},mount/1failure, crash) the widget no longer paints a dim ANSI message into the xterm buffer — it broadcasts a"stopped"Kino event with%{message: stopped_message}, and the JS hook anchors arole="status" aria-live="polite"<div>over the xterm container. Screen readers announce the message viaaria-live; sighted users see a centered italic line in the configured theme's foreground/background instead of a dead cursor sitting on the frozen final frame. The overlay ispointer-events: none, applied atz-index: 1, and usestextContentso a user-supplied:stopped_messageis never interpreted as HTML. Idempotent — refires of the event are ignored once an overlay is present. The:stopped_messageknob from the per-instance display options is unchanged; this is purely a wire-protocol + presentation switch from "ANSI bytes the user reads off the dead xterm buffer" to "structured payload an accessible DOM overlay renders".
Changed
"stopped"Kino event replaces the in-buffer ANSI message on server :DOWN. The wire protocol changed:Kino.JS.Live.broadcast_event(ctx, "ansi", {:binary, %{}, "\\e[2J… App stopped …\\e[0m"})is nowbroadcast_event(ctx, "stopped", %{message: stopped_message}). Thebuild_stopped_screen/1private helper is removed. Existing tests updated; theassert_broadcast_event(kino, "stopped", %{message: _})shape pins the new contract, plus a defensive test that the payload is a plain map (not{:binary, _, _}) so a regression to byte-stream-shaped messages would fail loudly. End-user:stopped_messageAPI is unchanged.Kino.ExRatatui.configure/1— global display defaults. Writes to the:kino_ex_ratatuiApplication environment so every subsequentnew/2andframe/2call picks them up without ceremony. Accepts the same seven display keys asnew/2. Validation runs at the call site (matchesnew/2's shape) so a typo inconfigure/1fails fast rather than producing a working-but-wrong widget. Calling twice merges into the prior config rather than replacing it, so related settings can split across cells. Merge order isper-instance opts > configure/1 > module defaults, key-by-key — partial config sticks. The same env is reachable viaApplication.get_all_env(:kino_ex_ratatui)so a releaseconfig/runtime.exsworks equivalently. New Configuration guide walks through the merge order, the atom theme shorthands, and an "when not to use it" pointer for runtime-dependent values.Property-based test pass over the display-options surface. New
stream_datadependency andtest/kino/ex_ratatui/property_test.exs(async: true) with six properties: every valid display value lands in the display map unchanged across all seven keys; the:dark/:light/:livebookatom shorthands round-trip; setting one display key never disturbs the other six (default fall-through invariant); reserved keys never appear inmount_optsafternew/2regardless of input shape; non-reserved keys are preserved inmount_optsin their original order; andframe/2with any opt outside its supported set always raisesArgumentError. Two more properties inconfigure_test.exs(async: false) cover global-state behavior:configure/1stores each{key, value}pair underApplication.get_env(:kino_ex_ratatui, key), and per-instance opts always win overconfigure/1for the same key (precedence invariant). Generators per key,uniq_list_ofto keep keyword-key uniqueness deterministic, and explicitApplication.delete_envcleanup between iterations so the global-state properties stay order-independent.:themeatom shorthands —:dark,:light,:livebook. In addition to a full xterm.jsIThememap,:themenow accepts three atoms resolved in the JS hook.:darkpicks a bundled Catppuccin Mocha-flavored palette (the same colors as the no-opts default);:lightpicks Catppuccin Latte.:livebookreads the user's OS-levelprefers-color-schemeand live-switches between:dark/:lightwhenever that preference changes — no cell re-eval needed. The previous map-only validator is unchanged; passing an unknown atom (e.g.theme: :neon) still raisesArgumentErrorwith a message naming the offending option. JS-side: asubscribeLivebookTheme/2helper on the live and static paths registers amatchMedia("(prefers-color-scheme: dark)")listener for the iframe's lifetime; iframes torn down on cell re-eval take the listener with them. Theming example extended with sections 4–5 walking through:livebookautoswitch andconfigure/1.Per-instance display options on
new/2andframe/2. Seven reserved opts now configure the xterm.js iframe per cell::theme(full xterm.jsIThememap —background,foreground,cursor,cursorAccent,selectionBackground, the 16-color ANSI palette, etc.),:font_family(CSS string),:font_size(px integer),:height(CSS length applied to the xterm container — accepts"600px","60vh","calc(100vh - 200px)", …),:cursor_blink(boolean),:scrollback(non-negative integer), and:stopped_message(string painted into the iframe when the runtime exits via{:stop, _}or a mount failure). Defaults preserve the previous look exactly. Reserved keys are stripped from the keyword list before reachingExRatatui.App.mount/1, so apps never see them as mount opts. Each value is validated at the call site — bad shapes raiseArgumentErrorwith a message naming the offending option, never silently.frame/2accepts the static-friendly subset (:theme,:font_family,:font_size); live-only opts and unknown keys raise so typos likecol:instead ofcols:no longer go through silently. The display payload flows to the JS bundle viahandle_connect/1(live mode) and the static info map (frame mode); the JS hook merges it onto its own copy of the defaults so out-of-band callers (custom payloads, future smart-cell variants) still get sensible values when only some opts are supplied. Newexamples/theming.livemdwalks through every option — One Dark, Solarized Light, custom Fira Code at 16px, no-blink + custom stopped message, and a side-by-side static gallery viaKino.Layout.grid/1of three theme variants on the same widget tree.Kino.ExRatatui.Telemetry—:telemetryintegration. Mirrors the shape ofExRatatui.Telemetryone layer up, emitting events at the boundaries this widget controls so consumers can plug in logging, metrics, or distributed tracing without reaching into the runtime. Two span events ([:kino_ex_ratatui, :transport, :connect]with:mod/:width/:height— wraps the lazySession+Transport.start_server/1boot triggered by the first"resize";[:kino_ex_ratatui, :render, :frame]with:mod/:byte_count— wraps theIO.iodata_to_binary/1+ Kino-bridge broadcast per-frame work) and three single events ([:kino_ex_ratatui, :transport, :disconnect]with:mod/:reason— fires exactly once per session, either from the runtime server's:DOWNor from the widget'sterminate/2if the runtime is still alive;[:kino_ex_ratatui, :input, :forward]with:mod/:byte_count— fires when bytes from xterm.js are forwarded toByteStream.forward_input/3;[:kino_ex_ratatui, :resize]with:mod/:width/:height— fires on resizes after the boot one). Public helpers:span/3,execute/3,attach_default_logger/1,detach_default_logger/0. New Telemetry guide walks through the full event catalogue and aTelemetry.Metricswiring example. Added{:telemetry, "~> 1.0"}as an explicit dependency and toextra_applicationsso the handler registry is available wherever the kino runs.
0.1.1 - 2026-04-30
Added
- README demo GIF (
assets/demo.gif) showing aKino.ExRatatuiwidget driving anExRatatui.Appinside a Livebook notebook.
0.1.0 - 2026-04-29
Added
- First release.
kino_ex_ratatuiruns anExRatatui.Appinside a Livebook notebook via xterm.js, implemented as a ~150-lineKino.JS.Livewidget on top ofExRatatui.Transport.ByteStream. Two entry points:Kino.ExRatatui.new/2for live App-driven kinos,Kino.ExRatatui.frame/2for one-shot static frames suitable for docs andKino.Layout.grid/1side-by-side comparisons. - JS bundle under
assets/—@xterm/xterm5.5 +@xterm/addon-fit0.10 bundled with esbuild 0.28 tolib/assets/kino_ex_ratatui/{main.js,main.css}. The bundle is committed so installing the hex package needs no Node toolchain.mix assets.installandmix assets.buildaliases are provided for contributors. - Lazy lifecycle. The runtime server and
ExRatatui.Sessionare created on the first"resize"event from the iframe, so dimensions always come from xterm.js'sFitAddonrather than a hardcoded default. Subsequent resize events flow throughByteStream.forward_resize/4. When the App returns{:stop, _}(ormount/1fails), the widget broadcasts the canonical alt-screen leave sequence so xterm.js restores its cursor and main buffer. - Test suite — 22 tests via
Kino.Test'sconfigure_livebook_bridge+push_event/3+assert_broadcast_event/3, covering: lazy boot, mount-opts pass-through, handleconnect payload, first/subsequent resize, input round-trip, input arriving before first resize, server:DOWN, terminate cleanup, mount failure, unrelatedhandle_infomessages, and `_assets_info/0. Runs async in 0.2s, 100% line coverage (test fixtures excluded viatest_coverage: [ignore_modules: [...]]`). - Three bundled example notebooks under
examples/—system_monitor.livemd(callback-runtime dashboard portingex_ratatui/examples/system_monitor.exswithGauge,Table,/procpolling),chat_interface.livemd(callback-runtime AI-chat mock exercisingMarkdown,Textarea,Throbber,Scrollbar, and/-prefixedSlashCommandsautocomplete viaPopup— ported from the original imperativeExRatatui.run/1loop inex_ratatui/examples/chat_interface.exs), andreducer_counter.livemd(reducer-runtime counter with aSubscription.intervalplus aKino.ExRatatui.frame/2static-frame demo). Each notebook cross-references the other two and links to the relevant runtime guide so any one of them is a complete jumping-off point.