Commit 932e0ed
Eric Bower
·
2026-03-23 11:21:28 -0400 EDT
parent 3b0e4e8
docs: cleanup and rewording I'm removing `AGENTS.md` mainly because it is not particularly useful and there is research to suggest these files don't really improve agent performance.
4 files changed,
+30,
-74
+0,
-47
| ... | ... | @@ -1,47 +0,0 @@ | |
| 1 | - | # zmx | |
| 2 | - | ||
| 3 | - | The goal of this project is to create a way to attach and detach terminal sessions without killing the underlying linux process. | |
| 4 | - | ||
| 5 | - | When researching `zmx`, also read the @README.md in the root of this project directory to learn more about the features, documentation, prior art, etc. | |
| 6 | - | ||
| 7 | - | ## tech stack | |
| 8 | - | ||
| 9 | - | - `zig` v0.15.1 | |
| 10 | - | - `libghostty-vt` for terminal escape codes and terminal state management | |
| 11 | - | ||
| 12 | - | ## commands | |
| 13 | - | ||
| 14 | - | - **Build:** `zig build` | |
| 15 | - | - **Build Check (Zig)**: `zig build check` | |
| 16 | - | - **Test (Zig):** `zig build test` | |
| 17 | - | - **Test filter (Zig)**: `zig build test -Dtest-filter=<test name>` | |
| 18 | - | - **Formatting (Zig)**: `zig fmt .` | |
| 19 | - | ||
| 20 | - | ## find any library API definitions | |
| 21 | - | ||
| 22 | - | Before trying anything else, run the `zigdoc` command to find an API with documentation: | |
| 23 | - | ||
| 24 | - | ``` | |
| 25 | - | zigdoc {symbol} | |
| 26 | - | # examples | |
| 27 | - | zigdoc ghostty-vt | |
| 28 | - | zigdoc std.ArrayList | |
| 29 | - | zigdoc std.mem.Allocator | |
| 30 | - | zigdoc std.http.Server | |
| 31 | - | ``` | |
| 32 | - | ||
| 33 | - | Only if that doesn't work should you grep the project dir. | |
| 34 | - | ||
| 35 | - | ## find zig std library source code | |
| 36 | - | ||
| 37 | - | To inspect the source code for zig's standard library, look inside the `zig_std_src` folder. | |
| 38 | - | ||
| 39 | - | ## find ghostty library source code | |
| 40 | - | ||
| 41 | - | To inspect the source code for zig's standard library, look inside the `ghostty_src` folder. | |
| 42 | - | ||
| 43 | - | ## Issue Tracking | |
| 44 | - | ||
| 45 | - | We use bd (beads, https://github.com/steveyegge/beads) for issue tracking instead of Markdown TODOs or external tools. | |
| 46 | - | ||
| 47 | - | Run `bd quickstart` to learn how to use it. |
+7,
-0
| ... | ... | @@ -2,6 +2,13 @@ | |
| 2 | 2 | ||
| 3 | 3 | Use spec: https://common-changelog.org/ | |
| 4 | 4 | ||
| 5 | + | ## Staged | |
| 6 | + | ||
| 7 | + | ### Fixed | |
| 8 | + | ||
| 9 | + | - `zmx list` will send "no sessions found" to stderr instead of stdout | |
| 10 | + | - `zmx wait` will send errors to stderr instead of stdout | |
| 11 | + | ||
| 5 | 12 | ## v0.4.2 - 2026-03-18 | |
| 6 | 13 | ||
| 7 | 14 | ### Changed |
+13,
-21
| ... | ... | @@ -15,7 +15,7 @@ | |
| 15 | 15 | ||
| 16 | 16 | ## features | |
| 17 | 17 | ||
| 18 | - | - Persist terminal shell sessions (pty processes) | |
| 18 | + | - Persist terminal shell sessions | |
| 19 | 19 | - Ability to attach and detach from a shell session without killing it | |
| 20 | 20 | - Native terminal scrollback | |
| 21 | 21 | - Multiple clients can connect to the same session |
| ... | ... | @@ -73,10 +73,10 @@ Commands: | |
| 73 | 73 | [r]un <name> [command...] Send command without attaching, creating session if needed | |
| 74 | 74 | [d]etach Detach all clients from current session (ctrl+\ for current client) | |
| 75 | 75 | [l]ist [--short] List active sessions | |
| 76 | - | [c]ompletions <shell> Completion scripts for shell integration (bash, zsh, or fish) | |
| 77 | 76 | [k]ill <name> Kill a session and all attached clients | |
| 78 | 77 | [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences) | |
| 79 | 78 | [w]ait <name>... Wait for session tasks to complete | |
| 79 | + | [c]ompletions <shell> Completion scripts for shell integration (bash, zsh, or fish) | |
| 80 | 80 | [v]ersion Show version information | |
| 81 | 81 | [h]elp Show this help message | |
| 82 | 82 | ``` |
| ... | ... | @@ -313,6 +313,8 @@ Host = d.* | |
| 313 | 313 | ControlPersist 10m | |
| 314 | 314 | ``` | |
| 315 | 315 | ||
| 316 | + | Architecturally, `ssh` supports multiplexing multiple channels of communication within a single connection to a server. `ControlMaster` is the setting that tells `ssh` to multiplex multiple PTY sessions to a single server over one tcp connection. Neat! | |
| 317 | + | ||
| 316 | 318 | Now you can spawn as many terminal sessions as you'd like: | |
| 317 | 319 | ||
| 318 | 320 | ```bash |
| ... | ... | @@ -322,9 +324,9 @@ ssh d.pico | |
| 322 | 324 | ssh d.dotfiles | |
| 323 | 325 | ``` | |
| 324 | 326 | ||
| 325 | - | This will create or attach to each session and since we are using `ControlMaster` the same `ssh` connection is reused for every call to `ssh` for near-instant connection times. | |
| 327 | + | Because the `attach` command is essentially an "upsert", this will create or attach to each session. | |
| 326 | 328 | ||
| 327 | - | Now you can use the [`autossh`](https://linux.die.net/man/1/autossh) tool to make your ssh connections auto-reconnect. For example, if you have a laptop and close/open your laptop lid it will automatically reconnect all your ssh connections: | |
| 329 | + | Now you can use the [`autossh`](https://linux.die.net/man/1/autossh) tool to make your ssh connections auto-reconnect. For example, if you have a laptop and close/open your lid it will automatically reconnect all your ssh connections: | |
| 328 | 330 | ||
| 329 | 331 | ```bash | |
| 330 | 332 | autossh -M 0 -q d.term |
| ... | ... | @@ -345,6 +347,8 @@ ash d.dotifles | |
| 345 | 347 | ||
| 346 | 348 | Wow! Now you can setup all your os tiling windows how you like them for your project and have as many windows as you'd like, almost replicating exactly what `tmux` does but with native windows, tabs, splits, and scrollback! It also has the added benefit of supporting all the terminal features your emulator supports, no longer restricted by what `tmux` supports. | |
| 347 | 349 | ||
| 350 | + | The end-game here would be to leverage your window manager's ability to automatically arrange your windows for each project with a single command. | |
| 351 | + | ||
| 348 | 352 | ## socket file location | |
| 349 | 353 | ||
| 350 | 354 | Each session gets its own unix socket file. The default location depends on your environment variables (checked in priority order): |
| ... | ... | @@ -373,7 +377,7 @@ We are evaluating what should be configurable and what should not. Every configu | |
| 373 | 377 | ## known issues | |
| 374 | 378 | ||
| 375 | 379 | - When upgrading versions of `zmx` where we make changes to the underlying IPC communication, it will kill all your sessions because it cannot communicate through the daemon socket properly | |
| 376 | - | - Terminal state rehydration with nested `zmx` sessions through SSH: host A `zmx` -> SSH -> host B `zmx` | |
| 380 | + | - Terminal state restoration with nested `zmx` sessions through SSH: host A `zmx` -> SSH -> host B `zmx` | |
| 377 | 381 | - Specifically cursor position gets corrupted | |
| 378 | 382 | - When re-attaching and kitty keyboard mode was previously enable, we try to re-send that CSI query to re-enable it | |
| 379 | 383 | - Some programs don't know how to handle that CSI query (e.g. `psql`) so when you type it echos kitty escape sequences erroneously |
| ... | ... | @@ -404,31 +408,19 @@ In this way, `ghostty-vt` doesn't sit in the middle of an active terminal sessio | |
| 404 | 408 | ||
| 405 | 409 | ## prior art | |
| 406 | 410 | ||
| 407 | - | Below is a list of projects that inspired me to build this project. | |
| 411 | + | Below is a list of projects that inspired me to build this project. Architecturally, `zmx` uses aspects of both projects. For example, `shpool` inspired the idea of having libghostty restore the terminal state on reattach. Abduco inspired the idea of one daemon (and unix socket) per session. | |
| 408 | 412 | ||
| 409 | 413 | ### shpool | |
| 410 | 414 | ||
| 411 | - | You can find the source code at this repo: https://github.com/shell-pool/shpool | |
| 415 | + | https://github.com/shell-pool/shpool | |
| 412 | 416 | ||
| 413 | 417 | `shpool` is a service that enables session persistence by allowing the creation of named shell sessions owned by `shpool` so that the session is not lost if the connection drops. | |
| 414 | 418 | ||
| 415 | - | `shpool` can be thought of as a lighter weight alternative to tmux or GNU screen. While tmux and screen take over the whole terminal and provide window splitting and tiling features, `shpool` only provides persistent sessions. | |
| 416 | - | ||
| 417 | - | The biggest advantage of this approach is that `shpool` does not break native scrollback or copy-paste. | |
| 418 | - | ||
| 419 | 419 | ### abduco | |
| 420 | 420 | ||
| 421 | - | You can find the source code at this repo: https://github.com/martanne/abduco | |
| 422 | - | ||
| 423 | - | abduco provides session management i.e. it allows programs to be run independently from its controlling terminal. That is programs can be detached - run in the background - and then later reattached. Together with dvtm it provides a simpler and cleaner alternative to tmux or screen. | |
| 424 | - | ||
| 425 | - | ### dtach | |
| 426 | - | ||
| 427 | - | You can find the source code at this repo: https://github.com/crigler/dtach | |
| 428 | - | ||
| 429 | - | A simple program that emulates the detach feature of screen. | |
| 421 | + | https://github.com/martanne/abduco | |
| 430 | 422 | ||
| 431 | - | dtach is a program written in C that emulates the detach feature of screen, which allows a program to be executed in an environment that is protected from the controlling terminal. For instance, the program under the control of dtach would not be affected by the terminal being disconnected for some reason. | |
| 423 | + | abduco provides session management (i.e. it allows programs to be run independently from its controlling terminal). Together with dvtm it provides a simpler alternative to tmux or screen. | |
| 432 | 424 | ||
| 433 | 425 | ## comparison | |
| 434 | 426 |
+10,
-6
| ... | ... | @@ -750,6 +750,10 @@ fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void { | |
| 750 | 750 | var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); | |
| 751 | 751 | const stdout = &stdout_writer.interface; | |
| 752 | 752 | ||
| 753 | + | var stderr_buffer: [1024]u8 = undefined; | |
| 754 | + | var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); | |
| 755 | + | const stderr = &stderr_writer.interface; | |
| 756 | + | ||
| 753 | 757 | // Highest match count seen so far. Lets us distinguish "sessions haven't | |
| 754 | 758 | // appeared yet" (keep polling) from "sessions we were tracking | |
| 755 | 759 | // disappeared" (fail -- daemon crashed or was killed). |
| ... | ... | @@ -780,8 +784,8 @@ fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void { | |
| 780 | 784 | // is no longer deleted, so this session would otherwise | |
| 781 | 785 | // persist as task_ended_at==0 forever → infinite "still | |
| 782 | 786 | // waiting". Count it as done+failed so wait terminates. | |
| 783 | - | try stdout.print("task unreachable: {s} ({s})\n", .{ session.name, session.error_name orelse "unknown" }); | |
| 784 | - | try stdout.flush(); | |
| 787 | + | try stderr.print("task unreachable: {s} ({s})\n", .{ session.name, session.error_name orelse "unknown" }); | |
| 788 | + | try stderr.flush(); | |
| 785 | 789 | agg_exit_code = 1; | |
| 786 | 790 | done += 1; | |
| 787 | 791 | continue; |
| ... | ... | @@ -806,8 +810,8 @@ fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void { | |
| 806 | 810 | // crashed and the remaining N-1 happen to be done, total==done | |
| 807 | 811 | // would be a false success. | |
| 808 | 812 | if (total < max_seen) { | |
| 809 | - | try stdout.print("error: {d} session(s) disappeared before completing\n", .{max_seen - total}); | |
| 810 | - | try stdout.flush(); | |
| 813 | + | try stderr.print("error: {d} session(s) disappeared before completing\n", .{max_seen - total}); | |
| 814 | + | try stderr.flush(); | |
| 811 | 815 | std.process.exit(1); | |
| 812 | 816 | return; | |
| 813 | 817 | } |
| ... | ... | @@ -827,8 +831,8 @@ fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void { | |
| 827 | 831 | // typo, not a slow start. | |
| 828 | 832 | zero_match_iters += 1; | |
| 829 | 833 | if (zero_match_iters >= 3) { | |
| 830 | - | try stdout.print("error: no matching sessions found\n", .{}); | |
| 831 | - | try stdout.flush(); | |
| 834 | + | try stderr.print("error: no matching sessions found\n", .{}); | |
| 835 | + | try stderr.flush(); | |
| 832 | 836 | std.process.exit(2); | |
| 833 | 837 | return; | |
| 834 | 838 | } |