BB.TUI.State (BB.TUI v0.1.0)

Copy Markdown View Source

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

t()

@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

append_event(state, path, message)

@spec append_event(t(), list(), term()) :: t()

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.

append_to_focused_arg(state, char)

@spec append_to_focused_arg(t(), String.t()) :: t()

Appends a character to the focused argument's value.

arg_value(state, cmd_name, map)

@spec arg_value(t(), atom(), map()) :: String.t()

Returns the current string value for an argument, falling back to the argument's :default (rendered as a string).

backspace_focused_arg(state)

@spec backspace_focused_arg(t()) :: t()

Deletes the last character from the focused argument's value.

clamp_position(pos, joint)

@spec clamp_position(float(), map()) :: float()

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

clamp_to_bounds(value, arg2)

@spec clamp_to_bounds(number(), {number() | nil, number() | nil} | nil) :: number()

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

clear_events(state)

@spec clear_events(t()) :: t()

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}

clear_render_pending(state)

@spec clear_render_pending(t()) :: t()

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

cycle_focused_enum(state, direction)

@spec cycle_focused_enum(t(), :next | :prev) :: t()

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.

cycle_panel(state)

@spec cycle_panel(t()) :: t()

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

cycle_panel_back(state)

@spec cycle_panel_back(t()) :: t()

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

cycle_parameter_tab(state)

@spec cycle_parameter_tab(t()) :: t()

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

dismiss_event_detail(state)

@spec dismiss_event_detail(t()) :: t()

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

dismiss_force_disarm(state)

@spec dismiss_force_disarm(t()) :: t()

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

enter_command_edit_mode(state)

@spec enter_command_edit_mode(t()) :: t()

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

exit_command_edit_mode(state)

@spec exit_command_edit_mode(t()) :: t()

Exits argument-edit mode. Keeps commands.form_values intact.

focus_next_arg(state)

@spec focus_next_arg(t()) :: t()

Focuses the next argument field, wrapping at the end.

focus_prev_arg(state)

@spec focus_prev_arg(t()) :: t()

Focuses the previous argument field, wrapping at the start.

focused_arg(state)

@spec focused_arg(t()) :: map() | nil

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

focused_arg_enum_values(state)

@spec focused_arg_enum_values(t()) :: [atom()] | 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

joint_step(joint)

@spec joint_step(map()) :: float()

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

jump_to_panel(state, panel)

@spec jump_to_panel(t(), atom()) :: t()

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

limit_proximity(pos, joint)

@spec limit_proximity(number() | nil, map()) :: :normal | :warning | :danger

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

mark_render_pending(state)

@spec mark_render_pending(t()) :: t()

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

panel_at(n)

@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

panel_number(panel)

@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

panels()

@spec panels() :: [atom()]

Returns the ordered list of panel names for tab cycling.

Examples

iex> BB.TUI.State.panels()
[:safety, :commands, :joints, :events, :parameters]

parameter_bounds(state, path)

@spec parameter_bounds(t(), list()) :: {number() | nil, number() | nil} | nil

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

parsed_args_for_selected(state)

@spec parsed_args_for_selected(t()) :: map()

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}

put_remote_parameters(state, bridge_name, payload)

@spec put_remote_parameters(t(), atom(), [map()] | {:error, term()}) :: t()

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}]}

remote_param_bounds(param)

@spec remote_param_bounds(map()) :: {number() | nil, number() | nil} | nil

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

remote_param_id(arg1)

@spec remote_param_id(map()) :: String.t()

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(%{})
""

scroll_down(state)

@spec scroll_down(t()) :: t()

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

scroll_help_down(state)

@spec scroll_help_down(t()) :: t()

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

scroll_help_up(state)

@spec scroll_help_up(t()) :: t()

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

scroll_up(state)

@spec scroll_up(t()) :: t()

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

select_next_command(state)

@spec select_next_command(t()) :: t()

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

select_next_joint(state)

@spec select_next_joint(t()) :: t()

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

select_next_param(state)

@spec select_next_param(t()) :: t()

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

select_prev_command(state)

@spec select_prev_command(t()) :: t()

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

select_prev_joint(state)

@spec select_prev_joint(t()) :: t()

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

select_prev_param(state)

@spec select_prev_param(t()) :: t()

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

selected_command(state)

@spec selected_command(t()) :: map() | nil

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

selected_event(state)

@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

selected_joint_name(state)

@spec selected_joint_name(t()) :: atom() | 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

selected_param(state)

@spec selected_param(t()) :: {list(), term()} | 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

selected_parameter_tab(state)

@spec selected_parameter_tab(t()) :: :local | {:bridge, atom()}

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

selected_remote_param(state)

@spec selected_remote_param(t()) :: map() | nil

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

set_command_result(state, result)

@spec set_command_result(t(), {:ok, term()} | {:error, term()}) :: t()

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}

set_joint_position(state, name, position)

@spec set_joint_position(t(), atom(), float()) :: t()

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

set_joint_target(state, name, target)

@spec set_joint_target(t(), atom(), float() | nil) :: t()

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
%{}

set_parameter_tabs(state, bridges)

@spec set_parameter_tabs(t(), [map()]) :: t()

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

show_force_disarm(state)

@spec show_force_disarm(t()) :: t()

Shows the force disarm confirmation popup.

Examples

iex> BB.TUI.State.show_force_disarm(%BB.TUI.State{}).safety.confirm_force_disarm?
true

sorted_joint_names(state)

@spec sorted_joint_names(t()) :: [atom()]

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]

start_command(state, marker)

@spec start_command(t(), term()) :: t()

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}

tick_throbber(state)

@spec tick_throbber(t()) :: t()

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

toggle_event_detail(state)

@spec toggle_event_detail(t()) :: t()

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

toggle_events_pause(state)

@spec toggle_events_pause(t()) :: t()

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

toggle_help(state)

@spec toggle_help(t()) :: t()

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

update_parameters(state, new_parameters)

@spec update_parameters(t(), [{list(), term()}]) :: t()

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}]

update_positions(state, new_positions)

@spec update_positions(t(), %{required(atom()) => float()}) :: t()

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

update_safety(state, safety_state, runtime_state)

@spec update_safety(t(), atom(), atom()) :: t()

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}