TermUI.Backend.SSH (TermUI v1.0.0)

View Source

SSH terminal backend for remote terminal sessions.

The SSH backend renders to an Erlang SSH channel IO device, enabling TermUI applications to run over SSH connections via OTP's :ssh application.

How It Works

When an SSH client connects and requests a PTY, the :ssh application creates an IO device (the channel's group leader) that implements the Erlang IO protocol. This backend writes ANSI escape sequences to that device and reads input from it.

SSH channels are already in raw mode from the client side — no stty or :shell.start_interactive is needed.

Usage

Start a TermUI Runtime with an explicit SSH backend:

device = Process.group_leader()  # SSH channel's IO device
{rows, cols} = get_pty_size(device)

{:ok, runtime} = TermUI.Runtime.start_link(
  root: MyApp.Root,
  backend: {TermUI.Backend.SSH, device: device, size: {rows, cols}}
)

Input Handling

SSH input is delivered externally. The host process reads bytes from the SSH device, parses escape sequences, and sends events to the Runtime:

send(runtime, {:ssh_input, %TermUI.Event.Key{key: :enter}})

The poll_event/2 callback returns {:timeout, state} since input is external.

Resize Events

Terminal size changes arrive as SSH window_change channel requests. Forward them to the Runtime:

send(runtime, {:ssh_resize, new_rows, new_cols})

Multiple Sessions

Each SSH connection gets its own Backend.SSH instance with its own device. There is no global state — multiple concurrent sessions work independently.

See Also

Summary

Types

Mouse tracking mode.

Current SGR style state for delta optimization.

t()

Internal state for the SSH backend.

Functions

Draws cells to the SSH terminal at specified positions.

Initializes the SSH backend with the given device and terminal size.

Returns timeout — SSH input is delivered externally.

Shuts down the SSH backend and restores terminal state.

Returns the cached terminal dimensions.

Updates the cached terminal size.

Types

mouse_mode()

@type mouse_mode() :: :none | :click | :drag | :all

Mouse tracking mode.

  • :none — No mouse tracking
  • :click — Button press/release only (mode 1000)
  • :drag — Press/release + motion while pressed (mode 1002)
  • :all — All mouse movement (mode 1003)

style_state()

@type style_state() :: %{
  fg: TermUI.Backend.color(),
  bg: TermUI.Backend.color(),
  attrs: [atom()]
}

Current SGR style state for delta optimization.

t()

@type t() :: %TermUI.Backend.SSH{
  alternate_screen: boolean(),
  current_style: style_state() | nil,
  cursor_position: {pos_integer(), pos_integer()} | nil,
  cursor_visible: boolean(),
  device: IO.device(),
  mouse_mode: mouse_mode(),
  size: {pos_integer(), pos_integer()}
}

Internal state for the SSH backend.

Fields

  • :device — SSH channel IO device PID
  • :size — Terminal dimensions as {rows, cols}
  • :cursor_visible — Whether cursor is currently visible
  • :cursor_position — Current cursor position as {row, col} or nil
  • :alternate_screen — Whether alternate screen buffer is active
  • :mouse_mode — Current mouse tracking mode
  • :current_style — Current SGR state for style delta tracking

Functions

draw_cells(state, cells)

@spec draw_cells(t(), [{TermUI.Backend.position(), TermUI.Backend.cell()}]) ::
  {:ok, t()}

Draws cells to the SSH terminal at specified positions.

Uses style delta optimization — only emits SGR escape sequences when the style changes from the previous cell. Cells should be sorted by position (row-major) for efficient cursor movement.

init(opts)

@spec init(keyword()) :: {:ok, t()}

Initializes the SSH backend with the given device and terminal size.

Options

  • :device (required) — SSH channel IO device (from Process.group_leader() in SSH shell)
  • :size — Terminal dimensions as {rows, cols} from PTY negotiation (default: {24, 80})
  • :alternate_screen — Use alternate screen buffer (default: true)
  • :hide_cursor — Hide cursor during rendering (default: true)
  • :mouse_tracking — Mouse tracking mode (default: :none)

poll_event(state, timeout)

@spec poll_event(t(), non_neg_integer()) :: {:timeout, t()}

Returns timeout — SSH input is delivered externally.

The host process reads from the SSH device and sends parsed events to the Runtime via send(runtime, {:ssh_input, event}).

shutdown(state)

@spec shutdown(t()) :: :ok

Shuts down the SSH backend and restores terminal state.

Writes cleanup sequences to the SSH device. Silently handles errors since the SSH channel may already be closed on disconnect.

size(ssh)

@spec size(t()) :: {:ok, {pos_integer(), pos_integer()}}

Returns the cached terminal dimensions.

SSH terminal size is provided at init from PTY negotiation and updated externally via update_size/3 when window_change events arrive.

update_size(state, rows, cols)

@spec update_size(t(), pos_integer(), pos_integer()) :: {:ok, t()}

Updates the cached terminal size.

Called when an SSH window_change event arrives with new dimensions. Returns the updated state.