# Core Domain Model: Sessions, Panes, Grid, and Scrollback

> `rmux-core` is the pure in-memory domain: no OS calls, no Tokio, no network. It models the full tmux object graph — sessions own windows, windows own panes, panes own a terminal grid plus scrollback. The grid module handles VT/ANSI state (cursor, SGR attributes, alternate screen, scroll regions, wide/combining characters). Parser traces under `crates/rmux-core/tests/parser_traces/` are golden-file tests that pin exact VT sequence handling (e.g. `alternate_screen_roundtrip.trace`, `unicode_wide_cjk.trace`, `sgr_256_and_rgb.trace`). The `command_parser` module translates tmux-syntax command strings; `formats` implements tmux-format string evaluation; `hooks` tracks registered hook callbacks. Safe-change rule: any logic that belongs in this crate must be pure and deterministic — if it requires I/O, it belongs in `rmux-server` or `rmux-os`.

- Repository: Helvesec/rmux
- GitHub: https://github.com/Helvesec/rmux
- Human wiki: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181
- Complete Markdown: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/llms-full.txt

## Source Files

- `crates/rmux-core/src/lib.rs`
- `crates/rmux-core/src/keys.rs`
- `crates/rmux-core/tests/parser_traces.rs`
- `crates/rmux-core/tests/parser_traces/alternate_screen_roundtrip.trace`
- `crates/rmux-core/tests/parser_traces/unicode_wide_cjk.trace`
- `crates/rmux-pty/src/lib.rs`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [crates/rmux-core/src/lib.rs](crates/rmux-core/src/lib.rs)
- [crates/rmux-core/src/session.rs](crates/rmux-core/src/session.rs)
- [crates/rmux-core/src/session/types.rs](crates/rmux-core/src/session/types.rs)
- [crates/rmux-core/src/window.rs](crates/rmux-core/src/window.rs)
- [crates/rmux-core/src/pane.rs](crates/rmux-core/src/pane.rs)
- [crates/rmux-core/src/grid.rs](crates/rmux-core/src/grid.rs)
- [crates/rmux-core/src/grid/cell.rs](crates/rmux-core/src/grid/cell.rs)
- [crates/rmux-core/src/screen.rs](crates/rmux-core/src/screen.rs)
- [crates/rmux-core/src/input/mod.rs](crates/rmux-core/src/input/mod.rs)
- [crates/rmux-core/src/hooks.rs](crates/rmux-core/src/hooks.rs)
- [crates/rmux-core/src/command_parser.rs](crates/rmux-core/src/command_parser.rs)
- [crates/rmux-core/tests/parser_traces.rs](crates/rmux-core/tests/parser_traces.rs)
- [crates/rmux-core/tests/parser_traces/alternate_screen_roundtrip.trace](crates/rmux-core/tests/parser_traces/alternate_screen_roundtrip.trace)
- [crates/rmux-core/tests/parser_traces/unicode_wide_cjk.trace](crates/rmux-core/tests/parser_traces/unicode_wide_cjk.trace)
- [crates/rmux-core/tests/parser_traces/sgr_256_and_rgb.trace](crates/rmux-core/tests/parser_traces/sgr_256_and_rgb.trace)
</details>

# Core Domain Model: Sessions, Panes, Grid, and Scrollback

`rmux-core` is the pure in-memory domain for the rmux terminal multiplexer. It carries no OS calls, no async runtime, and no network sockets — its only mandate is modeling the full tmux object graph with deterministic, testable behavior. Because every value type in this crate is `Clone + PartialEq + Eq`, the server layer can snapshot, compare, and restore state without touching the process boundary.

This page explains the ownership chain from session down to individual grid cells, the VT/ANSI parser and screen writer that animate each pane, the golden-file test strategy that pins exact terminal emulation behavior, and the auxiliary subsystems (`command_parser`, `formats`, `hooks`) that round out the domain.

---

## The Object Ownership Hierarchy

