module RatatuiRuby::Tea::Command
Commands represent side effects.
The MVU pattern separates logic from effects. Your update function returns a pure model transformation. Side effects go in commands. The runtime executes them.
Commands produce messages, not callbacks. The tag argument names the message so your update function can pattern-match on it. This keeps all logic in update and ensures messages are Ractor-shareable.
Examples
# Terminate the application [model, Command.exit] # Run a shell command; produces [:got_files, {stdout:, stderr:, status:}] [model, Command.system("ls -la", :got_files)] # No side effect [model, nil]
Constants
- All
-
An aggregating parallel command.
- Http
-
An HTTP request command.
- HttpResponse
-
Alias to
Message::HttpResponsefor backwards compatibility. New code should useRatatuiRuby::Tea::Message::HttpResponse. - Wait
-
A one-shot timer command.
Applications need timed events. Notification auto-dismissal, debounced search, and animation frames all depend on delays. Building timers from scratch with threads is error-prone. Cancellation is tricky.
This command waits, then sends a message. It responds to cancellation cooperatively. When cancelled, it sends
Command.cancel(self)so you know the timer stopped.Use it for delayed actions, debounced inputs, or animation loops.
Example: Notification dismissal
def update(msg, model) case msg in :save_clicked [model.with(notification: "Saved!"), Command.wait(3.0, :dismiss)] in :dismiss [model.with(notification: nil), nil] in Command::Cancel [model.with(notification: nil), nil] # User navigated away end end
Example: Animation loop
def update(msg, model) case msg in :start_animation [model.with(frame: 0), Command.tick(0.1, :animate)] in :animate frame = (model.frame + 1) % 10 [model.with(frame:), Command.tick(0.1, :animate)] end end
Public Class Methods
Source
# File lib/ratatui_ruby/tea/command.rb, line 523 def self.all(envelope, *) All.new(envelope, *) end
Creates an aggregating parallel command.
Applications load dashboards that combine user, settings, and stats. Fire-and-forget loses correlation. This command waits for all children and returns their results together in a single message.
- commands
-
One or more commands to run in parallel. Pass multiple
arguments or a single array.
Example
# Variadic syntax Command.all( Command.http(:get, "/users", :_), Command.http(:get, "/stats", :_), ) # Produces: [:all, [user_result, stats_result]]
Source
# File lib/ratatui_ruby/tea/command.rb, line 502 def self.batch(*) Batch.new(*) end
Creates a parallel batch command.
Applications fetch data from multiple sources. Dashboard panels load users, stats, and notifications. Waiting sequentially is slow. Managing threads and error handling manually is error-prone.
This command runs children in parallel. Each child sends its own messages independently. The batch completes when all children finish or when cancellation fires.
Use it for parallel fetches, concurrent refreshes, or any work that does not need coordinated results.
- commands
-
One or more commands to run in parallel. Pass multiple
arguments or a single array.
Example
# Variadic syntax Command.batch( Command.http(:get, "/users", :users), Command.http(:get, "/stats", :stats), ) # Array syntax Command.batch([cmd1, cmd2, cmd3])
Source
# File lib/ratatui_ruby/tea/command.rb, line 121 def self.cancel(handle) Cancel.new(handle:) end
Request cancellation of a running command.
The model stores the command handle (the command object itself). Returning Command.cancel(handle) signals the runtime to stop it.
- handle
-
The command object to cancel.
Example
# Dispatch and store handle cmd = FetchData.new(url) [model.with(active_fetch: cmd), cmd] # User clicks cancel when :cancel_clicked [model.with(active_fetch: nil), Command.cancel(model.active_fetch)]
Source
# File lib/ratatui_ruby/tea/command.rb, line 438 def self.custom(callable = nil, grace_period: nil, &block) c = callable || block # Debug mode: validate that callable can be made shareable (fail fast) if RatatuiRuby::Debug.enabled? begin c = Ractor.make_shareable(c) rescue Ractor::IsolationError raise RatatuiRuby::Error::Invariant, "Command.custom requires a Ractor-shareable callable. " \ "#{c.class} is not shareable. Use Ractor.make_shareable or define at top-level." end end # Production mode: skip validation (Ractors not yet used, avoid overhead) Wrapped.new(callable: c, grace_period:) end
Gives a callable unique identity for cancellation.
Reusable procs and lambdas share identity. Dispatch them twice, and Command.cancel would cancel both. Wrap them to get distinct handles.
The callable must be Ractor-shareable (cannot capture mutable state). Create resources like database connections inside the callable, not in the closure. See the Custom Commands guide for details.
- callable
-
Proc, lambda, or any object responding to +call(out, token)+. If omitted, the block is used.
- grace_period
-
Cleanup time override. Default: 2.0 seconds.
Example
# With callable cmd = Command.custom(->(out, token) { out.put(:fetched, data) }) # With block cmd = Command.custom(grace_period: 5.0) do |out, token| until token.canceled? out.put(:tick, Time.now) sleep 1 end end
Source
# File lib/ratatui_ruby/tea/command.rb, line 170 def self.error(command, exception) Error.new(command:, exception:) end
Creates an error sentinel.
The runtime produces this automatically when a command raises. Use this factory for testing or for commands that want to signal error completion without raising.
- command
-
The command that failed.
- exception
-
The exception that was raised.
Example
def update(message, model) case message in Command::Error(command:, exception:) model.with(error: "#{command.class} failed: #{exception.message}") end end
Source
# File lib/ratatui_ruby/tea/command.rb, line 65 def self.exit Exit.new end
Creates a quit command.
Returns a sentinel the runtime detects to terminate the application.
Example
def update(message, model) case message in { type: :key, code: "q" } [model, Command.exit] else [model, nil] end end
Source
# File lib/ratatui_ruby/tea/command.rb, line 529 def self.http(*, **) Http.new(*, **) end
Creates an HTTP request command. Supports DWIM arity - see Http.new for patterns.
Source
# File lib/ratatui_ruby/tea/command.rb, line 409 def self.map(inner_command, &mapper) Mapped.new(inner_command:, mapper:) end
Creates a mapped command for Fractal Architecture composition.
Wraps an inner command. When the inner command completes, the mapper block transforms the result into a parent message. This prevents monolithic update functions (the “God Reducer” anti-pattern).
- inner_command
-
The child command to wrap.
- mapper
-
Block that transforms child message to parent message.
Example
# Child returns Command.execute that produces [:got_files, {...}] child_command = Command.system("ls", :got_files) # Parent wraps to route as [:sidebar, :got_files, {...}] parent_command = Command.map(child_command) { |child_result| [:sidebar, *child_result] }
Source
# File lib/ratatui_ruby/tea/command.rb, line 349 def self.system(command, envelope, stream: false) System.new(command:, envelope:, stream:) end
Creates a shell execution command.
- command
-
Shell command string to execute.
- tag
-
Symbol or class to tag the result message.
- stream
-
If
true, the runtime sends incremental stdout/stderr
messages as they arrive. If <tt>false</tt> (default), waits for completion and sends a single message with all output.
Example (Batch Mode)
# Return this from update: [model.with(loading: true), Command.system("ls -la", :got_files)] # Then handle it later: def update(message, model) case message in [:got_files, {stdout:, status: 0}] [model.with(files: stdout.lines), nil] in [:got_files, {stderr:, status:}] [model.with(error: stderr), nil] end end
Example (Streaming Mode)
# Return this from update: [model.with(loading: true), Command.system("tail -f log.txt", :log, stream: true)] # Then handle incremental messages: def update(message, model) case message in [:log, :stdout, line] [model.with(lines: [*model.lines, line]), nil] in [:log, :stderr, line] [model.with(errors: [*model.errors, line]), nil] in [:log, :complete, {status:}] [model.with(loading: false, exit_status: status), nil] in [:log, :error, {message:}] [model.with(loading: false, error: message), nil] end end
Source
# File lib/ratatui_ruby/tea/command.rb, line 83 def self.uncancellable cancellation, _origin = Concurrent::Cancellation.new(Concurrent::Promises.resolvable_event) cancellation end
Creates a fresh cancellation that never fires.
Some I/O operations cannot be cancelled mid-execution. Ruby’s Net::HTTP blocks until completion or timeout — there is no way to interrupt it.
A shared singleton would be unsafe. If any code path accidentally resolves the origin, all commands using it become cancelled.
Use it for commands that wrap non-cancellable blocking I/O.
Example
token = Command.uncancellable HttpCommand.new(url).call(outlet, token)
Source
# File lib/ratatui_ruby/tea/command.rb, line 463 def self.wait(seconds, envelope) Wait.new(seconds:, envelope:) end
Creates a one-shot timer command.
Waits for seconds then sends TimerResponse to the update function. Use for delayed actions like notification dismissal or debounced search.
- seconds
-
Duration to wait (Float or Integer).
- envelope
-
Symbol to tag the result message.