v0.9.0
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. Thec:ExRatatui.App.handle_event/2andc:ExRatatui.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).