|
| 1 | +# exe-scroll |
| 2 | + |
| 3 | +`exe-scroll` is a small terminal session multiplexer in the spirit of |
| 4 | +[dtach](https://github.com/crigler/dtach), |
| 5 | +[abduco](https://github.com/martanne/abduco), and |
| 6 | +[zmx](https://github.com/qbit/zmx), written in Zig. It hosts a PTY-backed |
| 7 | +command behind a Unix socket so you can attach and detach without disturbing the |
| 8 | +program — and it uses Ghostty's `ghostty-vt` terminal emulator to keep |
| 9 | +scrollback, so reattaching repaints the screen (and optional history) exactly: |
| 10 | +colors, styles, cursor and all. |
| 11 | + |
| 12 | +## Usage |
| 13 | + |
| 14 | +The default invocation takes a single argument — a path for the session socket |
| 15 | +— and **attaches** to it if a live session is there, otherwise **creates** it: |
| 16 | + |
| 17 | +```sh |
| 18 | +# Create (no session yet): runs $SHELL behind /tmp/work/session.sock. |
| 19 | +exe-scroll /tmp/work/session.sock |
| 20 | + |
| 21 | +# ...later, from anywhere: attach to the same session. |
| 22 | +exe-scroll /tmp/work/session.sock |
| 23 | + |
| 24 | +# Create with an explicit command (everything after `--` is the command). |
| 25 | +exe-scroll /tmp/work/session.sock -- bash -l |
| 26 | +``` |
| 27 | + |
| 28 | +**Detach** (leaving the session running) by sending the attach client |
| 29 | +`SIGUSR2`: |
| 30 | + |
| 31 | +```sh |
| 32 | +kill -USR2 <pid-of-exe-scroll> |
| 33 | +``` |
| 34 | + |
| 35 | +The session keeps running; reattach later with the same command. |
| 36 | + |
| 37 | +### Telling the processes apart |
| 38 | + |
| 39 | +Each session involves a backgrounded *session server* (owns the pty and |
| 40 | +scrollback) and one *attach client* per attachment. They rewrite their command |
| 41 | +line so `ps` shows which is which: |
| 42 | + |
| 43 | +``` |
| 44 | +$ ps -o pid,command |
| 45 | + 4258 exe-scroll: attach /tmp/work/session.sock |
| 46 | + 2976 exe-scroll: session /tmp/work/session.sock |
| 47 | +``` |
| 48 | + |
| 49 | +So the `session` line is the one to send `SIGUSR1` (recreate socket), and an |
| 50 | +`attach` line is the one to send `SIGUSR2` (detach). |
| 51 | + |
| 52 | +### Options |
| 53 | + |
| 54 | +| flag | meaning | |
| 55 | +|------|---------| |
| 56 | +| `-R none\|screen\|scrollback` | what to replay on attach (default `scrollback`) | |
| 57 | +| `--version` | print version and exit | |
| 58 | +| `-h`, `--help` | help | |
| 59 | + |
| 60 | +Options may appear before or after the socket path. |
| 61 | + |
| 62 | +## Design notes |
| 63 | + |
| 64 | +- **Attach-or-create by default.** A single path argument is all you need; the |
| 65 | + tool connects if a session is live, otherwise spawns one. Intervening |
| 66 | + directories are created securely (mode `0700`). |
| 67 | +- **A session server and attach clients.** Creating a session forks a |
| 68 | + backgrounded *session server* that owns the pty and the scrollback. Each |
| 69 | + `exe-scroll <socket>` you run is an *attach client* that connects to it over |
| 70 | + the socket and relays your terminal. |
| 71 | +- **Signal-driven socket recreation (an abduco idea).** There's no polling |
| 72 | + timer and no file watcher. The session server understands `SIGUSR1`: on |
| 73 | + receipt, if its socket file is missing (or has been replaced), it binds a |
| 74 | + fresh one. So if a socket gets deleted out from under a live session, |
| 75 | + `kill -USR1 <server-pid>` brings it back and you can reattach. On exit the |
| 76 | + server only unlinks the socket if its on-disk inode still matches the one it |
| 77 | + created. |
| 78 | +- **Ghostty for scrollback.** Every byte of program output is fed into an |
| 79 | + in-process `ghostty-vt` terminal. On attach the server serializes that |
| 80 | + emulator back to VT sequences (optionally including scrollback) so the client |
| 81 | + repaints exactly. `ghostty-vt` is imported as a Zig module directly — no |
| 82 | + C ABI / FFI. The session retains a fixed 1 MiB scrollback budget. |
| 83 | +- **Simple, evolvable framing.** The server/client protocol is a trivial |
| 84 | + length-prefixed message format (`[type:u8][len:u32-le][payload]`), chosen to |
| 85 | + be easy to read, test and extend. There is intentionally no wire-protocol |
| 86 | + compatibility with dtach; unknown message types are ignored, so the protocol |
| 87 | + can grow. |
| 88 | + |
| 89 | +## Building |
| 90 | + |
| 91 | +exe-scroll imports Ghostty's `ghostty-vt` Zig module, so it builds against a |
| 92 | +[Ghostty](https://github.com/ghostty-org/ghostty) checkout. You need Zig 0.15.2 |
| 93 | +(macOS also needs the Xcode command-line tools for Ghostty's bundled C++). |
| 94 | + |
| 95 | +```sh |
| 96 | +# Just build. Fetches Ghostty at the pinned revision into ./.ghostty for you: |
| 97 | +make # -> zig-out/bin/exe-scroll |
| 98 | +make test # build + run the test suite |
| 99 | + |
| 100 | +# Already have a Ghostty checkout? Point at it to skip the fetch: |
| 101 | +make GHOSTTY_SRC=/path/to/ghostty |
| 102 | + |
| 103 | +# Reproducible static musl build for Linux (fetches the pinned toolchain via |
| 104 | +# mise, and a pinned ghostty revision): |
| 105 | +./build-static.sh amd64 # or arm64 |
| 106 | +``` |
| 107 | + |
| 108 | +The Zig toolchain version is pinned in `mise.toml` (with per-platform checksums |
| 109 | +in `mise.lock`); `build-static.sh` uses [mise](https://mise.jdx.dev) to fetch it |
| 110 | +consistently. |
| 111 | + |
| 112 | +## Platforms |
| 113 | + |
| 114 | +The source is POSIX and uses Zig's standard library for all the platform ABI |
| 115 | +(termios, signals, ioctls, errno, ...), so it builds and runs natively on |
| 116 | +Linux, macOS, and the BSDs. `build-static.sh` is Linux-specific: it produces a |
| 117 | +fully static musl binary. (Cross-compiling the macOS binary *from* Linux is not |
| 118 | +set up here because Ghostty's bundled C++ SIMD sources need the macOS SDK; build |
| 119 | +on the target OS, or wire up an SDK, for a native macOS binary.) |
| 120 | + |
| 121 | +## Testing |
| 122 | + |
| 123 | +`test_e2e.zig` drives the real binary over PTYs — attach-or-create, secure |
| 124 | +directory creation, detach (SIGUSR2) / reattach survival, replay modes, color |
| 125 | +preservation, and the abduco-style socket recreation. Run it with Zig's test |
| 126 | +harness: |
| 127 | + |
| 128 | +```sh |
| 129 | +zig build test # builds the binary and runs the e2e tests against it |
| 130 | +``` |
| 131 | + |
| 132 | +## License |
| 133 | + |
| 134 | +MIT (see [LICENSE](LICENSE)). This is an independent implementation; it does not |
| 135 | +reuse dtach's (GPL) source. |
0 commit comments