You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Chart widget — new ExRatatui.Widgets.Chart struct plus Chart.Dataset and Chart.Axis companions wrap ratatui's Chart for 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_axis are %Axis{} structs with :bounds ({min, max} numeric tuple), optional tick :labels (string / %Span{} / %Line{}), :title, :style, and :labels_alignment (:left / :center / :right). :legend_position accepts :top, :top_left, :top_right (default), :bottom, :bottom_left, :bottom_right, :left, :right, or nil to hide the legend entirely. :hidden_legend_constraints takes a {width_constraint, height_constraint} pair using the same shapes as ExRatatui.Layout (:length / :percentage / :ratio / :min / :max / :fill) — the legend is hidden whenever its rendered size would exceed those bounds against the chart area. :block wraps the chart in a framed Block. 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_alignment values raise ArgumentError at the bridge boundary. See the Chart section in Building UIs and the widget cheatsheet for examples
Canvas widget (#30) — new ExRatatui.Widgets.Canvas struct and six shape structs (Canvas.Line, Canvas.Rectangle, Canvas.Circle, Canvas.Points, Canvas.Map, Canvas.Label) wrap ratatui's Canvas for 2D plotting. Shapes live in a virtual coordinate space defined by :x_bounds / :y_bounds (both {min, max} tuples with min <= max) and are rasterized onto cells via :marker — :braille (default), :dot, :block, :bar, or :half_block. Rectangle draws an outline anchored at its bottom-left corner, Circle draws an outline centered on {x, y}, Points plots a list of {x, y} tuples, Map paints the world's coastlines at :low or :high resolution (pair with {-180, 180} × {-90, 90} bounds), and Label writes a styled text annotation at the given canvas-space coordinate. Every drawable shape takes a plain Color.t() rather than a Style — canvas pixels sample individual cells, so text modifiers don't apply; Label uses the color as the text foreground. :background_color fills the area, and :block wraps the canvas in a framed Block. Non-tuple bounds, inverted bounds, unknown markers, unknown map resolutions, missing required shape fields, negative width / height / radius, non-string Label.text, malformed Points.coords entries, and unknown shape structs all raise ArgumentError at the bridge boundary. See the Canvas section in Building UIs and the widget cheatsheet for examples
Calendar widget (#31) — new ExRatatui.Widgets.Calendar struct renders ratatui's Monthly calendar. :display_date is a native %Date{} that drives which month is shown and which day is highlighted. :events accepts either a list of {%Date{}, %Style{}} tuples or a %{Date => Style} map — map entries with a nil value are skipped so toggling individual days stays ergonomic. :show_month_header and :show_weekdays_header (booleans, default true) toggle the two header rows, with independent :header_style / :weekday_style overrides. Set :show_surrounding to a %Style{} to bleed the previous/next month into empty grid cells; leave it nil to hide them. :default_style paints unstyled days, and :block wraps the widget in a framed Block. Non-Date:display_date, non-boolean header toggles, and malformed event entries raise ArgumentError at the bridge boundary. See the Calendar section in Building UIs and the widget cheatsheet for examples
Sparkline widget (#27) — new ExRatatui.Widgets.Sparkline struct renders ratatui's Sparkline, a compact single-line bar chart for streaming or time-series data. :data is a list of non-negative integers with nil entries representing missing samples; set :max to a positive integer or leave nil to auto-scale. Choose a rendering style via :bar_set — :nine_levels for smooth gradients, :three_levels for 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 :block are all configurable. Floats, negative values, non-list data, unknown bar-set atoms, and empty custom lists raise ArgumentError at 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, and ExRatatui.Widgets.BarGroup structs render ratatui's BarChart in either :vertical or :horizontal orientation. Each %Bar{} carries a :label, non-negative integer :value, optional per-bar :style that overrides the chart-wide :bar_style, and an optional :text_value to 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 :data or :groups. Floats, negative values, non-list :groups, non-%BarGroup{} entries, non-string group labels, and negative :group_gap raise ArgumentError at the bridge boundary. See the BarChart section in Building UIs and the widget cheatsheet for examples
Focus management (#48) — new ExRatatui.Focus struct for multi-panel apps. Declare an ordered ring of focusable IDs with Focus.new/2, route every key event through Focus.handle_key/2, and pattern-match on Focus.current/1 to dispatch. Tab / Shift+Tab / back_tab are consumed by default; :next_keys / :prev_keys accept %Event.Key{} entries to override (:kind ignored, modifiers compared as a set). Focus.focused?/2 drives border styling without Focus ever 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.Widget protocol lets you build composite widgets in pure Elixir by implementing render/2 on 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, so ExRatatui.draw/2 accepts primitive and custom widgets interchangeably at the top level. Custom widgets inside Popup.content / WidgetList.items are not supported yet. The public widget() type splits into primitive_widget() (built-ins, unchanged) and widget() (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-time defimpl blocks (and your own tests of custom widgets) work without fuss
Rich text primitives (#26) — new ExRatatui.Text.Span and ExRatatui.Text.Line structs 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, and Block.title now accept any mix of String.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 TextInput contained 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 the unicode-width crate
Changed
Documentation expanded. Five new guides ship in guides/: Getting Started walks mix new → supervised todo app with TextInput + List + manual focus; State Machine Patterns covers mode-atom dispatch, screen stacks, modals via Popup, 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 covers Runtime.snapshot/1, enable_trace/2 events, buffer-inspection-as-dev-tool, common errors (terminal_init_failed, garbled output, SSH -t, mix run stdin), and Rust NIF rebuilds; Performance covers the render loop, render?: false, keeping render/2 cheap, 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 new examples/README.md catalogs 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, Focus ring semantics, and decode_event/1 round-tripping key/mouse/resize tuples via stream_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 same App module produces identical widget trees under local, SSH, and Erlang distribution; raw-example smoke tests for system_monitor (App-based) and chat_interface (raw ExRatatui.run/1 loop). Distributed integration now also exercises Chart, grouped BarChart, and Canvas with a Map shape 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.exs was split into per-widget files under test/ex_ratatui/widgets/, one-for-one with lib/ex_ratatui/widgets/. Cross-cutting integration tests (cross-transport parity, unicode rendering, stress, focus integration, full-stack rendering) now live under test/integration/. server_runtime_test.exs was renamed to runtime_test.exs and test_backend_test.exs folded into terminal_test.exs to match the modules they cover. server_test.exs was further split by transport: SSH and distributed unit tests now live in test/ex_ratatui/server/ssh_transport_test.exs and test/ex_ratatui/server/distributed_transport_test.exs, each organized under describe blocks for lifecycle, message handling, reducer support, and helpers. Duplicated fixture apps across the three server test files were consolidated into ExRatatui.Test.ServerApps (Echo, StopOnAnyEvent, FailingMount) under test/support/, and SSH test helpers into ExRatatui.Test.SshHelper
CI hardening. The distributed and slow jobs merged into a single extras job that runs both tag filters sequentially, saving a runner. The lint matrix now runs mix xref graph --format cycles --fail-above 0 to catch dependency cycles at CI time. Doctests added to ExRatatui.Command and ExRatatui.Subscription for the reducer side-effect helpers
API polish.ExRatatui moduledoc now documents the error-handling convention: programmer errors raise ArgumentError, runtime/I/O failures return {:error, reason}. ExRatatui.Command.async/2 docs now enumerate the mapper's error shapes and include an example. TextInput and Textarea moduledocs note their :state references are node-local NIF resources that must not be serialized, compared, or sent across distribution. The Bridge module is hidden from HexDocs (@moduledoc false) since it's internal to the NIF boundary
Shutdown robustness. The internal server's terminate/2 now 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