# `BB.TUI`
[🔗](https://github.com/mcass19/bb_tui/blob/v0.3.0/lib/bb/tui.ex#L1)

Terminal-based dashboard for Beam Bots robots.

BB.TUI provides a TUI interface for monitoring and controlling BB robots —
safety controls, runtime state, joint positions, event stream, and command
display — in terminal environments.

## Usage

    # Interactive — from IEx when robot is already running
    BB.TUI.run(MyApp.Robot)

    # Supervised — add to the app's supervision tree
    children = [
      {BB.Supervisor, MyApp.Robot},
      {BB.TUI, robot: MyApp.Robot}
    ]

    # Mix task — standalone
    $ mix bb.tui --robot MyApp.Robot

## Remote attach (distribution)

When the robot is running on a different BEAM node — for example a
Nerves device on the network — pass the `:node` option so the TUI
renders on the local terminal but pulls all data and dispatches all
commands across distribution:

    # On the dev node, after Node.connect/1 with the robot node
    BB.TUI.run(MyApp.Robot, node: :"robot@192.168.1.42")

See `BB.TUI.Robot` for the routing layer that backs this option.

## SSH transport

When the robot runs on a headless device (Nerves board, container, remote
host), the dashboard can be served over SSH so any SSH client can connect
without a local Elixir node or distribution setup on the client side:

    # In the robot's supervision tree
    children = [
      {BB.Supervisor, MyApp.Robot},
      {BB.TUI, robot: MyApp.Robot, transport: :ssh, port: 2222,
       auto_host_key: true, auth_methods: ~c"password",
       user_passwords: [{~c"admin", ~c"s3cret"}]}
    ]

Then from any machine:

    ssh admin@robot.local -p 2222

Each SSH client gets its own isolated session with independent panel
selection, scroll positions, and event streams. Multiple operators can
monitor the same robot simultaneously.

For Nerves devices already running `nerves_ssh`, plug into the existing
daemon as a subsystem instead — see `subsystem/1`.

See `ExRatatui.SSH.Daemon` for the full list of SSH options.

## Distributed transport (attach from a connected node)

As an alternative to the `:node` option — which keeps mount/render local
and routes data calls through `:rpc` — the TUI app can run _on the robot
node_ and be attached from any connected BEAM node. This is the
`ExRatatui.Distributed` transport: the remote node runs the app
(mount/render/handle_event), and the local node only renders the
widgets it receives and forwards terminal events back.

**1. On the robot node**, add the Distributed listener to its
supervision tree (alongside whatever else the node normally supervises):

    children = [
      {BB.Supervisor, MyApp.Robot},
      ExRatatui.Distributed.Listener
    ]

**2. From any connected node**, attach:

    iex --name dev@127.0.0.1 --cookie secret -S mix
    iex> Node.connect(:"robot@192.168.1.42")
    iex> ExRatatui.Distributed.attach(:"robot@192.168.1.42", BB.TUI.App,
    ...>   listener: ExRatatui.Distributed.Listener)

For local experimentation, `Dev.Application` already supervises a
matching `ExRatatui.Distributed.Listener` wired to `Dev.TestRobot`,
so two named shells sharing a cookie are enough to exercise the
full round-trip — see the README's "Testing distribution locally"
section.

**`:node` option vs `Distributed.attach/3` — which do I want?**

| Concern                       | `:node` option      | `Distributed.attach/3` |
|-------------------------------|---------------------|------------------------|
| Where app callbacks run       | Local (this) node   | Remote node            |
| Where robot code is needed    | Both nodes          | Remote node only       |
| Transport                     | Ad-hoc `:rpc.call`  | Erlang distribution    |
| Reconnect on remote crash     | Manual              | Monitor-driven cleanup |
| Good for                      | Dev/ops workstations already running BB.TUI | Thin clients attaching to long-running robots |

Both require Erlang distribution (same cookie, reachable EPMD/ports).

## Runtime inspection and tracing

The supervising runtime exposes a few debugging hooks — handy when
something goes wrong inside an SSH session that isn't otherwise observable:

    # Quick headless-or-not check plus dimensions, render count, etc.
    ExRatatui.Runtime.snapshot(pid)

    # Capture the last N state transitions in memory.
    ExRatatui.Runtime.enable_trace(pid, limit: 200)
    ExRatatui.Runtime.trace_events(pid)
    ExRatatui.Runtime.disable_trace(pid)

    # Deterministically drive input in tests (see test/bb/tui/integration_test.exs)
    ExRatatui.Runtime.inject_event(pid, %ExRatatui.Event.Key{code: "tab", kind: "press"})

See `ExRatatui.Runtime` for the full API.

## Telemetry

Every TUI session emits `:ex_ratatui`-prefixed `:telemetry` events
(mount, every keyboard/info dispatch, every frame, transport
connect/disconnect, session lifecycle). Metadata carries
`:mod` (always `BB.TUI.App` here) and `:transport`, so consumers
running multiple ex_ratatui apps can filter accordingly. For local
debugging, attach the default Logger handler:

    BB.TUI.attach_telemetry_logger()
    BB.TUI.detach_telemetry_logger()

For production observability, attach a custom `:telemetry` handler.
See `ExRatatui.Telemetry` for the event surface and the README's
Telemetry section for a Telemetry.Metrics-style wiring example.

## Reducer runtime

`BB.TUI.App` is built on the ExRatatui **reducer runtime**
(`use ExRatatui.App, runtime: :reducer`). Every keyboard event,
PubSub message, async result, and subscription tick flows through
a single `update/2` arrow; pure state transitions live in
`BB.TUI.State`.

  * `init/1` — validates the robot, subscribes to PubSub, snapshots
    ETS state.
  * `update({:event, ev}, state)` — terminal input.
  * `update({:info, msg}, state)` — PubSub, async results,
    `send_after` deliveries, subscription ticks.
  * `subscriptions/1` — declares the 100ms throbber tick whenever
    the dashboard has something animating; the runtime diffs the
    result so the timer only runs when needed.

Long-running command execution is owned by the runtime via
`ExRatatui.Command.async/2`, batched with `Command.send_after/2`
for the timeout. Both reach the reducer as `{:info, _}` messages.
Fast, fire-and-forget robot calls (arm / disarm / set_actuator /
set_parameter / publish) are invoked inline from `update/2`.

See the README for the full rationale and the cross-references to
`ExRatatui.Command`, `ExRatatui.Subscription`, and
`ExRatatui.Runtime`.

# `attach_telemetry_logger`

```elixir
@spec attach_telemetry_logger(keyword()) :: :ok | {:error, :already_exists}
```

Attaches a development-time Logger handler to every `:ex_ratatui`
telemetry event the runtime emits. Convenience delegate to
`ExRatatui.Telemetry.attach_default_logger/1`.

ExRatatui exposes spans for `mount`, every `handle_event`/`handle_info`
dispatch, every frame draw, transport connect/disconnect, and
session open/close. Every event's metadata carries `:mod` —
`BB.TUI.App` for any TUI session — so consumers running multiple
ex_ratatui apps can filter by metadata in their own handlers.

Pass `level: :info` to bump the verbosity, or `events: [...]` to
narrow which events the logger picks up. The same `:already_exists`
return value flows back from `:telemetry.attach_many/4` on a
second attach.

# `child_spec`

Returns a child specification for supervision trees.

Accepts all options supported by `start/2` and `start_ssh/2`. When
`transport: :ssh` is present, the spec starts an SSH daemon instead
of a local terminal.

## Examples

    iex> %{id: BB.TUI, start: {BB.TUI, :start, _}} = BB.TUI.child_spec(robot: MyApp.Robot)

    iex> spec = BB.TUI.child_spec(robot: MyApp.Robot, transport: :ssh, port: 2222)
    iex> spec.id
    BB.TUI

# `detach_telemetry_logger`

```elixir
@spec detach_telemetry_logger() :: :ok | {:error, :not_found}
```

Detaches the Logger handler previously installed by
`attach_telemetry_logger/1`. Returns `{:error, :not_found}` when no
handler is attached.

# `run`

```elixir
@spec run(
  module(),
  keyword()
) :: :ok | {:error, term()}
```

Runs the TUI dashboard interactively, blocking until the user quits.

Use this from IEx or scripts. For local transport, the terminal is
taken over for the duration and restored when the TUI exits (press
`q` to quit). For SSH transport, the daemon runs until the process
is stopped.

## Options

  * `:node` — connected remote node atom. When set, all robot data is
    fetched from that node via `:rpc.call/4` and PubSub messages are
    relayed back to the local TUI. The dev node must be connected to
    the remote node first via `Node.connect/1`.
  * `:transport` — `:local` (default) for the OS terminal, or `:ssh`
    to start an SSH daemon. When `:ssh`, all `ExRatatui.SSH.Daemon`
    options (`:port`, `:system_dir`, etc.) are accepted.
  * `:subscribe_paths` — PubSub paths the dashboard subscribes to,
    overriding the default control-plane set. For example,
    `[[:state_machine], [:command]]` narrows the dashboard to just
    state-machine and command traffic instead of the full firehose.
  * `:renderers` — a `%{path_prefix => module}` map registering consumer
    `BB.TUI.Renderer` implementations. A message whose path matches a
    registered prefix (longest prefix wins) is rendered by the consumer's
    module — the dashboard never inspects the payload itself. Default `%{}`
    (no renderers). See `BB.TUI.Renderer`.
  * `:test_mode` — `{width, height}` tuple for headless testing
    (optional).

## Examples

    # Local
    BB.TUI.run(MyApp.Robot)

    # Remote — render here, data from there
    Node.connect(:"robot@192.168.1.42")
    BB.TUI.run(MyApp.Robot, node: :"robot@192.168.1.42")

# `start`

```elixir
@spec start(
  module(),
  keyword()
) :: {:ok, pid()} | {:error, term()}
```

Starts the TUI dashboard as a linked process.

When `transport: :ssh` is set in `opts`, starts an SSH daemon that
serves the dashboard to connecting SSH clients. Otherwise starts a
local terminal session.

Use `run/2` for interactive use from IEx. Use `start/2` or the
child spec when adding to a supervision tree.

## Options

  * `:node` — connected remote node atom (see `run/2`).
  * `:transport` — `:local` (default) or `:ssh`. When `:ssh`, all
    `ExRatatui.SSH.Daemon` options are accepted (`:port`,
    `:system_dir`, `:auto_host_key`, etc.).
  * `:subscribe_paths` — PubSub paths the dashboard subscribes to,
    overriding the default control-plane set (see `run/2`).
  * `:renderers` — consumer `BB.TUI.Renderer` map (see `run/2`).
  * `:test_mode` — `{width, height}` tuple for headless testing
    (optional).

## Examples

    # Local terminal
    BB.TUI.start(MyApp.Robot)

    # SSH daemon on port 2222
    BB.TUI.start(MyApp.Robot, transport: :ssh, port: 2222, auto_host_key: true)

# `start_ssh`

```elixir
@spec start_ssh(
  module(),
  keyword()
) :: {:ok, pid()} | {:error, term()}
```

Starts the TUI dashboard as an SSH daemon.

Convenience wrapper around `start/2` that sets `transport: :ssh`
automatically. Each connecting SSH client gets its own isolated
dashboard session.

## Options

Accepts all `ExRatatui.SSH.Daemon` options:

  * `:port` — TCP port to listen on (default `2222`).
  * `:auto_host_key` — auto-generate an RSA host key on first boot
    (default `false`).
  * `:system_dir` — host key directory (alternative to
    `:auto_host_key`).
  * `:auth_methods` — e.g. `~c"password"` or `~c"publickey"`.
  * `:user_passwords` — `[{~c"user", ~c"pass"}]` pairs.
  * `:node` — remote BEAM node atom, forwarded to each client's
    `mount/1`.
  * `:subscribe_paths` — PubSub paths the dashboard subscribes to,
    forwarded to each client's `mount/1` (see `run/2`).
  * `:renderers` — consumer `BB.TUI.Renderer` map, forwarded to each
    client's `mount/1` (see `run/2`).

All other OTP `:ssh.daemon/2` options are forwarded as-is.

## Examples

    # Auto-generated host key, password auth
    BB.TUI.start_ssh(MyApp.Robot,
      port: 2222,
      auto_host_key: true,
      auth_methods: ~c"password",
      user_passwords: [{~c"admin", ~c"s3cret"}]
    )

    # In a supervision tree
    children = [
      {BB.Supervisor, MyApp.Robot},
      %{
        id: BB.TUI.SSH,
        start: {BB.TUI, :start_ssh, [MyApp.Robot, [port: 2222, auto_host_key: true]]}
      }
    ]

# `subsystem`

```elixir
@spec subsystem(module()) :: {charlist(), {module(), keyword()}}
```

Returns a subsystem tuple for plugging into an existing SSH daemon.

Use this when the robot already runs `nerves_ssh` (or any OTP
`:ssh.daemon/2`) and the dashboard should be added as an SSH
subsystem instead of spinning up a separate daemon.

## Nerves example

    # config/runtime.exs
    import Config

    if Application.spec(:nerves_ssh) do
      config :nerves_ssh,
        subsystems: [
          :ssh_sftpd.subsystem_spec(cwd: ~c"/"),
          BB.TUI.subsystem(MyApp.Robot)
        ]
    end

Then connect with:

    ssh -t nerves.local -s Elixir.BB.TUI.App

The `-t` flag is required — it forces PTY allocation, which the TUI
needs for interactive input.

## Examples

    iex> {name, {mod, args}} = BB.TUI.subsystem(SomeRobot)
    iex> name
    ~c"Elixir.BB.TUI.App"
    iex> mod
    ExRatatui.SSH
    iex> Keyword.fetch!(args, :subsystem)
    true

---

*Consult [api-reference.md](api-reference.md) for complete listing*
