Skip to main content

lib/bb/tui/panels/command_edit.ex

defmodule BB.TUI.Panels.CommandEdit do
  @moduledoc """
  Argument-edit popup for the commands panel.

  Floats above the dashboard while the user edits the arguments of the
  selected command. Each declared argument renders as a row showing
  the current value (string buffer) and type hint; the focused row
  gets a `›` prefix and a `▏` cursor. A hint footer describes the
  edit-mode keys.

  Pure function — takes state, returns a Popup 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.Paragraph
  alias ExRatatui.Widgets.Popup

  @doc """
  Renders the edit popup for the selected command, or `nil` when the
  selected command has no arguments. The caller should only invoke
  this when `state.commands.edit_mode?` is true.

  ## 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,
      ...>     edit_mode?: true,
      ...>     focused_arg: 0
      ...>   }
      ...> }
      iex> %ExRatatui.Widgets.Popup{} = BB.TUI.Panels.CommandEdit.render(state)

      iex> state = %BB.TUI.State{commands: %BB.TUI.State.Commands{available: [], selected: 0}}
      iex> BB.TUI.Panels.CommandEdit.render(state)
      nil
  """
  @spec render(State.t()) :: struct() | nil
  def render(%State{} = state) do
    case State.selected_command(state) do
      %{name: cmd_name, arguments: [_ | _] = args} ->
        build_popup(state, cmd_name, args)

      _ ->
        nil
    end
  end

  defp build_popup(state, cmd_name, args) do
    line_groups =
      args
      |> Enum.with_index()
      |> Enum.flat_map(fn {arg, i} ->
        arg_lines(state, cmd_name, arg, i == state.commands.focused_arg)
      end)

    text = line_groups ++ [%Line{spans: []}, hint_line(args)]

    content = %Paragraph{text: text, style: %Style{fg: :white}}

    %Popup{
      content: content,
      percent_width: 60,
      percent_height: 40,
      block: %Block{
        title: title_line(cmd_name),
        borders: [:all],
        border_type: :rounded,
        border_style: %Style{fg: Theme.cyan(), modifiers: [:bold]}
      }
    }
  end

  defp title_line(cmd_name) do
    %Line{
      spans: [
        %Span{content: " Edit ", style: %Style{}},
        %Span{
          content: to_string(cmd_name),
          style: %Style{fg: Theme.cyan(), modifiers: [:bold]}
        },
        %Span{content: " ", style: %Style{}}
      ]
    }
  end

  defp arg_lines(state, cmd_name, arg, focused?) do
    [field_line(state, cmd_name, arg, focused?) | doc_line(arg)]
  end

  defp field_line(state, cmd_name, %{enum_values: [_ | _]} = arg, focused?) do
    name = to_string(arg.name)
    type = arg.type
    value = State.arg_value(state, cmd_name, arg)
    prefix = if focused?, do: " › ", else: "   "
    required_marker = if Map.get(arg, :required, false), do: "*", else: ""

    %Line{
      spans: [
        %Span{content: prefix, style: %Style{fg: Theme.cyan()}},
        %Span{content: name, style: %Style{fg: :white, modifiers: [:bold]}},
        %Span{content: required_marker, style: %Style{fg: Theme.red(), modifiers: [:bold]}},
        %Span{content: " (#{type})", style: %Style{fg: Theme.dim_text()}},
        %Span{content: ": ", style: %Style{fg: Theme.dim_text()}},
        %Span{content: "‹ ", style: %Style{fg: Theme.cyan()}},
        %Span{content: enum_display(value), style: value_style(focused?)},
        %Span{content: " ›", style: %Style{fg: Theme.cyan()}}
      ]
    }
  end

  defp field_line(state, cmd_name, arg, focused?) do
    name = to_string(arg.name)
    type = arg.type
    value = State.arg_value(state, cmd_name, arg)
    prefix = if focused?, do: " › ", else: "   "
    cursor = if focused?, do: "▏", else: ""
    required_marker = if Map.get(arg, :required, false), do: "*", else: ""

    %Line{
      spans: [
        %Span{content: prefix, style: %Style{fg: Theme.cyan()}},
        %Span{content: name, style: %Style{fg: :white, modifiers: [:bold]}},
        %Span{content: required_marker, style: %Style{fg: Theme.red(), modifiers: [:bold]}},
        %Span{content: " (#{type})", style: %Style{fg: Theme.dim_text()}},
        %Span{content: ": ", style: %Style{fg: Theme.dim_text()}},
        %Span{content: value, style: value_style(focused?)},
        %Span{content: cursor, style: %Style{fg: Theme.cyan(), modifiers: [:bold]}}
      ]
    }
  end

  defp enum_display(":" <> rest) when byte_size(rest) > 0, do: rest
  defp enum_display(value), do: value

  defp doc_line(arg) do
    case Map.get(arg, :doc) do
      doc when is_binary(doc) and byte_size(doc) > 0 ->
        [%Line{spans: [%Span{content: "     " <> doc, style: %Style{fg: Theme.dim_text()}}]}]

      _ ->
        []
    end
  end

  defp value_style(true), do: %Style{fg: :white, modifiers: [:bold]}
  defp value_style(false), do: %Style{fg: Theme.dim_text()}

  defp hint_line(args) do
    base = " [Tab] next  [⇧Tab] prev  [⏎] execute  [Esc] cancel"

    extra =
      if Enum.any?(args, &match?(%{enum_values: [_ | _]}, &1)), do: "  [←/→] cycle", else: ""

    %Line{
      spans: [
        %Span{
          content: base <> extra,
          style: %Style{fg: Theme.dim_text()}
        }
      ]
    }
  end
end