The canonical hierarchy mirrors tmux's own data model:

```text
SessionStore
  └─ Session (named, has active window, cwd, timestamps)
       └─ BTreeMap<u32, Window>   (window_index → Window)
            └─ Vec<Pane>          (window-order list; gaps allowed by index)
                 └─ PaneGeometry  (x, y, cols, rows in terminal coordinates)

Screen (owned externally, keyed by PaneId)
  ├─ Grid (scrollback history + visible rows)
  │    ├─ VecDeque<GridLine>  (history, oldest first)
  │    └─ Vec<GridLine>       (visible rows)
  └─ cursor_x, cursor_y, mode flags, scroll region, saved grids, hyperlinks
```

`Session` does not own a `Screen`. The screen (VT state + grid) is owned by the server layer (`rmux-server`) and keyed by `PaneId`. The core crate defines the geometry and identity; the rendering state lives elsewhere. This keeps the core crate I/O-free.

Sources: [crates/rmux-core/src/lib.rs:4-7](), [crates/rmux-core/src/session.rs:38-52](), [crates/rmux-core/src/window.rs:79-101](), [crates/rmux-core/src/pane.rs:46-53]()

---

## Session

`Session` is the top-level object. It owns a `BTreeMap<u32, Window>` indexed by *window index* (the number that appears in the status bar), tracks which window is active and which was last active, and maintains monotonically increasing allocators for both `WindowId` and `PaneId`.

```rust
// crates/rmux-core/src/session.rs:38-52
pub struct Session {
    id: SessionId,
    name: SessionName,
    group_name: Option<SessionName>,
    windows: BTreeMap<u32, Window>,
    winlink_alert_flags: BTreeMap<u32, AlertFlags>,
    active_window: u32,
    last_window: Option<u32>,
    next_pane_id: u32,
    next_window_id: WindowIdAllocator,
    created_at: i64,
    activity_at: i64,
    last_attached_at: Option<i64>,
    cwd: Option<PathBuf>,
}
```

Key invariants:
- `active_window` always names a key present in `windows`. The `synchronized_active_window` helper re-derives a valid active window whenever windows are added or removed.
- Window indices may contain gaps (e.g., window 0, 2, 5 in the same session) after deletions or explicit user renumbering.
- `next_pane_id` is a session-global counter that skips already-allocated IDs, allowing pane IDs to be globally unique within a session and stable across layout changes.

### Session operations

| Operation | Method | Effect |
|---|---|---|
| Split pane | `split_pane_in_window_with_id_and_direction_before` | Delegates to `Window`, allocates `PaneId` |
| Kill pane | `kill_pane_in_window` | Removes pane; destroys window when it's the last |
| Move focus | `select_pane_in_window` | Updates `active_pane` on the target `Window` |
| Resize | `resize_pane_in_window` | Delegates to layout; `Zoom` short-circuits to `toggle_zoom_in_window` |
| Terminal resize | `resize_terminal` | Fans out `set_size` to every window |

Sources: [crates/rmux-core/src/session.rs:55-90](), [crates/rmux-core/src/session.rs:486-521]()

---

## Window

`Window` owns the pane list, the active layout, and the zoom state. Its pane collection is a `Vec<Pane>` ordered for *display* (window order), with pane *indices* as a stable but potentially gapped integer namespace:

```rust
// crates/rmux-core/src/window.rs:79-101
pub struct Window {
    id: WindowId,
    panes: Vec<Pane>,
    next_pane_index: u32,
    active_pane: u32,
    last_pane: Option<u32>,
    layout: LayoutName,
    layout_tree: Option<LayoutTree>,
    zoomed: bool,
    alert_flags: AlertFlags,
    requested_main_width: Option<u16>,
    requested_main_height: Option<u16>,
    // ...
}
```

The crucial split between *order* and *index*:

