Changelog
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
Added
Changed
Fixed
Removed
0.6.2 - 2026-01-27
Added
-
Router
actionDSL Enhancements: Multiple syntax forms for declaring actions with inline keybindings: -
Positional:
action :quit, -> { Command.exit }(original) -
Keyword:
action quit: -> { Command.exit }(cleaner) -
Inline keymap:
action quit: -> { Command.exit }, keymap: %i[ctrl_c q] -
key:singular alias:action go_home: FileList, key: :~ -
keys:plural alias:action move_down: FileList, keys: %i[down j] -
Anonymous:
action -> { Command.exit }, keymap: %i[ctrl_c q](no name, just binding) -
mousemap:for scroll bindings:action scroll_up: ..., mousemap: %i[scroll_up] -
Routed Actions: Actions can now target child fragments. When the value is a
Module, the Router synthesizes aMessage::Routedand dispatches to the child’s Update: -
action move_down: FileList— declaresFileListas target -
Key presses trigger
Message::Routed.new(envelope: :move_down, event: key_event) -
Message::Routed: New message type for routed actions between parent and child fragments:
-
deconstruct_keysreturns{ type: :routed, envelope:, event: } -
Predicate methods via
method_missing:msg.move_down?,msg.go_back? -
Carries original event for full context in child Update
-
Keymap
keyDSL Enhancements: Multiple syntax forms for declaring key handlers: -
Variadic keys:
key :down, :j, action: :move_down(multiple keys, one action) -
action:keyword:key :q, action: :quit(explicit action reference) -
Keyword syntax:
key q: -> { Command.exit }(hash-style) -
Multi-keyword:
keys ctrl_c: -> { ... }, q: -> { ... }(multiple bindings) -
Hash metaprogramming:
key(exit_bindings)whereexit_bindings = { q: ..., esc: ... } -
keysalias:alias_method :keys, :keyfor readability
Changed
Fixed
Removed
0.6.1 - 2026-01-26
Added
Changed
Fixed
Removed
0.6.0 - 2026-01-25
Added
-
rooibos CLI: New command-line interface installed as executable when you install the gem. Provides
rooibos new APP_NAMEto scaffold a completeRooibosapplication usingbundle gemconventions, androoibos runto launch the application. The scaffolded app includes a working TUI that displays “Hello,Rooibos!” and exits on q or Ctrl+C, plus a passing test demonstratingRooibos::TestHelperpatterns. -
Rooibos::Welcome: Built-in welcome screen fragment available viarequire "rooibos/welcome". ProvidesModel,View,Update, andInitconstants that scaffolded apps delegate to. Features keyboard Tab/Shift+Tab focus cycling, mouse hover states, and clickable buttons for visiting the website or exiting. Use as a starting point or reference for building your own fragments. -
Command::Custom#deconstruct_keys: Default pattern matching support for custom commands. Introspects public query methods and returns a hash with
:typeas a snake_case discriminator. Data.define members are included automatically. Respects thekeysargument for performance optimization. Override for hot paths or metaprogrammed methods. -
Rooibos::TestHelper#assert_no_errors: Test assertion to fail fast whenMessage::Erroris unexpectedly present in collected messages. Works with Minitest (viaflunk) and RSpec (viaraise). Include viainclude Rooibos::TestHelper. -
Message::Error: New message type for command errors. Includes
error?predicate anddeconstruct_keysfor pattern matching with{ type: :error, command:, exception: }. -
Message::Canceled: New message type for canceled commands. Includes
canceled?predicate (withcancelled?alias for British spelling) anddeconstruct_keysfor pattern matching with{ type: :canceled, command: }. Custom command authors should emit this whentoken.canceled?is true. -
Message Symbol Comparison: All
Message::*types now support symbol comparison viato_symand==, similar to RatatuiRuby events. Symbols use themessage_prefix to avoid collision with event types:msg == :message_timer,msg == :message_http,msg == :message_error.Message::Predicatesalso provides a smart-defaultdeconstruct_keysthat derives:typefrom the class name. -
Rooibos::Message.===for case/when dispatch: TheRooibos::Messagemodule now implements===for use in case/when statements. Matches only built-in framework message types (classes underRooibos::Message::), rejecting key events and user-defined message classes. Enables Update functions to distinguish framework responses from user input. -
Command.open: Opens a file or URL with the system’s default application. Cross-platform: uses
openon macOS,xdg-openon Linux,starton Windows. SendsMessage::Openon success (exit 0) orMessage::Erroron failure.
Changed
-
BREAKING:
Rooibos::TestHelperInclude Pattern:Rooibos::TestHelpernow includesRatatuiRuby::TestHelperinstead of the other way around. Previously, requiringrooibos/test_helperwould injectRooibosassertions intoRatatuiRuby::TestHelper. Now, useinclude Rooibos::TestHelperto get bothRooibosassertions and RatatuiRuby test terminal helpers. Update your test classes frominclude RatatuiRuby::TestHelpertoinclude Rooibos::TestHelper. -
BREAKING:
Rooibos.delegateMessage Format:Rooibos.delegatenow passesmessage[1](single value) to child UPDATEs instead ofmessage[1..](array slice). This aligns child fragments with the universal{ type:, envelope: }pattern. Update pattern matches fromin [{ type: :system, ... }]toin { type: :system, ... }. -
BREAKING: Command::Error → Message::Error: Moved
Command::ErrortoMessage::Error. Commands flow out from Update; Messages flow in to Update. Error was always sent to Update so it belongs in the Message module. Includeserror?predicate anddeconstruct_keysfor pattern matching with{ type: :error, command:, exception: }. Update pattern matches fromCommand::ErrortoMessage::Error. -
BREAKING: Timer/Batch Cancellation → Message::Canceled: When
Command.wait,Command.tick,Command.all, orCommand.batchare canceled, they now sendMessage::Canceledinstead ofCommand.cancel(self). Custom command authors should do the same: whentoken.canceled?, emitMessage::Canceled.new(command: self). Update pattern matches fromin Command::Canceltoin Message::Canceledorin { type: :canceled, command: }. -
Dependency Update: Now requires
ratatui_ruby ~> 1.2.0(was~> 1.0.0.beta.3). This stable release adds inline sync mode for deterministic event ordering in tests.
Fixed
-
Init runs after terminal initialization:
Initcallables now run after the terminal is initialized, enabling them to callRatatuiRuby.terminal_size, compute layout areas, or perform other terminal-dependent initialization. Previously Init ran before the terminal was ready, causing “Terminal is not initialized” errors. -
FPS timeout uses float division: The runtime now uses
1.0 / fpsinstead of1 / fpsfor poll timeout calculation. Integer division caused1 / 60to yield 0, resulting in busy-wait CPU spinning at 100%.
Removed
-
BREAKING: Command::Error class: Removed. Use
Message::Errorinstead. -
BREAKING: Command.error factory: Removed. Use
Message::Error.new(command:, exception:)instead.
0.5.0 - 2026-01-16
Added
Changed
-
BREAKING: Rebrand to
Rooibos: The gem is now namedrooibosand the module is top-levelRooibosinstead ofRatatuiRuby::Tea. Update your code, including: -
require "ratatui_ruby/tea"→require "rooibos" -
RatatuiRuby::Tea::*→Rooibos::* -
If you need a full migration guide, reach out to the mailing list
Fixed
Removed
0.4.0 - 2026-01-16
Added
-
Timer Commands:
Command.wait(seconds, tag)andCommand.tick(seconds, tag)for timed events. After the duration, sendstagto the update function. Responds to cancellation cooperatively — sendsCommand.cancel(self)when cancelled so you can handle it. UsesConcurrent::Cancellation.timeoutinternally.Command.tickis an alias forCommand.wait; the “recurring tick” pattern is achieved by re-dispatching in the update function. -
Command.uncancellable Factory: Creates a fresh
Concurrent::Cancellationthat never fires. Use for commands wrapping non-cancellable blocking I/O (e.g.,Net::HTTPrequests). -
Command.batch: Fire-and-forget parallel execution.
Command.batch(cmd1, cmd2)orCommand.batch([cmds])runs children in parallel; each child sends its own messages independently. On cancellation, emitsCommand.cancel(self)so you can detect the batch was stopped. Child errors surface asCommand::Errorso you can handle failures in your update function. One failing child does not stop the others. Requires all child commands to be Ractor-shareable. -
Command.all: Aggregating parallel execution.
Command.all(:tag, cmd1, cmd2)orCommand.all(:tag, [cmds])runs children in parallel and waits for all to complete, then sends a single aggregated message. Array syntax produces[:tag, [results]]; variadic syntax splats results as[:tag, result1, result2]. On cancellation, emitsCommand.cancel(self). Child errors surface asCommand::Error. Requires all child commands to be Ractor-shareable. -
Outlet#source: Command composition for multi-step workflows.
out.source(child_command, token, timeout: 30.0)runs a child command synchronously within a custom command, returning its result (ornilif cancelled/timed out). Exceptions from failed children propagate to the caller. Use this to orchestrate sequential API calls, conditional fetches, or any workflow that needs one result before starting the next. -
Command.http: Native HTTP client for API calls. Supports GET, POST, PUT, PATCH, DELETE with DWIM syntax:
Command.http(get: 'url'),Command.http(:get, 'url', :tag), etc. Returns hash-basedHttpResponsewithdeconstruct_keysfor pattern matching:{ type: :http, envelope:, status:, body:, headers: }or{ type: :http, envelope:, error: }. Optionalparser:keyword invokes a callable on the response body for JSON, YAML, CSV, or custom parsing. SSL, default 10s timeout, and cancellation-before-request are supported. Parsers and parsed results must be Ractor-shareable. -
Streaming Command Data Loss Fix: Fixed race condition in
Command.system(stream: true)where fast commands could lose stdout/stderr data. Reader threads nowjoininstead ofkillto ensure all output is processed before completion. -
Message::Predicates Mixin: Include in custom message types for safe predicate calls. Returns
falsefor any unknown predicate method (ending in?). Enables pattern matching workflows where messages can respond to predicates likemsg.http?ormsg.timer?without raisingNoMethodError. Includesrespond_to_missing?for introspection parity. -
Message::Timer: Predicate-rich response type for timer commands (
Command.wait,Command.tick). Includestimer?predicate anddeconstruct_keysfor pattern matching withtype: :timerdiscriminator. Containsenvelope(routing symbol) andelapsed(actual wait time in seconds). -
Message::HttpResponse: Moved from
Command::HttpResponsetoMessage::HttpResponsewith added predicates. Includeshttp?,success?, anderror?predicates. Implementsdeconstruct_keysfor pattern matching withtype: :httpdiscriminator. -
Message::System::Batch: Response type for
Command.system(batch mode). Includessystem?,success?(exit 0), anderror?(non-zero exit) predicates. Containsenvelope,stdout,stderr, andstatus. Implementsdeconstruct_keysfor pattern matching. -
Message::System::Stream: Response type for
Command.system(..., stream: true). Includessystem?,stdout?,stderr?, andcomplete?predicates. Containsenvelope,streamtype (:stdout,:stderr,:complete),content(for output lines), andstatus(for completion). Implements conditionaldeconstruct_keysbased on stream type. -
Message::All: Response type for
Command.allaggregating parallel execution. Includesall?predicate. Containsenvelope,resultsarray, andnestedboolean. Implementsdeconstruct_keysfor pattern matching. -
Command Parameter Rename: Renamed
tagparameter toenvelopeacross all commands for consistency:Command.wait,Command.tick,Command.system, andCommand.all. These now useenvelope:in their data definitions. Messages emit withenvelope:for pattern matching. -
Dependencies: Added
concurrent-ruby(~> 1.3) andconcurrent-ruby-edge(~> 0.7) for robust concurrency primitives. -
Rooibos.normalize_initHelper: NewRooibos.normalize_init(result)normalizes Init callable returns. Accepts the output of aFragment.Initcallable and always returns[model, command]. Use when composing child fragment initialization in parent fragments.
Changed
-
Terminology: “Bag” → “Fragment”: Renamed Fractal Architecture units from “bags” to “fragments” throughout the codebase. A fragment is a module containing
Model,INITIAL,UPDATE, andVIEWconstants. Parent fragments compose child fragments via routing. Theexamples/app_fractal_dashboard/bags/directory is nowexamples/app_fractal_dashboard/fragments/. All API documentation, code comments, and examples updated to reflect this terminology change. -
Runtime API Signature (Breaking):
Rooibos.runsignature changed: -
Fragment parameter is now positional instead of keyword:
Rooibos.run(MyApp)instead ofRooibos.run(fragment: MyApp) -
Removed
argv:andenv:parameters - Runtime now automatically usesARGVandENVglobals -
Added
fps:parameter (default 60) for configurable frame rate -
Renamed
init:parameter tocommand:in explicit parameters API for clarity -
Fragment Convention Rename (Breaking): Fragment constants have new naming conventions:
-
INITIALconstant →Initcallable. The runtime callsInit.()to get the initial model. -
UPDATEconstant →Updatecallable (capitalized). -
VIEWconstant →Viewcallable (capitalized). -
Init is now a lambda/callable instead of a frozen constant. This enables parameterized initialization and returning
[model, command]tuples for initial commands. -
Internal Method Rename (Breaking):
Runtime.normalize_update_resultrenamed toRuntime.normalize_update_returnfor clarity. Only affects code calling private Runtime internals. -
Command.custom Ractor Validation (Breaking):
Command.custom(callable)now validates in debug mode that the callable is Ractor-shareable. Callables that capture mutable state will raiseInvariant. Define callables at module level or useRactor.make_shareable. -
Model Validation Timing (Breaking): Runtime now validates model Ractor-shareability immediately after Init returns, not just during the Update cycle. This catches mutable models earlier, enforcing immutability at startup. Models must be frozen (
.freeze) or use immutable data structures (Data.define). This is a good breaking change that prevents subtle concurrency bugs. -
CancellationToken Replaced (Breaking): Custom commands now receive
Concurrent::Cancellationinstead ofCancellationToken. The method to check cancellation changes fromtoken.cancelled?(British) totoken.canceled?(American). -
Outlet Accepts Channel (Breaking):
Outlet.newnow accepts aConcurrent::Promises::Channelinstead ofThread::Queue. -
Outlet#put (Breaking):
out.put(msg), the common case now sendsmsgdirectly instead of[msg];out.put(a, b, c)sends[a, b, c]. Previously all calls wrapped arguments in an array. Update functions that matchedwhen Arraymay need adjustment. -
Command.all Output (Breaking):
Command.allnow emitsMessage::Allobjects instead of raw arrays. Previously emitted[:tag, [results]](nested) or[:tag, result1, result2](variadic). Now emitsMessage::Allwithenvelope,results, andnestedfields. Use hash pattern matching:in { type: :all, envelope:, results:, nested: }.
Fixed
-
Streaming Command Data Loss: Fixed race condition where fast commands (e.g.,
echo hello) could lose stdout/stderr messages. The streaming reader threads were being killed immediately after the child process exited, before they could finish reading buffered output. Now the runtime joins the reader threads to ensure all data is processed before sending:complete.
Removed
-
CancellationToken Class: Removed in favor of
Concurrent::Cancellationfrom concurrent-ruby-edge. -
CancellationToken::NONE: Removed. Use
Command.uncancellablefactory instead.
0.3.1 - 2026-01-11
Added
-
CancellationToken: Cooperative cancellation mechanism for long-running custom commands. Commands check
cancelled?periodically and stop gracefully whencancel!is called. IncludesCancellationToken::NONEnull object for commands that ignore cancellation. -
Command::Custom Mixin: Include in your class to mark it as a custom command. Provides
rooibos_command?brand predicate androoibos_cancellation_grace_period(default 2.0 seconds) for configuring cleanup time after cancellation. -
Command::Outlet: Messaging gateway for custom commands. Use
put(tag, *payload)to send results back to the update function. Validates Ractor-shareability in debug mode. -
Custom Command Dispatch: Runtime now dispatches custom commands (objects with
rooibos_command?returning true) in background threads. Commands receive anOutletfor messaging and aCancellationTokenfor cooperative shutdown. -
Command.custom Factory: Wraps lambdas/procs to give them unique identity for dispatch tracking. Each
Command.custom(callable)call produces a distinct wrapper, enabling targeted cancellation. Accepts optionalgrace_period:to override the default 2.0 second cleanup window. -
Command.cancel Factory: Request cancellation of a running command. Returns a
Command::Cancelsentinel that the runtime routes to the appropriate command’s CancellationToken. -
Runtime Cancellation Dispatch: The runtime now handles
Command::Cancelby signaling the target command’sCancellationToken, enabling cooperative cancellation of long-running commands. Respectsrooibos_cancellation_grace_period: waits for the grace period, then force-kills unresponsive threads. UseFloat::INFINITYto never force-kill. -
Graceful Shutdown: On exit, runtime signals all active commands then respects each command’s grace period. Commands with
Float::INFINITYgrace are waited on indefinitely (user has SIGKILL). Final queue messages are processed before returning. -
Automatic Error Propagation: Custom commands that raise unhandled exceptions now produce a
Command::Errormessage instead of corrupting the TUI display. The runtime catches exceptions and pushesCommand::Error.new(command:, exception:)to the queue. Pattern match onCommand::Errorin your update function to handle failures uniformly. Factory methodCommand.error(command, exception)is available for testing. -
Command::System Cancellation: Streaming shell commands now respect cooperative cancellation. When cancelled, sends
SIGTERMfor graceful shutdown, thenSIGKILLif the child process doesn’t exit. Prevents orphaned child processes from lingering after app exit.
Changed
Fixed
-
Ractor Enforcement is Debug-Only: The Ractor-shareability check now only runs in debug mode (and automated tests). Production skips this check for performance, matching the original specification. Previously, the check ran unconditionally.
Removed
0.3.0 - 2026-01-08
Added
-
Router DSL: New
Rooibos::Routermodule provides declarative routing for Fractal Architecture: -
route :prefix, to: ChildBag— declares a child bag route -
keymap { key "q", -> { Command.exit } }— declares keyboard handlers -
keymap { key "x", handler, when: -> (m) { m.ready? } }— guards (also:if:,only:,guard:,unless:,except:,skip:) -
keymap { only when: guard do ... end }— nested guard blocks apply to all keys within (also:skip when: ...) -
mousemap { click -> (x, y) { ... } }— declares mouse handlers -
action :name, handler— declares reusable actions for key/mouse handlers -
from_router— generates an UPDATE lambda from routes and handlers -
Composition Helpers: New helper methods for Fractal Architecture reduce boilerplate:
-
Rooibos.route(command, :prefix)— wraps a command to route results to a child bag -
Rooibos.delegate(message, :prefix, child_update, child_model)— dispatches prefixed messages to child bags -
Command Mapping:
Command.map(inner_command, &mapper)wraps a child command and transforms its result message. Essential for parent bags routing child command results. -
Shortcuts Module:
require "rooibos/shortcuts"andinclude Rooibos::Shortcutsfor short aliases: -
Cmd.exit— alias forCommand.exit -
Cmd.sh(command, tag)— alias forCommand.system -
Cmd.map(command, &block)— alias forCommand.map -
Sync Event Integration: Runtime now handles
Event::SyncfromRatatuiRuby::SyntheticEvents. When a Sync event is received, the runtime waits for all pending async threads and processes their results before continuing. Useinject_syncin tests for deterministic async verification. -
Streaming Command Output:
Command.systemnow accepts astream:keyword argument. Whenstream: true, the runtime sends incremental messages ([:tag, :stdout, line],[:tag, :stderr, line]) as output arrives, followed by[:tag, :complete, {status:}]when the command finishes. Invalid commands send[:tag, :error, {message:}]. Default behavior (stream: false) remains unchanged. -
Custom Shell Modal Example: Added
examples/app_fractal_dashboard/bags/custom_shell_modal.rbdemonstrating a 3-bag fractal architecture for a modal that runs arbitrary shell commands with streaming output. Features interleaved stdout/stderr, exit status indication, and Ractor-safe implementation usingtui.overlayfor opaque rendering.
Changed
-
Command Module Rename (Breaking): The
Cmdmodule is nowCommandwith Rubyish naming: -
Cmd::Quit→Command::Exit(useCommand.exitfactory) -
Cmd::Exec→Command::System(useCommand.system(cmd, tag)factory)
Fixed
Removed
0.2.0 - 2026-01-08
Added
-
The Elm Architecture (TEA): Implemented the core Model-View-Update (MVU) runtime. Use
Rooibos.run(model, view: ..., update: ...)to start an interactive application with predictable state management. -
Async Command System: Side effects (database, HTTP, shell) are executed asynchronously in a thread pool. Results are dispatched back to the main loop as messages, ensuring the UI never freezes.
-
Ractor Safety Enforcement: The runtime strictly enforces that all
ModelandMessageobjects are Ractor-shareable (deeply frozen). This guarantees thread safety by design and prepares for future parallelism. -
Flexible Update Returns: The
updatefunction supports multiple return signatures for developer ergonomics: -
[Model, Cmd]— Standard tuple. -
Model— Implicitly[Model, Cmd::None]. -
Cmd— Implicitly[CurrentModel, Cmd]. -
Startup Commands:
Rooibos.runaccepts aninit:parameter to dispatch an initial command immediately after startup, useful for loading initial data without blocking the first render. -
View Validation: The
viewfunction must return a valid widget. ReturningnilraisesRatatuiRuby::Error::Invariantto catch bugs early.
0.1.0 - 2026-01-07
Added
-
First Release: Empty release of
rooibos, a Ruby implementation of The Elm Architecture (TEA) forratatui_ruby. Scaffolding generated byratatui_ruby-devtools.