# retach — Terminal Multiplexer with Native Scrollback

## Problem

All existing terminal multiplexers (tmux, screen, zellij) intercept scrollback and require entering a special mode (copy mode) to scroll. On touchscreens (iPad + Blink/Termius over mosh) this is unusable. Eternal Terminal solves this but isn't a multiplexer and has limited client support on iOS.

## Solution

A minimal terminal multiplexer that passes scrollback through to the client terminal natively. The client terminal handles scrolling as usual — swipe, trackpad, scroll wheel — no special modes needed.

## Architecture

Daemon + client communicating over a single Unix socket.

```
Client (retach)        Daemon (retach server)         Shell
    |                        |                          |
    |--- input (keys) ------>|--- write to PTY -------->|
    |                        |                          |
    |<-- scrollback lines ---|<-- read from PTY --------|
    |<-- screen snapshot ----|    (VTE parsed)           |
    |                        |                          |
  stdout                   grid + scrollback buffer    bash/zsh
  (native scrollback)      (10k lines default)
```

### Per-Session I/O

Each session's PTY reader/writer and Screen are wrapped in per-session `Arc<Mutex<>>`. The global `SessionManager` is only locked briefly for session lookup/create/kill. PTY I/O never holds the global lock — sessions are fully independent.

### Passthrough Scrollback

- Daemon parses all PTY output through a VTE state machine, maintaining a virtual grid (rows x cols)
- When a line scrolls off the top of the grid → it is included in the next `ScreenUpdate` as an atomic operation: cursor positioned at the bottom, scrollback line written with `\r\n` (triggers real terminal scroll), then full grid redraw — all inside a single synchronized-output block
- Client writes the `ScreenUpdate` bytes to stdout → scrollback enters the native terminal buffer, then the visible screen is redrawn
- **Alternate screen** (vim, less, htop): when an app enters alt screen, passthrough stops; main buffer is saved. When it exits, main buffer is restored and passthrough resumes. Scrollback stays clean.

### Reattach

- Daemon stores a configurable scrollback buffer (default 10,000 lines, `--history N`)
- On reattach: daemon sends stored scrollback as `History` messages, then a full `ScreenUpdate`
- Client writes history lines to stdout with `\r\n` (terminal scrolls them into native scrollback)
- The `ScreenUpdate` prepends `rows - 1` newlines to flush remaining visible history lines into the scrollback buffer before clearing the screen and redrawing
- Looks as if you never disconnected

### Sessions

- All sessions managed through a single daemon socket at `$XDG_RUNTIME_DIR/retach/retach.sock` (fallback: `/tmp/retach-<uid>/retach.sock`)
- Socket directory created with `0o700` permissions (owner-only access)
- Daemon auto-starts on first `retach new` or `retach open <name>`
- Auto-generated session names use millisecond timestamps (not PIDs) to avoid collisions
- Multiple independent sessions supported
- Dead sessions (exited child processes) automatically cleaned up every 30 seconds

## CLI

```
retach open <name>              # attach if exists, create if not (CreateOrAttach)
retach new [name]               # create new session, fail if exists (CreateOnly)
retach new --history N          # custom scrollback size
retach attach <name>            # attach to existing, fail if not found (AttachOnly)
retach attach <name> --history N  # attach with custom history
retach list                     # list active sessions
retach kill <name>              # kill a session
retach server                   # start daemon manually (auto-started normally)
Ctrl+\                          # detach from inside a session
```

## Module Structure