- **Display order** (`Vec<Pane>` position) determines layout adjacency — what you see on screen and how splits are applied.
- **Stable index** (`Pane::index`) is the display handle used in tmux target syntax (e.g., `mysession:0.2`). It persists after a rotation or swap.

When panes are rotated (`rotate_panes`), the vector is rotated in place and each pane's `index` field is renumbered to match its new position. The previously-active pane identity is tracked through `last_pane` so `select-pane -l` can restore it.

### Alert flags

`AlertFlags` is a bitset distinguishing BELL (`0x1`), ACTIVITY (`0x2`), and SILENCE (`0x4`). Windows maintain two parallel flag values: a *queue* used for in-flight processing and a *winlink-visible* copy tracked per window-index at the session level. `take_alert_flags` drains and returns the queue atomically.

Sources: [crates/rmux-core/src/window.rs:53-70](), [crates/rmux-core/src/window.rs:332-374]()

---

## Pane

`Pane` is a lightweight geometry record. It holds a stable `PaneId` (survives moves and transfers), a display `index` (used in target syntax, can change), and a `PaneGeometry` (position and size within the terminal coordinate space):

```rust
// crates/rmux-core/src/pane.rs:46-53
pub struct Pane {
    id: PaneId,
    index: u32,
    geometry: PaneGeometry,
    active_point: u64,
}
```

`PaneGeometry` stores `(x, y, cols, rows)` — where `(x, y)` is the upper-left corner in the full terminal coordinate system. The geometry is purely derived from the layout tree; `Pane` itself never performs layout calculations.

The distinction between `id` and `index` is load-bearing:

```
PaneId  — stable across session lifetime; used to look up Screen state
index   — display handle; the `3` in `myses:0.3`; changes on rotate/swap
```

Sources: [crates/rmux-core/src/pane.rs:6-109]()

---

## Grid: Scrollback and Visible Rows

`Grid` is the backing store for one terminal screen surface. It splits lines into a `VecDeque<GridLine>` (history, oldest at front, bounded by `hlimit`) and a `Vec<GridLine>` (the visible viewport, exactly `sy` rows):

```rust
// crates/rmux-core/src/grid.rs:69-77
pub(crate) struct Grid {
    sx: u32,
    sy: u32,
    hlimit: usize,
    hscrolled: usize,
    history_enabled: bool,
    history: VecDeque<GridLine>,
    visible: Vec<GridLine>,
}
```

Absolute line addressing: rows `0..hsize` are history; rows `hsize..hsize+sy` are visible. This unified addressing is used by `absolute_line()` and `render_absolute_line()`.

### Scroll operations

`scroll_region_up(upper, lower, bg, to_history)` removes the top line of the scroll region, optionally pushes it to history, and inserts a blank row at the bottom. `scroll_region_down` is the mirror. Only full-width scrolls (`upper==0`) push to history.

### Width reflow

When the terminal width changes, `resize_width` calls `reflow_wrapped_lines`, which:
1. Collects cells from consecutive `WRAPPED` lines into a logical line.
2. Re-wraps at the new width, generating new `GridLine` entries with correct `WRAPPED` flags.
3. Splits the result back into history and visible at the `sy` boundary.

This ensures that a terminal session that was wrapped at 80 columns becomes naturally re-wrapped if the pane is widened, matching tmux's behavior.

Sources: [crates/rmux-core/src/grid.rs:283-519]()

### GridCell and GridLine

Each `GridLine` holds a `Vec<GridCell>` and `GridLineFlags`. Each `GridCell` carries:

| Field | Purpose |
|---|---|
| `text: String` | Rendered glyph (1–4 bytes for Unicode) |
| `width: u8` | Display columns (0 = padding, 1 = narrow, 2 = wide CJK/emoji) |
| `flags: GridCellFlags` | PADDING, CLEARED, TAB, EXTENDED, SELECTED, NOPALETTE |
| `attr: u16` | SGR attribute bitmask (BOLD, DIM, ITALIC, BLINK, REVERSE, …) |
| `fg / bg / us: Colour` | Foreground, background, underline colour |
| `link: u32` | OSC 8 hyperlink inner ID (0 = no link) |

