# xmux: functional requirements & use cases xmux is a stateless cross-environment session switcher that brings tmux's `prefix + s` experience across machines: one terminal that sees and switches in place between every reachable tmux, psmux, zellij, screen, tuios, and herdr session, local and over ssh. Each requirement has a stable ID and states one behavior the implementation is checked against, naming no source file, function, or test. --- ## A. Discovery & inventory - **FR-A1** - `xmux ls` lists every reachable session across all hosts, one line per session written as its `//` path. - **FR-A2** - A reachable mux with no sessions is reported as empty, a host on a dead machine as unreachable, and the case where every host is unreachable is distinguished. - **FR-A3** - `xmux doctor` reports config health, ssh availability, and per-host reachability with session counts. - **FR-A4** - Sessions are ordered deterministically (local, then WSL, then remote machines, each tier by host name, sessions by name), and a re-enumeration reproduces the same order. - **FR-A5** - The machine roster comes from the providers the `[discovery]` table enables (`~/.ssh/config` aliases and one-hop neighbours that answer ssh), is resolved again on every re-scan, drops a machine a record stops naming, and keeps a machine only a probe offered as an unreachable card. - **FR-A6** - A machine's mux is identified by what its binary answers as, so tmux, psmux, zellij, abduco, screen, tuios, and herdr mix freely across machines with no configuration. - **FR-A7** - A host is one mux on one machine, so a machine given several muxes (a `mux` list in `[local]` or `[[hosts]]`) contributes one `:` host per mux, and a listed mux that is not installed surfaces as unreachable. - **FR-A8** - Every command in an enumeration runs under a fixed per-command budget, so a timed-out listing shows that host as unreachable instead of holding it open. - **FR-A9** - A machine that names no mux is probed for each mux xmux supports and serves exactly the ones that answer, with no card when none does. - **FR-A10** - A remote machine's muxes are discovered asynchronously after launch, and its answer only adds hosts, without renaming or removing any card already shown. - **FR-A11** - A mux inside a WSL distribution is a host on its own machine `wsl.`, offered by the `[discovery] wsl` provider or a `[[wsl]]` entry, and behaves as FR-A7 to FR-A10 describe. - **FR-A12** - A re-enumeration reads a session as renamed only when the mux lists the same session identity under a new name, and then the card, its number, the selection, and the display follow the new name; a session killed and another created between two listings are a lost session and a new one. ## B. The switcher: "see the list, decide whether & where to move" - **FR-B1** - The nav renders one single-row card per session, flat with no window or pane rows, grouped under a non-selectable `{machine}/{mux}` section title in the deterministic order. - **FR-B2** - The host skeleton paints instantly and each host's sessions stream in independently. - **FR-B3** - The terminal view shows the confirmed session's live grid, follows the cursor, and keeps the prior grid on screen during a switch until the fresh attachment paints or its bounded wait ends. - **FR-B4** - Up/down step one card, left/right step one category, a fuzzy filter narrows the list over `/`, and `prefix R` re-scans every machine. - **FR-B5** - Quitting (`prefix q` or the ctl `quit` verb) leaves every mux session untouched. - **FR-B6** - Under a filter, `Enter` attaches the visible filtered session, never a filtered-out one. - **FR-B7** - Every host-state card shows one fixed-width state glyph (a spinner while waiting, `?` login needed, `▲` unreachable, `✗` listing failure, blank for empty) and names a mux only once that mux is confirmed. - **FR-B8** - The session xmux itself runs in is never mirrored into the terminal view and shows a screen saying why instead, while its card stays selectable. - **FR-B9** - The nav carries a prefix indicator that names the prefix at rest and opens the key list over the terminal view when the prefix is armed, without moving any card. - **FR-B10** - Every card carries a number, and `prefix ` jumps to the card with that number. - **FR-B11** - Every colour xmux paints is an ANSI-16 slot chosen by the `[ui] theme` (`auto-dark` or `auto-light`), so the terminal's own scheme resolves every hue. - **FR-B12** - A group is drawn the same way at every nav position, as a `{machine}/{mux}` title over its indented cards, flowing into whole-section columns in a band. - **FR-B13** - The nav marks off-screen cards on its seam line only, with a thickened `┃` stretch beside a side list and `‹ N` / `N ›` counts on a band. - **FR-B14** - With the prefix, the arrow pair facing the terminal focuses the terminal and the other pair focuses the nav, while bare arrows move between cards. - **FR-B15** - The nav's side is a placement pinned at runtime by `prefix p` when one exists, else the `[ui] nav-position` default, and the nav never moves on its own. - **FR-B16** - The nav's width, band height, side, and collapsed state are live and persisted, set by resize keys, border drag, `prefix z`, and auto-hide. - **FR-B17** - The resting prefix indicator is a label sized to its text, and the hint bar carries only advice for the current state, never the result of an action. - **FR-B18** - A prefix interaction lasts until the function it starts ends, and the key list and an auto-hidden nav show for exactly that span. While the terminal view holds the focus, a nav that would leave it smaller than 24 columns by 4 rows hides as an auto-hidden nav does. - **FR-B19** - A mouse click, release, wheel, or drag cancels a pending prefix chord, while bare hover and dragging the key list do not. - **FR-B20** - A held prefix key counts as repeated taps, each sending the doubled-prefix literal to the pane. - **FR-B21** - The nav shows three groups in order (session cards, reachable hosts with no sessions, unresolved machines) separated by one blank row or column. - **FR-B22** - A machine and its mux are always shown as one `{machine}/{mux}` label, except for a mux not yet known, where the card reads the machine alone. - **FR-B23** - When the mux moves xmux's own display client to another session, the nav selection follows in terminal focus and the client is carried back in nav focus. - **FR-B24** - `prefix h` opens the table of machine problems, grouped by login needed, unreachable, and inventory failure, where Enter or a click selects the machine and opens its login pane. - **FR-B25** - The nav attaches on the left, top, right, or bottom of the terminal view, with `prefix p` cycling the side clockwise and the layout inside the nav identical at every side. - **FR-B26** - A machine ssh refuses for a reason a login can fix is blocked: it shows `?` and the login pane, with the failing input field marked `✗`. - **FR-B27** - The login pane takes the address, port, username, and an optional masked password prefilled from what ssh would use, and lists each login step's progress after submit. What ssh would use for a machine is read again after xmux records or removes the machine's ssh config entries and on `prefix r`, so the login pane and the machine screen follow the file as it is. - **FR-B28** - A submitted password is held only in process memory, released to ssh only through xmux's private askpass broker for the exact account and machine, and forgotten on refusal, removal from the roster, or exit. - **FR-B29** - The login pane offers recording only while an entered address, port, or username differs from what ssh resolves for the machine; after a working login, recording writes one xmux-marked stanza with the values that worked at the top of `~/.ssh/config`, replacing an earlier xmux stanza and never storing the password. - **FR-B30** - After a working login, registering adds this PC's public key to the machine's authorized keys file with the `xmux-registered` mark and reports registered only when a key-only login then succeeds. - **FR-B31** - Persistent UI symbols use only one-cell glyphs that OS default terminal fonts render, and a terminal smaller than 24 columns by 4 rows shows only a size screen. - **FR-B32** - The result of a user action (a login, a new session, a re-scan, or the reason an action was refused) and a newer release at launch appear as a toast in the terminal view's corner, which `[ui] notifications` can turn off. A value typed into a popup that the popup cannot accept is stated inside that popup beside its field, and the popup stays open, with no toast. - **FR-B33** - `prefix m` opens the history of every toast and background event, newest first, bounded at 200 records. - **FR-B34** - A re-scan ends in one toast stating what changed since the re-scan request, and `prefix r` re-scans only the selected card's machine. - **FR-B35** - Every key xmux binds is defined once in one key table that both focus paths and every key surface read. - **FR-B36** - Pressing the prefix opens the key list at once, naming every key the prefix unlocks by section and never dropping the jump, help, or quit keys. - **FR-B37** - For three seconds after the user moves the selection, the hint bar names the selected card's most relevant keys and one fact about it. - **FR-B38** - The help lists every key by section and a legend of every glyph, with section tabs, scrolling, and case-insensitive search, closed by `Esc` or `prefix ?`. - **FR-B41** - Every popup body row wraps to the popup's width, except a text field, and a popup too tall for the window scrolls. - **FR-B39** - Logging out of an SSH machine (`prefix L`, confirmed by typing `logout`) removes this PC's matching public keys from the machine's key files and the ssh config stanza a login saved for the machine, then drops the held password and the machine's connections. A key line or a `Host` entry naming the machine exactly that xmux did not add changes only after one second confirmation covering both: an entry naming only this machine goes with its options, an entry naming other machines too loses only this name, and every other line of ssh config stays as it was. - **FR-B40** - When the selected card leaves the list, the selection moves to the nearest remaining node up its lineage, and a new card takes the selection only when the user asked for it (Selection by Interest in docs/principles.md). When nothing of the selected machine is left, the selection names nothing and the terminal view shows the landing list and attaches nothing until the user picks a card, or until a filter edit lists the node the user was on again. - **FR-B42** - Machines, hosts, and sessions each have a view screen linked to one another, reached through `Ctrl-↑` / `Ctrl-↓`, the section title parts, and the screen links. - **FR-B43** - From launch until the first execution the terminal view attaches to no session and shows the landing screen: how many machines the scan has reached, with the scan spinner, and every nav card in nav order and numbering as its `/`-separated path. The landing list and the nav share one hard selection, which only highlights; the first execution closes the landing screen for the rest of the run, opens the chosen card, and focuses the terminal view. - **FR-B44** - Every surface names the levels machine, host, and session, with `host` always one mux on a machine, a machine screen and a host screen open with their level and path (`machine db-01`, `host db-01/tmux`), and each states only the facts of its own level. A surface that names a session apart from its host's section, title, or screen writes its whole path (`db-01/tmux/pg-primary`). ## C. Switching (the keystone) - **FR-C1** - A same-server pick lands on the picked session, by `switch-client` when the mux can name xmux's own client and otherwise by reattaching by session name. - **FR-C2** - A cross-host pick switches entirely in process, with no picker and no detach, by handing the display to the target host's live attachment. - **FR-C3** - An unreachable host is marked `▲` with a view screen stating the verdict and diagnostics, and nothing reconnects until the user re-scans or selects the card. - **FR-C4** - Every dispatched switch or select command logs its exact argv and result, and a failed attach is logged at warn level, returns to the nav, and is not attached again until the user selects the card, executes it, or re-scans. - **FR-C5** - Keys typed after a pick reach the picked session in the order typed: they wait while its attachment starts and, for a mux whose client drops the keys it reads before its first frame, until that attachment draws. Keys still waiting after 5 s, or when the pick moves on, are dropped. ## D. App lifecycle - **FR-D1** - `xmux` with no subcommand is a persistent supervisor that owns the terminal and runs its mux clients as children over a single async event loop. - **FR-D2** - The app answers its control socket (`ping`, `dump`, `status`, `switch`) without blocking while a session is displayed. - **FR-D3** - The app runs inside a mux by attaching its mux clients as PTY children, so its attachments never nest. - **FR-D4** - The control socket is removed if stale before bind, owner-only (`0600`) on unix, and removed on exit, and a crashed instance's `ctl-*.sock` marker is swept on the next startup. - **FR-D5** - The app launches directly into the split view with the landing screen (FR-B43), preselects the first session to appear, and keeps that selection as later hosts answer; the preselect attaches nothing until the user executes a card. - **FR-D6** - An enumeration logs at INFO only when its session list changed and at WARN on failure. - **FR-D7** - Daily log files are kept for a bounded window, and a repeating recovered panic is logged only at each doubling of its count. - **FR-D8** - One command from `sh`, PowerShell, or CMD installs the build for the machine's OS and architecture after verifying its SHA-256, without elevation, and accepts a named version. - **FR-D9** - Installing a version never overwrites the binary a running xmux executes, because each version has its own directory and only the launcher is repointed. - **FR-D10** - `xmux update` updates by the install method read from the executable's path (in-place verified replace, the install script, winget, or Homebrew), with `--check` and `--method`. - **FR-D11** - xmux asks the release feed at most once a day without delaying launch and reports a newer version as a toast and in `doctor`, and one config key turns this off. - **FR-D12** - `doctor` opens with the running version, its binary path, the install method, and any recorded newer version, without network access. - **FR-D13** - `xmux uninstall` removes xmux by its install method after a `y` or `yes` confirmation, keeps settings and data by default, and refuses while an instance is running. ## E. Session management xmux aggregates and switches, so creating a session is the only session change it makes. - **FR-E1** - `prefix n` creates a session on the selected card's host and mux, which then appears in the nav, and is refused with a toast under an unreachable host. - **FR-E2** - There is no rename, kill, or window or pane command anywhere in xmux. - **FR-E3** - Creating a session runs off the key path, so a slow ssh round trip never freezes rendering or the control channel. ## F. Control channel - **FR-F1** - A per-instance socket `ctl-.sock` drives the running app with the verbs `ping`, `dump`, `status`, `switch`, `focus`, `rescan`, `quit`, `width`, `toggle-auto-hide`, `new-session`, and the `raw:` namespace, replying `err: …` on failure. - **FR-F2** - There is one unified socket, and its `switch ` verb runs the same switch action as a key press. - **FR-F3** - Every instance has a name, generated as `-` or given by `--name`, which `xmux send` and `xmux instances` resolve against live instances. - **FR-F4** - Control messages are length-framed (decimal count, newline, bytes) with a bounded read. ## G. Transport & safety - **FR-G1** - Every app-owned ssh uses a connect timeout and batch mode, except that a held password is answered only through the private askpass path, and the user's host-key policy is weakened to `accept-new` only for a submitted login under `ask`. - **FR-G2** - A session name from a remote list is POSIX single-quote escaped when it re-enters a remote shell command. - **FR-G3** - Mux session variables (`TMUX`, `TMUX_PANE`, `PSMUX*`) are stripped for listing, so a command run inside a mux is not refused as nesting. - **FR-G4** - A remote attach runs the mux-supplied attach argv in one `ssh -t` connection, and its password never appears in the terminal view. - **FR-G5** - A command bound for a WSL distribution is exec'd there in a login shell rather than passed as a command line. - **FR-G6** - A remote machine's shell family is read during its reachability probe, and a non-POSIX remote is never sent POSIX-only syntax. - **FR-G7** - xmux reaches a machine only for the launch scan, a user action, or an already open push stream, and no failure triggers its own retry except the one reattach per selection of a display client that zellij drops right after attaching. - **FR-G8** - A machine is asked one thing at a time, while separate machines are asked in parallel. --- ## Use cases (end-to-end scenarios) - **UC-1, jump from my laptop to a remote dev session.** Move the cursor to a remote session and land in it in one action. *(FR-B1, FR-C2, FR-D1/D2)* - **UC-2, hop between two same-server sessions.** Select a session on the current server for an instant switch-client. *(FR-C1)* - **UC-3, survey then stay put.** Look around the nav, then quit, leaving the current session untouched. *(FR-B5)* - **UC-4, find one session among many then go.** Filter, then press Enter on the visible match. *(FR-B4, FR-B6)* - **UC-5, the remote is down and I am not left in the dark.** An unreachable host shows `▲` and its reason, and the nav stays usable. *(FR-A2, FR-B7, FR-C4)* - **UC-6, deep in a remote, get back home.** Native detach (`prefix d`) returns to the split view to pick another session. *(FR-C2, FR-D1)* - **UC-7, spin up a throwaway on a remote and switch to it.** Create a session on the host's card, then switch to it. *(FR-E1, FR-C2)* - **UC-8, survey what's running everywhere before deciding.** The nav shows every session and the terminal view previews the selection. *(FR-B1, FR-B3, FR-B8)* - **UC-9, drive xmux from a script.** Dump, inject keys, and switch over the control channel. *(FR-F1, FR-F2)* - **UC-10, switch in either direction, local to remote to local.** Each pick re-attaches the next target, local or remote, with no picker. *(FR-C2, FR-D1)* - **UC-11, go straight to the session I can already see.** Press `prefix ` with the number on the card. *(FR-B10, FR-C1)* ## Accepted limitations The seamless cross-host switch is bought with these costs, accepted by design: - One live app per terminal owns the display, and a second one cannot share it. - Handing the display from one mux client to another can flash a repaint. - On Windows, ssh has no ControlMaster multiplexing, so each remote round trip opens a fresh connection. - A push-channel mux inside a WSL distribution that cannot allocate a terminal is reported unreachable. - A zellij client moved to another session from inside itself is followed only on Windows locally and on a Linux machine with `ss` reached locally, through WSL, or over a shared ssh connection; elsewhere the nav stays on the card it was on. - A herdr client moved to a saved machine from inside itself is followed only while it is its user's one herdr client on its machine, and only on a machine with `/proc`. A saved machine on the client's own machine moves the selection to that session; any other saved machine is named on the card of the session xmux opened, and the selection stays there. A herdr client that starts on the saved machine its user chose last is named on its card the same way while the nav holds the focus. - A session listed without an identity, which is every zellij session and every local psmux session its own server does not answer for, is not followed through a rename made inside its mux: the rename reads as a lost session and a new one, and the selection moves up to the host. - A Windows client attached to tmux older than 3.6 can show the end of a terminal reply as typed text in the session: Windows OpenSSH passes the reply on in pieces, and tmux before 3.6 ends it at the gap. tmux 3.6 fixes this upstream (tmux/tmux#4411, microsoft/terminal#7185).