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.
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.
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.
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.
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.
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.
Updates safety and runtime state from a state machine message.
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, parameters: BB.TUI.State.Parameters.t(), robot: module(), robot_struct: term(), safety: BB.TUI.State.Safety.t(), throttle: BB.TUI.State.Throttle.t(), ui: BB.TUI.State.UI.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
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
@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}
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(%{})
""
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}
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
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}