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.10.0 - 2026-05-19
Added
Single-binary distribution via Burrito. The new Packaging with Burrito guide walks through wrapping any
ExRatatui.Appinto a self-contained native binary per OS/arch — Linux x86_64, macOS x86_64, macOS aarch64, and Windows x86_64. End users download one file, run it, and the BEAM + ex_ratatui + Rust NIF unpack into a per-user cache on first launch.examples/burrito_demo/is the reference consumer, and.github/workflows/burrito_demo.ymlis the regression CI proving all four targets build + smoke-test green on every push.mix ex_ratatui.gen.burrito— Igniter-based generator. Patches a consumer'smix.exs(adds{:burrito, "~> 1.5"}, wiresreleases/0with the four standard targets), drops a CLI module withmain/1+--versionsmoke entry, and adds a.mise.tomlpinningzig 0.15.2.--ci githubadditionally scaffolds a.github/workflows/release.ymlthat builds + publishes binaries to GitHub Releases on tag push. Igniter is declaredoptional: true; projects that never run the generator pay nothing for it, and invoking the task without Igniter installed prints an install hint and exits cleanly.
Fixed
Precompiled
*-unknown-linux-muslNIFs are now self-contained. Added-C link-arg=-static-libgccto the musl rustflags innative/ex_ratatui/.cargo/config.toml. Previously the published musl artifact dynamically linkedlibgcc_s.so.1, which failed to load under Burrito's musl-based Linux payload. Affects bothx86_64-unknown-linux-muslandaarch64-unknown-linux-musltargets.ExRatatui.Widgets.CodeBlock— syntax-highlighted source code. Display-only widget powered by syntect's bundledSyntaxSetandThemeSet. Fields::content,:language(any syntect token name;nilfor plain text fallback),:theme(seven curated atoms —:base16_ocean_dark,:base16_ocean_light,:base16_eighties_dark,:base16_mocha_dark,:inspired_github,:solarized_dark,:solarized_light— or any raw string for custom theme sets),:line_numbers+:starting_line(right-aligned dim gutter with│separator, width grows with the last visible line),:highlight_lines(list of ints + ranges like[3, 7..9], normalised to a sorted unique list, rendered with a theme-derived background that brightens dark themes and dims light themes by 20/256 per channel). Composes withBlock,Popup, andWidgetListlike every other widget. One new widget type ("code_block") on the render decoder.ExRatatui.CodeBlock.highlight/3— raw highlighted lines for composite widgets. Returns[%ExRatatui.Text.Line{}]with per-token styled spans for callers building DiffViewer / Inspector-style composites without dropping a fullCodeBlockin the tree. Backed by a single new NIFhighlight_code/3(scheduled onDirtyCpu) returningVec<Vec<HighlightedSpan>>(NifMap withcontent,fg/bgasOption<(u8, u8, u8)>, plusbold/italic/underlinedflags).ExRatatui.CodeBlock.resolve_theme/1is the canonical theme-atom resolver used by both the widget encoder and the helper — single source of truth.ExRatatui.CodeBlock.from_native/1is a documented conversion seam for callers usingNative.highlight_code/3directly (e.g. hot loops reusing the same theme).[:ex_ratatui, :code_block, :highlight]telemetry span. EachExRatatui.CodeBlock.highlight/3call emits start + stop via:telemetry.span/3withlanguage(string ornil),theme(resolved syntect name), andbytes(source byte size); the stop event addsline_count. Mirrors the[:ex_ratatui, :image, :decode]span shape.Shared
widgets::highlighterRust module.OnceLock-cachedSyntaxSet(load_defaults_newlines) +ThemeSet(load_defaults),lines_for(code, language, theme)returningVec<Line<'static>>,theme_bg(theme)for the emphasis-color base. The ~25-line syntect→ratatui style/color/modifier translation is inlined (MIT-attributed in the module header) because the existingsyntect-tuiadapter is pinned to older ratatui versions (0.28/0.29) and we're on 0.30.examples/code_block_demo.exs. Interactive viewer that cycles through all seven themes and five sample languages (Rust, Python, Ruby, JavaScript, JSON), toggles the line-number gutter, and toggles emphasis on lines 3..5. Status panel echoes the active config live.Cheatsheet entry under
Text & contentnext to BigText, and a new"Widgets: Code"ex_doc group coveringExRatatui.CodeBlock+ExRatatui.Widgets.CodeBlock.Bundled Elixir syntax.
native/ex_ratatui/syntaxes/Elixir.sublime-syntaxis vendored from elixir-editors/elixir-sublime-syntax (MIT, copyright Po Chen — LICENSE alongside) and added to syntect'sSyntaxSetat startup viainto_builder()→SyntaxDefinition::load_from_str(include_str!(...))→.add()→.build().language: "elixir"now produces real per-token highlighting (defmodule/do/endas keywords, atoms, strings, sigils — six distinct fg colors on a typical snippet under base16-ocean.dark). Adds ~80 KB to the binary. Other BEAM languages: Erlang ships in syntect's defaults; EEx / HEEx / Surface are not yet bundled — same approach can extend to them when needed.Known limitation:
ExRatatui.Widgets.Markdownfenced code blocks still usebase16-ocean.dark— tui-markdown 0.3.7 hardcodes the syntect theme internally and does not expose it through its public API. A:code_themeknob on the Markdown widget depends on upstream cooperation and is out of scope for this release; users who need themed fenced blocks today can pre-extract the source and render viaCodeBlockdirectly.ExRatatui.Widgets.BigText— oversized 8×8 pixel text for slide titles and banners. Drop-in widget backed by tui-big-text 0.8.4; each character is rasterised through thefont8x8bitmap font.ExRatatui.BigText.new/2coerces text-like input through the shared text-coercion path (binary /%Span{}/%Line{}/%Text{}/ homogeneous lists), validates:pixel_sizeand:alignment, and merges any outer%Text{}style underneath the widget's own. Eightpixel_sizedensities cover the full upstream variant set::full(default — 1 cell per pixel, 8 rows tall),:half_height,:half_width,:quadrant,:third_height,:sextant,:quarter_height,:octant(1 row × half cols). Composes withBlock,Popup, andWidgetListlike every other widget. Adds one new widget type ("big_text") to the render decoder; no new NIFs.examples/big_text_demo.exs. Interactive viewer that cycles everypixel_size, three alignments, six colors, and four sample slide titles at runtime. Status panel echoes the active settings live. Helpful when picking a variant for a real slide deck.ExRatatui.Widgets.Image— image rendering across every transport. Decodes PNG / JPEG / GIF / WebP / BMP bytes and renders them through ratatui-image with Kitty, Sixel, iTerm2, or Unicode halfblocks.ExRatatui.Image.new/2returns a stateful widget handle ({:ok, %ExRatatui.Widgets.Image{}}) or{:error, {:decode_failed, msg}}; protocol / resize mode / background are set at construction and stored on a NIF resource so re-encoding only happens when the resolved protocol or rect dimensions change. Three resize strategies::fit(preserve aspect inside the rect),:crop(preserve aspect, fill, crop overflow),:scale(stretch to fill).Transport-aware protocol resolution. Each transport stamps a
TransportCapsvalue the render path consults:CellSessionforces:halfblocks(escape sequences can't survive cell diffing — Livebook / Kino apps that share model code with a real terminal just work); the local terminal can cache aPicker::from_query_stdioprobe viaExRatatui.Image.auto_local_protocol/1so:autoresolves to the detected protocol with the right font size; SSH and Distributed accept an:image_protocolopt at start time, optionally paired with:image_font_sizefor accurate Kitty / Sixel / iTerm2 scaling (ExRatatui.SSH.Daemon.start_link(..., image_protocol: :kitty, image_font_size: {10, 20}),ExRatatui.Distributed.attach(..., image_protocol: :kitty, image_font_size: {10, 20})). Explicit per-image protocol selections atImage.new/2are always honored.Image rendering over
ExRatatui.Distributed. Image widgets cross node boundaries via a snapshot path: the server runtime callsimage_snapshot/1on each%ExRatatui.Widgets.Image{state: ref}in the render tree before sending the widget list over the wire (a NIF ResourceArc can't cross nodes). The client node re-decodes the bytes into a freshImageResourceper draw. Snapshot wire shape is{bytes, protocol, resize, background}. Costs roughly the source PNG size per frame on the wire — fine for stills, watch bandwidth for animations on large images.probe_image_protocol: trueruntime opt formount/1.ExRatatui.Appapps can opt into auto-probing the local terminal by returning{:ok, state, probe_image_protocol: true}. The runtime callsExRatatui.Image.auto_local_protocol/1once after mount on the:localtransport —:autoimages then resolve to the detected protocol without the app needing access to the terminal reference. Skipped undertest_mode. Other transports ignore the opt.ExRatatui.Image.render_size/4. Pure-Elixir prediction of the rendered output pixel dimensions for a given (source dims, cell area, font size, resize mode) combination. Mirrors ratatui-image'sResize::needs_resize_pixels+fit_area_proportionallybyte-for-byte. Useful for status panels, layout decisions, or just understanding why:fitand:cropproduce identical output when the source is smaller than the target.:backgroundaccepts the fullExRatatui.Style.color/0shape. Named atoms (:red,:dark_gray, …),{:rgb, r, g, b},{:indexed, n}xterm 256-color codes, raw{r, g, b}tuples, andnilall work. Named and indexed values are converted to RGB at the Elixir boundary via the standard ANSI palette.New public API surface.
ExRatatui.Image.{new,dimensions,probe_terminal,auto_local_protocol,render_size}/{1,2,4},ExRatatui.Session.{set_image_protocol,set_image_font_size}/2,ExRatatui.set_image_protocol/2. Eight new NIFs:image_new/2,image_dimensions/1,image_snapshot/1,image_probe_terminal/0,session_set_image_protocol/2,session_set_image_font_size/2,terminal_set_image_protocol/2,terminal_set_local_probe/3.[:ex_ratatui, :image, :decode]telemetry span. EachImage.new/2call emits start + stop withformat(sniffed from magic bytes —:png/:jpeg/:gif/:webp/:bmp/:unknown),bytes, andwidth/heighton success. Per-render encode timing stays inside the existing[:ex_ratatui, :render, :frame]span.Examples.
examples/image_demo.exsis an interactive viewer with runtime protocol / resize toggling and a live status panel showing rendered output dimensions; supports--sshand--distributedflags for smoke-testing those transports end-to-end.examples/headless_image.exsrenders an image throughCellSession(with ANSI fg/bg per cell) for Livebook / snapshot consumers. Both acceptIMAGE_PATHto skip the network fetch, default topicsum.photos, and embed a 1×1 fallback PNG for offline use.Images guide and cheatsheet entry. Walks through the API, the full transport / protocol resolution table, the font-size caveat for Kitty / Sixel / iTerm2 scaling, telemetry, and known v1 limitations (no GIF animation, no SVG, no streaming decode,
Resize::Fitdoesn't upscale).
Changed
Rust toolchain bumped to 1.86+ to match ratatui-image 11.0.2's
rust-version(edition 2024). ex_ratatui itself stays on edition 2021; precompiled binaries are unaffected — you only hit this if you build the NIF yourself withEX_RATATUI_BUILD=1.Binary size grows by ~1.6 MB. ratatui-image pulls in
image(with PNG / JPEG / GIF / WebP / BMP decoders) and bundled Kitty / Sixel / iTerm2 encoders. No new system dependencies — chafa is feature-gated off, sixel uses pure-Rusticy_sixel.
Fixed
- Precompiled musl NIFs now load under musl runtimes.
native/ex_ratatui/.cargo/config.toml'sx86_64-unknown-linux-muslandaarch64-unknown-linux-musltargets now pass-C link-arg=-static-libgccalongside the existingtarget-feature=-crt-static. Without it the NIF.solinked against the build host's glibclibgcc_s.so.1and the musl loader on the consumer side refused it. Alpine and other musl-libc deploys can now consume the precompiled artifact directly — no source rebuild required.
0.9.0 - 2026-05-7
Added
Property-based tests for the
:intentsruntime opt. Newtest/ex_ratatui/property/intents_property_test.exs(async: true) with two properties under the 4-tuple{:cell_session, cs, cell_writer_fn, intent_writer_fn}transport tag: (1) for any mount-intent list and any sequence ofhandle_info-supplied batches, the writer receives every intent in concat order with no reordering, drops, or extras; (2) emptyintents: []batches never fire on the writer regardless of how many sequential empty handle_info calls a TUI makes. Complements the existing scenario unit tests inExRatatui.Server.IntentsTest(mount-time, handle_event, handle_info, stop-with-intent, drop-without-writer, shape validation) — together they pin the full intent contract. Reuses the existingExRatatui.Test.ServerApps.Intentsfixture.Documented
:intentsruntime opt across the public surface.ExRatatui.App's moduledoc now has a dedicated Runtime opts section listing every key (:commands,:intents,:render?,:trace?) with types, defaults, scope (callback vs reducer), and the "intents from{:stop, ...}fire before the server exits" guarantee. Thet:callback_opts/0typedoc points at it; a newt:intent/0typedoc names the opaque-term contract. TheExRatatui.App.handle_event/2andExRatatui.App.handle_info/2callback typespecs now include the{:noreply | :stop, state, callback_opts}3-tuple variants — they were always accepted by the runtime but the typespecs declared only the 2-tuple shape, hiding the feature from Dialyzer and HexDocs. Their@docstrings now point at the Runtime opts section. The Callback Runtime guide has a new Runtime opts section betweenhandle_info/2and Error Handling. The Reducer Runtime guide's existing Runtime Options table grew anintents:row with a follow-up Intents subsection that names the consumer-defined vocabulary contract and the transport-portability story. The Cell Sessions guide gained a Driving anExRatatui.Appover a CellSession section walking through the 3-tuple vs 4-tuple transport shapes, samplecell_writer_fnandintent_writer_fndefinitions, and the lifecycle the runtime drives. End-user code is unchanged; this is documentation of the existing feature.ExRatatui.CellSession— non-terminal rendering primitive — sibling ofExRatatui.Sessionfor consumers that aren't terminals (Phoenix LiveView painting<span>cells, embedded framebuffers, screenshot tools, headless tests). Backed by ratatui'sTestBackend, it exposes the cell buffer directly instead of ANSI bytes. Same widget tree, input parser, anddraw/2/resize/3/feed_input/2/close/1lifecycle asSession; the only API divergence istake_output/1being replaced bytake_cells/1(full snapshot) andtake_cells_diff/1(cells that changed since the last diff call). Cells are%{row, col, symbol, fg, bg, modifiers, skip}in row-major order, with colors and modifiers using the sameExRatatui.Stylevocabulary as the rest of the library.take_cells_diff/1returns a full payload on its first call, afterresize/3, and after reconstruction; otherwise only cells that differ structurally. Adds 9 NIFs, theExRatatui.CellSession{,.Cell,.Snapshot,.Diff}modules, a Rendering to Non-Terminal Surfaces guide, and a headlesscell_dump.exsexample.:intentsruntime opt +{:cell_session, cs, cell_writer_fn, intent_writer_fn}4-tuple transport tag —ExRatatui.Appcallbacks can now return{:ok, state, intents: [...]}frommount/1and{:noreply | :stop, state, intents: [...]}fromhandle_event/2/handle_info/2. Intents are opaque to ex_ratatui — they're forwarded verbatim to the transport'sintent_writer_fn(the optional 4th element of the cell_session transport tag) in the order they were emitted.phoenix_ex_ratatuiconsumes this to dispatch inter-page navigation ({:navigate, "/path"}→push_navigate,{:patch, "/path"}→push_patch,{:redirect, "/url"}→redirect); other consumers can define their own intent vocabulary. Transports that don't supply an intent writer (the existing 3-tuple cell_session shape, plus:local/:session/:distributed_server) silently drop intents — apps stay portable across transports. Intents from{:stop, state, intents: ...}transitions fire BEFORE the server exits, so a TUI can return{:stop, state, intents: [{:redirect, "/login"}]}and trust the redirect reaches the consumer before the linked-server EXIT propagates.{:cell_session, cell_session, cell_writer_fn}Server transport tag — ExRatatui.Server now accepts a fourth:transportshape (alongside:local,:session, and:distributed_server) that drives anExRatatui.Appagainst aCellSessioninstead of a byte-streamSession. On every render the Server callsCellSession.draw/2, thenCellSession.take_cells_diff/1, then hands the resulting%CellSession.Diff{}to the user-suppliedcell_writer_fn— same call shape as the byte-stream:sessiontransport, just a different payload type.ExRatatui.Transport.start_server/1accepts the new shape unchanged via thet:server_transport/0union; thet:cell_writer_fn/0type lives next to the existingt:writer_fn/0. Mount opts are augmented identically to the byte-stream path:opts[:transport] = :cell_session,opts[:width]/opts[:height]populated fromCellSession.size/1. Telemetry mirrors the existing taxonomy:[:ex_ratatui, :transport, :connect]and[:ex_ratatui, :session, :lifecycle, :open]fire on init withtransport: :cell_session;[:ex_ratatui, :session, :lifecycle, :close]and[:ex_ratatui, :transport, :disconnect]fire onterminate/2. Resize semantics match:sessionexactly — the transport must callCellSession.resize/3before forwarding{:ex_ratatui_resize, w, h}to the Server, then the Server updates the cached size and dispatches a%Event.Resize{}to the App; the next render's diff payload after a resize is always full (prior baseline at the old area is no longer comparable).
0.8.2 - 2026-04-29
Fixed
%Event.Resize{}now reacheshandle_event/2over byte-stream transports — when the runtime ran over a byte-stream transport (:sessionfor SSH / Kino / custom TCP,:distributed_serverfor distribution), the{:ex_ratatui_resize, w, h}message synthesized byExRatatui.Transport.ByteStreamwas intercepted by the Server's ownhandle_info/2clause: it updated the cachedwidth/heightand re-rendered, but never dispatched a%Event.Resize{}to the App. The local-tty path delivered Resize through the polling event loop, so an App that worked when run over the real terminal would silently miss resize events the moment it was supervised over SSH, distribution, or a Livebook notebook. The Server now updates its cached size first (so the follow-up render sees the new dims) and then routes the resize throughdispatch_event/2, giving the App'shandle_event/2exactly the same%Event.Resize{width: w, height: h}it would see on local tty. Apps that only had a fall-throughhandle_event(_, state)clause are unaffected; apps that explicitly track terminal size in their own state can now do so over every transport. The two transport tests (session_transport_test.exs,distributed_transport_test.exs) were updated to assert App delivery in addition to the re-render
0.8.1 - 2026-04-27
Added
- Telemetry instrumentation (#56) — new
ExRatatui.Telemetrymodule emits:telemetryevents across the runtime so apps can plug in logging, metrics, or OpenTelemetry tracing without forking the server. Five span events (:start/:stop/:exception) wrap the costly stages:[:ex_ratatui, :runtime, :init]aroundmount/1,[:ex_ratatui, :runtime, :event]around terminal-inputhandle_event/2,[:ex_ratatui, :runtime, :update]around info-messagehandle_info/2(subscriptions, async results, user messages),[:ex_ratatui, :render, :frame]around the build+draw cycle (adds:widget_countto stop metadata), and[:ex_ratatui, :transport, :connect]around local/SSH/distributed handshakes. Four single events fire for point-in-time facts:[:ex_ratatui, :session, :lifecycle, :open]when a session-backed runtime adopts a session (carries:width/:height),[:ex_ratatui, :session, :lifecycle, :close]when it releases the session (carries:reason; fires exactly once per session even when the transport's own teardown defensively closes the same ref afterwards),[:ex_ratatui, :render, :dropped]on draw errors (with a TODO placeholder for future frame-skip backpressure), and[:ex_ratatui, :transport, :disconnect]on server teardown. Every event carries:modand:transportin its metadata.ExRatatui.Telemetry.span/3is a thin helper that prefixes events with:ex_ratatuiand forwards start metadata to stop;execute/3does the same for single events and auto-adds:system_time.attach_default_logger/1/detach_default_logger/0ship a handler that logs every event at a configurable level. New Telemetry guide walks through the full event catalogue, aTelemetry.Metricsexample, and anopentelemetry_telemetrywiring snippet. Added{:telemetry, "~> 1.0"}as an explicit dependency and toextra_applicationsso the handler registry is available on every node (including peers in distributed tests) - Transport behaviour — new
ExRatatui.Transportmodule documents the protocol between the runtime server and the processes that carry I/O for a runningExRatatui.App. Declares theserver_transport,writer_fn, andto_servertypespecs, plus one optionalchild_spec/1callback and a publicstart_server/1entrypoint for custom transports to boot the runtime without depending on internal modules.ExRatatui.SSH,ExRatatui.SSH.Daemon, andExRatatui.Distributed.Listenerall adopt the behaviour. NewExRatatui.Transport.ByteStreamhelper packages the byte-pump pattern (forward_input/3,forward_resize/4) so any byte-oriented transport — SSH today, Kino/Livebook next, a custom TCP bridge — can plug in without reimplementing event dispatch or Resize absorption.ExRatatui.SSHnow delegates toByteStreaminstead of hand-rolling the loop. New Custom Transports guide walks through the contract and a working TCP example covering separation of acceptor and per-connection worker (so the listener survives across disconnects and serves concurrent clients), the alt-screen enter/leave sequences, runtime-server monitoring, and the raw-mode client requirement (stty raw -echo; nc …; stty sane) that plain TCP needs since it has no equivalent of SSH's PTY negotiation - Property-based test pass over the recently-added surface — five new files under
test/ex_ratatui/property/covering the modules introduced by the telemetry + transport-behaviour work, plus older surface that previously lacked sweeping coverage.session_input_property_test.exsprovesExRatatui.Session.feed_input/2's byte stitchability (splitting any byte stream at any boundary, including byte-by-byte, yields the same event sequence) along with shape invariants for printable bytes, bare ESC, and arrow keys.byte_stream_property_test.exscoversExRatatui.Transport.ByteStream.forward_input/3's contract: no%Event.Resize{}ever leaks as a regular event, every parsed event is delivered in order, andforward_resize/4always emits an:ex_ratatui_resizenotification.bridge_property_test.exscoversBridge.encode_commands!/1's list-level contract — length / order preservation,{map, map}shape, rect coordinate passthrough, andArgumentErroron malformed entries — across structurally simple widgets (Paragraph, Block, Clear, Gauge, Throbber).text_encode_property_test.exscoversText.Encode.to_wire_text!/to_wire_line!line/span count and content preservation, alignment serialization, and per-line agreement between the two encoders.normalize_property_test.exscoversSubscription.normalize/1andCommand.normalize/1idempotence, leaf-count preservation across nested batches, order preservation, and refusal of garbage shapes. The exhaustive per-widget validation property suite (one shape generator per widget × known-malformed shapes raisingArgumentError, ~22 widgets) is intentionally deferred — the structural invariants above are uniform across widgets, so the next pass is a per-widget effort rather than a generic one
Changed
- Internal
:sshtransport tag renamed to:session— the Server's internal transport tag always described "generic byte-stream session"; SSH just happened to be the first user. The new name makes room for other byte-stream transports (Kino, custom TCP) to share the same runtime without impersonating SSH. User-facing routing shorthand is untouched:{MyApp, transport: :ssh}still routes toSSH.Daemon. Affected surfaces:Runtime.snapshot/1.transportnow returns:sessionfor byte-stream sessions (was:ssh); telemetry metadata%{transport: _}on[:runtime, :init],[:transport, :connect], etc. is now:sessionon byte-stream events;mount(opts)[:transport]is now:sessionfor SSH apps (was:ssh). If your app branches onopts[:transport]to detect SSH specifically, switch to a different signal (you control the{MyApp, transport: :ssh}line in your own supervision tree)
0.8.0 - 2026-04-21
Added
- Chart widget — new
ExRatatui.Widgets.Chartstruct plusChart.DatasetandChart.Axiscompanions wrap ratatui'sChartfor x/y line, scatter, and bar plots with axes, labels, legend, and multi-series support. Each%Dataset{}carries a:name(shown in the legend), a list of{x, y}numeric tuples in:data, plus its own:marker(:braille/:dot/:block/:bar/:half_block),:graph_type(:line/:scatter/:bar), and:style, so a single chart can mix line and scatter overlays. The required:x_axis/:y_axisare%Axis{}structs with:bounds({min, max}numeric tuple), optional tick:labels(string /%Span{}/%Line{}),:title,:style, and:labels_alignment(:left/:center/:right).:legend_positionaccepts:top,:top_left,:top_right(default),:bottom,:bottom_left,:bottom_right,:left,:right, ornilto hide the legend entirely.:hidden_legend_constraintstakes a{width_constraint, height_constraint}pair using the same shapes asExRatatui.Layout(:length/:percentage/:ratio/:min/:max/:fill) — the legend is hidden whenever its rendered size would exceed those bounds against the chart area.:blockwraps the chart in a framedBlock. Missing axes, non-list:datasets, non-%Dataset{}entries, malformed data points, non-numeric coordinates, unknown markers, unknown:graph_types, unknown:legend_positions, malformed:bounds, malformed:hidden_legend_constraints, and unknown:labels_alignmentvalues raiseArgumentErrorat the bridge boundary. See the Chart section in Building UIs and the widget cheatsheet for examples - Canvas widget (#30) — new
ExRatatui.Widgets.Canvasstruct and six shape structs (Canvas.Line,Canvas.Rectangle,Canvas.Circle,Canvas.Points,Canvas.Map,Canvas.Label) wrap ratatui'sCanvasfor 2D plotting. Shapes live in a virtual coordinate space defined by:x_bounds/:y_bounds(both{min, max}tuples withmin <= max) and are rasterized onto cells via:marker—:braille(default),:dot,:block,:bar, or:half_block.Rectangledraws an outline anchored at its bottom-left corner,Circledraws an outline centered on{x, y},Pointsplots a list of{x, y}tuples,Mappaints the world's coastlines at:lowor:highresolution (pair with{-180, 180}×{-90, 90}bounds), andLabelwrites a styled text annotation at the given canvas-space coordinate. Every drawable shape takes a plainColor.t()rather than aStyle— canvas pixels sample individual cells, so text modifiers don't apply;Labeluses the color as the text foreground.:background_colorfills the area, and:blockwraps the canvas in a framedBlock. Non-tuple bounds, inverted bounds, unknown markers, unknown map resolutions, missing required shape fields, negativewidth/height/radius, non-stringLabel.text, malformedPoints.coordsentries, and unknown shape structs all raiseArgumentErrorat the bridge boundary. See the Canvas section in Building UIs and the widget cheatsheet for examples - Calendar widget (#31) — new
ExRatatui.Widgets.Calendarstruct renders ratatui'sMonthlycalendar.:display_dateis a native%Date{}that drives which month is shown and which day is highlighted.:eventsaccepts either a list of{%Date{}, %Style{}}tuples or a%{Date => Style}map — map entries with anilvalue are skipped so toggling individual days stays ergonomic.:show_month_headerand:show_weekdays_header(booleans, defaulttrue) toggle the two header rows, with independent:header_style/:weekday_styleoverrides. Set:show_surroundingto a%Style{}to bleed the previous/next month into empty grid cells; leave itnilto hide them.:default_stylepaints unstyled days, and:blockwraps the widget in a framedBlock. Non-Date:display_date, non-boolean header toggles, and malformed event entries raiseArgumentErrorat the bridge boundary. See the Calendar section in Building UIs and the widget cheatsheet for examples - Sparkline widget (#27) — new
ExRatatui.Widgets.Sparklinestruct renders ratatui'sSparkline, a compact single-line bar chart for streaming or time-series data.:datais a list of non-negative integers withnilentries representing missing samples; set:maxto a positive integer or leavenilto auto-scale. Choose a rendering style via:bar_set—:nine_levelsfor smooth gradients,:three_levelsfor low-density glyphs, or a non-empty list of strings that's proportionally mapped across ratatui's nine density slots. Direction (:left_to_right/:right_to_left), absent-value symbol and style, chart-wide:style, and:blockare all configurable. Floats, negative values, non-list data, unknown bar-set atoms, and empty custom lists raiseArgumentErrorat the bridge boundary. See the Sparkline section in Building UIs and the widget cheatsheet for examples - BarChart widget (#23) — new
ExRatatui.Widgets.BarChart,ExRatatui.Widgets.Bar, andExRatatui.Widgets.BarGroupstructs render ratatui'sBarChartin either:verticalor:horizontalorientation. Each%Bar{}carries a:label, non-negative integer:value, optional per-bar:stylethat overrides the chart-wide:bar_style, and an optional:text_valueto replace the numeric readout. Chart-level fields include:data(single anonymous group of bars),:groups(list of%BarGroup{}for side-by-side clusters with shared captions),:bar_width,:bar_gap,:group_gap(cells between adjacent clusters),:bar_style,:value_style,:label_style,:max(nil auto-scales to the largest value),:direction, and:block. Set either:dataor:groups. Floats, negative values, non-list:groups, non-%BarGroup{}entries, non-string group labels, and negative:group_gapraiseArgumentErrorat the bridge boundary. See the BarChart section in Building UIs and the widget cheatsheet for examples - Focus management (#48) — new
ExRatatui.Focusstruct for multi-panel apps. Declare an ordered ring of focusable IDs withFocus.new/2, route every key event throughFocus.handle_key/2, and pattern-match onFocus.current/1to dispatch. Tab / Shift+Tab /back_tabare consumed by default;:next_keys/:prev_keysaccept%Event.Key{}entries to override (:kindignored, modifiers compared as a set).Focus.focused?/2drives border styling withoutFocusever touching widget structs. Pure Elixir, no Rust changes. The new "Focus management" section in Building UIs walks through the caller pattern - Widget protocol (#24) — new
ExRatatui.Widgetprotocol lets you build composite widgets in pure Elixir by implementingrender/2on your own struct. The Bridge flattens custom widgets into primitive{widget, rect}tuples recursively (with a 32-level depth cap and argument validation) before encoding, soExRatatui.draw/2accepts primitive and custom widgets interchangeably at the top level. Custom widgets insidePopup.content/WidgetList.itemsare not supported yet. The publicwidget()type splits intoprimitive_widget()(built-ins, unchanged) andwidget()(primitive or any struct implementing the protocol); the new Custom Widgets guide walks through the API. Protocol consolidation is now limited to:prod, so test-timedefimplblocks (and your own tests of custom widgets) work without fuss - Rich text primitives (#26) — new
ExRatatui.Text.SpanandExRatatui.Text.Linestructs let text-bearing widget fields carry per-span colors and modifiers instead of a single style for the entire string.Paragraph.text,List.items,Table.rows/Table.header,Tabs.titles, andBlock.titlenow accept any mix ofString.t(),%Span{},%Line{}, or[%Span{}]. Plain strings continue to work on every field; fields that are semantically single-line (table cells, tab titles, block titles) raise on strings containing embedded newlines. The new "Rich Text" section in Building UIs walks through the API
Fixed
- TextInput cursor invisible at end of double-width input (#45) — when a
TextInputcontained CJK or other double-width characters that overflowed the widget's display width, moving the cursor to the end made it disappear. Viewport scrolling and span construction tracked positions in char counts but the widget's display width is measured in terminal cells, so wide chars consumed twice their accounted-for space and the trailing cursor span was truncated. Both the viewport adjustment and the rendered spans are now cell-aware via theunicode-widthcrate
Changed
- Documentation expanded. Five new guides ship in
guides/: Getting Started walksmix new→ supervised todo app withTextInput+List+ manual focus; State Machine Patterns covers mode-atom dispatch, screen stacks, modals viaPopup, multi-screen transitions, and sibling-GenServer escape hatches; Testing documents the headless backend,test_mode,Runtime.inject_event/2, and three assertion patterns (snapshot,test_pid,:sys.get_state); Debugging coversRuntime.snapshot/1,enable_trace/2events, buffer-inspection-as-dev-tool, common errors (terminal_init_failed, garbled output, SSH-t,mix runstdin), and Rust NIF rebuilds; Performance covers the render loop,render?: false, keepingrender/2cheap, large trees, poll-interval tuning, subscription cost, async effects, and timing with:timer.tc+ traces. The widgets cheatsheet was rewritten as task-grouped columns (Styles, Layout, Text, Lists, Progress, Input, Charts, Containers, Calendar) using{: .col-2}annotations for scan-ability. A newexamples/README.mdcatalogs all 12 examples with SSH and Erlang-distribution one-liners. The top-level README dropped its Learning Path, Testing sample, and Troubleshooting sections (all absorbed by the new guides) and trimmed the Examples table to two (hello_world+counter_app), now pointing to the examples catalog. Hex sidebar reordered: onboarding → concepts → patterns → ops → cheatsheet - Test suite expanded. New coverage: property-based invariants for
Layout.split/3, style encoding, text coercion,Focusring semantics, anddecode_event/1round-tripping key/mouse/resize tuples viastream_data; unicode and emoji rendering across CJK, combining marks, ZWJ sequences, and BMP/SMP emoji on every text-bearing widget; stress tests for 2 000-widget scenes, 1 000 redraws, and 1×1 / 500×500 terminals; cross-transport parity tests proving the sameAppmodule produces identical widget trees under local, SSH, and Erlang distribution; raw-example smoke tests forsystem_monitor(App-based) andchat_interface(rawExRatatui.run/1loop). Distributed integration now also exercises Chart, grouped BarChart, and Canvas with aMapshape to catch any future NIF-field regression across node boundaries. Coverage remains at 100% across all 55 modules - Test layout mirrors
lib/.test/ex_ratatui/widgets_test.exswas split into per-widget files undertest/ex_ratatui/widgets/, one-for-one withlib/ex_ratatui/widgets/. Cross-cutting integration tests (cross-transport parity, unicode rendering, stress, focus integration, full-stack rendering) now live undertest/integration/.server_runtime_test.exswas renamed toruntime_test.exsandtest_backend_test.exsfolded intoterminal_test.exsto match the modules they cover.server_test.exswas further split by transport: SSH and distributed unit tests now live intest/ex_ratatui/server/ssh_transport_test.exsandtest/ex_ratatui/server/distributed_transport_test.exs, each organized underdescribeblocks for lifecycle, message handling, reducer support, and helpers. Duplicated fixture apps across the three server test files were consolidated intoExRatatui.Test.ServerApps(Echo,StopOnAnyEvent,FailingMount) undertest/support/, and SSH test helpers intoExRatatui.Test.SshHelper - CI hardening. The
distributedandslowjobs merged into a singleextrasjob that runs both tag filters sequentially, saving a runner. The lint matrix now runsmix xref graph --format cycles --fail-above 0to catch dependency cycles at CI time. Doctests added toExRatatui.CommandandExRatatui.Subscriptionfor the reducer side-effect helpers - API polish.
ExRatatuimoduledoc now documents the error-handling convention: programmer errors raiseArgumentError, runtime/I/O failures return{:error, reason}.ExRatatui.Command.async/2docs now enumerate the mapper's error shapes and include an example.TextInputandTextareamoduledocs note their:statereferences are node-local NIF resources that must not be serialized, compared, or sent across distribution. TheBridgemodule is hidden from HexDocs (@moduledoc false) since it's internal to the NIF boundary - Shutdown robustness. The internal server's
terminate/2now cancels any armed subscription timers across all three transports, so pending ticks can't be delivered to a supervisor-restarted process carrying a stale mailbox
0.7.1 - 2026-04-13
Fixed
- SSH bare Esc key not detected — VTE's state machine swallows
0x1Bas the start of an escape sequence, so a bare Esc press over SSH produced no event. The SSH transport now schedules a 50 ms timeout after a lone0x1Bwith no follow-up bytes; if the timer fires it resets the parser and dispatches a synthetic%Event.Key{code: "esc"}press. Follow-up bytes (the normal case for multi-byte sequences like arrow keys) cancel the timer before it fires. AddedSession.reset_parser/1and its backingsession_reset_parserNIF - Distributed transport crashes on stateful widgets —
TextInputandTextareastore their mutable state in NIF resource references that cannot cross BEAM node boundaries via Erlang distribution. The distributed server now snapshots stateful widget state into plain tuples before sending, and the Rust decoder reconstructs temporary resources from the snapshot on the client node. Stateless widgets are unaffected. Addedtext_input_snapshotandtextarea_snapshotNIFs
0.7.0 - 2026-04-13
Added
- Reducer runtime for
ExRatatui.Appviause ExRatatui.App, runtime: :reducer. Reducer apps implementinit/1,render/2, andupdate/2, receive terminal input as{:event, event}, mailbox messages as{:info, msg}, and can declare subscriptions withsubscriptions/1 ExRatatui.Command— reducer side-effect helpers for immediate messages, delayed messages, async work, and batched command executionExRatatui.Subscription— reducer timer/subscription helpers for interval and one-shot self-messages reconciled by stableidExRatatui.Runtime— runtime inspection helpers exposing snapshots, trace events, and trace enable/disable controls for supervised TUI processesExRatatui.Runtime.inject_event/2— deterministic synthetic event injection for supervised apps undertest_mode- Example:
examples/reducer_counter_app.exs— simple reducer-driven counter showingupdate/2andsubscriptions/1
Changed
ExRatatui.Appnow supports two runtime styles: the existing callback runtime and the new reducer runtime selected withruntime: :reducer- The internal server now supports reducer runtime options for commands, render suppression, trace state, runtime snapshots, async command tracking, and subscription reconciliation
- Render-command encoding moved into a shared internal bridge, making
ExRatatui.draw/2andExRatatui.Session.draw/2share one validation and encoding path - Native render-command decoding was refactored into reusable helpers in
native/ex_ratatui/src/decode.rsand shared between local terminal rendering and session rendering - Bumped
ratatui-textareaRust dependency from 0.8 to 0.9 credodependency restricted to:devenvironment only
Fixed
- New subscriptions now store their timer reference correctly instead of keeping the
{timer_ref, token}tuple in thetimer_reffield, which broke timer cancellation/rearming paths in the reducer runtime - Async command mappers are now wrapped the same way as async functions, so mapper exceptions/exits return structured error tuples and
active_async_commandsbookkeeping is always decremented - The
mount/1callback contract now includes the supported{:ok, state, callback_opts}form, which keeps reducer-style startup shims aligned with Dialyzer and the runtime's actual behavior - Invalid reducer/runtime payloads and malformed render commands now fail earlier with clearer Elixir-side or Rust-side validation errors
- Parallel cold compile crash — the NIF bridge no longer loads its NIF via
@on_loadduring dependency compilation. Precompiled/source artifacts are still prepared at compile time, but the NIF now loads lazily on first use, which stops isolated compiler VMs from crashing under parallel cold compiles on this host test_modeinput flake — local supervised apps and distributed attach clients no longer poll the real terminal while running headless tests, which removes ambient crossterm events from async test runs and stops spurious renders like therender?: falsereducer flake- SSH
auto_host_keybootstrap — host-key generation now recreates the parent<priv_dir>/ssh/directory immediately before writing the key, so first boot succeeds even if the app's priv tree was absent or cleaned between runs
Docs
- Extracted runtime and widget content from README into dedicated guides:
guides/callback_runtime.md,guides/reducer_runtime.md, andguides/building_uis.md - Added widget cheatsheet:
guides/cheatsheets/widgets.cheatmd - README now documents the reducer runtime, reducer example app, command/subscription helpers, and runtime inspection API
- README and
ExRatatui.Appdocs now call out thatmount/1may return runtime opts and thatWidgetList.scroll_offsetis row-based with partial clipping semantics - Expanded public moduledocs for
ExRatatui.App,ExRatatui.Command,ExRatatui.Subscription, andExRatatui.Runtime - HexDocs module grouping now includes reducer-runtime modules
- README now notes that the native library loads lazily on first use
Tests
- Added reducer runtime coverage for commands, subscriptions, tracing, render suppression, async failure handling, and invalid runtime return values
- Added coverage for public
Command,Subscription,Runtime,App, and shared bridge validation paths - Elixir test coverage remains at 100%
0.6.2 - 2026-04-12
Added
- Distribution-attach transport — serve any
ExRatatui.Appto remote BEAM nodes over Erlang distribution. Newtransport: :distributedoption onExRatatui.Appand a standaloneExRatatui.Distributed.Listenerfor direct supervision-tree use. Each attaching node gets its own isolated TUI session; widget lists travel as plain BEAM terms with zero NIF on the app node ExRatatui.Distributed— main API module withattach/3for connecting to a remote TUIExRatatui.Distributed.Listener— supervisor wrapping aDynamicSupervisorfor per-attach sessions, with config stored in:persistent_term- Distributed.Client (internal) — local rendering proxy that takes over the terminal, polls events, and forwards them to the remote server
- Server (internal) learns a
transport: {:distributed_server, client_pid, width, height}init path that sends{:ex_ratatui_draw, widgets}over distribution instead of rendering locally - Guide:
guides/distributed_transport.md— architecture, quick start, options reference, testing, troubleshooting - README "Running over Erlang Distribution" section
examples/system_monitor.exsnow supports--distributedflag for running over Erlang distribution:peer-based integration tests for the full cross-node roundtrip (tagged:distributed, run withelixir --sname test -S mix test --only distributed)
Changed
test_modenow means fully headless local runtime behaviour: it disables live terminal input polling on both the server andDistributed.Client, and runtime snapshots expose whether polling is enabledServer event and resize handlers are now shared between
:sshand:distributed_servertransportsExRatatui.Appdispatch_start/1routes:distributedtoExRatatui.Distributed.Listener.start_link/1WidgetListscroll_offsetis now row-based — previouslyscroll_offsetskipped whole items by index; it now skips rows of content. To scroll to a specific item, sum the heights of all preceding items. Items partially above the viewport are clipped at the row level, enabling smooth pixel-row scrolling for chat histories and similar variable-height lists. This is a breaking change for callers that relied on the item-index interpretationMigration: If you set
scroll_offsetto an item index (e.g.,scroll_offset: selected), replace it with the cumulative row height of preceding items. For example, if all items have height 3:scroll_offset: selected * 3. For variable-height items, sum their heights:scroll_offset: items |> Enum.take(selected) |> Enum.map(&elem(&1, 1)) |> Enum.sum()
Fixed
WidgetListpartial-item clipping — items straddling the top edge of the viewport are now correctly rendered via an off-screen buffer blit instead of being skipped entirely
0.6.1 - 2026-04-09
Fixed
- SSH subsystem dispatch —
ssh host -s Elixir.MyApp.TUI(andExRatatui.SSH.subsystem/1undernerves_ssh) would hang forever instead of rendering. The channel handler was waiting for a{:ssh_cm, _, {:subsystem, ...}}message insidehandle_ssh_msg/2, but OTP:sshconsumes that request internally when it matches a name in the daemon's:subsystemsconfig — the handler only ever receives{:ssh_channel_up, ...}.ExRatatui.SSHnow detects subsystem-mode dispatch (via a newsubsystem: trueflag baked into the init args bysubsystem/1andExRatatui.SSH.Daemon) and synthesizes a default 80x24 session + starts the TUI server directly from{:ssh_channel_up, ...}. Shell-mode startup (viassh_cli) is unchanged — it still waits forpty_req+shell_reqas before - SSH subsystem +
-tpty_req races — when a client connects withssh -t -s Elixir.MyApp.TUI, OTP firesssh_channel_upfirst (we start an 80x24 session + server) and then delivers the client'spty_reqwith the real dimensions. The previouspty_reqhandler created a brand-newSessionon every call, which left the SSH channel pointing at a session the running Server no longer rendered into. The handler now splits onsession: nilvssession: %Session{}and resizes the existing session in place when one is already there, mirroring thewindow_changepath - SSH subsystem pty-size discovery on nerves_ssh — even with the pty_req race fixed, a subsystem TUI riding on
nerves_ssh(or any:ssh.daemonthat configures a default CLI handler) would stay stuck at the hardcoded 80x24 fallback instead of filling the client's real terminal. Root cause: OTP'sssh_connection:handle_cli_msg/3hands pty_req to the daemon's default CLI handler when the channel's user pid is stillundefined, and then silently orphans that CLI handler the moment the subsequent subsystem request rebinds the pid to us — so the subsystem handler never sees pty_req on those deployments, no matter how early it arrives.ExRatatui.SSHnow sidesteps the whole OTP path by emitting a Cursor Position Report roundtrip (ESC[s ESC[9999;9999H ESC[6n ESC[u) on{:ssh_channel_up, ...}: the client clamps the bogus cursor position to its real pty size, responds withESC[<row>;<col>R, the session's ANSI input parser decodes that as a%ExRatatui.Event.Resize{}, and the{:data, ...}handler resizes the session in place + notifies the running server via{:ex_ratatui_resize, w, h}. Shell-mode startup is unaffected — it still reads the dimensions straight offpty_req session_input.rsCPR parsing — the VTE-driven input parser now recognizesESC[<row>;<col>RCursor Position Report responses and emits them asNifEvent::Resize(col, row)so the SSH transport's CPR-based pty-size discovery has something to intercept. The handler runs before the simple-CSI dispatch that would otherwise silently drop anyRfinal byte- SSH subsystem startup — added a shell-vs-subsystem section to
ExRatatui.SSH's moduledoc and theguides/ssh_transport.mdguide explaining which message triggers server boot in each mode, plus a loud "always pass-t" caveat (OpenSSH doesn't allocate a PTY for subsystem invocations by default, which leaves the client's local terminal in cooked mode — keystrokes get line-buffered and echoed locally on top of the TUI)
0.6.0 - 2026-04-09
Added
- SSH transport — serve any
ExRatatui.Appto remote clients over OTP:ssh. Newtransport: :sshoption onExRatatui.Appand a standaloneExRatatui.SSH.Daemonfor direct supervision-tree use. Each connected client gets its own isolated TUI session; works as a primary daemon or as anerves_sshsubsystem viaExRatatui.SSH.subsystem/1 ExRatatui.Session— in-memory transport-agnostic terminal session (RustSharedWriter+Viewport::Fixed) withnew/2,draw/2,take_output/1,feed_input/2,resize/3,size/1, andclose/1ExRatatui.SSH—:ssh_server_channelimplementation that drives a Session per channel, parses ANSI input viavte, and handles PTY negotiation,window_change, and alt-screen lifecycleExRatatui.SSH.Daemon— GenServer wrapping:ssh.daemon/2withport/1anddaemon_ref/1introspection helpersExRatatui.SSH.Daemon:auto_host_keyoption — when set, the daemon resolves the OTP application that owns:mod, ensures<priv_dir>/ssh/exists, and generates a 2048-bit RSA host key on first boot. Subsequent boots reuse the same key. Lets Phoenix admin TUIs and similar drop the daemon straight into a supervision tree without hand-rolling host-key bootstrapExRatatui.SSH.Daemon:system_diraccepts binary paths in addition to charlists; the daemon converts them before forwarding to:ssh.daemon/2- VTE-based input parser covering arrows, function keys, SS3, CSI modifiers, Alt+letter, Ctrl+letter, and partial-sequence buffering across feeds (SSH delivers byte-at-a-time during interactive use)
- 7 new session NIFs on ExRatatui.Native:
session_new/2,session_close/1,session_draw/2,session_take_output/1,session_feed_input/2,session_resize/3,session_size/1 - Guide:
guides/ssh_transport.md— architecture, quick start,nerves_sshintegration, options reference, host-key generation, troubleshooting - Examples ship an SSH mode:
examples/system_monitor.exs --sshandexamples/task_managerviaTASK_MANAGER_SSH=1(multiple clients share one SQLite database) - README "Running Over SSH" section
- CI enforces 100% Elixir test coverage threshold (NIF modules excluded)
- Missing doctests for
ExRatatui,Event,Event.Key,Event.Mouse,Event.Resize, andSlashCommands - Callback documentation for all
ExRatatui.Appcallbacks
Changed
ExRatatui.Server learns a
transport: :local | :sshoption and an alternateinit/1path that drives an injected Session + writer function instead of the local terminalExRatatui.Appgains a:transportoption that dispatches between ExRatatui.Serverstart_link/1andExRatatui.SSH.Daemon.start_link/1
Docs
- Expanded moduledoc prose for
ExRatatui,Event, and event struct modules - Added coverage requirement note to CONTRIBUTING.md
Tests
- Bumped Elixir test coverage to 100% — added server, rendering, layout, event, and widget tests
- End-to-end SSH integration test exercising
:ssh.daemon/2+:ssh.connect/3round trip with a generated host key (mount → render bytes → keystroke roundtrip → window_change)
0.5.1 - 2026-03-25
Added
ExRatatui.Widgets.Markdown— markdown rendering widget with syntax-highlighted code blocks, powered bytui-markdown(pulldown-cmark + syntect)ExRatatui.Widgets.Textarea— multiline text editor with undo/redo, cursor movement, and Emacs-style shortcuts. Second stateful widget — state lives in Rust via ResourceArcExRatatui.Widgets.Throbber— loading spinner widget with 12 animation sets (braille, dots, ascii, arrow, clock, and more)ExRatatui.Widgets.Popup— centered modal overlay widget for dialogs, confirmations, and command palettesExRatatui.Widgets.WidgetList— vertical list of heterogeneous widgets with selection and scrolling, ideal for chat message historiesExRatatui.Widgets.SlashCommands— slash command parsing, matching, and autocomplete popup rendering- Textarea NIF functions:
textarea_new/0,textarea_handle_key/3,textarea_get_value/1,textarea_set_value/2,textarea_cursor/1,textarea_line_count/1 - Example:
chat_interface.exs— AI chat interface demonstrating Markdown, Textarea, Throbber, Popup, WidgetList, and SlashCommands
Fixed
- Replaced deprecated
Padding::zero()withPadding::ZEROin Rust widget renderers - Wired up unused
stylefield inWidgetListrender function (was#[allow(dead_code)]) - Fixed flaky Rust throbber step test —
calc_step(0)uses random index, now tests with deterministic non-zero steps - Throbber animation set test now covers all 12 sets (was 7)
0.5.0 - 2026-03-22
Added
ExRatatui.Widgets.Tabs— a tab bar widget for switching between views, with customizable selection highlight, divider, and paddingExRatatui.Widgets.Scrollbar— a scrollbar widget for indicating scroll position, supporting all four orientations (vertical right/left, horizontal bottom/top)ExRatatui.Widgets.LineGauge— a thin single-line progress bar using line-drawing characters, with separate filled/unfilled stylesExRatatui.Widgets.Checkbox— a checkbox widget for boolean toggles, with customizable checked/unchecked symbols and stylesExRatatui.Widgets.TextInput— a single-line text input widget with cursor navigation, viewport scrolling, and placeholder support. First stateful widget — state lives in Rust via ResourceArc- Example:
widget_showcase.exs— interactive demo with tabs, progress bars, checkboxes, text input, scrollable logs, and scrollbar (replaces individualtabs_demo.exs,scrollbar_demo.exs,line_gauge_demo.exs) - Doctests for
Tabs,Scrollbar,LineGauge, andCheckboxstruct modules - Updated
task_manager.exsexample to useTabs(header),LineGauge(progress),Scrollbar(task table), andTextInput(new task creation — replaces hand-rolled input buffer with proper cursor navigation, viewport scrolling, and placeholder support) - Updated
examples/task_manager/App to useTabs(filter bar with Tab/Shift+Tab navigation),LineGauge(replacesGauge),Scrollbar(task table), andTextInput(replaces hand-rolled input buffer) - Comprehensive
TextInputstate management tests: get/set value, cursor positioning, backspace, delete, left/right, home/end, and mid-text insertion
Fixed
Checkboxmoduledoc now correctly states that:checked_symboland:unchecked_symboldefault tonil(rendered as"[x]"/"[ ]"by the Rust backend)- Removed redundant
.padding()call in Rust tabs renderer that was always overwritten
0.4.2 - 2026-03-06
Added
ExRatatui.Widgets.Clear— a widget that resets all cells in its area to empty (space) characters, useful for rendering overlays
Fixed
- Put back
Elixir.prefix fromListcalls intask_manager.exsexample
0.4.1 - 2026-02-23
Fixed
init_terminalNIF now cleans up raw mode and alternate screen on partial initialization failure- All I/O-bound NIFs (
init_terminal,restore_terminal,draw_frame,terminal_size) now run on the DirtyIo scheduler to avoid blocking normal BEAM schedulers App.render/2callback typespec narrowed fromterm()toExRatatui.widget()for proper Dialyzer coverageConstraint::Ratiowith denominator zero now returns an error instead of panickingGaugeratio now validates the value is finite, preventing a panic on NaN inputApp.mount/1callback typespec now includes{:error, reason}returnExRatatui.run/1afterblock no longer masks the original exception if terminal restore also fails- Server render errors now log the full stacktrace for easier debugging
- Added missing
@impl trueon fallbackterminate/2clause in the server ExRatatui.Framestruct defaults towidth: 0, height: 0instead ofnil(typespec now matches actual usage)- Deduplicated
encode_constraint/1—ExRatatui.Layoutis now the single source of truth - Fixed flaky
poll_eventtests that failed when terminal events arrived during the test run Event.Mousetypespec fields are now non-nullable to match actual NIF output- Fixed
system_monitor.exsto cache hostname between refreshes withMap.get_lazy/3 - Removed unnecessary
Elixir.prefix fromListcalls intask_manager.exsexample - Added server tests for
{:stop, state}fromhandle_info/2andterminate/2callback
Docs
- HexDocs "View Source" links now point to the correct version tag
- Expanded
ExRatatuimoduledoc with quick start, core API overview, and cross-references - README demo GIF now uses an absolute URL so it renders on Hex.pm
- README modifiers list now shows all six supported modifiers
- Documented
:test_modeoption inExRatatui.Appfor headless testing - Clarified
system_monitor.exsis Linux/Nerves only in README
0.4.0 - 2026-02-23
Changed
- BREAKING: Terminal state is now per-process via Rust ResourceArc instead of a global mutex
ExRatatui.run/1closure now receives the terminal reference (1-arity)draw/1is nowExRatatui.draw/2(terminal reference as first argument)ExRatatui.init_test_terminal/2returns a terminal reference instead of:okget_buffer_content/0is nowExRatatui.get_buffer_content/1ExRatatui.Appbehaviour users: no API changes
- Terminal is automatically restored when the terminal reference is garbage collected (crash safety)
- Test terminal instances are now independent, enabling
async: truefor rendering tests
Added
- Comprehensive API documentation: all key codes, mouse events, colors, modifiers, and App options
- Doctests for Layout, Style, Frame, all widgets, and test backend
- CONTRIBUTING.md with development setup
0.3.0 - 2026-02-23
Added
- Typespecs (
@type t) for all widget, event, and frame structs - Function specs (
@spec) for all public API functions - Dialyzer static analysis in CI
Changed
- Extracted
Event.Key,Event.Mouse,Event.ResizeandLayout.Rectinto their own files
Fixed
- Server
start_link/1now supportsname: nilto start without process registration - App-based TUI processes hanging on macOS — the event poll loop now delegates the timeout to the NIF on the DirtyIo scheduler instead of using
Process.send_after/3, which was causing the GenServer to stop processing messages
0.2.0 - 2026-02-21
Changed
- Simplified release workflow by using
rustler-precompiled-actioninstead of manual build and packaging steps
Added
- Precompiled NIF target for
riscv64gc-unknown-linux-gnu(Nerves RISC-V boards) - System monitor example (
examples/system_monitor.exs) for running on Nerves devices via SSH
0.1.1 - 2026-02-19
Changed
- Improved HexDocs module grouping: Frame moved under Layout, App under new Application group
- Added demo GIF to README
Fixed
- Changelog formatting for ex_doc compatibility
0.1.0 - 2026-02-19
Added
- Widgets: Paragraph (with alignment, wrapping, scrolling), Block (borders, titles, padding), List (selectable with highlight), Table (headers, rows, column constraints), and Gauge (progress bar)
- Layout engine: Constraint-based area splitting via
ExRatatui.Layout.split/3with support for:percentage,:length,:min,:max, and:ratioconstraints - Event polling: Non-blocking keyboard, mouse, and resize event handling on BEAM's DirtyIo scheduler
- Styling system: Named colors, RGB (
{:rgb, r, g, b}), 256-color indexed ({:indexed, n}), and text modifiers (bold, italic, underlined, dim, crossed out, etc.) - Terminal lifecycle:
ExRatatui.run/1for automatic terminal init and cleanup - OTP App behaviour:
ExRatatui.Appwith LiveView-inspired callbacks (mount/1,render/2,handle_event/2,handle_info/2) for building supervised TUI applications - GenServer runtime: manages terminal lifecycle, self-scheduling event polling, and callback dispatch under OTP supervision
- Frame struct:
ExRatatui.Framecarries terminal dimensions torender/2callbacks - Test backend: Headless
TestBackendviainit_test_terminal/2andget_buffer_content/0for CI-friendly rendering verification - Precompiled NIFs: Via
rustler_precompiledfor Linux, macOS, and Windows (x86_64 and aarch64) — no Rust toolchain required - Examples:
hello_world.exs(minimal display),counter.exs(interactive key events),counter_app.exs(App-based counter),task_manager.exs(full app with all widgets), andexamples/task_manager/(supervised Ecto + SQLite CRUD app)