Class: Rooibos::Runtime
- Inherits:
-
Object
- Object
- Rooibos::Runtime
- Defined in:
- lib/rooibos/runtime.rb
Overview
Runs the Model-View-Update event loop.
Applications need a render loop. You poll events, update state, redraw. Every frame. The boilerplate is tedious and error-prone.
This class handles the loop. You provide the model, view, and update. It handles the rest.
Use it to build applications with predictable state.
Example
– SPDX-SnippetBegin SPDX-FileCopyrightText: 2026 Kerrick Long SPDX-License-Identifier: MIT-0 ++
Rooibos.run(
model: { count: 0 }.freeze,
view: ->(model, tui) { tui.paragraph(text: model[:count].to_s) },
update: ->(, model) { .q? ? [model, Command.exit] : [model, nil] }
)
– SPDX-SnippetEnd ++
Constant Summary collapse
- QUIT =
Sentinel value avoids accidentally quitting from application exceptions.
Object.new.freeze
Class Method Summary collapse
-
.normalize_init(result) ⇒ Object
Normalizes Init callable return value to
[model, command]tuple. -
.run(root_fragment = nil, fps: 60, model: nil, view: nil, update: nil, command: nil, update_every_frame: false) ⇒ Object
Starts the MVU event loop.
Class Method Details
.normalize_init(result) ⇒ Object
Normalizes Init callable return value to [model, command] tuple.
Init callables return initial state and optional startup command. They can use DWIM (Do What I Mean) syntax: return just a model, just a command, or a full tuple.
This method handles all formats. Use it when composing child fragment Inits.
- result
-
The Init return value (model, command, or
[model, command]tuple).
Examples
– SPDX-SnippetBegin SPDX-FileCopyrightText: 2026 Kerrick Long SPDX-License-Identifier: MIT-0 ++
# Just model
model, cmd = Rooibos.normalize_init(Model.new(...))
# => [Model.new(...), nil]
# Just command
model, cmd = Rooibos.normalize_init(Command.http(...))
# => [nil, Command.http(...)]
# Tuple (already normalized)
model, cmd = Rooibos.normalize_init([Model.new(...), Command.http(...)])
# => [Model.new(...), Command.http(...)]
– SPDX-SnippetEnd ++
169 170 171 |
# File 'lib/rooibos/runtime.rb', line 169 def self.normalize_init(result) Transition.from(result, nil).to_a end |
.run(root_fragment = nil, fps: 60, model: nil, view: nil, update: nil, command: nil, update_every_frame: false) ⇒ Object
Starts the MVU event loop.
Runs until the update function returns a Command.exit command.
Root Fragment with Init
Pass a fragment module with Init, Update, and View constants. This allows your application to do work at startup, and your Update will be called with the result. It also allows you to create a more complex model with access to ARGV and ENV.
module MyApp
# Init is any callable, and returns an immutable Model and/or Command, according to your application's needs.
# The Model is your application's initial state, and the Command is any command to run at startup.
Init = -> () {
# To do work at startup:
return [Data.define(:count).new(count: 0), Command.http("https://api.example.com/data")]
# To start idle:
return Data.define(:count).new(count: 0)
}
# Update has access to a single Message, and your fragment's latest immutable Model.
# It returns a new Model and/or a Command, according to your application's needs.
Update = ->(, model) {
[model, Command.exit]
}
# View has access to your fragment's latest immutable Model, and a RatatuiRuby::TUI.
# It returns a tree of RatatuiRuby::Widget and/or Custom Widgets.
View = ->(model, tui) {
tui.paragraph(text: model.count.to_s)
}
end
Rooibos.run(MyApp)
Root Fragment with auto-Init
Pass a fragment module with a Model class and Update and View constants. Your application will be idle until a RatatuiRuby::Event message is sent to your Update.
module MyApp
# Model is anything that responds to <tt>new</tt>.
Model = Data.define(:count).new(count: 0)
# Update has access to a single Message, and your fragment's latest immutable Model.
# It returns a new Model and/or a Command, according to your application's needs.
Update = ->(, model) {
[model, Command.exit]
}
# View has access to your fragment's latest immutable Model, and a RatatuiRuby::TUI.
# It returns a tree of RatatuiRuby::Widget and/or Custom Widgets.
View = ->(model, tui) {
tui.paragraph(text: model.count.to_s)
}
end
Rooibos.run(MyApp)
Explicit Parameters API
Tests need deterministic state. Init reads from the filesystem, network, or environment—sources that change between runs. Injecting a known model makes tests reproducible.
Pass model:, view:, and update: directly. The runtime skips Init and uses your model as the starting state.
Rooibos.run(
model: Ractor.make_shareable(MyApp::Model.new(count: 0)),
view: MyApp::View,
update: MyApp::Update
)
Parameters
- root_fragment
-
Module with Model, Init, Update, View constants. *Mutually exclusive with model/view/update.*
- fps
-
Target frames per second for the application. Higher values feel more responsive, but may spike CPU usage.
- model
-
Initial application state (immutable). *Required if fragment not provided.*
- view
-
Callable receiving
(model, tui), returns a widget. *Required if fragment not provided.* - update
-
Callable receiving
(message, model), returns[new_model, command]or justnew_model. *Required if fragment not provided.* - command
-
Optional callable to run at startup. Returns a message for update.
- update_every_frame
-
When
true,Event::None(idle frame) events are passed to Update instead of being dropped. Use for animations, physics, or any per-frame state change. Defaultfalse.
Raises
- Rooibos::Error::Invariant
-
If both fragment and any of (model, view, update, command) are provided.
128 129 130 131 132 133 134 135 136 137 |
# File 'lib/rooibos/runtime.rb', line 128 def self.run(root_fragment = nil, fps: 60, model: nil, view: nil, update: nil, command: nil, update_every_frame: false) @fragment = fragment_from_kwargs(root_fragment, model:, view:, update:, command:) @view = @fragment::View @update = @fragment::Update @init_callable = init_callable @timeout = 1.0 / fps @update_every_frame = update_every_frame start_runtime end |