Wide glyphs occupy two adjacent cells: the first cell has `width=2`, and the immediately following cell is a `PADDING` cell (`width=0`, `text=" "`). The `owning_cell_x` helper resolves a padding cell back to its owner, which is essential for correct cursor movement and overwrite logic.

```rust
// crates/rmux-core/src/grid/cell.rs:14-27
impl GridCellFlags {
    pub const PADDING: Self = Self(0x1);
    pub const CLEARED: Self = Self(0x2);
    pub const TAB:     Self = Self(0x4);
    pub const EXTENDED: Self = Self(0x8);
    pub const SELECTED: Self = Self(0x10);
    pub const NOPALETTE: Self = Self(0x20);
}
```

`GridLineFlags` track:

| Flag | Meaning |
|---|---|
| `WRAPPED` | This row's logical content continues on the next row |
| `EXTENDED` | Extended cell storage in use |
| `DEAD` | Line is marked for removal |
| `START_PROMPT` | Line begins a shell prompt block (OSC 133) |
| `START_OUTPUT` | Line begins shell output |

Sources: [crates/rmux-core/src/grid/cell.rs:11-90](), [crates/rmux-core/src/grid/cell.rs:92-240]()

---

## Screen: VT State Over the Grid

`Screen` wraps `Grid` and adds all the mutable VT terminal state that changes as sequences are processed:

```rust
// crates/rmux-core/src/screen.rs:32-55
pub struct Screen {
    grid: Grid,
    cursor_x: u32,
    cursor_y: u32,
    pending_wrap: bool,
    saved_cursor_x/y: Option<u32>,
    saved_state: SavedState,       // DECSC/DECRC
    saved_grid: Option<SavedGrid>, // alternate screen (smcup/rmcup)
    rupper: u32, rlower: u32,      // DECSTBM scroll region
    mode: u32,                     // terminal mode flags
    cursor_style: u32,
    title/window_name/path: String,
    title_stack: Vec<String>,
    tabs: Vec<bool>,               // tab stop positions
    hyperlinks: Hyperlinks,        // OSC 8 intern table
    bell_count: u64,
    utf8_config: Utf8Config,
}
```

### Alternate screen

`Screen::is_alternate()` returns `true` when `saved_grid.is_some()`. Entering the alternate screen (`?1049h`) snapshots the current grid and cursor into `saved_grid`, then clears the main grid. Exiting (`?1049l`) restores the saved grid and cursor. The golden trace `alternate_screen_roundtrip.trace` pins this behavior: after exit, the primary content and cursor position are exactly restored while the alternate content disappears.

### Pending wrap

When a character is written to the very last column and `MODE_WRAP` is set, the cursor does not immediately wrap — instead `pending_wrap = true`. The actual wrap and linefeed happen the next time any printable character arrives. This is the canonical xterm/VT100 behavior and is observable in trace output as cursor position `x = max_x` with `pending_wrap: true`.

### Wide character writes

`write_char` calls `overwrite_for_write(x, width)` first, which clears any dangling padding cell at the target column and the cells immediately following. It then writes the primary cell with `width=2` and fills the next column with a padding cell. The `wide_overwritten_by_narrow` fixture pins that overwriting a wide glyph with a narrow one clears the orphaned padding cell.

Sources: [crates/rmux-core/src/screen.rs:32-55](), [crates/rmux-core/src/screen.rs:130-136](), [crates/rmux-core/src/screen.rs:382-448]()

---

## VT Parser: `InputParser`

`InputParser` (in `crates/rmux-core/src/input/`) is a pure-Rust DEC-style state machine that processes byte streams from the PTY. It is modeled on tmux's `input_ctx` and uses a transition table driven by the current `InputState`:

