State struct and pure update functions for the BB TUI dashboard.
All state transitions are pure functions — no side effects, no process
communication. The BB.TUI.App module handles IO and delegates here
for state changes.
High-rate-stream controls live in the BB.TUI.State.Throttle substruct
(throttle.debounce_ms/throttle.last_seen back append_event/3's log
debouncing; throttle.render_pending?/throttle.flush_ms drive
BB.TUI.App's coalesced sensor re-render). See BB.TUI.App for the flow.
Examples
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{active_panel: :safety, show_help?: false}}
iex> state.ui.active_panel
:safety
Summary
Functions
Appends an event to the event list, capping at 100.
Appends a character to the focused argument's value.
Returns the current string value for an argument, falling back to the
argument's :default (rendered as a string).
Deletes the last character from the focused argument's value.
Clamps a position value within a joint's limits.
Clamps a numeric value into {min, max} bounds. Either bound may be
nil to leave that side open. A nil bounds tuple returns value
unchanged.
Clears all events and resets scroll offset.
Clears the pending-render flag once the coalesced frame has been rendered.
Cycles the focused argument to the next (or previous) value in its enum list. A no-op when not in edit mode or when the focused arg isn't enum-typed.
Cycles the active panel to the next one in order.
Cycles the active panel to the previous one in order (Shift+Tab).
Cycles to the next parameter tab, wrapping back to :local.
Cycles the visualization render mode to the next one in order, wrapping around.
Dismisses the event detail popup.
Dismisses the force disarm confirmation popup.
Enters argument-edit mode for the selected command, if it has arguments.
Exits argument-edit mode. Keeps commands.form_values intact.
Focuses the next argument field, wrapping at the end.
Focuses the previous argument field, wrapping at the start.
Returns the currently-focused command argument map, or nil when the
selected command has no arguments.
Returns the enum-value list for the focused argument when the arg is
enum-typed ({:in, [...]} in the underlying Spark schema), otherwise
nil.
Computes the step size for a joint based on its limits.
Jumps directly to the named panel, leaving everything else unchanged. A no-op when the target isn't a known panel — so a stray key never silently parks the dashboard in an unreachable state.
Returns the proximity of a joint position to its nearest limit.
Flags that sensor-driven state changed and a coalesced re-render is due.
Switches to the next top-level tab, wrapping around.
Orbits the visualization camera by yaw/pitch deltas (radians).
Returns the panel atom at a 1-based index, or nil when the index is
out of range. Mirror of panel_number/1, used by the number-key
jump handler.
Returns the 1-based number of a panel, suitable for number-key jump
hints in panel titles and help text. Returns nil for unknown
panels.
Returns the ordered list of panel names for tab cycling.
Returns {min, max} bounds for the parameter at path when the
Spark-style metadata declares them, otherwise nil.
Returns the form values for the selected command, parsed by type.
Switches to the previous top-level tab, wrapping around.
Records the latest observed entry for a renderer-supplied slot_key.
Stores the latest remote-parameter snapshot for a bridge.
Returns {min, max} bounds for a remote parameter when the bridge
carries them as flat :min / :max keys (matching bb_liveview's
shape), otherwise nil. Either bound may be nil to leave that side
open.
Returns the sort key used when rendering a remote parameter row.
Returns the ordered list of Viewport3D render modes.
Returns the renderer module registered for path, or nil.
Resets the visualization camera to the default framing.
Scrolls the event panel down (newer events).
Scrolls the help popup down by one line.
Scrolls the help popup up by one line.
Scrolls the event panel up (older events).
Selects the next command in the list.
Selects the next joint in the sorted list.
Selects the next parameter in the sorted list.
Selects the previous command in the list.
Selects the previous joint in the sorted list.
Selects the previous parameter in the sorted list.
Returns the currently selected command map, or nil.
Returns the currently selected event, or nil if no events.
Returns the name of the currently selected joint, or nil if no joints exist.
Returns the currently selected parameter as {path, value}, or nil.
Returns the currently selected parameter tab.
Returns the currently-focused remote parameter for the selected
bridge tab, or nil when the active tab is :local, the bridge has
no fetched list yet, or the fetch errored.
Sets the command execution result.
Updates the position of a specific joint in state.
Records the last-commanded target position for a joint. The panel
renders it as a secondary marker on the position bar so the operator
can see what the joint is moving toward. Pass nil to clear the
target (e.g. when the joint has reached it).
Replaces the discovered parameter tabs and resets the selected tab.
Shows the force disarm confirmation popup.
Returns sorted joint names, matching the render order of the joints panel.
Marks a command as currently executing.
Returns the ordered list of top-level tabs.
Increments the throbber animation step.
Toggles the event detail popup for the currently selected event.
Toggles the event stream pause state.
Toggles the help overlay.
Updates parameters from a parameter list.
Updates joint positions from a sensor message.
Records the latest battery or power telemetry from a sensor payload.
Updates safety and runtime state from a state machine message.
Returns the visualization-tab camera, defaulting when unset.
Returns the visualization render mode.
Zooms the visualization camera; a positive delta moves farther from the robot.
Types
@type t() :: %BB.TUI.State{ commands: BB.TUI.State.Commands.t(), events: BB.TUI.State.Events.t(), joints: BB.TUI.State.Joints.t(), node: node() | nil, observed: %{optional(term()) => %{display: map(), meta: map()}}, parameters: BB.TUI.State.Parameters.t(), power: BB.TUI.State.Power.t(), renderers: %{optional([atom()]) => module()}, robot: module(), robot_struct: term(), safety: BB.TUI.State.Safety.t(), throttle: BB.TUI.State.Throttle.t(), ui: BB.TUI.State.UI.t(), viz: BB.TUI.State.Viz.t() }
Functions
Appends an event to the event list, capping at 100.
Events are prepended (newest first) and the list is trimmed to 100 entries. When events are paused, the event is dropped.
Under high-rate streams, a repeat of the same {path, payload-type} seen
within throttle.debounce_ms (default 1s) is dropped so a fast sensor
cannot flood the log; distinct paths or payload types always pass through.
A debounce window of 0 disables this.
Appends a character to the focused argument's value.
Returns the current string value for an argument, falling back to the
argument's :default (rendered as a string).
Deletes the last character from the focused argument's value.
Clamps a position value within a joint's limits.
Returns the position unchanged if the joint has no limits.
Examples
iex> BB.TUI.State.clamp_position(2.0, %{limits: %{lower: -1.0, upper: 1.0}})
1.0
iex> BB.TUI.State.clamp_position(-2.0, %{limits: %{lower: -1.0, upper: 1.0}})
-1.0
iex> BB.TUI.State.clamp_position(99.0, %{type: :continuous})
99.0
Clamps a numeric value into {min, max} bounds. Either bound may be
nil to leave that side open. A nil bounds tuple returns value
unchanged.
Examples
iex> BB.TUI.State.clamp_to_bounds(5, {0, 10})
5
iex> BB.TUI.State.clamp_to_bounds(-3, {0, 10})
0
iex> BB.TUI.State.clamp_to_bounds(99, {0, 10})
10
iex> BB.TUI.State.clamp_to_bounds(99, {nil, 10})
10
iex> BB.TUI.State.clamp_to_bounds(-3, {0, nil})
0
iex> BB.TUI.State.clamp_to_bounds(7, nil)
7
Clears all events and resets scroll offset.
Examples
iex> list = [{~U[2026-01-01 00:00:00Z], [:test], %{}}]
iex> state = %BB.TUI.State{events: %BB.TUI.State.Events{list: list, scroll_offset: 5}}
iex> new_state = BB.TUI.State.clear_events(state)
iex> {new_state.events.list, new_state.events.scroll_offset}
{[], 0}
Clears the pending-render flag once the coalesced frame has been rendered.
iex> state = %BB.TUI.State{throttle: %BB.TUI.State.Throttle{render_pending?: true}}
iex> BB.TUI.State.clear_render_pending(state).throttle.render_pending?
false
Cycles the focused argument to the next (or previous) value in its enum list. A no-op when not in edit mode or when the focused arg isn't enum-typed.
Stores the chosen value as the leading-colon atom literal (":foo")
so parsed_args_for_selected/1 decodes it back to :foo when the
command executes.
Cycles the active panel to the next one in order.
When active_panel is unknown (e.g. set out-of-band to a stale
value), resets to the first panel.
Examples
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{active_panel: :safety}}
iex> BB.TUI.State.cycle_panel(state).ui.active_panel
:commands
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{active_panel: :parameters}}
iex> BB.TUI.State.cycle_panel(state).ui.active_panel
:safety
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{active_panel: :unknown}}
iex> BB.TUI.State.cycle_panel(state).ui.active_panel
:safety
Cycles the active panel to the previous one in order (Shift+Tab).
When active_panel is unknown, resets to the last panel so a stale
state still lands somewhere navigable.
Examples
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{active_panel: :commands}}
iex> BB.TUI.State.cycle_panel_back(state).ui.active_panel
:safety
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{active_panel: :safety}}
iex> BB.TUI.State.cycle_panel_back(state).ui.active_panel
:parameters
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{active_panel: :unknown}}
iex> BB.TUI.State.cycle_panel_back(state).ui.active_panel
:parameters
Cycles to the next parameter tab, wrapping back to :local.
Resets param_selected so the new tab starts at the first row.
Examples
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{tabs: [:local, {:bridge, :mavlink}], tab_selected: 0, selected: 3}}
iex> next = BB.TUI.State.cycle_parameter_tab(state)
iex> next.parameters.tab_selected
1
iex> next.parameters.selected
0
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{tabs: [:local, {:bridge, :mavlink}], tab_selected: 1}}
iex> BB.TUI.State.cycle_parameter_tab(state).parameters.tab_selected
0
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{tabs: [:local], tab_selected: 0}}
iex> BB.TUI.State.cycle_parameter_tab(state).parameters.tab_selected
0
Cycles the visualization render mode to the next one in order, wrapping around.
Examples
iex> state = %BB.TUI.State{viz: %BB.TUI.State.Viz{render_mode: :auto}}
iex> BB.TUI.State.cycle_render_mode(state).viz.render_mode
:kitty
iex> state = %BB.TUI.State{viz: %BB.TUI.State.Viz{render_mode: :ascii}}
iex> BB.TUI.State.cycle_render_mode(state).viz.render_mode
:auto
Dismisses the event detail popup.
Examples
iex> state = %BB.TUI.State{events: %BB.TUI.State.Events{show_detail?: true}}
iex> BB.TUI.State.dismiss_event_detail(state).events.show_detail?
false
Dismisses the force disarm confirmation popup.
Examples
iex> state = %BB.TUI.State{safety: %BB.TUI.State.Safety{confirm_force_disarm?: true}}
iex> BB.TUI.State.dismiss_force_disarm(state).safety.confirm_force_disarm?
false
Enters argument-edit mode for the selected command, if it has arguments.
No-op when the selected command has no arguments — argument-less commands execute directly on Enter.
Examples
iex> cmd = %{name: :move, arguments: [%{name: :angle, type: "float", default: 0.0}]}
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{available: [cmd], selected: 0}}
iex> BB.TUI.State.enter_command_edit_mode(state).commands.edit_mode?
true
iex> cmd = %{name: :home, arguments: []}
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{available: [cmd], selected: 0}}
iex> BB.TUI.State.enter_command_edit_mode(state).commands.edit_mode?
false
Exits argument-edit mode. Keeps commands.form_values intact.
Focuses the next argument field, wrapping at the end.
Focuses the previous argument field, wrapping at the start.
Returns the currently-focused command argument map, or nil when the
selected command has no arguments.
Examples
iex> cmd = %{name: :move, arguments: [%{name: :angle}, %{name: :side}]}
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{available: [cmd], selected: 0, focused_arg: 1}}
iex> BB.TUI.State.focused_arg(state)
%{name: :side}
iex> BB.TUI.State.focused_arg(%BB.TUI.State{commands: %BB.TUI.State.Commands{available: []}})
nil
Returns the enum-value list for the focused argument when the arg is
enum-typed ({:in, [...]} in the underlying Spark schema), otherwise
nil.
Examples
iex> cmd = %{name: :move, arguments: [%{name: :side, enum_values: [:left, :right]}]}
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{available: [cmd], selected: 0, focused_arg: 0}}
iex> BB.TUI.State.focused_arg_enum_values(state)
[:left, :right]
iex> cmd = %{name: :move, arguments: [%{name: :angle, enum_values: nil}]}
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{available: [cmd], selected: 0, focused_arg: 0}}
iex> BB.TUI.State.focused_arg_enum_values(state)
nil
Computes the step size for a joint based on its limits.
Returns (upper - lower) / 100 for joints with limits, or a default
step of π/50 (~3.6°) for unlimited joints.
Examples
iex> BB.TUI.State.joint_step(%{limits: %{lower: -1.0, upper: 1.0}})
0.02
iex> BB.TUI.State.joint_step(%{type: :continuous})
:math.pi() / 50
Jumps directly to the named panel, leaving everything else unchanged. A no-op when the target isn't a known panel — so a stray key never silently parks the dashboard in an unreachable state.
Examples
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{active_panel: :safety}}
iex> BB.TUI.State.jump_to_panel(state, :events).ui.active_panel
:events
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{active_panel: :safety}}
iex> BB.TUI.State.jump_to_panel(state, :unknown).ui.active_panel
:safety
Returns the proximity of a joint position to its nearest limit.
Returns :danger when within 5.0% of a limit,
:warning when within 15.0% of a limit,
or :normal otherwise.
Joints without limits always return :normal.
Examples
iex> joint = %{limits: %{lower: -1.0, upper: 1.0}}
iex> BB.TUI.State.limit_proximity(0.0, joint)
:normal
iex> joint = %{limits: %{lower: -1.0, upper: 1.0}}
iex> BB.TUI.State.limit_proximity(0.75, joint)
:warning
iex> joint = %{limits: %{lower: -1.0, upper: 1.0}}
iex> BB.TUI.State.limit_proximity(0.96, joint)
:danger
iex> joint = %{limits: %{lower: -1.0, upper: 1.0}}
iex> BB.TUI.State.limit_proximity(-0.96, joint)
:danger
iex> BB.TUI.State.limit_proximity(99.0, %{type: :continuous})
:normal
Flags that sensor-driven state changed and a coalesced re-render is due.
The reducer returns render?: false for sensor messages and sets this
flag; BB.TUI.App's subscriptions callback then arms the one-shot
:sensor_flush tick that performs the single batched render.
iex> BB.TUI.State.mark_render_pending(%BB.TUI.State{}).throttle.render_pending?
true
Switches to the next top-level tab, wrapping around.
Examples
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{active_tab: :control}}
iex> BB.TUI.State.next_tab(state).ui.active_tab
:visualization
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{active_tab: :visualization}}
iex> BB.TUI.State.next_tab(state).ui.active_tab
:control
Orbits the visualization camera by yaw/pitch deltas (radians).
@spec panel_at(pos_integer()) :: atom() | nil
Returns the panel atom at a 1-based index, or nil when the index is
out of range. Mirror of panel_number/1, used by the number-key
jump handler.
Examples
iex> BB.TUI.State.panel_at(1)
:safety
iex> BB.TUI.State.panel_at(5)
:parameters
iex> BB.TUI.State.panel_at(9)
nil
@spec panel_number(atom()) :: pos_integer() | nil
Returns the 1-based number of a panel, suitable for number-key jump
hints in panel titles and help text. Returns nil for unknown
panels.
Examples
iex> BB.TUI.State.panel_number(:safety)
1
iex> BB.TUI.State.panel_number(:parameters)
5
iex> BB.TUI.State.panel_number(:unknown)
nil
@spec panels() :: [atom()]
Returns the ordered list of panel names for tab cycling.
Examples
iex> BB.TUI.State.panels()
[:safety, :commands, :joints, :events, :parameters]
Returns {min, max} bounds for the parameter at path when the
Spark-style metadata declares them, otherwise nil.
Looks at state.parameters.metadata[path].type for the standard
{head, opts} shape used by Spark.Options and extracts the
:min / :max keyword values. Either bound may be absent (returned
as nil); both absent collapses to nil (no bounds).
Examples
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{metadata: %{[:speed] => %{type: {:integer, [min: 0, max: 100]}}}}}
iex> BB.TUI.State.parameter_bounds(state, [:speed])
{0, 100}
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{metadata: %{[:gain] => %{type: {:float, [min: 0.0]}}}}}
iex> BB.TUI.State.parameter_bounds(state, [:gain])
{0.0, nil}
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{metadata: %{[:speed] => %{type: :integer}}}}
iex> BB.TUI.State.parameter_bounds(state, [:speed])
nil
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{metadata: %{[:speed] => %{type: {:integer, [doc: "rpm"]}}}}}
iex> BB.TUI.State.parameter_bounds(state, [:speed])
nil
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{metadata: %{}}}
iex> BB.TUI.State.parameter_bounds(state, [:unknown])
nil
Returns the form values for the selected command, parsed by type.
Mirrors BB.LiveView.Components.Command's parse_value/1:
"true"/"false" → boolean, ":foo" → atom, numeric → number,
else string.
Falls back to arg.default for arguments the user has not touched.
Examples
iex> cmd = %{
...> name: :move,
...> arguments: [
...> %{name: :angle, type: "float", default: 1.5},
...> %{name: :side, type: "atom", default: :left}
...> ]
...> }
iex> state = %BB.TUI.State{
...> commands: %BB.TUI.State.Commands{
...> available: [cmd],
...> selected: 0,
...> form_values: %{move: %{angle: "2.5"}}
...> }
...> }
iex> BB.TUI.State.parsed_args_for_selected(state)
%{angle: 2.5, side: :left}
Switches to the previous top-level tab, wrapping around.
Examples
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{active_tab: :control}}
iex> BB.TUI.State.prev_tab(state).ui.active_tab
:visualization
Records the latest observed entry for a renderer-supplied slot_key.
The dashboard keeps only the freshest %{display: ..., meta: ...} per
slot_key (a faster slot overwrites the previous reading rather than
accumulating; the event log carries the history). The status bar reads
state.observed to surface the freshest slot at a glance.
bb_tui never inspects display or meta beyond the generic keys the status
bar reads (display.label, meta.seq, meta.freshness) — both are supplied
verbatim by the consumer's renderer.
Examples
iex> state = BB.TUI.State.put_observed(%BB.TUI.State{}, {:wheels, :imu},
...> %{display: %{label: "imu"}, meta: %{seq: 1, freshness: :fresh}})
iex> state.observed
%{{:wheels, :imu} => %{display: %{label: "imu"}, meta: %{seq: 1, freshness: :fresh}}}
Stores the latest remote-parameter snapshot for a bridge.
Examples
iex> next = BB.TUI.State.put_remote_parameters(%BB.TUI.State{}, :mavlink, [%{id: "PITCH_P", value: 0.1}])
iex> next.parameters.remote
%{mavlink: [%{id: "PITCH_P", value: 0.1}]}
Returns {min, max} bounds for a remote parameter when the bridge
carries them as flat :min / :max keys (matching bb_liveview's
shape), otherwise nil. Either bound may be nil to leave that side
open.
Examples
iex> BB.TUI.State.remote_param_bounds(%{id: "X", value: 1, min: 0, max: 100})
{0, 100}
iex> BB.TUI.State.remote_param_bounds(%{id: "X", value: 1, min: 0})
{0, nil}
iex> BB.TUI.State.remote_param_bounds(%{id: "X", value: 1})
nil
Returns the sort key used when rendering a remote parameter row.
Bridges typically use string ids ("PITCH_P"), but some (BB.Bridge
implementations are free to use atoms) return atom ids. Both
normalize to a binary so the panel and the navigation index agree on
ordering.
Examples
iex> BB.TUI.State.remote_param_id(%{id: "PITCH_P"})
"PITCH_P"
iex> BB.TUI.State.remote_param_id(%{id: :gain})
"gain"
iex> BB.TUI.State.remote_param_id(%{})
""
@spec render_modes() :: [atom()]
Returns the ordered list of Viewport3D render modes.
Examples
iex> BB.TUI.State.render_modes()
[:auto, :kitty, :sixel, :iterm2, :half_block, :braille, :ascii]
Returns the renderer module registered for path, or nil.
Registered renderers are keyed by a path prefix; a message's path matches
a prefix when the prefix is a leading sublist of the path ([:demo] matches
[:demo], [:demo, :imu], [:demo, :imu, 1], …). When several prefixes
match, the longest wins — a routing-table style most-specific match — so a
consumer can register a broad [:demo] renderer and override a narrower
[:demo, :raw] one. Returns nil when no prefix matches, so the caller falls
through to the built-in handling.
Examples
iex> state = %BB.TUI.State{renderers: %{[:demo] => A, [:demo, :raw] => B}}
iex> BB.TUI.State.renderer_for(state, [:demo, :imu])
A
iex> state = %BB.TUI.State{renderers: %{[:demo] => A, [:demo, :raw] => B}}
iex> BB.TUI.State.renderer_for(state, [:demo, :raw, 1])
B
iex> state = %BB.TUI.State{renderers: %{[:demo] => A}}
iex> BB.TUI.State.renderer_for(state, [:other])
nil
iex> BB.TUI.State.renderer_for(%BB.TUI.State{}, [:demo])
nil
Resets the visualization camera to the default framing.
Scrolls the event panel down (newer events).
Examples
iex> list = [{~U[2026-01-01 00:00:00Z], [:test], %{}}]
iex> state = %BB.TUI.State{events: %BB.TUI.State.Events{list: list, scroll_offset: 0}}
iex> BB.TUI.State.scroll_down(state).events.scroll_offset
0
Scrolls the help popup down by one line.
Examples
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{show_help?: true, help_scroll_offset: 0}}
iex> BB.TUI.State.scroll_help_down(state).ui.help_scroll_offset
1
Scrolls the help popup up by one line.
Examples
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{show_help?: true, help_scroll_offset: 0}}
iex> BB.TUI.State.scroll_help_up(state).ui.help_scroll_offset
0
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{show_help?: true, help_scroll_offset: 5}}
iex> BB.TUI.State.scroll_help_up(state).ui.help_scroll_offset
4
Scrolls the event panel up (older events).
Examples
iex> state = %BB.TUI.State{events: %BB.TUI.State.Events{scroll_offset: 0}}
iex> BB.TUI.State.scroll_up(state).events.scroll_offset
0
iex> state = %BB.TUI.State{events: %BB.TUI.State.Events{scroll_offset: 5}}
iex> BB.TUI.State.scroll_up(state).events.scroll_offset
4
Selects the next command in the list.
Examples
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{selected: 0, available: [%{name: :a}, %{name: :b}]}}
iex> BB.TUI.State.select_next_command(state).commands.selected
1
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{selected: 1, available: [%{name: :a}, %{name: :b}]}}
iex> BB.TUI.State.select_next_command(state).commands.selected
1
Selects the next joint in the sorted list.
Examples
iex> entries = %{a: %{joint: %{}, position: 0.0}, b: %{joint: %{}, position: 0.0}}
iex> state = %BB.TUI.State{joints: %BB.TUI.State.Joints{entries: entries, selected: 0}}
iex> BB.TUI.State.select_next_joint(state).joints.selected
1
iex> entries = %{a: %{joint: %{}, position: 0.0}, b: %{joint: %{}, position: 0.0}}
iex> state = %BB.TUI.State{joints: %BB.TUI.State.Joints{entries: entries, selected: 1}}
iex> BB.TUI.State.select_next_joint(state).joints.selected
1
Selects the next parameter in the sorted list.
Examples
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{list: [{[:a], 1}, {[:b], 2}], selected: 0}}
iex> BB.TUI.State.select_next_param(state).parameters.selected
1
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{list: [{[:a], 1}, {[:b], 2}], selected: 1}}
iex> BB.TUI.State.select_next_param(state).parameters.selected
1
Selects the previous command in the list.
Examples
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{selected: 1}}
iex> BB.TUI.State.select_prev_command(state).commands.selected
0
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{selected: 0}}
iex> BB.TUI.State.select_prev_command(state).commands.selected
0
Selects the previous joint in the sorted list.
Examples
iex> state = %BB.TUI.State{joints: %BB.TUI.State.Joints{entries: %{a: %{joint: %{}, position: 0.0}}, selected: 1}}
iex> BB.TUI.State.select_prev_joint(state).joints.selected
0
iex> state = %BB.TUI.State{joints: %BB.TUI.State.Joints{entries: %{a: %{joint: %{}, position: 0.0}}, selected: 0}}
iex> BB.TUI.State.select_prev_joint(state).joints.selected
0
Selects the previous parameter in the sorted list.
Examples
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{list: [{[:a], 1}], selected: 1}}
iex> BB.TUI.State.select_prev_param(state).parameters.selected
0
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{list: [{[:a], 1}], selected: 0}}
iex> BB.TUI.State.select_prev_param(state).parameters.selected
0
Returns the currently selected command map, or nil.
Examples
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{available: [%{name: :a}, %{name: :b}], selected: 1}}
iex> BB.TUI.State.selected_command(state)
%{name: :b}
iex> BB.TUI.State.selected_command(%BB.TUI.State{commands: %BB.TUI.State.Commands{available: []}})
nil
@spec selected_event(t()) :: {DateTime.t(), list(), term()} | nil
Returns the currently selected event, or nil if no events.
Examples
iex> list = [{~U[2026-01-01 00:00:00Z], [:test], %{payload: :ok}}]
iex> state = %BB.TUI.State{events: %BB.TUI.State.Events{list: list, scroll_offset: 0}}
iex> {_, [:test], _} = BB.TUI.State.selected_event(state)
iex> state = %BB.TUI.State{events: %BB.TUI.State.Events{list: [], scroll_offset: 0}}
iex> BB.TUI.State.selected_event(state)
nil
Returns the name of the currently selected joint, or nil if no joints exist.
Examples
iex> entries = %{elbow: %{joint: %{}, position: 0.0}, shoulder: %{joint: %{}, position: 0.0}}
iex> state = %BB.TUI.State{joints: %BB.TUI.State.Joints{entries: entries, selected: 1}}
iex> BB.TUI.State.selected_joint_name(state)
:shoulder
iex> state = %BB.TUI.State{joints: %BB.TUI.State.Joints{entries: %{}, selected: 0}}
iex> BB.TUI.State.selected_joint_name(state)
nil
Returns the currently selected parameter as {path, value}, or nil.
Parameters are sorted by path to match the render order.
Examples
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{list: [{[:b], 2}, {[:a], 1}], selected: 0}}
iex> BB.TUI.State.selected_param(state)
{[:a], 1}
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{list: [], selected: 0}}
iex> BB.TUI.State.selected_param(state)
nil
Returns the currently selected parameter tab.
Examples
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{tabs: [:local, {:bridge, :mavlink}], tab_selected: 1}}
iex> BB.TUI.State.selected_parameter_tab(state)
{:bridge, :mavlink}
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{tabs: [:local], tab_selected: 0}}
iex> BB.TUI.State.selected_parameter_tab(state)
:local
Returns the currently-focused remote parameter for the selected
bridge tab, or nil when the active tab is :local, the bridge has
no fetched list yet, or the fetch errored.
Sort order matches the panel's render (Enum.sort_by(remote_param_id/1)).
Examples
iex> remote = [%{id: "ROLL_P", value: 0.0}, %{id: "PITCH_P", value: 0.1}]
iex> state = %BB.TUI.State{
...> parameters: %BB.TUI.State.Parameters{
...> tabs: [:local, {:bridge, :mavlink}],
...> tab_selected: 1,
...> remote: %{mavlink: remote},
...> selected: 0
...> }
...> }
iex> BB.TUI.State.selected_remote_param(state)
%{id: "PITCH_P", value: 0.1}
iex> state = %BB.TUI.State{parameters: %BB.TUI.State.Parameters{tabs: [:local], tab_selected: 0}}
iex> BB.TUI.State.selected_remote_param(state)
nil
iex> state = %BB.TUI.State{
...> parameters: %BB.TUI.State.Parameters{
...> tabs: [:local, {:bridge, :mavlink}],
...> tab_selected: 1,
...> remote: %{mavlink: {:error, :nodedown}}
...> }
...> }
iex> BB.TUI.State.selected_remote_param(state)
nil
Sets the command execution result.
Examples
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{result: nil, executing: self()}}
iex> new_state = BB.TUI.State.set_command_result(state, {:ok, :done})
iex> {new_state.commands.result, new_state.commands.executing}
{{:ok, :done}, nil}
Updates the position of a specific joint in state.
Examples
iex> entries = %{shoulder: %{joint: %{}, position: 0.0, target: nil}}
iex> state = %BB.TUI.State{joints: %BB.TUI.State.Joints{entries: entries}}
iex> BB.TUI.State.set_joint_position(state, :shoulder, 1.5).joints.entries.shoulder.position
1.5
Records the last-commanded target position for a joint. The panel
renders it as a secondary marker on the position bar so the operator
can see what the joint is moving toward. Pass nil to clear the
target (e.g. when the joint has reached it).
Examples
iex> entries = %{shoulder: %{joint: %{}, position: 0.0, target: nil}}
iex> state = %BB.TUI.State{joints: %BB.TUI.State.Joints{entries: entries}}
iex> BB.TUI.State.set_joint_target(state, :shoulder, 1.5).joints.entries.shoulder.target
1.5
iex> state = %BB.TUI.State{joints: %BB.TUI.State.Joints{entries: %{}}}
iex> BB.TUI.State.set_joint_target(state, :missing, 1.5).joints.entries
%{}
Replaces the discovered parameter tabs and resets the selected tab.
Always keeps :local at the head, so cycling never lands in a state
where no local-parameter view is reachable.
Examples
iex> next = BB.TUI.State.set_parameter_tabs(%BB.TUI.State{}, [%{name: :mavlink}])
iex> next.parameters.tabs
[:local, {:bridge, :mavlink}]
iex> next.parameters.tab_selected
0
Shows the force disarm confirmation popup.
Examples
iex> BB.TUI.State.show_force_disarm(%BB.TUI.State{}).safety.confirm_force_disarm?
true
Returns sorted joint names, matching the render order of the joints panel.
Examples
iex> entries = %{elbow: %{joint: %{}, position: 0.0}, shoulder: %{joint: %{}, position: 0.0}}
iex> state = %BB.TUI.State{joints: %BB.TUI.State.Joints{entries: entries}}
iex> BB.TUI.State.sorted_joint_names(state)
[:elbow, :shoulder]
Marks a command as currently executing.
Examples
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{executing: nil, result: {:ok, :old}}}
iex> pid = self()
iex> new_state = BB.TUI.State.start_command(state, pid)
iex> {new_state.commands.executing, new_state.commands.result}
{pid, nil}
@spec tabs() :: [atom()]
Returns the ordered list of top-level tabs.
Examples
iex> BB.TUI.State.tabs()
[:control, :visualization]
Increments the throbber animation step.
Examples
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{throbber_step: 3}}
iex> BB.TUI.State.tick_throbber(state).ui.throbber_step
4
Toggles the event detail popup for the currently selected event.
Examples
iex> state = %BB.TUI.State{events: %BB.TUI.State.Events{show_detail?: false}}
iex> BB.TUI.State.toggle_event_detail(state).events.show_detail?
true
Toggles the event stream pause state.
Examples
iex> state = %BB.TUI.State{events: %BB.TUI.State.Events{paused?: false}}
iex> BB.TUI.State.toggle_events_pause(state).events.paused?
true
iex> state = %BB.TUI.State{events: %BB.TUI.State.Events{paused?: true}}
iex> BB.TUI.State.toggle_events_pause(state).events.paused?
false
Toggles the help overlay.
Examples
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{show_help?: false}}
iex> BB.TUI.State.toggle_help(state).ui.show_help?
true
iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{show_help?: true}}
iex> BB.TUI.State.toggle_help(state).ui.show_help?
false
Updates parameters from a parameter list.
BB.Parameter.list/2 returns {path, metadata} tuples where metadata
is a map carrying :value plus schema-derived fields like :type,
:doc, and :default. The plain value is mirrored into
state.parameters.list so navigation code keeps working with simple
{path, value} tuples, while the rest of the metadata is stashed in
state.parameters.metadata keyed by path. Plain-value inputs (no
metadata map) leave the metadata side-channel untouched for that path.
Examples
iex> next = BB.TUI.State.update_parameters(%BB.TUI.State{}, [{[:speed], %{value: 100, type: :integer, doc: "rpm"}}])
iex> next.parameters.list
[{[:speed], 100}]
iex> next.parameters.metadata
%{[:speed] => %{type: :integer, doc: "rpm", default: nil}}
iex> BB.TUI.State.update_parameters(%BB.TUI.State{}, [{[:speed], 42}]).parameters.list
[{[:speed], 42}]
Updates joint positions from a sensor message.
Only updates joints that exist in the current state; unknown joint names are silently ignored.
Examples
iex> entries = %{shoulder: %{joint: %{}, position: 0.0}}
iex> state = %BB.TUI.State{joints: %BB.TUI.State.Joints{entries: entries}}
iex> BB.TUI.State.update_positions(state, %{shoulder: 42.0}).joints.entries.shoulder.position
42.0
Records the latest battery or power telemetry from a sensor payload.
Recognizes BB.Message.Sensor.BatteryState and BB.Message.Sensor.PowerState
payloads and stashes the freshest of each in state.power; any other payload
passes through untouched. BB.TUI.App calls this for every [:sensor | _]
message, and the status bar renders the result.
Examples
iex> battery = %BB.Message.Sensor.BatteryState{voltage: 12.0, percentage: 0.8}
iex> BB.TUI.State.update_power(%BB.TUI.State{}, battery).power.battery.percentage
0.8
iex> reading = %BB.Message.Sensor.PowerState{voltage: 11.5, current: 2.0}
iex> BB.TUI.State.update_power(%BB.TUI.State{}, reading).power.power.voltage
11.5
iex> BB.TUI.State.update_power(%BB.TUI.State{}, %{names: [:a], positions: [0.0]}).power
%BB.TUI.State.Power{}
Updates safety and runtime state from a state machine message.
Examples
iex> state = BB.TUI.State.update_safety(%BB.TUI.State{}, :armed, :idle)
iex> {state.safety.state, state.safety.runtime}
{:armed, :idle}
@spec viz_camera(t()) :: ExRatatui.ThreeD.Camera.t()
Returns the visualization-tab camera, defaulting when unset.
Returns the visualization render mode.
Zooms the visualization camera; a positive delta moves farther from the robot.