Main TUI application using the ExRatatui.App reducer runtime.
Renders the dashboard layout and handles every keyboard event,
PubSub message, and side effect through a single update/2 arrow.
Pure state transitions live in BB.TUI.State; this module's job is
to wire input and async results to those transitions and return
declarative ExRatatui.Command values for IO.
Layout
┌ Safety ────────┬─ Joint Control ──────────────────────────────┐
│ ● ARMED │ Joint Type Position Range │
│ Runtime: Idle │ elbow rev -63.8° ████████░░░░░░ │
│ [a] Arm │ gripper SIM pri 30.6mm ███░░░░░░░░░░░ │
│ [d] Disarm │ ... │
├ Commands ──────┤ │
│ ▶ home Ready │ │
│ calibrate │ │
├ Events (47) ───┴── Parameters ───────────────────────────────┤
│ 18:23:12 sensor.sim │ speed 100 │
│ 18:23:11 state_m... │ controller.kp 0.5 │
└──────────────────────┴───────────────────────────────────────┘
Robot | ● Armed | idle | [q]Quit [Tab]Panel [?]HelpReducer callbacks
init/1— validates the robot module, subscribes to PubSub ({:bb, _, _}messages flow intoupdate/2as{:info, _}automatically), and snapshots ETS state. NoTask.Supervisoris required: long-running command execution is owned by the runtime viaExRatatui.Command.async/2.render/2— composes panel functions into a flat[{widget, rect}]list (the events panel contributes two panes: the list and an overlayExRatatui.Widgets.Scrollbar).update/2— the single dispatch arrow. Receives{:event, ev}for terminal input and{:info, msg}for everything else (PubSub, async results, subscription ticks,send_aftermessages). Returns{:noreply, state}for pure transitions or{:noreply, state, commands: [cmd]}when an effect should fire.subscriptions/1— declares the throbber tick interval whenever the dashboard has something animating (a:disarmingsafety state or a command currently executing), and a one-shot:sensor_flushtick whenever a sensor render is pending (see High-rate sensor handling). The runtime diffs the result against the previous one, so timers only run when needed. This replaces the previously-dormantProcess.send_after-style throbber tick.
High-rate sensor handling
Robot sensor readings arrive on the [:sensor | _] path and can be
very fast. Two mechanisms keep the UI responsive without dropping
meaningful information:
BB.TUI.State.append_event/3debounces the event log — a repeat of the same{path, payload-type}withinthrottle.debounce_ms(default 1s) is dropped, so one fast sensor cannot evict every other event from the 100-entry log.- Sensor messages update state but suppress their immediate render
(
render?: false); a one-shot:sensor_flushtick (throttle.flush_ms, default ~33ms / 30fps) armed bysubscriptions/1performs a single coalesced redraw. Every other message — key presses, command results, safety/param/state events — still renders immediately.
Both intervals live in the BB.TUI.State.Throttle substruct, so tests
can shrink or disable them (a debounce window of 0 disables it).
Async commands
Pressing Enter on a Ready command executes it when the command has no arguments, or enters an inline argument-edit mode when the command declares arguments. From edit mode, Tab/Shift+Tab cycle fields, typing edits the focused field, Enter executes with the parsed values, and Esc exits without executing.
Execution returns a Command.async/2 that calls
BB.Command.await/2, which waits on the spawned command via
GenServer.call, falls back to bb's ResultCache if the handler
finishes before we can await, and enforces the timeout internally.
The result arrives as a single {:command_result, _} info message
(success, error, or {:error, :timeout}).
Side-effect convention
Fast, fire-and-forget calls (Robot.arm/2, Robot.disarm/2,
Robot.set_actuator/4, Robot.set_parameter/4,
Robot.publish/4, Robot.force_disarm/2) are invoked inline from
update/2 rather than wrapped in a Command.async/2. They are
effectively constant-time PubSub publishes; the boilerplate of
routing through a no-op result mapper would dwarf the call. Only
Robot.execute_command/4, which monitors a spawned command process
and waits for its :DOWN, goes through Command.async/2.
Configuration
The wait window for BB.Command.await/2 is compile-time configurable
via Application.compile_env/3:
# config/config.exs
config :bb_tui, command_timeout: 30_000Default is 30_000 ms. The test suite overrides this to 100 ms in
config/test.exs to keep timeout assertions snappy. Because the
value is read with compile_env, downstream apps need to recompile
:bb_tui after changing the config (mix deps.compile bb_tui --force).