# `ExRatatui.SSH`
[🔗](https://github.com/mcass19/ex_ratatui/blob/v0.7.1/lib/ex_ratatui/ssh.ex#L1)

OTP `:ssh_server_channel` implementation that serves an `ExRatatui.App`
over a single SSH channel.

One instance is spawned per SSH channel (i.e. per connected client). It
owns an `ExRatatui.Session` for that channel and a linked internal
server running the user's app module in `:ssh` transport mode. Bytes
the ratatui backend writes into the session's in-memory buffer are
shipped back to the client via `:ssh_connection.send/3`, and bytes
the client types come in as `{:data, _, _, _}` events that get fed
through the session's ANSI parser and dispatched to the server as
`{:ex_ratatui_event, event}` messages.

## Two entry points

SSH has two ways a client can ask a server to run "something":

  * **Shell** — `ssh host` with no command; the client expects the server
    to start the user's default shell and wire it to the channel.
  * **Subsystem** — `ssh host -s <name>`; the client asks for a named
    non-shell handler (this is what `nerves_ssh` uses for its
    `subsystems:` config, what `sftp` rides on, etc.).

Both are supported here. Shell mode is required for the standalone
daemon so plain `ssh` just works. Subsystem mode is how `nerves_ssh`
plugs this into its existing daemon — see `subsystem/1` for the exact
shape it expects.

### Shell vs subsystem startup

The two modes use different triggers to spin up the server:

  * **Shell mode** waits for the client's `pty_req` (to size the
    session) and then `shell_req` (to launch). If the client skips the
    pty, the channel is rejected on the shell request.
  * **Subsystem mode** can't wait for those messages: OTP `:ssh`
    matches the subsystem name against the daemon's `:subsystems`
    config and consumes the `{:subsystem, ...}` request itself to
    dispatch us — `handle_ssh_msg/2` never sees it. Worse, when the
    client passes `-t` OTP *also* consumes the `pty_req` before
    the subsystem dispatch fires: OTP hands it to the daemon's
    default CLI handler (IEx on a `nerves_ssh` device) and silently
    orphans that CLI process the moment our subsystem handler
    takes over the channel. Neither pty_req nor its dimensions
    reach us through any channel request.

    To work around that, on `{:ssh_channel_up, ...}` we synthesize a
    default 80x24 session, start the server, and immediately emit a
    Cursor Position Report roundtrip —
    `ESC[s ESC[9999;9999H ESC[6n ESC[u` — which parks the cursor at
    (9999, 9999) so the client clamps it to the actual pty size,
    then asks the client to report the position. The response
    arrives on the next `{:data, ...}` channel message, is parsed
    into a `%Event.Resize{}` by the session's ANSI input parser,
    and the data handler resizes the session in place + notifies
    the server. Clients can still send `{:window_change, ...}`
    afterwards to track real runtime resizes.

`subsystem/1` bakes `subsystem: true` into the init args so the
channel handler can tell the two flows apart. Shell-mode init (via
`ssh_cli:`) leaves the flag at its default `false`.

### Client-side caveat: always pass `-t` in subsystem mode

OpenSSH does **not** allocate a PTY by default for `ssh host -s name`
— that's designed for protocols like `sftp` that don't need one. For
an interactive TUI you MUST force it with `-t`:

    ssh -t nerves.local -s Elixir.MyApp.TUI   # ✓ works
    ssh nerves.local -s Elixir.MyApp.TUI      # ✗ local tty stays in
                                              #   cooked mode, keys are
                                              #   line-buffered + echoed
                                              #   locally, screen redraw
                                              #   bleeds into the shell
                                              #   prompt on exit

Without `-t`, render bytes still reach the client and the TUI runs —
it just can't be driven interactively.

## Subsystem helper

Returns a `{charlist, {module, init_args}}` tuple in exactly the shape
OTP `:ssh` expects for its `subsystems:` option. Plug it into a
`nerves_ssh` subsystems list from `config/runtime.exs`:

    # config/runtime.exs
    import Config

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

This **must** live in `runtime.exs`, not `target.exs`. On a fresh
`MIX_TARGET=rpi4 mix compile` Mix evaluates compile-time configs
before it builds deps for the target, so `ExRatatui.SSH` isn't on the
code path yet and the `subsystem/1` call would crash with `module
ExRatatui.SSH is not available`. `runtime.exs` runs at device boot
after every beam file is loaded but before the OTP application
controller starts `:nerves_ssh`, which is exactly the window we need.
The `Application.spec(:nerves_ssh)` guard keeps host builds (where
`:nerves_ssh` isn't a dep) silent. See `guides/ssh_transport.md` for
the full write-up.

## Dependency injection for tests

`init/1` accepts optional `:sender` and `:starter` overrides so unit
tests can substitute fakes for `:ssh_connection.send/3` and the
internal server's start function without standing up real
infrastructure. Defaults point at the real OTP + Server functions;
production callers never pass either key.

# `t`

```elixir
@type t() :: %ExRatatui.SSH{
  app_opts: keyword(),
  channel_id: non_neg_integer() | nil,
  conn: term() | nil,
  esc_timer: reference() | nil,
  mod: module(),
  rendering: boolean(),
  replier: (term(), boolean(), :success | :failure, non_neg_integer() -&gt; :ok),
  sender: (term(), non_neg_integer(), iodata() -&gt; :ok | {:error, term()}),
  server_pid: pid() | nil,
  session: ExRatatui.Session.t() | nil,
  starter: (keyword() -&gt; {:ok, pid()} | {:error, term()}),
  subsystem_mode: boolean()
}
```

# `subsystem`

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

Returns a `{charlist_name, {ExRatatui.SSH, init_args}}` tuple in the
shape OTP `:ssh`'s `:subsystems` option (and `nerves_ssh`'s
`subsystems:` list) expects.

The charlist name is the full module name (e.g. `~c"Elixir.MyApp.TUI"`)
so each app module gets its own distinct subsystem. Two apps
configured into the same daemon will not collide.

The init args include `subsystem: true` so the channel handler knows
it was spawned via OTP's subsystem dispatch (which consumes the
`{:subsystem, ...}` message internally) and can start the TUI server
as soon as the channel is up, instead of waiting for a shell request
that will never arrive.

## Examples

    iex> ExRatatui.SSH.subsystem(SomeTUIModule)
    {~c"Elixir.SomeTUIModule", {ExRatatui.SSH, [mod: SomeTUIModule, subsystem: true]}}

---

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