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.6.0-linux-aarch64.tar.gz
38- https://zmx.sh/a/zmx-0.6.0-linux-x86_64.tar.gz
39- https://zmx.sh/a/zmx-0.6.0-macos-aarch64.tar.gz
40- https://zmx.sh/a/zmx-0.6.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### packages (unofficial)
55
56- [Alpine Linux](https://pkgs.alpinelinux.org/package/edge/testing/x86_64/zmx)
57- [Arch AUR tracking releases](https://aur.archlinux.org/packages/zmx)
58- [Arch AUR tracking git](https://aur.archlinux.org/packages/zmx-git)
59- [openSUSE Tumbleweed](https://software.opensuse.org/package/zmx)
60- [Gentoo (overlay)](https://codeberg.org/samuelhautamaki/samuelhautamaki-gentoo)
61- [zmx-rpm packaging](https://github.com/engie/zmx-rpm/)
62
63### src
64
65- Requires zig `v0.15`
66- Clone the repo
67- Run build cmd
68
69Be sure to add `~/.local/bin` to your `PATH`:
70
71```bash
72zig build -Doptimize=ReleaseSafe --prefix ~/.local
73```
74
75## usage
76
77> [!IMPORTANT]
78> We recommend closing the terminal window to detach from the session but you can also press `ctrl+\` or run `zmx detach`.
79
80Run `zmx help` for more information on usage, with examples.
81
82```
83Usage: zmx <command> [args...]
84
85Commands:
86 [a]ttach <name> [command...] Attach to session, creating if needed
87 [r]un <name> [-d] [command...] Send command without attaching
88 [s]end <name> <text...> Send raw input to session PTY
89 [p]rint <name> <text...> Inject text into session display
90 [wr]ite <name> <file_path> Write stdin to file_path through the session
91 [d]etach Detach all clients (ctrl+\\ for current client)
92 [l]ist|ls [--short|--where k=v] List active sessions
93 [g]et <name> Get session labels
94 set <name> k=v ... Set session labels
95 [un]set <name> key ... Remove session labels
96 [cl]ear <name> Clear all session labels
97 [k]ill <name>... [--force] Kill session and all attached clients
98 [hi]story <name> [--vt|--html] Output session scrollback
99 [w]ait <name>... Wait for session tasks to complete
100 [t]ail <name>... Follow session output
101 [c]ompletions <shell> Shell completions (bash, zsh, fish, nu)
102 [v]ersion Show version and metadata (socket dir, log dir)
103 [h]elp Show this help
104```
105
106## shell prompt
107
108When 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.
109
110We recommend checking for that env var inside your prompt and displaying some indication there.
111
112### fish
113
114Place this file in `~/.config/fish/config.fish`:
115
116```fish
117functions -c fish_prompt _original_fish_prompt 2>/dev/null
118
119function fish_prompt --description 'Write out the prompt'
120 if set -q ZMX_SESSION
121 echo -n "[$ZMX_SESSION] "
122 end
123 _original_fish_prompt
124end
125```
126
127### bash and zsh
128
129Depending on the shell, place this in either `.bashrc` or `.zshrc`:
130
131```bash
132if [[ -n $ZMX_SESSION ]]; then
133 export PS1="[$ZMX_SESSION] ${PS1}"
134fi
135```
136
137### powerlevel10k zsh theme
138
139[powerlevel10k](https://github.com/romkatv/powerlevel10k) is a theme for zsh that overwrites the default prompt statusline.
140
141Place this in `.zshrc`:
142
143```bash
144function prompt_my_zmx_session() {
145 if [[ -n $ZMX_SESSION ]]; then
146 p10k segment -b '%k' -f '%f' -t "[$ZMX_SESSION]"
147 fi
148}
149POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS+=my_zmx_session
150```
151
152### oh-my-posh
153
154[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:
155
156```toml
157[[blocks.segments]]
158 template = '{{ if .Env.ZMX_SESSION }} {{ .Env.ZMX_SESSION }}{{ end }}'
159 foreground = 'p:orange'
160 background = 'p:black'
161 type = 'text'
162 style = 'plain'
163```
164
165### Starship
166
167[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:
168
169```toml
170format = """
171${env_var.ZMX_SESSION}\
172...
173"""
174
175[env_var.ZMX_SESSION]
176symbol = " "
177format = "[$symbol$env_value]($style) "
178description = "zmx session name"
179style = "bold magenta"
180```
181
182## shell completion
183
184Shell 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:
185
186```bash
187ssh remote-server zmx attach session-na<TAB>
188# <- auto-complete suggestions appear here
189```
190
191> NOTICE: when installing `zmx` with `homebrew` completions are automatically installed.
192
193### bash
194
195Add this to your `.bashrc` file:
196
197```bash
198if command -v zmx &> /dev/null; then
199 eval "$(zmx completions bash)"
200fi
201```
202
203### zsh
204
205Add this to your `.zshrc` file:
206
207```zsh
208if command -v zmx &> /dev/null; then
209 eval "$(zmx completions zsh)"
210fi
211```
212
213### fish
214
215Add this to `~/.config/fish/completions/zmx.fish`:
216
217```fish
218if type -q zmx
219 zmx completions fish | source
220end
221```
222
223## session picker
224
225You 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.
226
227Requires [fzf](https://github.com/junegunn/fzf).
228
229- **Enter** selects a matched session (or creates one if no sessions exist)
230- **Ctrl-N** creates a new session using the typed query, even when a fuzzy match is highlighted
231
232<details>
233<summary>bash and zsh</summary>
234
235```bash
236zmx-select() {
237 local display
238 display=$(zmx list 2>/dev/null | while IFS=$'\t' read -r name pid clients created dir; do
239 name=${name#*name=}
240 pid=${pid#*pid=}
241 clients=${clients#*clients=}
242 dir=${dir#*start_dir=}
243 printf "%-20s pid:%-8s clients:%-2s %s\n" "$name" "$pid" "$clients" "$dir"
244 done)
245
246 local output query key selected session_name
247 output=$({ [[ -n "$display" ]] && echo "$display"; } | fzf \
248 --print-query \
249 --expect=ctrl-n \
250 --height=80% \
251 --reverse \
252 --prompt="zmx> " \
253 --header="Enter: select | Ctrl-N: create new" \
254 --preview='zmx history {1}' \
255 --preview-window=right:60%:follow \
256 )
257 local rc=$?
258
259 query=$(echo "$output" | sed -n '1p')
260 key=$(echo "$output" | sed -n '2p')
261 selected=$(echo "$output" | sed -n '3p')
262
263 if [[ "$key" == "ctrl-n" && -n "$query" ]]; then
264 session_name="$query"
265 elif [[ $rc -eq 0 && -n "$selected" ]]; then
266 session_name=$(echo "$selected" | awk '{print $1}')
267 elif [[ -n "$query" ]]; then
268 session_name="$query"
269 else
270 return 130
271 fi
272
273 zmx attach "$session_name"
274}
275```
276
277You 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.
278
279```bash
280if command -v zmx &> /dev/null && command -v fzf &> /dev/null && [[ -z "$ZMX_SESSION" ]]; then
281 zmx-select && exit
282fi
283```
284
285#### Alternative: gentle hint (server use)
286
287If 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:
288
289```bash
290if command -v zmx &> /dev/null && [[ -z "$ZMX_SESSION" ]]; then
291 local count
292 count=$(zmx ls --short 2>/dev/null | wc -l)
293 if [[ "$count" -gt 0 ]]; then
294 echo "zmx: $count session(s) active — \`zmx-select\` to attach" >&2
295 fi
296fi
297```
298
299Choose the auto-launch pattern for dedicated dev machines, and the hint pattern for shared servers where you frequently run quick commands.
300
301</details>
302
303## session prefix
304
305We 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.
306
307```bash
308export ZMX_SESSION_PREFIX="d."
309zmx a runner # ZMX_SESSION=d.runner
310zmx a tests # ZMX_SESSION=d.tests
311zmx k tests # kills d.tests
312zmx wait # suspends until all tasks prefixed with "d." are complete
313```
314
315## philosophy
316
317The 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.
318
319Instead, this tool specifically focuses on session persistence and defers window management to your os wm.
320
321## ssh workflow
322
323### Try it out quickly
324
325If 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:
326
327```bash
328ssh -t dev-box zmx attach default
329```
330
331Without `-t`, the remote shell will not know it's talking to a terminal, and the display will likely get messed up.
332
333This option isn't needed if you follow the configuration steps below, because `RequestTTY yes` does the same thing.
334
335### Configure it for regular use
336
337Using `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.
338
339First, create an `ssh` config entry for your remote dev server:
340
341```bash
342Host = d.*
343 HostName 192.168.1.xxx
344
345 RemoteCommand zmx attach %k
346 RequestTTY yes
347 ControlPath ~/.ssh/cm-%r@%h:%p
348 ControlMaster auto
349 ControlPersist 10m
350```
351
352Architecturally, `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!
353
354Now you can spawn as many terminal sessions as you'd like:
355
356```bash
357ssh d.term
358ssh d.irc
359ssh d.pico
360ssh d.dotfiles
361```
362
363Because the `attach` command is essentially an "upsert", this will create or attach to each session.
364
365Now 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:
366
367```bash
368autossh -M 0 -q d.term
369```
370
371Or create an `alias`/`abbr`:
372
373```fish
374abbr -a ash "autossh -M 0 -q"
375```
376
377```bash
378ash d.term
379ash d.irc
380ash d.pico
381ash d.dotifles
382```
383
384Wow! 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.
385
386The end-game here would be to leverage your window manager's ability to automatically arrange your windows for each project with a single command.
387
388## socket file location
389
390Each session gets its own unix socket file. The default location depends on your environment variables (checked in priority order):
391
3921. `ZMX_DIR` => uses exact path (e.g., `/custom/path`)
3931. `XDG_RUNTIME_DIR` => uses `{XDG_RUNTIME_DIR}/zmx` (recommended on Linux, typically results in `/run/user/{uid}/zmx`)
3941. `TMPDIR` => uses `{TMPDIR}/zmx-{uid}` (appends uid for multi-user safety)
3951. `/tmp` => uses `/tmp/zmx-{uid}` (default fallback, appends uid for multi-user safety)
396
397## permissions
398
399You can configure the permissions for the socket directory and log files using the following environment variables:
400
401- `ZMX_DIR_MODE` => sets the mode for the socket and log directories (octal, defaults to `0750`)
402- `ZMX_LOG_MODE` => sets the mode for the log files (octal, defaults to `0640`)
403
404This 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.
405
406## debugging
407
408We 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.
409
410The log directory is resolved in this order:
411
4121. `ZMX_DIR/logs` if `ZMX_DIR` is set
4131. `XDG_STATE_HOME/zmx/logs` if `XDG_STATE_HOME` is set
4141. `HOME/.local/state/zmx/logs`
4151. `TMPDIR/zmx-$UID` (or `/tmp/zmx-$UID`) as a last resort
416
417## a smol contract
418
419- Write programs that solve a well defined problem.
420- Write programs that behave the way most users expect them to behave.
421- Write programs that a single person can maintain.
422- Write programs that compose with other smol tools.
423- Write programs that can be finished.
424
425## known issues
426
427- 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
428- Terminal state restoration with nested `zmx` sessions through SSH: host A `zmx` -> SSH -> host B `zmx`
429 - Specifically cursor position gets corrupted
430 - Essentially this is unspecified and unsupported behavior
431- When re-attaching and kitty keyboard mode was previously enable, we try to re-send that CSI query to re-enable it
432 - Some programs don't know how to handle that CSI query (e.g. `psql`) so when you type it echos kitty escape sequences erroneously
433
434## impl
435
436- The `daemon` and client processes communicate via a unix socket
437- Both `daemon` and `client` loops leverage `poll(2)`
438- Each session creates its own unix socket file
439- We restore terminal state and output using `libghostty-vt`
440
441### libghostty-vt
442
443We use `libghostty-vt` to restore the previous state of the terminal when a client re-attaches to a session.
444
445How it works:
446
447- user creates session `zmx attach term`
448- user interacts with terminal stdin
449- stdin gets sent to pty via daemon
450- daemon sends pty output to client *and* `ghostty-vt`
451- `ghostty-vt` holds terminal state and scrollback
452- user disconnects
453- user re-attaches to session
454- `ghostty-vt` sends terminal snapshot to client stdout
455
456In 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.
457
458## prior art
459
460Below 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.
461
462### shpool
463
464https://github.com/shell-pool/shpool
465
466`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.
467
468### abduco
469
470https://github.com/martanne/abduco
471
472abduco 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.
473
474## comparison
475
476| Feature | zmx | shpool | abduco | dtach | tmux |
477| ------------------------------ | --- | ------ | ------ | ----- | ---- |
478| 1:1 Terminal emulator features | ✓ | ✓ | ✓ | ✓ | ✗ |
479| Terminal state restore | ✓ | ✓ | ✗ | ✗ | ✓ |
480| Window management | ✗ | ✗ | ✗ | ✗ | ✓ |
481| Multiple clients per session | ✓ | ✗ | ✓ | ✓ | ✓ |
482| Native scrollback | ✓ | ✓ | ✓ | ✓ | ✗ |
483| Configurable detach key | ✗ | ✓ | ✓ | ✓ | ✓ |
484| Auto-daemonize | ✓ | ✓ | ✓ | ✓ | ✓ |
485| Daemon per session | ✓ | ✗ | ✓ | ✓ | ✗ |
486| Session listing | ✓ | ✓ | ✓ | ✗ | ✓ |
487
488## community tools
489
490- [emacs-term-sessions](https://github.com/ArthurHeymans/emacs-term-sessions) Persistent terminal sessions in Emacs, both local and remote.
491- [pi-zmx](https://github.com/deevus/pi-zmx) -- [pi](https://pi.dev) extension for zmx.
492- [zsm](https://github.com/mdsakalu/zmx-session-manager) -- TUI session manager for zmx. List, preview, filter, and kill sessions from an interactive terminal UI.
493- [zmosh](https://github.com/mmonad/zmosh) -- A fork of zmx that adds encrypted UDP auto-reconnect for remote sessions (like mosh).
494- [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.