defmodule BB.TUI.Panels.Commands do
@moduledoc """
Commands panel — displays available robot commands with execution state.
Shows each command name with a Ready / Blocked badge based on the
current runtime state. Each row is a `%ExRatatui.Text.Line{}` so
the badge can render in its own color (green for Ready, dim for
Blocked) without affecting the surrounding text. The trailing
result / executing rows render as colored badges too — green ✔ for
success, red ✖ for failure, yellow ⏳ while a command is in flight.
Argument editing for commands with declared arguments is handled by
`BB.TUI.Panels.CommandEdit`, which renders as a popup above this
panel while `state.commands.edit_mode?` is true.
Pure function — takes state, returns a widget struct.
"""
alias BB.TUI.State
alias BB.TUI.Theme
alias ExRatatui.Style
alias ExRatatui.Text.{Line, Span}
alias ExRatatui.Widgets.Block
alias ExRatatui.Widgets.List, as: WidgetList
@doc """
Renders the commands panel as a List widget. Arrow keys select,
Enter executes when focused.
## Examples
iex> state = %BB.TUI.State{
...> commands: %BB.TUI.State.Commands{
...> available: [%{name: :home, allowed_states: [:idle]}],
...> selected: 0, result: nil, executing: nil
...> },
...> safety: %BB.TUI.State.Safety{runtime: :idle}
...> }
iex> %ExRatatui.Widgets.List{items: [%ExRatatui.Text.Line{spans: spans}]} =
...> BB.TUI.Panels.Commands.render(state, false)
iex> Enum.map_join(spans, "", & &1.content)
"home ● Ready"
"""
@spec render(State.t(), boolean()) :: struct()
def render(%State{} = state, focused?) do
items = format_commands(state)
result_line =
case state.commands.result do
{:ok, result} -> [result_line(:ok, "✔ #{inspect(result, limit: 50)}")]
{:error, reason} -> [result_line(:error, "✖ #{inspect(reason, limit: 50)}")]
nil -> []
end
executing_line =
if state.commands.executing do
[executing_line()]
else
[]
end
all_items = items ++ executing_line ++ result_line
%WidgetList{
items: all_items,
selected: if(state.commands.available != [], do: state.commands.selected),
highlight_style: Theme.highlight_style(),
highlight_symbol: "\u{25B6} ",
block: %Block{
title: title_line(state),
borders: [:all],
border_type: :rounded,
border_style: Theme.border_style(focused?)
}
}
end
@doc """
Returns the title string for the commands panel (legacy, plain string).
## Examples
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{available: [%{name: :a}, %{name: :b}]}}
iex> BB.TUI.Panels.Commands.title(state)
" Commands (2) "
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{available: []}}
iex> BB.TUI.Panels.Commands.title(state)
" Commands "
"""
@spec title(State.t()) :: String.t()
def title(%State{commands: %{available: []}}), do: " Commands "
def title(%State{commands: %{available: cmds}}), do: " Commands (#{length(cmds)}) "
@doc ~S"""
Returns the rich-text panel title — the count renders bold-cyan.
## Examples
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{available: [%{name: :a}]}}
iex> %ExRatatui.Text.Line{spans: spans} =
...> BB.TUI.Panels.Commands.title_line(state)
iex> Enum.map_join(spans, "", & &1.content)
" 2 Commands (1) "
iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{available: []}}
iex> %ExRatatui.Text.Line{spans: spans} =
...> BB.TUI.Panels.Commands.title_line(state)
iex> Enum.map_join(spans, "", & &1.content)
" 2 Commands "
"""
@spec title_line(State.t()) :: Line.t()
def title_line(%State{commands: %{available: []}}) do
%Line{spans: badge() ++ [%Span{content: "Commands ", style: Theme.panel_title_style()}]}
end
def title_line(%State{commands: %{available: cmds}}) do
%Line{
spans:
badge() ++
[
%Span{content: "Commands (", style: Theme.panel_title_style()},
%Span{
content: Integer.to_string(length(cmds)),
style: %Style{fg: Theme.cyan(), modifiers: [:bold]}
},
%Span{content: ") ", style: Theme.panel_title_style()}
]
}
end
defp badge, do: Theme.panel_badge_spans(State.panel_number(:commands))
@doc """
Checks whether a command can execute in the current runtime state.
## Examples
iex> BB.TUI.Panels.Commands.command_ready?(%{allowed_states: [:idle, :executing]}, :idle)
true
iex> BB.TUI.Panels.Commands.command_ready?(%{allowed_states: [:idle]}, :executing)
false
iex> BB.TUI.Panels.Commands.command_ready?(%{}, :idle)
true
"""
@spec command_ready?(map(), atom()) :: boolean()
def command_ready?(%{allowed_states: allowed}, runtime_state) do
runtime_state in allowed
end
def command_ready?(_cmd, _runtime_state), do: true
# ── Private: rich-text rows ─────────────────────────────────
defp format_commands(%State{
commands: %{available: commands},
safety: %{runtime: runtime_state}
}) do
Enum.map(commands, fn cmd ->
name = to_string(Map.get(cmd, :name, inspect(cmd)))
if command_ready?(cmd, runtime_state) do
ready_row(name)
else
blocked_row(name)
end
end)
end
defp ready_row(name) do
%Line{
spans: [
%Span{content: name, style: %Style{fg: :white}},
%Span{content: " ", style: %Style{}},
%Span{content: "● Ready", style: Theme.ready_style()}
]
}
end
defp blocked_row(name) do
%Line{
spans: [
%Span{content: name, style: %Style{fg: Theme.dim_text()}},
%Span{content: " ", style: %Style{}},
%Span{content: "○ Blocked", style: Theme.blocked_style()}
]
}
end
defp executing_line do
%Line{
spans: [
%Span{content: "⏳ Executing…", style: %Style{fg: Theme.yellow(), modifiers: [:bold]}}
]
}
end
defp result_line(:ok, text) do
%Line{spans: [%Span{content: text, style: %Style{fg: Theme.green(), modifiers: [:bold]}}]}
end
defp result_line(:error, text) do
%Line{spans: [%Span{content: text, style: %Style{fg: Theme.red(), modifiers: [:bold]}}]}
end
end