git clone https://github.com/neurosnap/zmx.git
1<h1>
2<p align="center">
3 <img src="./logo.png" alt="Logo" width="128">
4 <br>zmx
5</h1>
6<p align="center">
7 Session attach/detach for the terminal.
8 <br />
9 <a href="https://zmx.sh">Docs</a>
10 ·
11 <a href="https://bower.sh/you-might-not-need-tmux">You might not need tmux</a>
12 ·
13 Sponsored by <a href="https://pico.sh">pico.sh</a>
14</p>
15
16## features
17
18- Persist terminal shell sessions
19- Ability to attach and detach from a shell session without it being killed
20- Native terminal scrollback
21- Multiple clients can connect to the same session
22- Re-attaching to a session restores previous terminal state and output
23- Send commands to a session without attaching to it
24- Print scrollback history of a terminal session in plain text
25- Works on mac and linux
26- This project does **NOT** provide windows, tabs, or splits
27
28## demos
29
30- [zmx - intro](https://youtu.be/UIXj0_rhPgI)
31- [zmx - ai portal](https://youtu.be/CV3skPYHP4Q)
32
33## install
34
35### binaries
36
37- https://zmx.sh/a/zmx-0.7.0-linux-aarch64.tar.gz
38- https://zmx.sh/a/zmx-0.7.0-linux-x86_64.tar.gz
39- https://zmx.sh/a/zmx-0.7.0-macos-aarch64.tar.gz
40- https://zmx.sh/a/zmx-0.7.0-macos-x86_64.tar.gz
41
42### homebrew
43
44```bash
45brew install neurosnap/tap/zmx
46```
47
48### mise-en-place
49
50```bash
51mise use zmx
52```
53
54### NixOS / nixpkgs
55
56Run immediately without installation:
57
58```sh
59nix run github:NixOS/nixpkgs/nixpkgs-unstable#zmx
60# or build main yourself
61nix run github:neurosnap/zmx
62```
63
64Start a shell with zmx available while it runs:
65
66```sh
67nix shell github:NixOS/nixpkgs/nixpkgs-unstable#zmx
68# or built main yourself
69nix run github:neurosnap/zmx
70```
71
72### packages (unofficial)
73
74- [Alpine Linux](https://pkgs.alpinelinux.org/package/edge/testing/x86_64/zmx)
75- [Arch AUR tracking releases](https://aur.archlinux.org/packages/zmx)
76- [Arch AUR tracking git](https://aur.archlinux.org/packages/zmx-git)
77- [openSUSE Tumbleweed](https://software.opensuse.org/package/zmx)
78- [Gentoo (overlay)](https://codeberg.org/samuelhautamaki/samuelhautamaki-gentoo)
79- [zmx-rpm packaging](https://github.com/engie/zmx-rpm/)
80
81### src
82
83- Requires zig `v0.16`
84- Clone the repo
85- Run build cmd
86
87Be sure to add `~/.local/bin` to your `PATH`:
88
89```bash
90zig build -Doptimize=ReleaseSafe --prefix ~/.local
91```
92
93## usage
94
95> [!IMPORTANT]
96> We recommend closing the terminal window to detach from the session but you can also press `ctrl+\` or run `zmx detach`. If you need `ctrl+\` for something else (e.g. vim's `ctrl+\ ctrl+n` to escape its own `:terminal`), set `ZMX_NO_DETACH_KEY` to disable the shortcut and rely on `zmx detach` or closing the window instead.
97
98Run `zmx help` for more information on usage, with examples.
99
100```
101Usage: zmx <command> [args...]
102
103Commands:
104 [a]ttach <name> [command...] Attach to session, creating if needed
105 [r]un <name> [-d] [command...] Send command without attaching
106 [s]end <name> <text...> Send raw input to session PTY
107 [p]rint <name> <text...> Inject text into session display
108 [wr]ite <name> <file_path> Write stdin to file_path through the session
109 [d]etach Detach all clients (ctrl+\\ for current client)
110 [l]ist|ls [--short|--where k=v] List active sessions
111 [g]et <name> Get session labels
112 set <name> k=v ... Set session labels
113 [un]set <name> key ... Remove session labels
114 [cl]ear <name> Clear all session labels
115 [k]ill <name>... [--force] Kill session and all attached clients
116 [hi]story <name> [--vt|--html] Output session scrollback
117 [w]ait <name>... Wait for session tasks to complete
118 [t]ail <name>... Follow session output
119 [c]ompletions <shell> Shell completions (bash, zsh, fish, nu)
120 [v]ersion Show version and metadata (socket dir, log dir)
121 [h]elp Show this help
122```
123
124## shell prompt
125
126When you attach to a `zmx` session, we don't provide any indication that you are inside `zmx`. We do provide an environment variable `ZMX_SESSION` which contains the session name.
127
128We recommend checking for that env var inside your prompt and displaying some indication there.
129
130### fish
131
132Place this file in `~/.config/fish/config.fish`:
133
134```fish
135functions -c fish_prompt _original_fish_prompt 2>/dev/null
136
137function fish_prompt --description 'Write out the prompt'
138 if set -q ZMX_SESSION
139 echo -n "[$ZMX_SESSION] "
140 end
141 _original_fish_prompt
142end
143```
144
145### bash and zsh
146
147Depending on the shell, place this in either `.bashrc` or `.zshrc`:
148
149```bash
150if [[ -n $ZMX_SESSION ]]; then
151 export PS1="[$ZMX_SESSION] ${PS1}"
152fi
153```
154
155### powerlevel10k zsh theme
156
157[powerlevel10k](https://github.com/romkatv/powerlevel10k) is a theme for zsh that overwrites the default prompt statusline.
158
159Place this in `.zshrc`:
160
161```bash
162function prompt_my_zmx_session() {
163 if [[ -n $ZMX_SESSION ]]; then
164 p10k segment -b '%k' -f '%f' -t "[$ZMX_SESSION]"
165 fi
166}
167POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS+=my_zmx_session
168```
169
170### oh-my-posh
171
172[oh-my-posh](https://ohmyposh.dev) is a popular shell themeing and prompt engine. This code will display an icon and session name as part of the prompt if (and only if) you have zmx active:
173
174```toml
175[[blocks.segments]]
176 template = '{{ if .Env.ZMX_SESSION }} {{ .Env.ZMX_SESSION }}{{ end }}'
177 foreground = 'p:orange'
178 background = 'p:black'
179 type = 'text'
180 style = 'plain'
181```
182
183### Starship
184
185[Starship](https://starship.rs) is a popular shell themeing and prompt engine. This code will display an icon and session name as part of the prompt if (and only if) you have zmx active:
186
187```toml
188format = """
189${env_var.ZMX_SESSION}\
190...
191"""
192
193[env_var.ZMX_SESSION]
194symbol = " "
195format = "[$symbol$env_value]($style) "
196description = "zmx session name"
197style = "bold magenta"
198```
199
200## shell completion
201
202Shell auto-completion for `zmx` commands and session names can be enabled using the `completions` subcommand. Once configured, you'll get auto-complete for both local `zmx` commands and sessions:
203
204```bash
205ssh remote-server zmx attach session-na<TAB>
206# <- auto-complete suggestions appear here
207```
208
209> NOTICE: when installing `zmx` with `homebrew` completions are automatically installed.
210
211### bash
212
213Add this to your `.bashrc` file:
214
215```bash
216if command -v zmx &> /dev/null; then
217 eval "$(zmx completions bash)"
218fi
219```
220
221### zsh
222
223Add this to your `.zshrc` file:
224
225```zsh
226if command -v zmx &> /dev/null; then
227 eval "$(zmx completions zsh)"
228fi
229```
230
231### fish
232
233Add this to `~/.config/fish/completions/zmx.fish`:
234
235```fish
236if type -q zmx
237 zmx completions fish | source
238end
239```
240
241## session picker
242
243You can add an interactive session picker to your shell that lets you fuzzy-find existing sessions, preview their scrollback history, or create new ones -- all from a single prompt. This is especially useful for remote SSH workflows: add it to your shell startup so that connecting to a machine immediately presents the picker.
244
245Requires [fzf](https://github.com/junegunn/fzf).
246
247- **Enter** selects a matched session (or creates one if no sessions exist)
248- **Ctrl-N** creates a new session using the typed query, even when a fuzzy match is highlighted
249
250<details>
251<summary>bash and zsh</summary>
252
253```bash
254zmx-select() {
255 local display
256 display=$(zmx list 2>/dev/null | while IFS=$'\t' read -r name pid clients created dir; do
257 name=${name#*name=}
258 pid=${pid#*pid=}
259 clients=${clients#*clients=}
260 dir=${dir#*start_dir=}
261 printf "%-20s pid:%-8s clients:%-2s %s\n" "$name" "$pid" "$clients" "$dir"
262 done)
263
264 local output query key selected session_name
265 output=$({ [[ -n "$display" ]] && echo "$display"; } | fzf \
266 --print-query \
267 --expect=ctrl-n \
268 --height=80% \
269 --reverse \
270 --prompt="zmx> " \
271 --header="Enter: select | Ctrl-N: create new" \
272 --preview='zmx history {1}' \
273 --preview-window=right:60%:follow \
274 )
275 local rc=$?
276
277 query=$(echo "$output" | sed -n '1p')
278 key=$(echo "$output" | sed -n '2p')
279 selected=$(echo "$output" | sed -n '3p')
280
281 if [[ "$key" == "ctrl-n" && -n "$query" ]]; then
282 session_name="$query"
283 elif [[ $rc -eq 0 && -n "$selected" ]]; then
284 session_name=$(echo "$selected" | awk '{print $1}')
285 elif [[ -n "$query" ]]; then
286 session_name="$query"
287 else
288 return 130
289 fi
290
291 zmx attach "$session_name"
292}
293```
294
295You can call `zmx-select` manually, bind it to a key, or auto-launch it on shell startup when outside a zmx session. With `&& exit`, the normal flow becomes: connect via SSH → pick a session → work → detach or exit the session → SSH disconnects automatically. Cancelling the picker with **Ctrl-C** drops you into a regular shell as an escape hatch.
296
297```bash
298if command -v zmx &> /dev/null && command -v fzf &> /dev/null && [[ -z "$ZMX_SESSION" ]]; then
299 zmx-select && exit
300fi
301```
302
303#### Alternative: gentle hint (server use)
304
305If you use zmx on a shared server and SSH in frequently for quick operations, auto-launching the picker on every connection may be too aggressive. Instead, show a one-line reminder when active sessions exist:
306
307```bash
308if command -v zmx &> /dev/null && [[ -z "$ZMX_SESSION" ]]; then
309 local count
310 count=$(zmx ls --short 2>/dev/null | wc -l)
311 if [[ "$count" -gt 0 ]]; then
312 echo "zmx: $count session(s) active — \`zmx-select\` to attach" >&2
313 fi
314fi
315```
316
317Choose the auto-launch pattern for dedicated dev machines, and the hint pattern for shared servers where you frequently run quick commands.
318
319</details>
320
321## session prefix
322
323We allow users to set an environment variable `ZMX_SESSION_PREFIX` which will prefix the name of the session for all commands. This means if that variable is set, every command that accepts a session will be prefixed with it.
324
325```bash
326export ZMX_SESSION_PREFIX="d."
327zmx a runner # ZMX_SESSION=d.runner
328zmx a tests # ZMX_SESSION=d.tests
329zmx k tests # kills d.tests
330zmx wait # suspends until all tasks prefixed with "d." are complete
331```
332
333## philosophy
334
335The entire argument for `zmx` instead of something like `tmux` that has windows, panes, splits, etc. is that job should be handled by your os window manager. By using something like `tmux` you now have redundant functionality in your dev stack: a window manager for your os and a window manager for your terminal. Further, in order to use modern terminal features, your terminal emulator **and** `tmux` need to have support for them. This holds back the terminal enthusiast community and feature development.
336
337Instead, this tool specifically focuses on session persistence and defers window management to your os wm.
338
339## ssh workflow
340
341### Try it out quickly
342
343If you'd like to try out `zmx` and `ssh` without fiddling your `ssh` config, make sure to pass the `-t` option to `ssh`. Here's an example:
344
345```bash
346ssh -t dev-box zmx attach default
347```
348
349Without `-t`, the remote shell will not know it's talking to a terminal, and the display will likely get messed up.
350
351This option isn't needed if you follow the configuration steps below, because `RequestTTY yes` does the same thing.
352
353### Configure it for regular use
354
355Using `zmx` with `ssh` is a first-class citizen. Instead of using `ssh` to remote into your system with a single terminal and `n` tmux panes, you open `n` terminals and run `ssh` for all of them. This might sound tedious, but there are tools to make this a delightful workflow.
356
357First, create an `ssh` config entry for your remote dev server:
358
359```bash
360Host = d.*
361 HostName 192.168.1.xxx
362
363 RemoteCommand zmx attach %k
364 RequestTTY yes
365 ControlPath ~/.ssh/cm-%r@%h:%p
366 ControlMaster auto
367 ControlPersist 10m
368```
369
370Architecturally, `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!
371
372Now you can spawn as many terminal sessions as you'd like:
373
374```bash
375ssh d.term
376ssh d.irc
377ssh d.pico
378ssh d.dotfiles
379```
380
381Because the `attach` command is essentially an "upsert", this will create or attach to each session.
382
383Now 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:
384
385```bash
386autossh -M 0 -q d.term
387```
388
389Or create an `alias`/`abbr`:
390
391```fish
392abbr -a ash "autossh -M 0 -q"
393```
394
395```bash
396ash d.term
397ash d.irc
398ash d.pico
399ash d.dotifles
400```
401
402Wow! 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.
403
404The end-game here would be to leverage your window manager's ability to automatically arrange your windows for each project with a single command.
405
406## socket file location
407
408Each session gets its own unix socket file. The default location depends on your environment variables (checked in priority order):
409
4101. `ZMX_DIR` => uses exact path (e.g., `/custom/path`)
4111. `XDG_RUNTIME_DIR` => uses `{XDG_RUNTIME_DIR}/zmx` (recommended on Linux, typically results in `/run/user/{uid}/zmx`)
4121. `TMPDIR` => uses `{TMPDIR}/zmx-{uid}` (appends uid for multi-user safety)
4131. `/tmp` => uses `/tmp/zmx-{uid}` (default fallback, appends uid for multi-user safety)
414
415## permissions
416
417You can configure the permissions for the socket directory and log files using the following environment variables:
418
419- `ZMX_DIR_MODE` => sets the mode for the socket and log directories (octal, defaults to `0750`)
420- `ZMX_LOG_MODE` => sets the mode for the log files (octal, defaults to `0640`)
421
422This is particularly useful when running `zmx` as a system service with a shared group. For example, setting `ZMX_DIR_MODE=0770` and `ZMX_LOG_MODE=0660` allows group members to attach to the session.
423
424## debugging
425
426We store global logs for cli commands in `{log_dir}/zmx.log`. We store session-specific logs in `{log_dir}/{session_name}.log`. Right now they are enabled by default and cannot be disabled. The idea here is to help with initial development until we reach a stable state.
427
428The log directory is resolved in this order:
429
4301. `ZMX_DIR/logs` if `ZMX_DIR` is set
4311. `XDG_STATE_HOME/zmx/logs` if `XDG_STATE_HOME` is set
4321. `HOME/.local/state/zmx/logs`
4331. `TMPDIR/zmx-$UID` (or `/tmp/zmx-$UID`) as a last resort
434
435## a smol contract
436
437- Write programs that solve a well defined problem.
438- Write programs that behave the way most users expect them to behave.
439- Write programs that a single person can maintain.
440- Write programs that compose with other smol tools.
441- Write programs that can be finished.
442
443## known issues
444
445- 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
446- Terminal state restoration with nested `zmx` sessions through SSH: host A `zmx` -> SSH -> host B `zmx`
447 - Specifically cursor position gets corrupted
448 - Essentially this is unspecified and unsupported behavior
449- When re-attaching and kitty keyboard mode was previously enable, we try to re-send that CSI query to re-enable it
450 - Some programs don't know how to handle that CSI query (e.g. `psql`) so when you type it echos kitty escape sequences erroneously
451
452## impl
453
454- The `daemon` and client processes communicate via a unix socket
455- Both `daemon` and `client` loops leverage `poll(2)`
456- Each session creates its own unix socket file
457- We restore terminal state and output using `libghostty-vt`
458
459### libghostty-vt
460
461We use `libghostty-vt` to restore the previous state of the terminal when a client re-attaches to a session.
462
463How it works:
464
465- user creates session `zmx attach term`
466- user interacts with terminal stdin
467- stdin gets sent to pty via daemon
468- daemon sends pty output to client *and* `ghostty-vt`
469- `ghostty-vt` holds terminal state and scrollback
470- user disconnects
471- user re-attaches to session
472- `ghostty-vt` sends terminal snapshot to client stdout
473
474In this way, `ghostty-vt` doesn't sit in the middle of an active terminal session, it simply receives all the same data the client receives so it can re-hydrate clients that connect to the session. This enables users to pick up where they left off as if they didn't disconnect from the terminal session at all. It also has the added benefit of being very fast, the only thing sitting in-between you and your PTY is a unix socket.
475
476## prior art
477
478Below 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.
479
480### shpool
481
482https://github.com/shell-pool/shpool
483
484`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.
485
486### abduco
487
488https://github.com/martanne/abduco
489
490abduco 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.
491
492## comparison
493
494| Feature | zmx | shpool | abduco | dtach | tmux |
495| ------------------------------ | --- | ------ | ------ | ----- | ---- |
496| 1:1 Terminal emulator features | ✓ | ✓ | ✓ | ✓ | ✗ |
497| Terminal state restore | ✓ | ✓ | ✗ | ✗ | ✓ |
498| Window management | ✗ | ✗ | ✗ | ✗ | ✓ |
499| Multiple clients per session | ✓ | ✗ | ✓ | ✓ | ✓ |
500| Native scrollback | ✓ | ✓ | ✓ | ✓ | ✗ |
501| Configurable detach key | ✗ | ✓ | ✓ | ✓ | ✓ |
502| Auto-daemonize | ✓ | ✓ | ✓ | ✓ | ✓ |
503| Daemon per session | ✓ | ✗ | ✓ | ✓ | ✗ |
504| Session listing | ✓ | ✓ | ✓ | ✗ | ✓ |
505
506## community tools
507
508- [emacs-term-sessions](https://github.com/ArthurHeymans/emacs-term-sessions) Persistent terminal sessions in Emacs, both local and remote.
509- [pi-zmx](https://github.com/deevus/pi-zmx) -- [pi](https://pi.dev) extension for zmx.
510- [zsm](https://github.com/mdsakalu/zmx-session-manager) -- TUI session manager for zmx. List, preview, filter, and kill sessions from an interactive terminal UI.
511- [zmosh](https://github.com/mmonad/zmosh) -- A fork of zmx that adds encrypted UDP auto-reconnect for remote sessions (like mosh).
512- [zmx-picker](https://github.com/EarthmanMuons/zmx-picker) -- fzf-based session picker and project launcher. Jump to a running zmx session or start one inside any of your git/jj repos.