Skip to main content

lib/bb/tui/panels/safety.ex

defmodule BB.TUI.Panels.Safety do
  @moduledoc """
  Safety panel — displays safety state, runtime state, and control hints.

  Combines safety status indicator, runtime state, and keyboard shortcuts
  in a single left-sidebar panel. When the robot is disarming, shows a
  throbber animation.

  Pure function — takes state, returns a widget struct.
  """

  alias BB.TUI.State
  alias BB.TUI.Theme
  alias ExRatatui.Text.Line
  alias ExRatatui.Text.Span
  alias ExRatatui.Widgets.Block
  alias ExRatatui.Widgets.Paragraph
  alias ExRatatui.Widgets.Throbber

  @doc """
  Renders the safety panel with status indicator, runtime state, and key hints.

  When the robot is in the `:disarming` state, returns a Throbber widget
  instead to show the animated transition indicator.

  ## Examples

      iex> state = %BB.TUI.State{safety: %BB.TUI.State.Safety{state: :armed, runtime: :idle}}
      iex> %ExRatatui.Widgets.Paragraph{} = BB.TUI.Panels.Safety.render(state, true)

      iex> state = %BB.TUI.State{ui: %BB.TUI.State.UI{throbber_step: 0}, safety: %BB.TUI.State.Safety{state: :disarming, runtime: :disarming}}
      iex> %ExRatatui.Widgets.Throbber{} = BB.TUI.Panels.Safety.render(state, false)
  """
  @spec render(State.t(), boolean()) :: struct()
  def render(%State{safety: %{state: :disarming}} = state, focused?) do
    %Throbber{
      label: "DISARMING",
      step: state.ui.throbber_step,
      throbber_set: :dots,
      style: Theme.disarming_style(),
      throbber_style: Theme.disarming_style(),
      block: block(focused?)
    }
  end

  def render(%State{} = state, focused?) do
    {symbol, label, style} = state_display(state.safety.state)
    runtime_label = format_runtime(state.safety.runtime)

    text = """
    #{symbol} #{label}

    Runtime: #{runtime_label}

    [a] Arm
    [d] Disarm\
    """

    text =
      if state.safety.state == :error do
        text <> "\n[f] Force Disarm"
      else
        text
      end

    %Paragraph{
      text: text,
      style: style,
      block: block(focused?)
    }
  end

  defp state_display(:armed), do: {"\u{25CF}", "ARMED", Theme.armed_style()}
  defp state_display(:disarmed), do: {"\u{25CB}", "DISARMED", Theme.disarmed_style()}
  defp state_display(:error), do: {"\u{2716}", "ERROR", Theme.error_style()}
  defp state_display(_other), do: {"\u{25CB}", "UNKNOWN", Theme.disarmed_style()}

  defp format_runtime(:idle), do: "Idle"
  defp format_runtime(:executing), do: "Executing..."
  defp format_runtime(:disarmed), do: "Disarmed"
  defp format_runtime(:error), do: "Error"
  defp format_runtime(other), do: to_string(other)

  defp block(focused?) do
    %Block{
      title: title_line(),
      borders: [:all],
      border_type: :rounded,
      border_style: Theme.border_style(focused?)
    }
  end

  defp title_line do
    spans =
      Theme.panel_badge_spans(State.panel_number(:safety)) ++
        [%Span{content: "Safety ", style: Theme.panel_title_style()}]

    %Line{spans: spans}
  end
end