```
Ground → EscEnter → EscDispatch
                  → CsiEnter → CsiDispatch
       → OscString → (exit: dispatch OSC)
       → DcsEnter → DcsDispatch
       → TopBitSet (UTF-8 accumulation)
```

The parser maintains separate buffers for intermediate bytes (`interm_buf`), CSI/DCS parameter bytes (`param_buf`), and OSC/APC string bodies (`input_buf`). All screen effects are delivered through the `ScreenWriter` trait — the parser itself is I/O-free and contains no reference to `Screen`.

### SGR handling

SGR parameters are parsed in `dispatch_csi` → `sgr.rs`. The colour encoding:
- ANSI 0–7: encoded as the raw integer
- 256-colour: `colour | COLOUR_FLAG_256`
- RGB: `colour | COLOUR_FLAG_RGB | (r << 16) | (g << 8) | b`
- Default: `COLOUR_DEFAULT` sentinel

The trace `sgr_256_and_rgb.trace` confirms that `\x1b[38;5;208m` is stored as `fg=x256=208` and `\x1b[38;2;10;20;30m` as `fg=rgb=10,20,30`.

### UTF-8 accumulation

The `TopBitSet` handler builds multi-byte sequences in `utf8_buf`. On completion it decodes the codepoint and calls `writer.collect_add(ch, &self.cell)`. If the continuation sequence is invalid (wrong length or non-continuation byte), `U+FFFD` is emitted. The `unicode_wide_cjk` trace shows three Japanese characters (each 3 UTF-8 bytes) correctly parsed and stored as `w=2` cells with adjacent padding.

### Reply buffer

Some sequences require responses back to the PTY (e.g., DA1 `\x1b[c` → `\x1b[?1;2c`). `InputParser::take_replies()` drains these into a `Vec<u8>` for the server to write. The `device_attributes_reply` trace verifies this round-trip.

Sources: [crates/rmux-core/src/input/mod.rs:113-162](), [crates/rmux-core/src/input/mod.rs:246-308]()

---

## Parser Trace Golden Tests

`crates/rmux-core/tests/parser_traces.rs` defines 30+ `Fixture` structs and runs each through a fresh `InputParser` + `Screen`. The result is serialized to a multi-section text format and compared against the corresponding `.trace` file in `tests/parser_traces/`.

**Fixture shape:**
```rust
struct Fixture {
    name: &'static str,   // slug → filename
    cols: u16,
    rows: u16,
    history: usize,
    resize_after: Option<(u16, u16)>,  // optional reflow exercise
    feeds: &'static [&'static [u8]],   // sequential byte chunks
}
```

**Trace format sections:**

| Section | What it records |
|---|---|
| `parser_state` / `parser_pending_hex` | Final parser state and any buffered incomplete sequence |
| `mode_bits` | Terminal mode bitmask (e.g., `0x00000011` = cursor-visible + auto-wrap) |
| `cursor`, `alternate`, `history_size` | Cursor position, alternate-screen flag, history row count |
| `visible_lines` / `history_lines` | Per-cell dump: text, width, attr, fg/bg/us, padding marker |
| `transcript` / `transcript_joined` | Captured text with and without wrap-joining |
| `transcript_with_sequences` | Capture with embedded octal-escaped ANSI SGR |
| `saved_transcript` | Content of the saved (primary) grid when alternate screen is active |

**Regeneration:** set `RMUX_REGEN_PARSER_TRACES=1` before running to update goldens after intentional behavior changes.

**Guard tests:**
- `no_orphan_golden_files`: every `.trace` file must have a matching fixture; stale files fail the build.
- `golden_trace_files_have_no_trailing_whitespace`: enforces clean diffs.
- `terminal_parser_boundary_stays_private`: asserts that `TerminalParser` and the `vt100` crate are never exported, keeping the parser migration seam private.

Sources: [crates/rmux-core/tests/parser_traces.rs:1-29](), [crates/rmux-core/tests/parser_traces.rs:352-428](), [crates/rmux-core/tests/parser_traces.rs:490-522]()

