diff --git a/mux/README.md b/mux/README.md index cd90980745c7..36722efb7a8e 100644 --- a/mux/README.md +++ b/mux/README.md @@ -1,158 +1,53 @@ # cmux-mux -A decoupled terminal-multiplexer backend for cmux, with a bundled tmux-like TUI. The multiplexer core owns workspaces → screens → split panes → tabs: a workspace holds screens (like tmux windows; the status bar switches between them), each screen is a binary split tree of panes mirroring the cmux app's pane system, and each pane holds one or more tabs (surfaces). A surface can be a real PTY whose output feeds libghostty-vt, or a local Chrome/Chromium page driven over the Chrome DevTools Protocol and rendered in the TUI with kitty graphics. Frontends only read render snapshots and send input, so PTY session state can be drawn by the Ratatui TUI in any terminal today and attached to real Ghostty surfaces in the cmux app later. +`cmux-mux` is the Rust TUI multiplexer in this repository. It keeps a tmux-style tree of workspaces, screens, split panes, and tabs, uses Ghostty's VT engine for PTY state, and exposes the same state over a JSON-lines control socket for attach clients and other frontends. -## Layout +## Documentation -- `crates/ghostty-vt-sys` — raw FFI. build.rs compiles `libghostty-vt.a` from `../ghostty` with zig (`-Demit-lib-vt=true`, ReleaseFast) and generates bindings from `include/ghostty/vt.h` with bindgen. -- `crates/ghostty-vt` — safe wrapper: `Terminal` (vt parsing, modes, callbacks, plain-text dump), `RenderState` (dirty-tracked viewport snapshots), `KeyEncoder` (legacy + kitty keyboard protocol, synced from terminal modes). -- `crates/mux-cdp` — sync CDP transport and Chrome lifecycle for local browser surfaces. -- `crates/mux-core` — the backend: session model (`model.rs`), orchestrator (`mux.rs`), surface runtimes (`surface.rs` / `browser.rs`), layout math shared by frontends (`layout.rs`), and the JSON control socket (`server.rs`). -- `crates/mux-tui` — the `cmux-mux` binary: crossterm + Ratatui frontend (`app.rs` event loop, `ui/` drawing, `session/` local-or-remote session abstraction). +- [Docs index](docs/README.md) +- [Getting started](docs/getting-started.md) +- [Concepts](docs/concepts.md) +- [Keyboard](docs/keyboard.md) +- [Mouse](docs/mouse.md) +- [Configuration](docs/configuration.md) +- [Control socket protocol](docs/protocol.md) +- [Browser panes](docs/browser-panes.md) -## Build and run +## Build -Requires zig 0.15.2 (same pin as CI, see `scripts/install-zig-ci.sh`) and a Rust toolchain. The ghostty submodule must be initialized. +Builds need zig 0.15.2, a Rust toolchain, and the `ghostty` submodule initialized. The `ghostty-vt-sys` crate builds `libghostty-vt.a` from the submodule with zig before compiling the Rust crates. ```bash cd mux -cargo run -p mux-tui # TUI, session "main" -cargo run -p mux-tui -- --headless --session agents # backend only -cargo run -p mux-tui -- attach --session agents # attach/reattach a TUI to it -cargo test # unit + integration tests +cargo build -p mux-tui ``` -Detach with prefix-d while attached; the headless session keeps running and `attach` reconnects with full screen state (VT replay + live stream). A local (non-attach) `cmux-mux` ends its session on quit. +## Run -## Platforms - -cmux-mux supports macOS and Linux. Runtime sockets live under `$XDG_RUNTIME_DIR/cmux-mux-` when `XDG_RUNTIME_DIR` is set, then `$TMPDIR/cmux-mux-`, then `/tmp/cmux-mux-`. Config uses `CMUX_MUX_CONFIG`, then `$XDG_CONFIG_HOME/cmux/mux.json`, then `~/.config/cmux/mux.json`; Ghostty selection colors are seeded from `$XDG_CONFIG_HOME/ghostty/config`, `~/.config/ghostty/config`, and on macOS the Ghostty Application Support config. Launched Chrome profiles use the macOS Application Support path or `$XDG_DATA_HOME/cmux-mux/chrome-profile`, falling back to `~/.local/share/cmux-mux/chrome-profile`. - -PTY tabs use `$SHELL`; if it is unset, Unix falls back to `/bin/bash` when present and then `/bin/sh`. Chrome discovery checks configured `browser.chrome_binary` first. macOS then checks the standard Chrome, Chromium, Brave, and Edge app bundles before PATH names; Linux checks `google-chrome`, `google-chrome-stable`, `chromium`, and `chromium-browser` from PATH, then common `/usr/bin`, `/snap/bin`, and `/opt` locations. - -Windows support via ConPTY is planned for phase 2; the transport, config, and shell seams are already isolated, but Windows is not documented as supported yet. - -Keys (prefix Ctrl-b, tmux-style): `c` new screen, `n`/`p` next/previous screen, `&` close screen, `,` rename screen, `t` new PTY tab, `B` new browser tab URL prompt, `Tab`/`BackTab` next/previous tab, `1`-`9` select tab, `%` split right, `"` split down, `h j k l`/arrows move focus, `x` close tab, `X` close pane, `$` rename workspace, `w`/`W` switch/create workspace, `s` toggle the workspace sidebar, PageUp/PageDown scrollback, `d` quit, `Ctrl-b` twice sends a literal Ctrl-b. Modeless Alt shortcuts are also on by default: `Alt-n` smart-splits the focused pane, `Alt-h/j/k/l` or Alt-arrows move focus, `Alt-[`/`Alt-]` switch screens, `Alt-t` opens a tab, and `Alt-=`/`Alt--` resize the focused split. - -Every pane draws a border box; the active pane's border is highlighted, the pane under the mouse gets a hover shade, and the box is where flashing notifications will hook in later. The top border doubles as an always-visible tab bar: tabs are numbered (`1`, `2`, ...; the process title follows the number when reported), clicking a title switches, dragging a tab reorders it within the pane or moves it to another pane's tab bar, the trailing `+` opens a new tab, and when tabs overflow, `‹`/`›` arrows (or the wheel over the bar) scroll them while the active tab stays visible. User-assigned tab names replace the generated number/title label outright. Drag a shared pane border to resize that split live; dragging a corner moves both intersecting splits, and outer pane edges are inert. Click anywhere in a pane to focus it. The status bar shows the active workspace's screens: click an entry to switch, the trailing `+` for a new screen; it spans only the pane region (not the sidebar). Right-click a pane for rename tab / new tab / split right / split down / close tab / close pane; right-click a workspace in the sidebar for rename/close; right-click a screen in the status bar for rename/close. Context menus and prompts draw muted borders; menu items keep one-cell side padding and the hover/selection highlight spans the full inner row. Right-press, drag, and release on a row activates that row. Prompts use readline-style editing with shortcut buttons (`Clear ^C`, `Cancel esc`, `OK ⏎`); Enter commits, Esc cancels, Ctrl-C clears, and empty tab/screen names fall back to defaults. Right-clicking while the prompt is open shakes it instead of opening a menu. The sidebar reserves two lines per workspace (name, then the active pane's title) under a `workspaces` header with a blank line after it and between entries; click an entry to switch, drag entries to reorder workspaces, `+ new workspace` to create one, and drag the sidebar's right border to resize it for the current session. - -Drag to select text in PTY panes; on release the selection is copied to the host clipboard via OSC 52 (works over SSH). The highlight is scroll-stable: it survives viewport scrolling, and holding a selection drag on the top or bottom content edge auto-scrolls while extending the range. Typing clears the selection. Wheel scrolls the PTY pane under the mouse, focusing it first (arrow keys on the alternate screen). Browser panes receive text input, Enter/Backspace/Tab/Esc/navigation keys, left click/drag/release, and wheel scroll through CDP. The scrollbar defaults to a dedicated column just inside the right border; `scrollbar.position = "border"` restores the old border-overlay placement. A `▕` thumb appears whenever the surface has any scrollback (hidden only when no scrolling is possible at all). Hovering or dragging the thumb renders it as `▐`; clicking the thumb anchors a drag without moving the viewport, while clicking the track outside the thumb jumps there and then drags relative to that anchor. - -Indexed colors pass through to the host terminal's palette, so `cmux-mux` inherits the host theme like tmux. Truecolor cells pass through unchanged; palette entries overridden by an inner app with OSC 4 render as the override RGB because the host palette does not know about that inner override. - -## Browser panes - -Press prefix-`B` or right-click a pane and choose `New browser tab` to open a URL prompt. Bare domains get `https://` prepended; `about:blank` and explicit schemes pass through unchanged. Browser panes share one local Chrome DevTools Protocol connection per mux session. cmux first uses `CMUX_MUX_CDP_URL`, then `browser.cdp_url`, then probes `127.0.0.1` discovery ports such as 9222, and only then launches its own Chrome/Chromium-family binary in `--headless=new` mode. Launched Chrome uses a persistent cmux profile by default so logins survive restarts; set `browser.ephemeral` to use a temporary profile deleted on shutdown. - -Chrome 136 and newer ignore `--remote-debugging-port` for the default user data directory, so everyday Chrome profiles are not attachable. Reuse works with Chrome instances started with a custom `--user-data-dir` and a debugging port, or with other tooling/headless instances that expose `/json/version`. Headful Chrome may throttle screencast frames when its window or tab is hidden or occluded. - -Frames stream as `Page.screencastFrame` PNGs into the TUI. The frame is rendered with the kitty graphics protocol after each Ratatui draw; overlapping cmux menus and prompts temporarily delete the image placement so terminal UI stays readable. - -Terminal support: - -| Terminal | Browser frame rendering | -| --- | --- | -| Ghostty | Supported via kitty graphics | -| kitty | Supported via kitty graphics | -| WezTerm | Supported when kitty graphics are enabled | -| Other terminals | TUI remains usable and shows `terminal has no kitty graphics support` | - -If no reusable browser is found and no Chrome binary is found, browser tab creation fails in the status line with an error naming `browser.chrome_binary`. Attach clients do not stream browser pixels in v1: remote attach shows a placeholder for browser surfaces, and browser creation over attach returns `browser panes are not supported over attach yet`. `list-workspaces` reports browser tabs with `kind: "browser"` and `browser_source: "external"` or `"launched"`. - -## Configuration - -`CMUX_MUX_CONFIG`, `$XDG_CONFIG_HOME/cmux/mux.json`, or `~/.config/cmux/mux.json`; every key is optional: - -```json -{ - "theme": { - "selection_background": "#3a3a3a", - "selection_foreground": null, - "sidebar_rail": "#87afd7", - "sidebar_active_bg": 236, - "tab_rail": "#87afd7", - "tab_bg": 236, - "tab_active_bg": null, - "border_active": "#87afd7", - "border_inactive": "#444444" - }, - "tabs": { - "min_width": 7, - "solid_background": true, - "show_titles": false, - "agents": ["claude", "codex", "opencode", "pi"] - }, - "sidebar": { "width": 22, "max_width": 0 }, - "browser": { - "chrome_binary": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - "cdp_url": "http://127.0.0.1:9222", - "discover": true, - "discover_ports": [9222], - "user_data_dir": "/Users/me/Library/Application Support/cmux-mux/chrome-profile", - "ephemeral": false - }, - "scrollbar": { "position": "column" }, - "keys": { - "prefix": "ctrl+b", - "alt_shortcuts": true, - "new-screen": "c", "next-screen": ["n", "alt+]"], "prev-screen": ["p", "alt+["], - "close-screen": "&", "rename-screen": ",", - "new-tab": ["t", "alt+t"], "new_browser_tab": "B", - "next-tab": "tab", "prev-tab": "backtab", "close-tab": "x", "close-pane": "X", - "new-pane-smart": "alt+n", - "split-right": "%", "split-down": "\"", - "rename-tab": "none", "rename-workspace": "$", - "next-workspace": "w", "new-workspace": "W", - "toggle-sidebar": "s", - "focus-left": ["h", "left", "alt+h", "alt+left"], - "focus-right": ["l", "right", "alt+l", "alt+right"], - "focus-up": ["k", "up", "alt+k", "alt+up"], - "focus-down": ["j", "down", "alt+j", "alt+down"], - "resize-grow": "alt+=", "resize-shrink": "alt+-", - "scroll-up": "pageup", "scroll-down": "pagedown", - "detach": "d" - } -} +```bash +cd mux +cargo run -p mux-tui +cargo run -p mux-tui -- --session agents +cargo run -p mux-tui -- --headless --session agents +cargo run -p mux-tui -- attach --session agents ``` -Colors are `#rrggbb`, `#rgb`, or an xterm-256 index. The selection colors default to the user's Ghostty config (`selection-background`/`selection-foreground` from the platform paths above), falling back to a dark grey. `sidebar_rail` controls the active workspace rail, `sidebar_active_bg` its two-row background, `tab_rail` the active tab chip rail, `tab_bg` inactive solid tab chips, and `tab_active_bg` overrides the focused/unfocused active tab chip backgrounds when set. Tabs are numbered `1 2 3…` by default; recognized agent programs (the `agents` list) surface after the number, `show_titles` restores full process titles, and a user-assigned tab name overrides both. `sidebar.max_width` defaults to `0` for unlimited, while live drag still leaves at least 40 columns for panes. `scrollbar.position` is `"column"` by default or `"border"` for the old right-border overlay. Browser config is optional: `chrome_binary` overrides binary discovery, `cdp_url` accepts `ws://...` or `http://host:port`, `discover` defaults to true, `discover_ports` defaults to `[9222]`, `user_data_dir` overrides the launched profile path, and `ephemeral` restores temporary-profile behavior. When `ephemeral` is true it takes precedence over `user_data_dir`: cmux creates and later deletes a fresh temp profile and never deletes the configured directory. Every prefix/modeless binding is remappable via `keys` (formats: `"c"`, `"%"`, `"ctrl+b"`, `"alt+enter"`, `"tab"`, `"backtab"`, `"pageup"`); values may be a string, an array of strings, or `"none"` to unbind. Set `"alt_shortcuts": false` to remove default Alt chords without blocking user-configured Alt chords. `1`-`9` stay fixed to tab selection. The old key name `"rename-pane"` is still accepted as an alias for `"rename-tab"`. +The default session is `main`. Default sockets live at `$TMPDIR/cmux-mux-/.sock`; use `--socket ` for an explicit path. Detach from an attached TUI with prefix `d`, which is `Ctrl-b d` by default. -## Control socket +Use `--term ` to set `TERM` for child PTYs. Without it, children get `xterm-256color`; `CMUX_MUX_TERM` can override the process default in the surface layer. -Every instance serves a JSON-lines protocol on a unix socket (default under the platform runtime directory, also exported to children as `CMUX_MUX_SOCKET`). One request per line: +## Development ```bash -SESSION=main -SOCK=${CMUX_MUX_SOCKET:-${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/cmux-mux-$(id -u)/${SESSION}.sock} -printf '%s\n' '{"id":1,"cmd":"identify"}' | nc -U "$SOCK" -printf '%s\n' '{"id":2,"cmd":"list-workspaces"}' | nc -U "$SOCK" -printf '%s\n' '{"id":3,"cmd":"send","surface":1,"text":"ls\r"}' | nc -U "$SOCK" -printf '%s\n' '{"id":4,"cmd":"read-screen","surface":1}' | nc -U "$SOCK" +cd mux +cargo test ``` -Commands: `identify`, `list-workspaces` (each workspace carries `screens`, each with its split-tree `layout` plus `panes` with their `tabs`; each tab includes `kind: "pty" | "browser"` and browser tabs include `browser_source`), `send` (text or base64 `bytes`, PTY only), `read-screen` (PTY only), `vt-state` (PTY only), `new-tab` (PTY tab in a pane), `new-browser-tab` (local browser tab in a pane), `new-screen` (in a workspace), `new-workspace`, `split` (`dir`: `right`/`down`), `set-ratio` (`pane`, `dir`: `right`/`down`, `ratio`), `move-tab` (`surface`, target `pane`, insertion `index`), `move-workspace` (`workspace`, insertion `index`), `set-default-colors` (`fg`/`bg`: `#rrggbb`), `close-surface`, `close-pane`, `close-screen`, `close-workspace`, `rename-surface`, `rename-pane`, `rename-screen`, `rename-workspace`, `resize-surface`, `focus-pane`, `select-tab` (within a pane), `select-screen`, `select-workspace`, `scroll-surface` (PTY only), `subscribe`, `attach-surface` (PTY only). - -`subscribe` turns the connection full-duplex: the server pushes `{"event":...}` lines (tree-changed, surface-output, surface-resized, surface-exited, title-changed, bell). A changed `resize-surface` broadcasts `{"event":"surface-resized","surface":1,"cols":120,"rows":40}` with the final cell size; a same-size resize emits no event. `attach-surface` sends a `vt-state` event carrying a base64 VT replay of a PTY surface's complete state (screen, styles, cursor, modes, palette, kitty keyboard state, charsets — produced by ghostty's formatter), then streams every subsequent pty byte as `output` events. Replaying state then stream into a fresh terminal reproduces the surface exactly; the snapshot and stream tap are taken under the same terminal lock, so there is no gap and no duplication. This is the attach surface for the cmux app: a real Ghostty surface can adopt a tab by replaying `vt-state` and following the stream, because both sides speak the same VT engine. Browser surfaces are local-only in v1; PTY-only socket commands against them return `ok:false` with a clear error. +The smoke scripts expect a built `cmux-mux` binary unless `CMUX_MUX_BIN` is set. -When several attach clients render the same surface at different sizes, the PTY uses latest-interaction sizing: a client reasserts its visible pane sizes only after local user interaction (key, mouse, paste, focus gained, or terminal resize). Mux-driven redraws update each client's mirror from `surface-resized` but do not reassert that client's viewport, so idle clients cannot fight over the PTY size. - -## Design notes - -- The pty reader thread is the only writer into a surface's `Terminal`; renderers take the terminal lock just long enough to snapshot into their own `RenderState`, so slow frontends never block pty IO. -- Query responses (DSR, DECRQM, ...) generated during parsing are queued by the write-pty callback and flushed to the pty after each parse batch. -- On TUI startup, cmux-mux probes the host terminal's default foreground/background with OSC 10/11 and caches any replies on the session. Inner apps that query OSC 10/11/4, such as Codex blending UI backgrounds from the terminal background, get libghostty-vt replies that match the host terminal. If the host does not answer the startup probe, dynamic color queries stay unanswered as before. -- Input is encoded with ghostty's key encoder synced from the active surface's terminal modes each keystroke, so cursor-key application mode and the kitty keyboard protocol work end to end. -- Browser input is sent through CDP `Input.*` commands; CDP screencast frames are acknowledged immediately so Chrome keeps streaming. -- Browser surfaces share a single CDP browser connection; closing a tab closes only its target, and mux shutdown kills Chrome only when cmux launched it. -- Exited surfaces are reaped by the mux itself (tab removed, pane/workspace collapsed), so headless sessions and every frontend see the same tree without frontend-side cleanup. -- Surfaces spawn at their final render size (`new-tab`/`new-workspace`/`split` take optional `cols`/`rows`, and the TUI predicts sizes from its layout): spawning at 80x24 and resizing a frame later makes shells repaint their first prompt, which left zsh's reverse-video `%` partial-line marker on screen. -- Children get `TERM=xterm-256color` by default; set `--term xterm-ghostty` (or `CMUX_MUX_TERM`) when the ghostty terminfo is installed. - -## Current limitations - -- Scrollback from before an attach is not replayed (the VT replay covers the screen and state, not history); the mirror accumulates its own scrollback from the live stream. -- Browser frame streaming over attach is not implemented; attach clients show a placeholder for browser tabs. -- Reused headful Chrome instances can pause screencast frames when their windows or tabs are hidden. -- No PTY mouse-event forwarding to applications (viewport scroll and alternate-screen arrow fallback only). -- Kitty graphics generated by PTY applications are tracked by the engine but not rendered by the TUI. -- Pane split ratios are adjustable from the TUI and control socket, but not persisted across new splits. +```bash +cd mux +cargo build -p mux-tui +python3 scripts/smoke-tui.py +python3 scripts/smoke-attach.py +``` diff --git a/mux/docs/README.md b/mux/docs/README.md new file mode 100644 index 000000000000..302288bb9468 --- /dev/null +++ b/mux/docs/README.md @@ -0,0 +1,13 @@ +# cmux-mux docs + +`cmux-mux` is a tmux-style multiplexer that speaks Ghostty's VT engine on both ends: PTY output is parsed into Ghostty terminal state, and attach clients receive Ghostty VT replay plus live output so another frontend can reconstruct the same surface. + +## Contents + +- [Getting started](getting-started.md): build prerequisites, local and headless runs, sockets, detach and attach. +- [Concepts](concepts.md): session tree, focus, collapse behavior, tab naming, smart split, PTY and browser surfaces. +- [Keyboard](keyboard.md): prefix model, modeless Alt layer, default bindings, and `mux.json` key remapping. +- [Mouse](mouse.md): clickable UI, drag reorder, resize, scrollbars, menus, selection, pointer shape, and dialogs. +- [Configuration](configuration.md): full `mux.json` reference with defaults and a worked example. +- [Control socket protocol](protocol.md): JSON-lines framing, protocol v6 attach streams, events, and compatibility rules. +- [Browser panes](browser-panes.md): CDP-backed browser tabs, rendering, input, profiles, and current limitations. diff --git a/mux/docs/browser-panes.md b/mux/docs/browser-panes.md new file mode 100644 index 000000000000..b641d2b12279 --- /dev/null +++ b/mux/docs/browser-panes.md @@ -0,0 +1,54 @@ +# Browser Panes + +Browser panes are local Chrome/Chromium targets controlled with the Chrome DevTools Protocol. They live in the same pane and tab tree as PTY tabs, but their rendering and input path are CDP-based instead of VT-based. + +## Requirements + +Browser panes need a local CDP endpoint or a launchable Chrome/Chromium-family binary. The TUI can reuse an external endpoint, discover one on configured local ports, or launch Chrome itself in `--headless=new` mode. + +Endpoint selection order: + +1. `CMUX_MUX_CDP_URL` +2. `browser.cdp_url` in `mux.json` +3. Discovery on `browser.discover_ports` when `browser.discover` is true +4. A launched Chrome using `browser.chrome_binary` or binary discovery in `mux-cdp` + +Binary discovery checks configured paths, known macOS and Linux Chrome-family paths, then `PATH` names such as `google-chrome`, `chromium`, `brave-browser`, and `microsoft-edge`. + +Set `CMUX_MUX_CDP_DEBUG` to print browser runtime debug messages to stderr. + +## Creating Panes + +Use prefix `B`, or right-click a pane and choose `New browser tab`. The prompt starts with `https://`. + +Bare domains get `https://` prepended. Inputs containing `://`, or starting with `about:`, `file:`, `data:`, `chrome:`, or `devtools:`, pass through unchanged. + +Browser tabs are created inside an existing pane when one is active. If the session has no workspaces, creating a browser tab creates a workspace, screen, and pane around it. + +## Rendering + +The browser runtime creates a target, attaches with CDP, enables the page domain, sets device metrics from the pane's cell size and detected cell pixels, and starts `Page.screencastFrame`. + +The TUI draws the latest PNG frame with the kitty graphics protocol after each Ratatui frame. If a context menu or prompt overlaps the pane, the graphics placement is omitted for that frame so the terminal UI stays readable. + +If the host terminal does not support kitty graphics, the pane displays `terminal has no kitty graphics support`. If the browser frame has not arrived yet, it displays a loading message. + +## Input + +Printable character keys and paste use CDP insert-text. Enter, Backspace, Tab, Esc, arrows, Home, End, PageUp, PageDown, and Delete use CDP key events with modifier bits for Alt, Control, Super, and Shift. + +Left click, drag, release, and wheel events inside browser content are forwarded as CDP mouse input. Wheel deltas are scaled by the detected cell height. + +## Profiles and Lifecycle + +Browser panes share one browser runtime per mux session. Closing a browser tab closes only its target. Mux shutdown kills Chrome only when cmux launched it. + +Launched Chrome uses a persistent cmux profile unless `browser.ephemeral` is true. `browser.user_data_dir` overrides the persistent profile path. When ephemeral mode is true, Chrome uses a temporary profile that is deleted on shutdown and ignores `browser.user_data_dir`. + +The default launched profile is `~/Library/Application Support/cmux-mux/chrome-profile` on macOS. On non-macOS targets it is `$XDG_DATA_HOME/cmux-mux/chrome-profile` when `XDG_DATA_HOME` is set, then `~/.local/share/cmux-mux/chrome-profile`. + +## Limitations + +Browser panes are local-only as of protocol v6. `attach-surface` returns an error for browser surfaces, attach clients do not receive browser frame streams, and a remote TUI shows a placeholder for browser panes. + +Headful external Chrome can throttle screencast frames when the window or tab is hidden or occluded. Chrome 136 and newer do not allow `--remote-debugging-port` with the default user data directory, so reusable everyday Chrome profiles may not expose CDP. diff --git a/mux/docs/concepts.md b/mux/docs/concepts.md new file mode 100644 index 000000000000..35f351820634 --- /dev/null +++ b/mux/docs/concepts.md @@ -0,0 +1,49 @@ +# Concepts + +## Tree + +The mux tree is: + +```text +session -> workspaces -> screens -> split-tree panes -> tabs +``` + +A session is one mux backend and one control socket. A workspace owns one or more screens. A screen is the layout selected in the status bar. A screen layout is a binary split tree whose leaves are panes. A pane owns an ordered tab list, and each tab is a surface. + +The UI uses tmux-style verbs for screens. Prefix `c` creates a screen, prefix `n` and `p` switch screens, prefix `&` closes a screen, and prefix `,` renames a screen. PTY tabs use prefix `t`, tab chips, and tab context menus. + +## Active and Focus State + +The session tracks the active workspace. Each workspace tracks its active screen. Each screen tracks its active pane. Each pane tracks its active tab. + +Focusing a pane makes that pane's screen and workspace active. Selecting a workspace or screen changes that level's active item. Selecting a tab changes the active tab in one pane. + +Pane focus tracks recent activity. When closing the active pane or the last tab in it, mux chooses the most recently active remaining pane on that screen instead of always choosing a neighbor. + +## Tabs and Names + +Tabs are surfaces. A PTY tab wraps a child process connected to a pseudo-terminal. A browser tab wraps a local Chrome/Chromium target. + +`rename-tab` sets the surface name. Empty tab names clear the custom name and fall back to the generated tab label. The old config key `rename-pane` is still accepted as an alias for the `rename-tab` key binding, but the UI rename action targets the tab surface, not the pane object. + +Pane names still exist in the control socket through `rename-pane`. They are separate from the tab labels shown in the TUI. + +## Smart Split + +The modeless `Alt-n` binding creates a new pane with smart split direction. The TUI first tries the focused pane. If that pane cannot split in the chosen direction, it tries the largest pane that can. + +Direction follows a zellij-style rule using the terminal cell ratio. Tall enough panes split down. Wide enough panes split right. Panes below the configured size thresholds do not split. + +## Collapse Behavior + +Closing a tab removes one surface. If the pane still has tabs, the active tab index moves to a remaining tab. + +If a pane loses its last tab, that pane is removed from the split tree and its parent split collapses to the remaining child. If that empties the screen, the screen is removed. If that empties the workspace, the workspace is removed. If every workspace is gone, mux emits an `empty` event. + +Closing a pane closes all tabs in that pane. Closing a screen closes every pane and tab in that screen. Closing a workspace closes every screen, pane, and tab in that workspace. + +## PTY and Browser Surfaces + +A PTY surface parses child-process output with libghostty-vt. Frontends render snapshots of that terminal state. Attach clients receive a VT replay first, then a base64 stream of subsequent PTY bytes, plus ordered resize frames when the surface geometry changes. + +A browser surface is a local Chrome/Chromium target controlled through the Chrome DevTools Protocol. The local TUI draws browser frames with kitty graphics and forwards keyboard, mouse, and wheel input over CDP. Browser surfaces are listed in the tree, but `attach-surface` does not stream browser pixels as of protocol v6. diff --git a/mux/docs/configuration.md b/mux/docs/configuration.md new file mode 100644 index 000000000000..db9afc81a6e1 --- /dev/null +++ b/mux/docs/configuration.md @@ -0,0 +1,158 @@ +# Configuration + +`cmux-mux` reads `~/.config/cmux/mux.json`, or `$XDG_CONFIG_HOME/cmux/mux.json` when `XDG_CONFIG_HOME` is set. Set `CMUX_MUX_CONFIG` to use another file; it takes precedence over both. Every documented key is optional. Unknown keys in the typed sections make the raw config invalid, so the TUI logs an error and falls back to defaults. + +Colors accept `#rrggbb`, `#rgb`, an xterm-256 number, or a numeric string. + +## Theme + +Selection colors are resolved in this order: explicit `mux.json`, Ghostty config keys `selection-background` and `selection-foreground`, then built-in defaults. Ghostty configs are read from `$XDG_CONFIG_HOME/ghostty/config` (when set), `~/.config/ghostty/config`, and on macOS `~/Library/Application Support/com.mitchellh.ghostty/config`; later entries in the file win. + +| Key | Type | Default | Effect | +| --- | --- | --- | --- | +| `theme.selection_background` | color | `#3a3a3a`, seeded from Ghostty when present | Selection background in PTY panes | +| `theme.selection_foreground` | color or null | `null`, seeded from Ghostty when present | Selection foreground; `null` keeps each cell's foreground | +| `theme.sidebar_rail` | color | `110` | Rail color for the active workspace rows | +| `theme.sidebar_active_bg` | color | `236` | Background for the active workspace rows | +| `theme.tab_rail` | color | `110` | Rail color inside the active tab chip | +| `theme.tab_bg` | color | `236` | Background for inactive solid tab chips | +| `theme.tab_active_bg` | color or null | `null` | Overrides the focused and unfocused active-tab chip backgrounds | +| `theme.border_active` | color | `110` | Focused pane border | +| `theme.border_inactive` | color | `238` | Unfocused pane border | + +## Tabs + +| Key | Type | Default | Effect | +| --- | --- | --- | --- | +| `tabs.min_width` | integer | `7` | Minimum tab label width, clamped to 3 through 40 | +| `tabs.solid_background` | boolean | `true` | Renders tab chips with solid backgrounds | +| `tabs.show_titles` | boolean | `false` | Shows full process titles after tab numbers | +| `tabs.agents` | string array | `["claude","codex","opencode","pi"]` | Agent names surfaced in tab labels when `show_titles` is false | + +Tabs are numbered by default. A recognized agent program can appear after the number. A user-assigned tab name replaces the generated label. + +## Sidebar + +| Key | Type | Default | Effect | +| --- | --- | --- | --- | +| `sidebar.width` | integer | `22` | Sidebar width, clamped to 10 through 60 on load | +| `sidebar.max_width` | integer | `0` | Maximum live drag width; `0` means no configured maximum | + +Live sidebar dragging also leaves at least 40 columns for pane content. + +## Browser + +| Key | Type | Default | Effect | +| --- | --- | --- | --- | +| `browser.chrome_binary` | string | `null` | Chrome/Chromium binary to launch when no external CDP endpoint is used | +| `browser.cdp_url` | string | `null` | External CDP endpoint, accepted as `http://host:port` or `ws://...` | +| `browser.discover` | boolean | `true` | Probe discovery ports before launching Chrome | +| `browser.discover_ports` | integer array | `[9222]` | Local ports to probe for `/json/version` | +| `browser.user_data_dir` | string | `null` | Persistent profile directory for launched Chrome | +| `browser.ephemeral` | boolean | `false` | Use a temporary launched Chrome profile and delete it on shutdown | + +When `browser.ephemeral` is true, it takes precedence over `browser.user_data_dir`: launched Chrome uses a fresh temporary profile, and the configured directory is not deleted. + +The default launched profile is `~/Library/Application Support/cmux-mux/chrome-profile` on macOS. On non-macOS targets it is `$XDG_DATA_HOME/cmux-mux/chrome-profile` when `XDG_DATA_HOME` is set, then `~/.local/share/cmux-mux/chrome-profile`. + +## Scrollbar + +| Key | Type | Default | Effect | +| --- | --- | --- | --- | +| `scrollbar.position` | `"column"` or `"border"` | `"column"` | Dedicated scrollbar column or right-border overlay | + +## Keys + +| Key | Type | Default | Effect | +| --- | --- | --- | --- | +| `keys.prefix` | chord string | `"ctrl+b"` | Prefix chord | +| `keys.alt_shortcuts` | boolean | `true` | Enables default modeless Alt bindings when true | +| `keys.new-tab` | chord string or array or `"none"` | `["t","alt+t"]` | New PTY tab | +| `keys.new_browser_tab` | chord string or array or `"none"` | `"B"` | Browser URL prompt | +| `keys.new-pane-smart` | chord string or array or `"none"` | `"alt+n"` | New pane using smart split direction | +| `keys.next-tab` | chord string or array or `"none"` | `"tab"` | Next tab | +| `keys.prev-tab` | chord string or array or `"none"` | `"backtab"` | Previous tab | +| `keys.split-right` | chord string or array or `"none"` | `"%"` | Split right | +| `keys.split-down` | chord string or array or `"none"` | `"\""` | Split down | +| `keys.close-tab` | chord string or array or `"none"` | `"x"` | Close active tab | +| `keys.close-pane` | chord string or array or `"none"` | `"X"` | Close active pane | +| `keys.rename-tab` | chord string or array or `"none"` | unbound | Rename active tab | +| `keys.rename-pane` | chord string or array or `"none"` | alias | Alias for `rename-tab` | +| `keys.rename-screen` | chord string or array or `"none"` | `","` | Rename active screen | +| `keys.rename-workspace` | chord string or array or `"none"` | `"$"` | Rename active workspace | +| `keys.close-screen` | chord string or array or `"none"` | `"&"` | Close active screen | +| `keys.prev-screen` | chord string or array or `"none"` | `["p","alt+["]` | Previous screen | +| `keys.next-screen` | chord string or array or `"none"` | `["n","alt+]"]` | Next screen | +| `keys.new-screen` | chord string or array or `"none"` | `"c"` | New screen | +| `keys.next-workspace` | chord string or array or `"none"` | `"w"` | Next workspace | +| `keys.new-workspace` | chord string or array or `"none"` | `"W"` | New workspace | +| `keys.toggle-sidebar` | chord string or array or `"none"` | `"s"` | Toggle sidebar | +| `keys.focus-left` | chord string or array or `"none"` | `["h","left","alt+h","alt+left"]` | Focus left | +| `keys.focus-right` | chord string or array or `"none"` | `["l","right","alt+l","alt+right"]` | Focus right | +| `keys.focus-up` | chord string or array or `"none"` | `["k","up","alt+k","alt+up"]` | Focus up | +| `keys.focus-down` | chord string or array or `"none"` | `["j","down","alt+j","alt+down"]` | Focus down | +| `keys.resize-grow` | chord string or array or `"none"` | `"alt+="` | Grow the focused split | +| `keys.resize-shrink` | chord string or array or `"none"` | `"alt+-"` | Shrink the focused split | +| `keys.scroll-up` | chord string or array or `"none"` | `"pageup"` | Scroll active PTY up 10 rows | +| `keys.scroll-down` | chord string or array or `"none"` | `"pagedown"` | Scroll active PTY down 10 rows | +| `keys.detach` | chord string or array or `"none"` | `"d"` | Quit local TUI or detach attached TUI | + +Each action override replaces all default chords for that action. Values may be a string, an array of strings, or `"none"`. Non-string array entries are ignored. Set `keys.alt_shortcuts` to `false` to remove default Alt chords before applying user overrides; explicitly configured Alt chords still work. Prefix `1` through `9` stay fixed to tab selection. + +Chord strings can be single characters or a key name with optional `ctrl`, `control`, `alt`, `option`, or `shift` modifiers. Examples: `"c"`, `"%"`, `"ctrl+b"`, `"alt+enter"`, `"tab"`, `"backtab"`, `"shift+tab"`, `"pageup"`, `"pagedown"`, `"esc"`, `"space"`, `"left"`, `"right"`, `"up"`, `"down"`, `"home"`, and `"end"`. + +## Example + +```json +{ + "theme": { + "selection_background": "#355c7d", + "selection_foreground": null, + "sidebar_rail": "#87afd7", + "sidebar_active_bg": 236, + "tab_rail": "#87afd7", + "tab_bg": 236, + "tab_active_bg": null, + "border_active": "#87afd7", + "border_inactive": "#444444" + }, + "tabs": { + "min_width": 9, + "solid_background": true, + "show_titles": false, + "agents": ["claude", "codex", "opencode", "pi"] + }, + "sidebar": { + "width": 24, + "max_width": 40 + }, + "browser": { + "chrome_binary": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "cdp_url": "http://127.0.0.1:9222", + "discover": true, + "discover_ports": [9222, 9223], + "user_data_dir": "/Users/me/Library/Application Support/cmux-mux/chrome-profile", + "ephemeral": false + }, + "scrollbar": { + "position": "column" + }, + "keys": { + "prefix": "ctrl+a", + "alt_shortcuts": false, + "new-tab": ["t", "alt+t"], + "new_browser_tab": "B", + "new-pane-smart": "alt+n", + "next-tab": "tab", + "prev-tab": "backtab", + "next-screen": ["n", "alt+]"], + "prev-screen": ["p", "alt+["], + "rename-tab": "r", + "rename-screen": ",", + "focus-left": ["h", "left", "alt+h", "alt+left"], + "focus-right": ["l", "right", "alt+l", "alt+right"], + "close-pane": "none", + "detach": "d" + } +} +``` diff --git a/mux/docs/getting-started.md b/mux/docs/getting-started.md new file mode 100644 index 000000000000..0d7c049aed81 --- /dev/null +++ b/mux/docs/getting-started.md @@ -0,0 +1,76 @@ +# Getting started + +## Prerequisites + +Builds need zig 0.15.2, a Rust toolchain, and the `ghostty` submodule. `ghostty-vt-sys` compiles `libghostty-vt.a` from that submodule, so an uninitialized submodule fails before the TUI starts. + +```bash +cd mux +cargo build -p mux-tui +``` + +## Local session + +A normal run starts an in-process mux, opens the TUI, and serves the control socket. + +```bash +cd mux +cargo run -p mux-tui +cargo run -p mux-tui -- --session agents +``` + +The default session is `main`. Quitting a local TUI shuts down that in-process session and removes its socket. + +Use `--term ` to set `TERM` for child PTYs. Without it, children get `xterm-256color`; the surface layer also honors `CMUX_MUX_TERM` when no CLI value is supplied. + +## Headless server and attach + +Headless mode starts only the mux backend and control socket. + +```bash +cd mux +cargo run -p mux-tui -- --headless --session agents +``` + +Attach a TUI to that session from another terminal. + +```bash +cd mux +cargo run -p mux-tui -- attach --session agents +``` + +Detach from an attached TUI with prefix `d`. With default keys, that is `Ctrl-b d`. The server keeps running, and another `attach` reconnects to the same tree. PTY tabs attach with a Ghostty VT-state replay followed by a live output stream. + +## Sessions and sockets + +The default socket path is: + +```text +$TMPDIR/cmux-mux-/.sock +``` + +The usual default is `$XDG_RUNTIME_DIR/cmux-mux-/main.sock` when `XDG_RUNTIME_DIR` is set, then `$TMPDIR/cmux-mux-/main.sock`, then `/tmp/cmux-mux-/main.sock`. `--session ` changes the final file name. `--socket ` bypasses the session-derived path. Server-started child processes receive `CMUX_MUX_SOCKET` with the socket path. + +## Platforms and XDG + +cmux-mux supports macOS and Linux; Windows support via ConPTY is planned for phase 2. The TUI config path resolves `CMUX_MUX_CONFIG`, then `$XDG_CONFIG_HOME/cmux/mux.json`, then `~/.config/cmux/mux.json`. + +Launched Chrome profile paths are platform-specific. On macOS the default is `~/Library/Application Support/cmux-mux/chrome-profile`. On Linux and other non-macOS targets, `XDG_DATA_HOME` is used when set, then `~/.local/share/cmux-mux/chrome-profile`. + +## Development flow + +Run tests from `mux/`. + +```bash +cargo test +``` + +Run the smoke scripts against a built binary. Set `CMUX_MUX_BIN` to test a non-default binary. + +```bash +cargo build -p mux-tui +python3 scripts/smoke-tui.py +python3 scripts/smoke-attach.py +``` + +This checkout does not contain `scripts/mux-dev.sh`; use the cargo and smoke commands above for the TUI flow. diff --git a/mux/docs/keyboard.md b/mux/docs/keyboard.md new file mode 100644 index 000000000000..5f7b50ee3748 --- /dev/null +++ b/mux/docs/keyboard.md @@ -0,0 +1,125 @@ +# Keyboard + +## Prefix Model + +`cmux-mux` uses a tmux-style prefix. The default prefix is `Ctrl-b`. After the prefix, the next key is interpreted as a mux command. Pressing the prefix twice sends a literal `Ctrl-b` to the active surface. + +Unknown prefixed keys are swallowed. Unprefixed non-Alt keys go to the active surface. Alt chords that are bound in the key table are modeless commands by default. + +## Default Bindings + +These defaults come from `Keys::default`. + +| Binding | Action | +| --- | --- | +| `Ctrl-b t` | New PTY tab in the active pane | +| `Alt-t` | New PTY tab in the active pane | +| `Ctrl-b B` | Open the browser-tab URL prompt | +| `Alt-n` | Smart split into a new pane | +| `Ctrl-b Tab` | Next tab in the active pane | +| `Ctrl-b BackTab` | Previous tab in the active pane | +| `Ctrl-b 1` through `Ctrl-b 9` | Select tab 1 through 9 in the active pane | +| `Ctrl-b %` | Split the active pane right | +| `Ctrl-b "` | Split the active pane down | +| `Ctrl-b x` | Close the active tab | +| `Ctrl-b X` | Close the active pane | +| `Ctrl-b ,` | Rename the active screen | +| `Ctrl-b $` | Rename the active workspace | +| `Ctrl-b &` | Close the active screen | +| `Ctrl-b p` | Previous screen in the active workspace | +| `Alt-[` | Previous screen in the active workspace | +| `Ctrl-b n` | Next screen in the active workspace | +| `Alt-]` | Next screen in the active workspace | +| `Ctrl-b c` | New screen in the active workspace | +| `Ctrl-b w` | Next workspace | +| `Ctrl-b W` | New workspace | +| `Ctrl-b s` | Toggle the workspace sidebar | +| `Ctrl-b h` or `Ctrl-b Left` | Focus left | +| `Alt-h` or `Alt-Left` | Focus left | +| `Ctrl-b l` or `Ctrl-b Right` | Focus right | +| `Alt-l` or `Alt-Right` | Focus right | +| `Ctrl-b k` or `Ctrl-b Up` | Focus up | +| `Alt-k` or `Alt-Up` | Focus up | +| `Ctrl-b j` or `Ctrl-b Down` | Focus down | +| `Alt-j` or `Alt-Down` | Focus down | +| `Alt-=` | Grow the focused split | +| `Alt--` | Shrink the focused split | +| `Ctrl-b PageUp` | Scroll the active PTY viewport up 10 rows | +| `Ctrl-b PageDown` | Scroll the active PTY viewport down 10 rows | +| `Ctrl-b d` | Quit a local TUI or detach an attached TUI | + +The screen bindings intentionally use tmux verbs: `c` creates a screen, `n` and `p` switch screens, `&` closes a screen, and `,` renames a screen. Tabs use `t`, `Tab`, `BackTab`, fixed number selectors, and tab-bar mouse actions. + +## Modeless Alt Layer + +Any configured Alt chord is active without the prefix. Default modeless commands are `Alt-t`, `Alt-n`, `Alt-[`, `Alt-]`, `Alt-h/j/k/l`, Alt arrows, `Alt-=`, and `Alt--`. `Alt-n` is the default zellij-style smart split binding. + +Set `keys.alt_shortcuts` to `false` to remove the default Alt bindings. This kill switch only removes defaults; Alt chords explicitly configured in `mux.json` still work. + +## Fixed Number Selection + +`1` through `9` select tabs by visible tab number after the prefix. These number bindings are fixed and are not configured through `mux.json`. + +## Remapping + +Keys are read from `~/.config/cmux/mux.json`, or from the file named by `CMUX_MUX_CONFIG`. + +Each action accepts a string, an array of strings, or `"none"`. Setting an action replaces all default chords for that action before adding the configured chords. `"none"` leaves the action unbound. + +```json +{ + "keys": { + "prefix": "ctrl+a", + "alt_shortcuts": false, + "new-tab": ["t", "alt+t"], + "new-pane-smart": "alt+n", + "next-screen": ["n", "alt+]"], + "prev-screen": ["p", "alt+["], + "focus-left": ["h", "left", "alt+h", "alt+left"], + "rename-tab": "r", + "rename-screen": ",", + "close-pane": "none" + } +} +``` + +Supported action keys are: + +```text +new-tab +new_browser_tab +new-pane-smart +next-tab +prev-tab +split-right +split-down +close-tab +close-pane +rename-tab +rename-screen +rename-workspace +close-screen +prev-screen +next-screen +new-screen +next-workspace +new-workspace +toggle-sidebar +focus-left +focus-right +focus-up +focus-down +resize-grow +resize-shrink +scroll-up +scroll-down +detach +``` + +`rename-pane` is still accepted as an alias for `rename-tab`. + +## Chord Format + +Chord strings are case-sensitive for single characters. Uppercase letters and symbols represent the shifted character. + +Supported examples include `"c"`, `"%"`, `"ctrl+b"`, `"alt+enter"`, `"tab"`, `"backtab"`, `"shift+tab"`, `"pageup"`, `"pagedown"`, `"esc"`, `"space"`, `"left"`, `"right"`, `"up"`, `"down"`, `"home"`, and `"end"`. diff --git a/mux/docs/mouse.md b/mux/docs/mouse.md new file mode 100644 index 000000000000..1408b044a090 --- /dev/null +++ b/mux/docs/mouse.md @@ -0,0 +1,55 @@ +# Mouse + +## Click Targets + +The sidebar shows a `workspaces` header, two rows per workspace, and `+ new workspace`. Click either row of a workspace to select it. Click `+ new workspace` to create one. Drag the sidebar's right border to set a session-local width override. Configured `sidebar.max_width` limits the drag width when it is greater than zero, and the TUI still leaves at least 40 columns for panes. + +Each pane has a border box. Click inside a pane to focus it. The top border is the tab bar: click a tab chip to select it, click `+` to create a PTY tab, click `‹` or `›` to scroll overflowing tabs, or wheel over the bar to scroll tab chips while keeping the active tab visible. + +The status bar lists screens for the active workspace. Click a screen segment to select it. Click the trailing `+` to create a screen. + +## Drag Reorder + +Drag a tab chip to reorder it within the same pane. Drag it to another pane's tab bar to move the tab across panes. The dragged tab is dimmed, and the target insertion point is shown with a `▌` marker. + +Drag a workspace entry in the sidebar to reorder workspaces. The drop position is shown with a horizontal `─` marker. + +## Scrollbars + +The scrollbar is visible only when a PTY surface can scroll. With the default `scrollbar.position = "column"`, it uses a dedicated column just inside the right border. With `"border"`, it overlays the right border. + +The thumb is `▕` normally and `▐` while hovered or dragged. Clicking the thumb starts a drag without moving the viewport. Clicking the track outside the thumb jumps to that relative position, then starts a drag from the clicked anchor. + +Wheel over a PTY pane focuses that pane first. On the normal screen it scrolls by three rows. On the alternate screen, if the inner app is not tracking mouse input, the TUI sends three up or down arrow keys. + +## Resize + +Drag pane borders to resize the matching split. Dragging a corner adjusts both intersecting split axes. The ratio is clamped from 0.05 to 0.95. Outer edges that do not correspond to a split do not change layout. + +Drag the sidebar border to resize the sidebar for the current TUI session. The configured base width still comes from `sidebar.width`. + +## Context Menus + +Right-click a pane for rename tab, new tab, new browser tab, split right, split down, close tab, and close pane. Right-click a workspace row for rename or close. Right-click a screen in the status bar for rename or close. + +Menus draw bordered overlays. Up and Down move the selected row, Enter activates it, and Esc closes the menu. A right press, drag to a row, and release activates that row. A plain right-click opens the menu and leaves it open. + +## Selection and Clipboard + +Drag inside a PTY pane to select text. Releasing copies non-empty selected text to the host clipboard with OSC 52. The selection stores absolute scrollback rows, so it remains stable while the viewport scrolls. + +Holding a selection drag at the top or bottom content edge auto-scrolls and extends the selection. Typing clears the selection. If the selected surface exits, the selection is cleared. + +Browser panes receive left press, drag, and release as CDP mouse events instead of starting text selection. + +## Pointer Shape + +The TUI emits OSC 22 `pointer` over clickable UI and OSC 22 `default` elsewhere. Terminals without pointer-shape support ignore it. + +## Rename and URL Dialogs + +Rename and browser URL prompts are centered bordered dialogs using the same `TextInput` editor. Buttons are labeled with shortcuts: `[ Clear ^C ]`, `[ Cancel esc ]`, and `[ OK ⏎ ]`. + +Enter commits. Esc cancels. Ctrl-C clears. Home and Ctrl-A move to the start. End and Ctrl-E move to the end. Alt-Left and Alt-B move one word left. Alt-Right and Alt-F move one word right. Backspace deletes left, Delete and Ctrl-D delete right, Ctrl-W and Alt-Backspace delete a word left, Alt-D deletes a word right, Ctrl-K deletes to the end, and Ctrl-U deletes to the start. + +Click OK to commit, Clear to clear, Cancel or outside the dialog to close, and the input row to move the cursor. Right-clicking while a prompt is open shakes the dialog instead of opening a menu. diff --git a/mux/docs/protocol.md b/mux/docs/protocol.md new file mode 100644 index 000000000000..e2291a1a1fc6 --- /dev/null +++ b/mux/docs/protocol.md @@ -0,0 +1,141 @@ +# Control Socket Protocol + +As of protocol v6, every server speaks JSON Lines over a Unix domain socket. Send one JSON object per line. Every request receives one response line. `subscribe` and `attach-surface` also push event lines on the same connection. + +Default socket path: + +```text +$TMPDIR/cmux-mux-/.sock +``` + +`identify` reports the protocol version: + +```json +{"id":1,"cmd":"identify"} +{"id":1,"ok":true,"data":{"app":"cmux-mux","version":"...","protocol":6,"session":"main","pid":12345}} +``` + +Responses have this shape: + +```json +{"id":1,"ok":true,"data":{}} +{"id":2,"ok":false,"error":"unknown surface 99"} +``` + +Bad JSON returns `ok:false` with no request id. + +## Command Contract + +The full API contract is intended to live in `mux/spec/`, but that directory is not present in this checkout. Until it lands, `mux-core/src/server.rs` is the command source of truth. + +The server command set in this branch is: + +```text +identify +list-workspaces +send +read-screen +vt-state +new-tab +new-browser-tab +new-workspace +new-screen +split +set-ratio +move-tab +move-workspace +set-default-colors +close-surface +close-pane +close-screen +close-workspace +rename-pane +rename-surface +rename-screen +rename-workspace +resize-surface +focus-pane +select-tab +select-screen +select-workspace +subscribe +attach-surface +scroll-surface +``` + +`move-tab` moves a surface to a target pane and insertion index. It supports same-pane reorder and cross-pane moves. + +```json +{"id":10,"cmd":"move-tab","surface":4,"pane":2,"index":0} +``` + +`move-workspace` moves a workspace to an insertion index. + +```json +{"id":11,"cmd":"move-workspace","workspace":3,"index":0} +``` + +## Events + +`subscribe` starts event streaming: + +```json +{"id":20,"cmd":"subscribe"} +``` + +Response data is `{}`. Future event lines may interleave with responses. + +Subscribed event lines are: + +```json +{"event":"surface-output","surface":4} +{"event":"surface-resized","surface":4,"cols":120,"rows":40} +{"event":"surface-exited","surface":4} +{"event":"title-changed","surface":4} +{"event":"bell","surface":4} +{"event":"tree-changed"} +{"event":"empty"} +``` + +`surface-resized` reports the final clamped cell size and is emitted only when the surface size actually changes. + +## Attach Surface + +`attach-surface` streams a PTY surface. Browser surfaces return `browser panes are not supported over attach yet`. + +```json +{"id":30,"cmd":"attach-surface","surface":4} +``` + +The server first sends: + +```json +{"event":"vt-state","surface":4,"cols":120,"rows":40,"data":""} +``` + +Then it sends ordered stream frames: + +```json +{"event":"output","surface":4,"data":""} +{"event":"resized","surface":4,"cols":132,"rows":43,"data":""} +``` + +The `resized` attach frame carries the new cell size and a fresh VT replay captured at that size. It is delivered in the same attach stream as output frames, so a client can reset its local terminal, apply the replay, and continue consuming later output in order. + +When the stream ends, it sends: + +```json +{"event":"detached","surface":4} +``` + +## Client Compatibility + +The remote TUI requires protocol v6. It refuses servers reporting any other protocol version because attach streams need resize markers carrying replay data. + +Attach clients mirror PTY surfaces locally. On first render, a client can resize the server surface before requesting `attach-surface`, so the initial VT replay is captured at the visible geometry. + +When several attach clients render the same surface at different sizes, sizing follows latest local interaction. A client reasserts its visible sizes after key input, mouse input, paste, focus gained, or terminal resize. Mux-driven redraws update local mirrors from `surface-resized` without reasserting an idle client's viewport. + +## Browser Limitations + +Browser surfaces appear in `list-workspaces` as `kind: "browser"` with `browser_source: "external"` or `"launched"`. PTY and VT commands against browser surfaces return errors. `attach-surface` does not stream browser pixels as of protocol v6, and the remote TUI shows a placeholder for browser panes.