Class: Rooibos::Runtime

Inherits:
Object
  • Object
show all
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: ->(message, model) { message.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

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 = ->(message, 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 = ->(message, 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 just new_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. Default false.

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