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
297Using `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.
298
299First, create an `ssh` config entry for your remote dev server:
300
301```bash
302Host = d.*
303    HostName 192.168.1.xxx
304
305    RemoteCommand zmx attach %k
306    RequestTTY yes
307    ControlPath ~/.ssh/cm-%r@%h:%p
308    ControlMaster auto
309    ControlPersist 10m
310```
311
312Architecturally, `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!
313
314Now you can spawn as many terminal sessions as you'd like:
315
316```bash
317ssh d.term
318ssh d.irc
319ssh d.pico
320ssh d.dotfiles
321```
322
323Because the `attach` command is essentially an "upsert", this will create or attach to each session.
324
325Now 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:
326
327```bash
328autossh -M 0 -q d.term
329```
330
331Or create an `alias`/`abbr`:
332
333```fish
334abbr -a ash "autossh -M 0 -q"
335```
336
337```bash
338ash d.term
339ash d.irc
340ash d.pico
341ash d.dotifles
342```
343
344Wow! 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.
345
346The end-game here would be to leverage your window manager's ability to automatically arrange your windows for each project with a single command.
347
348## socket file location
349
350Each session gets its own unix socket file. The default location depends on your environment variables (checked in priority order):
351
3521. `ZMX_DIR` => uses exact path (e.g., `/custom/path`)
3531. `XDG_RUNTIME_DIR` => uses `{XDG_RUNTIME_DIR}/zmx` (recommended on Linux, typically results in `/run/user/{uid}/zmx`)
3541. `TMPDIR` => uses `{TMPDIR}/zmx-{uid}` (appends uid for multi-user safety)
3551. `/tmp` => uses `/tmp/zmx-{uid}` (default fallback, appends uid for multi-user safety)
356
357## permissions
358
359You can configure the permissions for the socket directory and log files using the following environment variables:
360
361- `ZMX_DIR_MODE` => sets the mode for the socket and log directories (octal, defaults to `0750`)
362- `ZMX_LOG_MODE` => sets the mode for the log files (octal, defaults to `0640`)
363
364This 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.
365
366## debugging
367
368We store global logs for cli commands in `{socket_dir}/logs/zmx.log`. We store session-specific logs in `{socket_dir}/logs/{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.
369
370## a smol contract
371
372- Write programs that solve a well defined problem.
373- Write programs that behave the way most users expect them to behave.
374- Write programs that a single person can maintain.
375- Write programs that compose with other smol tools.
376- Write programs that can be finished.
377
378## known issues
379
380- 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
381- Terminal state restoration with nested `zmx` sessions through SSH: host A `zmx` -> SSH -> host B `zmx`
382  - Specifically cursor position gets corrupted
383  - Essentially this is unspecified and unsupported behavior
384- When re-attaching and kitty keyboard mode was previously enable, we try to re-send that CSI query to re-enable it
385  - Some programs don't know how to handle that CSI query (e.g. `psql`) so when you type it echos kitty escape sequences erroneously
386
387## impl
388
389- The `daemon` and client processes communicate via a unix socket
390- Both `daemon` and `client` loops leverage `poll(2)`
391- Each session creates its own unix socket file
392- We restore terminal state and output using `libghostty-vt`
393
394### libghostty-vt
395
396We use `libghostty-vt` to restore the previous state of the terminal when a client re-attaches to a session.
397
398How it works:
399
400- user creates session `zmx attach term`
401- user interacts with terminal stdin
402- stdin gets sent to pty via daemon
403- daemon sends pty output to client *and* `ghostty-vt`
404- `ghostty-vt` holds terminal state and scrollback
405- user disconnects
406- user re-attaches to session
407- `ghostty-vt` sends terminal snapshot to client stdout
408
409In 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.
410
411## prior art
412
413Below 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.
414
415### shpool
416
417https://github.com/shell-pool/shpool
418
419`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.
420
421### abduco
422
423https://github.com/martanne/abduco
424
425abduco 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.
426
427## comparison
428
429| Feature                        | zmx | shpool | abduco | dtach | tmux |
430| ------------------------------ | --- | ------ | ------ | ----- | ---- |
431| 1:1 Terminal emulator features | ✓   | ✓      | ✓      | ✓     | ✗    |
432| Terminal state restore         | ✓   | ✓      | ✗      | ✗     | ✓    |
433| Window management              | ✗   | ✗      | ✗      | ✗     | ✓    |
434| Multiple clients per session   | ✓   | ✗      | ✓      | ✓     | ✓    |
435| Native scrollback              | ✓   | ✓      | ✓      | ✓     | ✗    |
436| Configurable detach key        | ✗   | ✓      | ✓      | ✓     | ✓    |
437| Auto-daemonize                 | ✓   | ✓      | ✓      | ✓     | ✓    |
438| Daemon per session             | ✓   | ✗      | ✓      | ✓     | ✗    |
439| Session listing                | ✓   | ✓      | ✓      | ✗     | ✓    |
440
441## community tools
442
443- [pi-zmx](https://github.com/deevus/pi-zmx) -- [pi](https://pi.dev) extension for zmx.
444- [zsm](https://github.com/mdsakalu/zmx-session-manager) -- TUI session manager for zmx. List, preview, filter, and kill sessions from an interactive terminal UI.
445- [zmosh](https://github.com/mmonad/zmosh) -- A fork of zmx that adds encrypted UDP auto-reconnect for remote sessions (like mosh).