class RatatuiRuby::Tea::Runtime
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
RatatuiRuby::Tea.run( model: { count: 0 }.freeze, view: ->(m, tui) { tui.paragraph(text: m[:count].to_s) }, update: ->(msg, m) { msg.q? ? [m, Cmd.quit] : [m, nil] } )
Public Class Methods
Source
# File lib/ratatui_ruby/tea/runtime.rb, line 142 def self.dispatch(cmd, queue) case cmd when Cmd::Exec Thread.new do require "open3" stdout, stderr, status = Open3.capture3(cmd.command) msg = [cmd.tag, { stdout:, stderr:, status: status.exitstatus }] queue << Ractor.make_shareable(msg) rescue => e # Should we send an error message? For now, crash in debug, ignore in prod? # Better to rely on Open3 not raising for standard execution. end # TODO: Add Batch, Sequence, NetHttp end end
Dispatches a command to the worker pool.
Spawns a thread for async commands. Pushes result to queue. Handles nested commands (Batch, Sequence) recursively.
Source
# File lib/ratatui_ruby/tea/runtime.rb, line 115 def self.normalize_update_result(result, previous_model) return result if result.is_a?(Array) && result.size == 2 && valid_cmd?(result[1]) return [previous_model, result] if valid_cmd?(result) [result, nil] end
Detects whether result is a +[model, cmd]+ tuple, a plain model, or a Cmd alone.
Returns +[model, cmd]+ in all cases.
Source
# File lib/ratatui_ruby/tea/runtime.rb, line 45 def self.run(model:, view:, update:, init: nil) validate_ractor_shareable!(model, "model") # Execute init command synchronously if provided if init init_msg = init.call result = update.call(init_msg, model) model, _cmd = normalize_update_result(result, model) validate_ractor_shareable!(model, "model") end queue = Queue.new catch(:quit) do RatatuiRuby.run do |tui| loop do tui.draw do |frame| widget = view.call(model, tui) validate_view_result!(widget) frame.render_widget(widget, frame.area) end # 1. Handle user input (blocks up to 16ms) msg = tui.poll_event # If provided, handle the event unless msg.is_a?(RatatuiRuby::Event::None) result = update.call(msg, model) model, cmd = normalize_update_result(result, model) validate_ractor_shareable!(model, "model") throw :quit if cmd.is_a?(Cmd::Quit) dispatch(cmd, queue) if cmd end # 2. Check for background outcomes until queue.empty? begin bg_msg = queue.pop(true) result = update.call(bg_msg, model) model, cmd = normalize_update_result(result, model) validate_ractor_shareable!(model, "model") throw :quit if cmd.is_a?(Cmd::Quit) dispatch(cmd, queue) if cmd rescue ThreadError break end end end end end model end
Starts the MVU event loop.
Runs until the update function returns a Cmd.quit command.
- model
-
Initial application state (immutable).
- view
-
Callable receiving
(model, tui), returns a widget. - update
-
Callable receiving
(msg, model), returns[new_model, cmd]or justnew_model. - init
-
Optional callable to run at startup. Returns a message for update.
Source
# File lib/ratatui_ruby/tea/runtime.rb, line 123 def self.valid_cmd?(value) value.nil? || value.class.name&.start_with?("RatatuiRuby::Tea::Cmd::") end
Returns true if value is a valid command (nil or a Cmd type).
Source
# File lib/ratatui_ruby/tea/runtime.rb, line 105 def self.validate_view_result!(widget) return unless widget.nil? raise RatatuiRuby::Error::Invariant, "View returned nil. Return a widget, or use TUI#clear for an empty screen." end
Validates the view returned a widget.
Views return widget trees. Returning nil is a bug—you forgot to return something. For an intentionally empty screen, use TUI#clear.