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.7.0] - 2026-02-18
Added
-
Curated RuboCop config shipped with gem:
Rooibosnow includes a curated RuboCop configuration atlib/rooibos/rubocop.yml. Useinherit_gem: { rooibos: lib/rooibos/rubocop.yml }in your.rubocop.ymlto adopt it. -
Message::Predicatesnew predicates: Addedmilestone?andcustom?predicate methods. Usemessage.milestone?to check for milestone messages (e.g., completion signals) andmessage.custom?to check for custom user-defined message types. -
Command.deliver(message): New built-in command for sending structured messages to Update. Wraps any message and delivers it via the runtime. Works with pattern matching and predicates. -
Command.bubble(message): New command for outward message propagation through the fragment hierarchy. UnlikeCommand.deliver(which goes directly to the root),Command.bubbleflows through each fragment level, giving each outer fragment an opportunity to observe or intercept the message before it reaches the next level. Use with the Router DSL (observe,intercept) or handle manually by checking forCommand::Bubbleand extracting the message. -
Message::Bubbled: New message type wrapping bubbled messages in the fragment hierarchy. Providesbubbled?predicate for identification. -
Router
receiveDSL: New handlers for accepting and processing messages, replacing thekeymapandmousemapDSLs. Five forms available, each stops further receive/forward/otherwise processing on match:-
receive_events :q, handler— match by key/event symbol (also accepts arrays:receive_events [:q, :ctrl_c], handler) -
receive_routed :envelope, handler— match byMessage::Routedenvelope -
receive_instances_of SomeClass, handler— match by message class -
receive_all handler— match any message -
receive predicate, handler— match by custom predicate lambda:receive -> (msg, model) { msg.key? && model[:mode] == :insert }, handler -
All forms accept
when:andunless:guard keywords -
ALL forms aliased as
intercept
-
-
Router
observeDSL: Non-stopping message observation. Same five forms asreceive:observe,observe_events,observe_instances_of,observe_routed,observe_all. Observers run before receive/intercept/forward, allow model updates and command accumulation, and pass the message through to subsequent handlers. Multiple observers run in declaration order and accumulate model and command changes. -
Router
forwardDSL: Forward messages to nested fragments. Five forms:-
forward_events :enter, to: :panel, as: :action_name— forward key events with envelope renaming -
forward_instances_of SomeClass, to: :panel— forward by message class -
forward_routed :envelope, to: :panel, as: :new_envelope— forward by routed envelope -
forward_all to: :panel— forward all messages -
forward predicate, to: :panel— forward by custom predicate lambda -
Supports
broadcast: trueto send to all routes, andwhen:/unless:guards -
Destination can be a symbol (
:panel), a Fragment (PanelFragment), or a captured Route object
-
-
Router
route_toblocks: Scoped forwarding that infers the destination for allforwarddeclarations within the block, reducing repetition. Accepts a route name, Fragment, or captured Route. -
Router
otherwiseguards:otherwisenow supportswhen:andunless:guard keywords and multiple clauses with first-match semantics. Also resolves destinations by Fragment or captured Route. -
Router
only/skipguard blocks: Scoped guard blocks apply a shared condition to all handlers declared within.onlypasses when the guard is true;skippasses when it is false. Blocks nest and combine (all guards must pass). -
Router
routereturns Route objects:routenow returns aRoutedata object. Capture it to disambiguate when multiple routes target the same fragment Module. -
Router unnamed routes: Routes can omit the symbol prefix when using
read:/write:callable accessors for custom model extraction. Combine with a captured Route object for forwarding.Changed
-
rooibos newgenerates curated RuboCop config: Scaffolded applications now get a.rubocop.ymlthat inherits theRooiboscurated cop profile viainherit_gem, replacing the bare defaults thatbundle gem --linter=rubocopproduces. -
BREAKING:
keymapDSL removed: Thekeymap do |map|/map.keyDSL is removed. Usereceive_eventsfor handling events andforward_eventsfor routing to nested fragments. Migrate: -
keymap { |map| map.key :q, -> { Command.exit } }becomesreceive_events :q, -> (_msg, _model) { Command.exit } -
keymap { |map| map.key :j, :move_down }becomesreceive_events :j, :move_down -
Guards move to keyword args:
receive_events :q, handler, when: -> (_msg, model) { model[:active] } -
BREAKING:
mousemapDSL removed: Themousemap do |map|/map.scroll/map.clickDSL is removed. Usereceive_eventswith event symbols. Migrate: -
mousemap { |map| map.scroll :up, handler }becomesreceive_events :scroll_up, handler -
BREAKING:
actioninline keybinding options removed: Thekeymap:,keys:,key:, andmousemap:keyword arguments onactiondeclarations are removed. Bind keys separately withreceive_events: -
Before:
action quit: -> { Command.exit }, keymap: %i[ctrl_c q] -
After:
action :quit, -> { Command.exit }+receive_events [:q, :ctrl_c], :quit -
BREAKING: Runtime validates Init, View, and Update for Ractor shareability: At startup, the runtime now checks that all three fragment callables can be made Ractor-shareable. Fragments using lambdas that capture mutable state or are defined in non-shareable scopes will fail validation. Convert to module-level callables, use classes, or use
Ractor.make_shareable.
Fixed
-
Gem Size: Reduced gem size from 812KB to 116KB by excluding doc/, examples/, and other development files.
-
Batch cancellation reliably delivers children’s
Message::Canceled: When aCommand.batchwas cancelled, the batch itself responded instantly but children’s cancellation messages could arrive after the runtime had already moved on. Now the lifecycle waits for children spawned viaOutlet#standingto complete before resolving the batch’s future, so allMessage::Canceledmessages arrive before the next Update cycle. -
Command::Cancelno longer includesMessage::Predicates: Commands should only includeCommand::Custom, not message mixins.Cancelis a sentinel command intercepted by the runtime before dispatch — it is never sent to Update as a message. Removed the buggy (for non-messages) behavior introduced byinclude Message::Predicates. -
Message::Predicates
deconstruct_keys: Now callssuperto preserve Data.define fields. Previously, includingPredicatesin aData.defineclass would shadow all fields, returning only{ type: ... }. Now correctly returns{ type: :my_message, envelope:, ...all_fields }. -
Message::Predicates type-based predicates: Predicates like
message.user_fetched?now returntruewhen the predicate matches the message’s:type. Previously all unknown predicates returnedfalse, even when they matched the type. -
Message::Predicates envelope-based predicates: Predicates like
message.profile?now returntruewhen the predicate matches the message’s:envelope. This enables convenient checks likeif message.profile?alongside type checks.
Removed
-
Rooibos.routehelper: Removed. Use the Router DSL (forward,forward_events,forward_routed) instead of manual command prefix wrapping. -
Rooibos.delegatehelper: Removed. Use the Router DSL (receive,observe,forward) instead of manual message prefix dispatch.
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.delegate Message 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 forum
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 orphans unresponsive threads. UseFloat::INFINITYto wait indefinitely for cooperative exit. -
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.