```
src/
  main.rs                    -- entry point, tracing init
  cli.rs                     -- command parsing (clap derive)
  pty.rs                     -- PTY creation, shell spawning, Arc-wrapped writer/master/child
  session.rs                 -- Session + SessionManager with get_or_create(), dead cleanup
  protocol/
    mod.rs                   -- re-exports
    messages.rs              -- ClientMsg, ServerMsg, SessionInfo
    codec.rs                 -- encode/decode with limits, ProtocolError, read_one_message()
  screen/
    mod.rs                   -- Screen public API (parser + grid + scrollback)
    style.rs                 -- Style, Color, to_sgr(), apply_sgr(), parse_extended_color()
    cell.rs                  -- Cell
    grid.rs                  -- Grid (scroll_up, resize, sanitize_dimensions)
    performer.rs             -- ScreenPerformer (VTE Perform trait, alt screen save/restore)
    render.rs                -- render_screen(), render_line() — pure functions
  server/
    mod.rs                   -- run_server() with dead session cleanup task
    socket.rs                -- socket_dir() with 0o700 permissions, socket_path()
    client_handler.rs        -- handle_client() — initial message routing
    session_bridge.rs        -- SessionIo struct, handle_session() — PTY↔client bridge
  client/
    mod.rs                   -- connect(), list_sessions(), kill_session()
    raw_mode.rs              -- RawMode RAII guard with tracing on restore failure
    server_launcher.rs       -- ensure_server_running()
```

## Protocol Messages

```
Client → Server:
  Connect { name, history, cols, rows, mode }  # create/attach with ConnectMode
  Input(Vec<u8>)                               # keyboard input
  Resize { cols, rows }                        # terminal resize
  Detach                                       # graceful detach
  ListSessions                                 # request session list
  KillSession { name }                         # kill a session
  RefreshScreen                                # request full screen redraw

  ConnectMode:
    CreateOrAttach    # Open: attach if exists, create if not
    CreateOnly        # New: fail if session already exists
    AttachOnly        # Attach: fail if session doesn't exist

Server → Client:
  Connected { name, new_session }         # connection confirmed (new or reattach)
  ScrollbackLine(Vec<u8>)                # passthrough line (native scrollback)
  ScreenUpdate(Vec<u8>)                  # current screen (ANSI)
  History(Vec<Vec<u8>>)                  # scrollback buffer on reattach (chunked)
  SessionList(Vec<SessionInfo>)          # active sessions
  SessionKilled { name }                 # kill confirmed
  SessionEnded                           # shell exited
  Error(String)                          # error message
```

### Protocol Hardening

- Frames length-prefixed with 4-byte big-endian u32
- `encode()` uses `u32::try_from()` to prevent silent truncation of messages > 4 GiB
- `decode_frame()` rejects frames larger than 16 MiB (`MAX_FRAME_SIZE`)
- `read_one_message<T>()` generic helper eliminates duplicated read-loops
- `bincode_config()` with `Options::with_limit(MAX_FRAME_SIZE)` prevents OOM from malicious Vec length prefixes
- History messages chunked to stay under `MAX_FRAME_SIZE` on reattach
- Single `drain()` per read cycle (cursor-based frame processing) instead of O(n*m) per-frame drain

### Resilience

- **Client:** panic hook + SIGINT/SIGTERM handlers restore terminal from raw mode; stdin task has cancellation via watch channel; runtime shutdown timeout (100ms) prevents hanging on blocked stdin thread
- **Server:** graceful shutdown on SIGTERM/SIGINT; `SocketGuard` RAII removes socket file on any exit; `accept()` loop continues on transient errors instead of crashing; SIGHUP ignored for SSH disconnect survival
- **Sessions:** dead sessions dropped outside manager lock via `take_dead_sessions()` to avoid blocking Tokio workers

## Dependencies

- `vte` — terminal escape sequence parser
- `portable-pty` — cross-platform PTY
- `tokio` — async runtime (Unix sockets, signals, spawn_blocking)
- `clap` — CLI parsing (derive)
- `serde` + `bincode` — protocol serialization
- `nix` — raw terminal mode (termios), user ID
- `term_size` — terminal dimensions
- `anyhow` — error handling (application-level)
- `thiserror` — typed error enum (ProtocolError in codec)
- `tracing` + `tracing-subscriber` — structured logging (env-filter, default `retach=info`)

## Logging

Structured logging via `tracing` to stderr:

```bash
RUST_LOG=retach=debug retach new   # see debug-level structured logs
RUST_LOG=retach=trace retach new   # maximum verbosity
```

Server logs are stored in `$XDG_RUNTIME_DIR/retach/retach.log` (fallback: `/tmp/retach-<uid>/retach.log`) when auto-started.

## Non-Goals

- Panes / splits
- Status bar
- Configuration file
- Mouse event passthrough
- Tabs
- Scroll regions (DECSTBM) — planned
