Skip to content

v0.10.1

Choose a tag to compare

@github-actions github-actions released this 01 Jun 14:09
· 52 commits to main since this release
v0.10.1
b1e581d

Added

  • ExRatatui.Theme — pure-data palette struct (Layer A theming). Eleven semantic slots (:primary, :accent, :border, :border_focused, :surface, :surface_alt, :text, :text_dim, :success, :warning, :danger) each accepting the full t:ExRatatui.Style.color/0 shape (named atoms, {:rgb, r, g, b}, {:indexed, n}, or nil). Two starter constructors ship: default/0 (terminal-respecting; surface: nil so the same theme works on light and dark terminals) and light/0 (dark text on white surface). Three composition helpers cover the common patterns: border_style(theme, focused: true|false), text_style(theme, dim: true|false), selection_style(theme). Apps thread the theme through render code by hand — no Application.get_env magic, no automatic widget injection. Layer B (opt-in per-widget defaults at render time) is intentionally deferred.

  • Bracketed paste (%ExRatatui.Event.Paste{content: binary}). ExRatatui.run/2 enables crossterm's EnableBracketedPaste on init and disables it on restore. The local terminal's poll_event/1 decodes Event::Paste(String) into a new %Event.Paste{} struct so pasted multi-line / multi-byte content arrives as one event instead of being shredded across individual keystrokes. Two batch-insert NIFs consume the payload in one shot: ExRatatui.text_input_insert_str/2 (single-line — strips every control char) and ExRatatui.textarea_insert_str/2 (multi-line — \n and \r\n become real new lines via ratatui_textarea::insert_str; lone \r is dropped). Pasting a 5,000-character URL is now one NIF call rather than 5,000. Best-effort: terminals without bracketed paste ignore the request and the per-char keystream still arrives — apps don't need a conditional path. Out of scope this release: bracketed paste decode on the byte-stream input parser used by :session / :ssh / :distributed transports — their custom VTE state machine doesn't decode CSI 200~/201~ markers yet, so apps on those transports construct %Event.Paste{} directly and feed it into their event pipeline.

  • %ExRatatui.Event.FocusGained{} / %ExRatatui.Event.FocusLost{} — terminal-window focus reporting. Opt-in via ExRatatui.run(fun, focus_events: true). When enabled, crossterm emits focus events as the parent emulator window gains or loses focus; apps use them to pause expensive animations / background ticks while the user is elsewhere. Two separate payload-less structs (matching crossterm's variant shape, and avoiding any clash with the existing ExRatatui.Focus module). Off by default: enabling focus reporting leaves CSI ?1004h on the user's tty, and any window-switch queues focus bytes that leak back into unrelated stdin consumers (a plain shell or mix test started later) as ^[[I / ^[[O sequences. Same upstream caveat as bracketed paste: only the local terminal decodes these; byte-stream transports would need VTE-parser work.

  • Local-terminal mouse capture is now opt-in via ExRatatui.run(fun, mouse_capture: true). When enabled, crossterm's EnableMouseCapture reports clicks / scroll / drag / move as %Event.Mouse{} from poll_event/1. Off by default because mouse-reporting mode captures the terminal's native text selection — the user can no longer select and copy text the usual way until the app exits. ExRatatui.Server.start_link (and therefore every ExRatatui.App) accepts the same :mouse_capture and :focus_events opts for the :local transport. SSH and Distributed transports decode mouse sequences unconditionally because their VTE-based input parser handles them regardless of this flag — Focus.handle_mouse/2 has always worked there.

  • ExRatatui.Focus gains mouse routing. New fields and functions extend the existing keyboard focus ring with a regions: %{id => Rect} map and handle_mouse/2. Apps register hit-test regions after computing layout (typically inside a %Event.Resize{} handler) via Focus.set_region/3 or Focus.set_regions/2; Focus.handle_mouse/2 mirrors handle_key/2 shape — on a left-button-down inside a registered region, focus moves to that ID and the event is passed through so the underlying widget can still react (toggle a checkbox, place a cursor, start a drag). Every other mouse kind (right/middle click, scroll, drag, move, up) is pass-through with focus untouched. Focus.at/3 exposes the same hit-test for apps that prefer "scroll the widget under the cursor" over "scroll the focused widget". Overlapping regions resolve to the smallest by area (leaf-inside-container picks the leaf). Boundaries are half-open (x >= rx and x < rx + w) — natural for ratatui rect semantics.

  • %ExRatatui.Widgets.Block.Title{} + multi-title support on Block. Four new Block fields close the most-asked ergonomics gap: :titles (list of additional titles, each a %Block.Title{content, position, alignment, style} struct OR a raw line-like value that inherits the block defaults), :title_position (:top default, :bottom), :title_alignment (:left default, :center, :right), :title_style (default %Style{} for any title without its own). Backward compatible — the existing :title field stays a single-title shortcut at the block's default position and alignment. The classic "filename | scroll %" header is title: "filename", titles: [%Block.Title{content: "[3/12]", alignment: :right}]. All title fields validated at Bridge encode with ArgumentError; entries in :titles that are neither %Block.Title{} nor line-like are rejected with a descriptive message.

  • Eight additional Block border types. Beyond :plain / :rounded / :double / :thick, the widget now accepts :light_double_dashed, :heavy_double_dashed, :light_triple_dashed, :heavy_triple_dashed, :light_quadruple_dashed, :heavy_quadruple_dashed (the border line broken into 2/3/4 dash segments) and :quadrant_inside / :quadrant_outside (blocky half-cell borders drawn inside or outside the content area). Closes the upstream BorderType surface — every ratatui variant is now reachable.

  • Layout :margin / :horizontal_margin / :vertical_margin opts. ExRatatui.Layout.split/4 can now inset the area before splitting — margin: 1 leaves a 1-cell border on all four sides; :horizontal_margin / :vertical_margin override per-axis. Complements the existing :spacing (gaps between segments). Validated from Elixir like the other opts.

  • Style :underline_color. %ExRatatui.Style{} gains an :underline_color field — the color of the underline drawn by the :underlined modifier, distinct from :fg. nil (default) uses the foreground color. Honoured by terminals with colored-underline support (kitty, wezterm); others fall back to a plain underline.

  • ExRatatui.Layout.Padding ergonomic constructors. uniform/1, symmetric/2 (horizontal, vertical), horizontal/1, vertical/1, and new/4 return the {left, right, top, bottom} tuple that Block's :padding field already accepts — %Block{padding: Padding.uniform(1)} instead of padding: {1, 1, 1, 1}. Pure functions, no struct, no runtime cost beyond building the tuple.

  • ExRatatui.set_terminal_title/1. Sets the terminal window / tab title via OSC 0/2 (crossterm SetTitle). Useful for daemon TUIs, dashboards, and multi-tab terminals. Best-effort: terminals that don't honour the escape ignore it. One new NIF set_terminal_title/1.

  • Layout Flex modes + Constraint::Fill + segment spacing. ExRatatui.Layout.split/4 adds two keyword opts: :flex (one of :legacy, :start, :end, :center, :space_between, :space_around) and :spacing (non-negative cells inserted between every pair of adjacent segments). New constraint shape {:fill, weight} distributes the leftover space after higher-priority constraints (Length / Percentage / Ratio / Min / Max) are satisfied — {:fill, 1} + {:fill, 2} splits the remainder 1:2. Unlocks centered popups (flex: :center), end-aligned status bars (flex: :end), evenly-spaced toolbars (flex: :space_between), growable dashboard panels ({:fill, 1} / {:fill, 2}), and gutters between segments (spacing: 2). Backward compatible: existing Layout.split/3 calls land via default opts. Both opts raise ArgumentError from Elixir on invalid values.

  • List: :direction, :scroll_padding, :repeat_highlight_symbol. direction: :bottom_to_top paints the first item at the bottom of the area and grows upward — natural for chat logs, REPL history, and event streams where the newest entry pins to the bottom edge. scroll_padding keeps the selected item at least N rows from the viewport edge when the list auto-scrolls (same idea as vim's scrolloff). repeat_highlight_symbol: true repeats the highlight symbol on every wrapped row of a multi-line item instead of only the first. All three validated at Bridge encode.

  • Table: :footer, :header_style, :footer_style, :column_highlight_style, :cell_highlight_style, :selected_column, :highlight_spacing. Footer renders at the bottom of the area; header / footer styles override the table's :style for those rows. column_highlight_style / cell_highlight_style paint the selected column / cell (intersection of selected row + column) — useful for spreadsheet-style navigation. :selected_column is the gating field for the column / cell styles: without it the styles never fire (ratatui's TableState::select_column/1 is the activation mechanism). Validated like :selected against the widest of widths / header / first-row column count. highlight_spacing (:always, :when_selected default — matches ratatui, :never) controls when the symbol column is reserved; :always pins column positions even when nothing is selected so the row doesn't shift on select.

  • Three new examples covering the round-1 surface end-to-end:

    • examples/chat_log.exs%List{direction: :bottom_to_top} history pinned to the bottom edge, multi-line %Textarea{} composer with bracketed paste support via textarea_insert_str/2, multi-title %Block{} header with right-aligned unread count, Tab-cycled focus between log and composer.
    • examples/data_table.exs — every new Table field on one screen: footer, header / footer styles, column / cell highlight styles wired to a live :selected_column, and :highlight_spacing: :always. Arrow keys move the row + column cursor.
    • examples/theme_picker.exs — visual reference for every ExRatatui.Theme slot plus a live preview rendering border_style/2 (focused + unfocused), text_style/2 (default + dim), and selection_style/1 in real widgets. Press 1/2/3 to switch between default/0, light/0, and a custom Nord-ish theme.
  • Numeric input validation pass on every bounded widget. Gauge and LineGauge raise ArgumentError when :ratio is outside 0.0..1.0 or not a number (integers 0 and 1 still coerce to floats — the existing "clamped automatically" moduledoc note was a lie and now matches behavior). List, Table, Tabs, WidgetList reject :selected values that are not nil or an integer in 0..length(items) - 1; empty collections get a distinct error message ("expected nil (collection is empty)"). Failures surface from Elixir with widget.field context strings instead of a downstream Rust panic. Sparkline and BarChart numeric validation was already in place — this pass closed the remaining gaps.

  • test/ex_ratatui/property/widget_render_property_test.exs — per-widget property invariants. Two ExUnitProperties properties stress every stateless widget type the library ships (24 of 29 total): Bridge.encode_commands!/1 accepts arbitrary valid inputs at any rect, and CellSession.draw + take_cells produces a snapshot with exactly width × height cells. Covers Paragraph, Block, Clear, List, Table, Gauge, LineGauge, Tabs, Sparkline, BarChart, Throbber, Scrollbar, Checkbox, Calendar, Markdown, BigText, plus the stateful TextInput / Textarea / Image and composite Popup / WidgetList / Canvas / CodeBlock. Chart is intentionally excluded from the render property — upstream ratatui panics on multiple Chart input combinations (narrow axis bounds, empty datasets, :bar graph_type with sparse data, small rects after block borders) and proving "Chart never panics" isn't a guarantee the rendering layer can give. The encode property still covers Chart at any rect.

  • guides/transports.md — canonical cross-transport feature matrix. Five-row transport table introduces each entry point (Local, Session, SSH, Distributed, CellSession); 14-row feature matrix covers widget rendering, every Event.* variant, image protocols + auto-probe, OSC 52 clipboard, intents, runtime opts, and telemetry — with three states (✓ supported, — not applicable, ✗ known gap) and inline notes on every gap row. Replaces the implicit assumption that the per-transport guides spell this out; they don't, and now they don't have to.

  • guides/paste_and_clipboard.md. Walks through bracketed paste behaviour, the two insert_str helpers, the SSH / distributed transport caveat, and ships a short OSC 52 snippet for apps that want clipboard copy without bundling another module. OSC 52 is not built into the library — too many opinions in the ecosystem (system clipboard via arboard, OSC 52, route through a Phoenix LiveView intent). The guide is the canonical place to discover the snippet.

Changed

  • Native.init_terminal arity bumped to 2(focus_events :: bool, mouse_capture :: bool). ExRatatui.run/1 becomes ExRatatui.run/2 accepting :focus_events and :mouse_capture opts; run/2 with no opts is equivalent to the old run/1. ExRatatui.Server.start_link (and every ExRatatui.App) accepts the same two opts for the :local transport. The init_terminal/0 form is gone — direct NIF callers (custom transports, examples) must pass both booleans explicitly.

  • [:ex_ratatui, :transport, :connect] span metadata gains :focus_events and :mouse_capture for the :local transport, reflecting the boolean opts the app started with. Other transports' connect metadata is unchanged.

  • ExRatatui.Widgets.Block defaults. defstruct now includes :titles, :title_position, :title_alignment, :title_style with backward-compatible defaults ([], :top, :left, nil). Apps constructing blocks with %Block{...} and only the old fields are unaffected; the doctest in Block.moduledoc shows the new shape.

  • ExRatatui.Widgets.List defaults. defstruct adds :direction (:top_to_bottom), :scroll_padding (0), :repeat_highlight_symbol (false). Matches ratatui's defaults.

  • ExRatatui.Widgets.Table defaults. defstruct adds :footer (nil), :column_highlight_style (nil), :cell_highlight_style (nil), :header_style (nil), :footer_style (nil), :highlight_spacing (:when_selected — matches ratatui).

  • Rust-side BlockData, ListData, TableData gain Default impls. Test fixtures and Rust callers (one per widget that composes a Block) shrink to ..Default::default() — same backward-compat guarantee inside the native code as on the Elixir side.