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