Skip to main content

lib/mix/tasks/bb.tui.ex

defmodule Mix.Tasks.Bb.Tui do
  @shortdoc "Launches the BB TUI robot dashboard"

  @moduledoc """
  Starts the terminal dashboard for a Beam Bots robot.

      $ mix bb.tui --robot MyApp.Robot

  The dashboard connects to a running robot's supervision tree and
  displays safety controls, runtime state, joint positions, event
  stream, and available commands.

  ## Options

    * `--robot` — (required) The robot module to connect to.
    * `--node` — (optional) Connected remote node atom. When set, the
      TUI renders on the local terminal but pulls all data and dispatches
      all commands across distribution. The dev node must already be
      connected to the remote node (e.g. via `--sname`/`--name` and
      `Node.connect/1`).
    * `--ssh` — (optional) Start an SSH daemon instead of a local
      terminal. Each connecting SSH client gets its own isolated
      dashboard session.
    * `--port` — (optional) TCP port for the SSH daemon (default 2222).
      Ignored unless `--ssh` is set.

  ## Examples

      # Local
      $ mix bb.tui --robot MyApp.Robot

      # Remote — render here, data from there
      $ iex --name dev@127.0.0.1 --cookie secret -S mix bb.tui \\
          --robot MyApp.Robot --node robot@192.168.1.42

      # SSH daemon — accessible from any SSH client
      $ mix bb.tui --robot MyApp.Robot --ssh
      $ mix bb.tui --robot MyApp.Robot --ssh --port 3333

  ## Keybindings

  ### Global

    * `q` — quit
    * `Tab` — cycle active panel
    * `?` — toggle help overlay
    * `a` — arm robot
    * `d` — disarm robot
    * `f` — force disarm (error state only)

  ### Events panel

    * `j` / `Down` — scroll down
    * `k` / `Up` — scroll up
    * `Enter` — show event details
    * `p` — pause / resume stream
    * `c` — clear events

  ### Commands panel

    * `j` / `Down` — select next command
    * `k` / `Up` — select previous command
    * `Enter` — execute selected command

  ### Joints panel

    * `j` / `Down` — select next joint
    * `k` / `Up` — select previous joint
    * `l` / `Right` — increase position (1% step)
    * `h` / `Left` — decrease position (1% step)
    * `L` — increase position (10% step)
    * `H` — decrease position (10% step)

  ### Parameters panel

    * `j` / `Down` — select next parameter
    * `k` / `Up` — select previous parameter
    * `l` / `Right` — increase value (+1 int, +0.1 float)
    * `h` / `Left` — decrease value (-1 int, -0.1 float)
    * `L` — increase value x10
    * `H` — decrease value x10
    * `Enter` — toggle boolean parameter
  """

  use Mix.Task

  @impl true
  def run(args) do
    {opts, _rest} =
      OptionParser.parse!(args,
        strict: [robot: :string, node: :string, ssh: :boolean, port: :integer]
      )

    robot =
      case Keyword.get(opts, :robot) do
        nil ->
          Mix.raise("--robot option is required. Usage: mix bb.tui --robot MyApp.Robot")

        module_str ->
          Module.concat([module_str])
      end

    tui_opts =
      case Keyword.get(opts, :node) do
        nil -> []
        node_str -> [node: String.to_atom(node_str)]
      end

    tui_opts =
      if Keyword.get(opts, :ssh, false) do
        port = Keyword.get(opts, :port, 2222)

        tui_opts
        |> Keyword.put(:transport, :ssh)
        |> Keyword.put(:port, port)
        |> Keyword.put(:auto_host_key, true)
        |> Keyword.put(:auth_methods, ~c"password")
        |> Keyword.put(:user_passwords, [{~c"admin", ~c"admin"}])
      else
        tui_opts
      end

    Mix.Task.run("app.start")

    if Keyword.get(opts, :ssh, false) do
      Mix.shell().info("SSH daemon listening on port #{Keyword.get(tui_opts, :port, 2222)}")

      Mix.shell().info(
        "Connect with: ssh admin@localhost -p #{Keyword.get(tui_opts, :port, 2222)}"
      )

      Mix.shell().info("Default credentials: admin / admin")
    end

    BB.TUI.run(robot, tui_opts)
  end
end