---

## Command Parser

`crates/rmux-core/src/command_parser.rs` implements a pure tmux-compatible tokenizer mirroring `cmd-parse.y`. It produces `ParsedCommands` — a list of `ParsedCommand` values, each holding a canonical command name, argument vector, and source line.

Key design points:
- Command names are resolved via `lookup_command_at` using exact alias then unique-prefix matching against the frozen `COMMAND_TABLE`.
- Default aliases are baked in: `"split-pane"` → `"split-window"`, `"splitp"` → `"split-window"`, `"choose-window"` → `"choose-tree -w"`, etc.
- `CommandArgument` is an enum that can be a scalar `String` or a nested `Commands(ParsedCommands)` for brace-delimited blocks.
- `%if`/`%endif` conditionals at parse time are evaluated against a `FormatVariables` context, enabling config-file conditional loading without I/O.
- The parser is pure and deterministic: given identical input and context it always produces identical output.

Sources: [crates/rmux-core/src/command_parser.rs:1-12](), [crates/rmux-core/src/command_parser.rs:500-507]()

---

## Formats

`crates/rmux-core/src/formats/` implements tmux's `#{}` format-string evaluation. A format string is evaluated against a `FormatVariables` context that provides values for named variables (`session_name`, `window_index`, `pane_pid`, etc.). The module handles:

- Variable expansion: `#{session_name}` → current session name
- Conditional: `#{?flag,true,false}`
- Modifiers: `#{s/pattern/replacement/flags:variable}`, `#{b:variable}`, `#{l:literal}`, etc.
- Colour parsing: `#{fg}`, `#{bg}` attributes

Because `formats` has no dependencies on I/O or timing (beyond `time.rs` for `#{t:...}`), it is exercised by unit tests in `src/formats/tests/`.

---

## Hooks

`HookStore` (`crates/rmux-core/src/hooks.rs`) is the in-memory registry for tmux-style event callbacks. It stores bindings at four scopes: global-session, global-window, session-local, and pane-local.

```rust
// crates/rmux-core/src/hooks.rs:21-28
pub struct HookStore {
    session_global: HookBindings,
    window_global: HookBindings,
    sessions: HashMap<SessionName, HookBindings>,
    windows: HashMap<WindowTarget, HookBindings>,
    panes: HashMap<PaneTarget, HookBindings>,
}
```

`HookStore::dispatch(scope, hook)` resolves the closest registered binding (pane → window → global) and returns a `Vec<HookDispatch>` — command strings ready to be enqueued by the server. The store is pure: it neither runs commands nor calls into OS or network code. Scoped cleanup (`remove_session`, `remove_window`, `remove_pane`) ensures no dangling entries outlive their owners. Renaming a session re-keys all associated window and pane entries atomically.

Sources: [crates/rmux-core/src/hooks.rs:270-277](), [crates/rmux-core/src/hooks.rs:279-309]()

---

## Crate Boundary Rule

The crate header explicitly states the safe-change constraint:

> *This crate models sessions, windows, panes, layout geometry, and exact target resolution without any OS, network, or process integration.*

The `#![forbid(unsafe_code)]` and `#![deny(missing_docs)]` attributes at the crate root enforce this. The `terminal_parser_boundary_stays_private` test verifies at compile time that `TerminalParser` and the `vt100` crate are not re-exported — allowing the internal parser to be swapped without breaking any consumer.

Any logic that requires:
- File I/O, environment reads → `rmux-os`
- PTY lifecycle, process spawning → `rmux-pty`
- Tokio tasks, server state, client connections → `rmux-server`

does not belong here. `rmux-core` must remain a pure, sync, deterministic library that can be unit-tested with no external dependencies beyond in-process Rust.

Sources: [crates/rmux-core/src/lib.rs:1-2](), [crates/rmux-core/tests/parser_traces.rs:490-522]()
