|
| 1 | +# tap |
| 2 | + |
| 3 | +A terminal session manager for tiling-WM users. Start a command, detach from it, |
| 4 | +and reattach later from any terminal, with no in-terminal tiling layer to fight |
| 5 | +your window manager. Multiple people can attach to one session at once. |
| 6 | + |
| 7 | +```sh |
| 8 | +nix run .#tap # start a session in the current terminal |
| 9 | +nix run .#tap -- attach # reattach to the most recent session |
| 10 | +``` |
| 11 | + |
| 12 | +## Why not tmux |
| 13 | + |
| 14 | +If you use a tiling window manager (i3, Sway, Aerospace, yabai) you already tile |
| 15 | +windows. tmux then runs a second tiling system inside the terminal: competing |
| 16 | +keybinds, nested navigation, panes that duplicate what your WM windows already |
| 17 | +do. tap keeps the one feature you actually came for, session persistence, and |
| 18 | +drops the rest. Your WM tiles; tap persists. |
| 19 | + |
| 20 | +## Commands |
| 21 | + |
| 22 | +```sh |
| 23 | +tap # start a session (interactive $SHELL) |
| 24 | +tap start <cmd...> # start a session running a command |
| 25 | +tap start -d <cmd...> # start in the background, do not attach |
| 26 | +tap attach [id] # attach to a session (most recent if omitted) |
| 27 | +tap list # list live sessions |
| 28 | +tap kill [id] # stop a session and its child |
| 29 | +tap scrollback [-s id] # print the session's screen as text |
| 30 | +tap cursor [-s id] # print the cursor position |
| 31 | +tap size [-s id] # print the negotiated size |
| 32 | +tap inject [-s id] <text> # type text into a session without attaching |
| 33 | +tap subscribe [-s id] # stream the session's raw output |
| 34 | +``` |
| 35 | + |
| 36 | +Detach from an attached session with `Ctrl-\`. Open the scrollback in `$EDITOR` |
| 37 | +with `Alt-e`. Both keybinds are configurable (see Configuration). |
| 38 | + |
| 39 | +## Multiplayer |
| 40 | + |
| 41 | +Several clients can attach to the same session and see the same screen live. |
| 42 | +Input from any client reaches the shared child, so two people can drive one |
| 43 | +terminal. The session is sized to the **smallest** attached client (the |
| 44 | +element-wise minimum of every client's rows and columns), the same rule tmux |
| 45 | +uses, so no client ever sees clipped output. A client whose own terminal is |
| 46 | +larger than the negotiated size gets a dim warning on its bottom row noting that |
| 47 | +the extra space is unused. |
| 48 | + |
| 49 | +```sh |
| 50 | +# terminal A |
| 51 | +tap start --id pair bash |
| 52 | +# terminal B (anywhere on the same machine) |
| 53 | +tap attach pair |
| 54 | +``` |
| 55 | + |
| 56 | +## Running it |
| 57 | + |
| 58 | +tap is a package in this repository's Cargo workspace, exposed as a flake app: |
| 59 | + |
| 60 | +```sh |
| 61 | +nix run .#tap -- <args> # run |
| 62 | +nix build .#tap # build the binary |
| 63 | +``` |
| 64 | + |
| 65 | +## Architecture |
| 66 | + |
| 67 | +tap is one daemon per session plus any number of thin clients, connected over a |
| 68 | +per-session Unix socket speaking newline-delimited JSON (binary payloads ride as |
| 69 | +base64). Every interactive `tap`, even for a single user, is a client of a |
| 70 | +daemon. That uniformity is what makes resize-while-attached and multi-client |
| 71 | +sharing work: the daemon owns the PTY and the screen, and clients only render it |
| 72 | +and forward keystrokes. |
| 73 | + |
| 74 | +The code is three composable crates: |
| 75 | + |
| 76 | +- [`tap-pty`](pty) is the reusable session engine: it spawns a child on a real |
| 77 | + PTY (via [`pty-process`](https://docs.rs/pty-process)), mirrors it in a |
| 78 | + [`vt100`](https://docs.rs/vt100) emulator, and fans the raw output out to many |
| 79 | + subscribers. A new subscriber gets an atomic resync snapshot (the escape |
| 80 | + sequences that reproduce the current screen) joined to the live stream with no |
| 81 | + gap and no duplicated byte, which is what makes attaching to a running |
| 82 | + full-screen TUI paint correctly. |
| 83 | +- [`tap-protocol`](protocol) is the wire types and runtime paths, with no I/O. |
| 84 | +- `tap` (this crate) is the CLI, the daemon, and the attach client. |
| 85 | + |
| 86 | +Integration tests in [`tests/integration.rs`](tests/integration.rs) drive the |
| 87 | +real `tap` binary on a PTY using this repository's [`tui`](../tui) driver and |
| 88 | +assert on the rendered grid: round-trip input, full-screen resync on a second |
| 89 | +attach, multiplayer min-size negotiation, and resize-while-attached. |
| 90 | + |
| 91 | +## Configuration |
| 92 | + |
| 93 | +Optional, at `~/.config/tap/config.toml`: |
| 94 | + |
| 95 | +```toml |
| 96 | +editor = "nvim" # overrides $EDITOR / $VISUAL for the Alt-e keybind |
| 97 | + |
| 98 | +[keybinds] |
| 99 | +editor = "Alt-e" |
| 100 | +detach = "Ctrl-\\" |
| 101 | + |
| 102 | +[timing] |
| 103 | +escape_timeout_ms = 50 # window to tell a lone ESC from an Alt- sequence |
| 104 | +``` |
| 105 | + |
| 106 | +Session sockets and the session index live under `$XDG_RUNTIME_DIR/tap` (falling |
| 107 | +back to `~/.tap`). Set `TAP_RUNTIME_DIR` to relocate them, for example to isolate |
| 108 | +test state. |
| 109 | + |
| 110 | +## Known limitations |
| 111 | + |
| 112 | +- Unix only (Linux and macOS). It relies on PTYs, `SIGWINCH`, and `setsid`. |
| 113 | +- Sessions are local to one machine and one user; there is no network transport |
| 114 | + or cross-user access. The socket lives in the user's runtime directory. |
| 115 | +- A client that falls far behind the output stream is resynced from a fresh |
| 116 | + snapshot rather than a byte-exact replay, so a burst can skip intermediate |
| 117 | + frames on a slow link. |
| 118 | +- The `Alt-e` editor keybind is detected with a short escape timeout; on a slow |
| 119 | + or split input read a literal `Alt-e` can be delayed by `escape_timeout_ms`. |
0 commit comments