repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

(175) commits
Eric Bower  ·  2025-12-20
fix: timing issue with first attach and sending terminal state

On first attach we don't want to send the terminal state but there
appears to be a timing issue where sometimes we already have pty output
before the Init handler can complete.
Eric Bower  ·  2025-12-19
docs: update changelog
Eric Bower  ·  2025-12-19
fix: correct cursor position when reattaching to session

Serialize terminal state BEFORE resizing the terminal in handleInit.
Previously, the resize was done first, which caused cursor reflow and
moved the cursor position. The shell's SIGWINCH-triggered prompt redraw
would then run after our snapshot was sent, leaving the cursor at the
wrong position (end of line instead of after the prompt).

By capturing the terminal state before resize, we preserve the correct
cursor position from when the user detached.
Eric Bower  ·  2025-12-19
docs: update detach key
Eric Bower  ·  2025-12-19
refactor: input shortcut handling

Another code clarity change that abstracts handling the shortcuts for
detach
Eric Bower  ·  2025-12-19
refactor: daemonLoop and terminal serialization

Code cleanup to make the daemonLoop and ghostty interaction easier to
understand
Eric Bower  ·  2025-12-19
feat: ctrl+b + d to detach

With support to add more commands
Eric Bower  ·  2025-12-19
fix: reset terminal modes on detach

Closes: https://github.com/neurosnap/zmx/issues/26
Eric Bower  ·  2025-12-19
chore: update ghostty-vt
Tristan Partin  ·  2025-12-18
Use XDG_RUNTIME_DIR for the socket directory if defined

XDG_RUNTIME_DIR[0] is a file path for user-specific runtime files, like
sockets.

It will typically be defined on Linux. I can't say for sure on BSDs, and
I know for certain it is not defined on macOS.

Link: https://specifications.freedesktop.org/basedir/latest/ [0]
Signed-off-by: Tristan Partin <tristan@partin.io>
Eric Bower  ·  2025-12-18
revert: don't force pty resize

This seems to be causing issues with multiple clients connected, instead
we will require clients to force resize themselves for now
Eric Bower  ·  2025-12-16
chore: publish v0.1.1
Eric Bower  ·  2025-12-16
docs(changelog): v0.1.1
Eric Bower  ·  2025-12-15
feat(list): sort by session name
Eric Bower  ·  2025-12-15
fix: send SIGWINCH to PTY on re-attach

TIOCSWINSZ alone doesn't notify the shell when terminal size is set.
Now we explicitly signal the foreground process group after setting
the window size, fixing display issues on re-attach.
Eric Bower  ·  2025-12-12
chore: remove dummy integration tests
Eric Bower  ·  2025-12-11
fix(getTerminalSize): return default size if cols and rows are 0

The issue is that `getTerminalSize` can return 0x0 even when ioctl(TIOCGWINSZ) succeeds. The ghostty terminal library panics when initialized with zero dimensions.

The fix in `getTerminalSize` now validates that both rows and cols are positive before using them, otherwise falling back to the default 24x80.

Closes: https://github.com/neurosnap/zmx/issues/25
Ordo  ·  2025-12-10
add zsh shell prompt doc (#24)

Eric Bower  ·  2025-12-10
fix: flake.nix only supports linux atm

`ghostty` does not support packaging nix on mac which I think means we will not be able to support it:

- https://github.com/ghostty-org/ghostty/blob/cf06417b7dfbd0daeb58a9143f9b6ee194cbce26/nix/package.nix#L49
- https://github.com/ghostty-org/ghostty#2824

When our flake tries to build on macOS, it pulls ghostty as a Zig dependency, and ghostty's build.zig calls into apple-sdk/build.zig which needs the Darwin SDK — but ghostty's Nix infrastructure hasn't set that up.
Eric Bower  ·  2025-12-10
feat: homebrew install
Eric Bower  ·  2025-12-09
docs: changelog spec ref
Eric Bower  ·  2025-12-09
docs: update binaries
Eric Bower  ·  2025-12-09
chore: v0.1.0
Eric Bower  ·  2025-12-09
docs: readme
Eric Bower  ·  2025-12-09
docs: readme
Eric Bower  ·  2025-12-09
docs: added tmux to comparison table
Eric Bower  ·  2025-12-09
fix: remove mac flag
Jesse Farebrother  ·  2025-12-05
feat: add version command
a65a2b0
(version-cmd)
Jesse Farebrother  ·  2025-12-05
chore: build.zig version build option
Eric Bower  ·  2025-12-05
refactor!: postfix uid for socket dir

- first we look for TMPDIR env var or else we default to `/tmp`
- then we look for ZMX_DIR env var or else we create a `/tmp/zmx-{uid}` folder

Since this is a breaking change, you can kill all of your previous
sessions by using `ZMX_DIR=/tmp/zmx zmx kill {sesh}`

Closes: https://github.com/neurosnap/zmx/issues/15

BREAKING CHANGE: this moves sockets to a new location which means none
of your previous sessions will be available
Eric Bower  ·  2025-12-08
docs: comparison table
Eric Bower  ·  2025-12-06
docs: index site content
Eric Bower  ·  2025-12-06
feat: docs site
Eric Bower  ·  2025-12-06
fix(attach): cannot attach to session within session

Closes: https://github.com/neurosnap/zmx/issues/20
Eric Bower  ·  2025-12-05
docs(readme): fish prompt file loc
Eric Bower  ·  2025-12-05
chore(release): add shasum files as well
Eric Bower  ·  2025-12-05
fix(release): strip mac metadata
Eric Bower  ·  2025-12-05
fix(release): strip apple xattrs
Eric Bower  ·  2025-12-05
docs: links for binaries
Britton Robitzsch  ·  2025-12-05
add nix flake (#5)

Eric Bower  ·  2025-12-05
fix(macos): release build
Eric Bower  ·  2025-12-05
chore: upload script
Eric Bower  ·  2025-12-05
feat: add release build
Eric Bower  ·  2025-12-05
fix: support kitty keyboard ctrl+\ with event flags

References: https://github.com/neurosnap/zmx/issues/10
Eric Bower  ·  2025-12-05
chore: add kitty graphics protocol example script
Eric Bower  ·  2025-12-03
refactor: when daemon is unresponsive, remove it

This commit does a better job removing a unix socket file when
the daemon is unresponsive, likely indicating that the process has
already been killed.

We accomplish this by creating a probSession fn that will connect to the
unix socket, send the Info event, and timeout after 1 second.  After
that point we remove the unix socket file.
Eric Bower  ·  2025-12-03
fix: send SIGTERM to pty child process before shutting down

The issue was that when `zmx kill` was called, the daemon would call `shutdown()` and exit the loop, but then get stuck at `waitpid(daemon.pid, 0)` waiting for the PTY child shell to exit. Since the shell was never killed, the daemon would hang forever, preventing the defer block from running that deletes the socket file.

The fix sends SIGTERM to the PTY child process before shutting down, allowing the shell to exit gracefully so waitpid returns and the socket cleanup defer runs.

References: https://github.com/neurosnap/zmx/issues/10
Eric Bower  ·  2025-12-03
fix: print help text with unknown commands

We should print the help text when possible and refrain from using the
logger when the user provides invalid input since that gets sent to our
log files.

Closes: https://github.com/neurosnap/zmx/issues/11
Eric Bower  ·  2025-12-03
fix: ghostty should not send color pallet on re-attach

When `zmx` starts a session, `ghostty-vt.Terminal.init()` uses Ghostty's default palette, not foot's palette. On reattach, it dumps all 256 colors from ghostty's defaults, which may differ from foot's defaults.

References: https://github.com/neurosnap/zmx/issues/7
Eric Bower  ·  2025-12-03
feat: increase max scrollback

Closes: https://github.com/neurosnap/zmx/issues/9
Daniel Wennberg  ·  2025-12-03
Bump minimum Zig version (#6)

Eric Bower  ·  2025-12-02
fix: dont send ghostty snapshot on first attach

chore: clear main screen and set cursor to home on first attach
Eric Bower  ·  2025-12-02
fix: dont use alt screen on attach

The problem is that the client switches to the alternate screen buffer on attach, which doesn't have scrollback. When ghostty replays content, it goes into the alt screen where scrollback doesn't accumulate.

The alternate screen buffer (\x1b[?1049h) never has scrollback - that's by design in terminals.

To get scrollback working properly we now stay on main screen and clear on detach.
Eric Bower  ·  2025-12-02
docs: readme
Eric Bower  ·  2025-12-02
docs: readme
Eric Bower  ·  2025-12-01
docs: readme
Eric Bower  ·  2025-12-01
docs: readme
Eric Bower  ·  2025-12-01
docs: readme
Eric Bower  ·  2025-12-01
docs: readme
Eric Bower  ·  2025-12-01
docs: readme
Eric Bower  ·  2025-11-29
chore: license
Eric Bower  ·  2025-11-29
docs: readme
Eric Bower  ·  2025-11-29
chore: logo
Eric Bower  ·  2025-11-29
chore: logo
Eric Bower  ·  2025-11-29
docs: logo
Eric Bower  ·  2025-11-29
chore: update logo
Eric Bower  ·  2025-11-29
docs: logo
Eric Bower  ·  2025-11-29
docs: readme
Eric Bower  ·  2025-11-28
docs: readme
Eric Bower  ·  2025-11-28
feat: shorthand aliases for commands
Eric Bower  ·  2025-11-28
docs: readme
7f18a23
(v0.0.1)
Eric Bower  ·  2025-11-28
docs: readme
Eric Bower  ·  2025-11-27
feat: render terminal snapshot to client on re-attach

This change uses libghostty-vt to restore the previous state of the
terminal when a client re-attaches to a session.

How it works:

- user creates session `zmx attach term`
- user interacts with terminal stdin
- stdin gets sent to pty via daemon
- daemon sends pty output to client *and* ghostty-vt
- ghostty-vt holds terminal state and scrollback
- user disconnects
- user re-attaches to session
- ghostty-vt sends terminal snapshot to client stdout

In 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.
Eric Bower  ·  2025-11-27
chore: cleanup build files
Eric Bower  ·  2025-11-27
docs: clarify detach cmd
Eric Bower  ·  2025-11-26
chore: move code around
Eric Bower  ·  2025-11-26
docs: readme
Eric Bower  ·  2025-11-26
docs: readme
Eric Bower  ·  2025-11-26
fix: discard any unread input when restoring terminal settings on detach
Eric Bower  ·  2025-11-26
feat: handle resize events
Eric Bower  ·  2025-11-26
fix: free socket_path and the clients array backing storage before the daemon process exits
Eric Bower  ·  2025-11-26
fix: close the server socket and delete the socket file if spawnPty fails
Eric Bower  ·  2025-11-26
fix: close the PTY master fd when the daemon exits
Eric Bower  ·  2025-11-26
fix: client socket fd ensure the socket is closed when the function exits
Eric Bower  ·  2025-11-26
fix: ensure the fd is closed if initUnix, bind, or listen fails
Eric Bower  ·  2025-11-26
fix: reap the pty child process and prevent zombies
Eric Bower  ·  2025-11-26
fix: use relative path since we are already inside the socket_dir
Eric Bower  ·  2025-11-26
fix: properly allocate NUL-terminated copies of each argument

the child process will exec (replacing memory) or exit, so no cleanup needed.
Eric Bower  ·  2025-11-26
feat: provide attach with a command
Eric Bower  ·  2025-11-26
docs: readme
Eric Bower  ·  2025-11-26
docs(readme): shell prompt section
Eric Bower  ·  2025-11-26
refactor: cleanup logs
Eric Bower  ·  2025-11-26
feat: on re-attach send SIGWINCH and in-band resize event
Eric Bower  ·  2025-11-26
feat: send logs to a file with rotation
Eric Bower  ·  2025-11-25
fix: cpu spike from unhandled events in our daemon and client loops
Eric Bower  ·  2025-11-24
docs: readme
Eric Bower  ·  2025-11-24
feat: ctrl+\ detach client
Eric Bower  ·  2025-11-23
docs: readme
Eric Bower  ·  2025-11-23
feat: list command
Eric Bower  ·  2025-11-23
chore(docs): readme
Eric Bower  ·  2025-11-23
daemon-per-session (#4)

This is a ground-up rewrite of this tool.  We have dramatically simplified how this tool works.  Instead of having a single daemon that manages all sessions, we have a daemon-per-session.

We employ a simple architecture: `fork() -> setsid() on child -> forkpty() -> attach on parent`

This has a bunch of benefits, one of which is we can safely remove libxev and use simple `poll()` loops for daemon and clients.
Eric Bower  ·  2025-10-16
feat: attach will start in the cwd of client
Eric Bower  ·  2025-10-16
shore: fix cursor error
Eric Bower  ·  2025-10-15
fix: filter our client input queries from echoing to stdout
Eric Bower  ·  2025-10-15
chore: centralize winsize change logic
Eric Bower  ·  2025-10-15
feat: in-band resize window events

https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83
Eric Bower  ·  2025-10-15
fix: flush buffer so prompt always shows after cmd
Eric Bower  ·  2025-10-15
fix: properly clear terminal after detach
Eric Bower  ·  2025-10-15
fix: reattach rendering
Eric Bower  ·  2025-10-15
fix: restore errors
Eric Bower  ·  2025-10-15
docs: render
Eric Bower  ·  2025-10-15
chore: tests
Eric Bower  ·  2025-10-15
feat: rendering colors on reattach
Eric Bower  ·  2025-10-15
chore: cursor position and escape sequences
Eric Bower  ·  2025-10-15
chore: Wide/Double-Width Character Handling
Eric Bower  ·  2025-10-15
refactor: move render terminal snapshot to separate module
Eric Bower  ·  2025-10-15
chore(reattach): row iteration
Eric Bower  ·  2025-10-15
chore: checkpoint
Eric Bower  ·  2025-10-14
fix: need clrf
Eric Bower  ·  2025-10-14
perf: increase buffer sizes
Eric Bower  ·  2025-10-14
chore: fmt
Eric Bower  ·  2025-10-14
feat: restore terminal state
Eric Bower  ·  2025-10-14
feat: restore modes
Eric Bower  ·  2025-10-14
feat: restore terminal modes
Eric Bower  ·  2025-10-14
chore: reset
Eric Bower  ·  2025-10-14
refactor: renderTerminalSnapshot
Eric Bower  ·  2025-10-14
refactor: renderTerminalSnapshot
Eric Bower  ·  2025-10-14
docs: fmt
Eric Bower  ·  2025-10-14
refactor: renderTerminalSnapshot
Eric Bower  ·  2025-10-13
refactor: increase pty buffer sizes

This improves performance when writing to ghostty-vt
Eric Bower  ·  2025-10-13
chore: remove pty json handling
Eric Bower  ·  2025-10-13
chore: suppress ghostty-vt warnings
Eric Bower  ·  2025-10-13
refactor: remove sanitized buffer for now
Eric Bower  ·  2025-10-13
refactor(daemon): we dont need to buffer pty read for partial utf8

ghostty already handles this for us.
Eric Bower  ·  2025-10-13
fix: replace to device attribute queries within daemon
Eric Bower  ·  2025-10-13
chore: rm scrollback buffer

ghostty handles this for us
Eric Bower  ·  2025-10-13
refactor: revert env var
Eric Bower  ·  2025-10-13
chore: forward more env vars from daemon to children ptys
Eric Bower  ·  2025-10-13
fix: forward TERM to pty
Eric Bower  ·  2025-10-13
fix: change detach key to ctrl+b d like tmux
Eric Bower  ·  2025-10-13
fix: move DA query handling to proper location
Eric Bower  ·  2025-10-13
fix(attach): when daemon shuts down don't print EOF msg in attach
Eric Bower  ·  2025-10-13
fix(attach): carriage return required in raw mode
Eric Bower  ·  2025-10-13
fix: respond to Device Attribute queries to prevent fish shell warning

Fish shell sends a Primary Device Attribute (DA1) query (ESC [ c) when
it starts to detect terminal capabilities. Previously, zmx forwarded
this query to the PTY but never sent a response back to the client,
causing fish to wait 2 seconds and display a terminal compatibility
warning.

This commit intercepts DA queries in handlePtyInput() before they reach
the PTY and responds directly to the client with VT220 capabilities:
- Primary DA (ESC [ c): VT220 with color (22) and clipboard (52)
- Secondary DA (ESC [ > c): Terminal version info

The response format matches ghostty's deviceAttributes implementation.

Fixes the "fish could not read response to Primary Device Attribute
query" warning on first attach.
Eric Bower  ·  2025-10-13
fix(attach): use-after-free bug
Eric Bower  ·  2025-10-13
docs(protocol): pty binary format
Eric Bower  ·  2025-10-13
fix(attach): pty_out needs to use binary protocol
Eric Bower  ·  2025-10-13
refactor(attach): use xev.Stream for stdout writes
Eric Bower  ·  2025-10-12
refactor: use binary format for pty_in and pty_out
Eric Bower  ·  2025-10-12
fix: vt.resize on attach

fix: filter out characters ghostty cannot handle
Eric Bower  ·  2025-10-12
refactor: resize ghostty term on attach
Tim Culverhouse  ·  2025-10-13
support macOS and FreeBSD PTY functions (#1)

Replace Linux-specific PTY headers with platform-specific imports
to enable cross-platform compatibility. On macOS, use util.h for
openpty(). On FreeBSD, use libutil.h for openpty(). On Linux and
other platforms, continue using pty.h.

Remove unused utmp.h include that was never referenced in the code.

This change allows the daemon to compile and run on macOS and
FreeBSD in addition to Linux without requiring platform-specific
preprocessor directives or build configurations.

Amp-Thread-ID: https://ampcode.com/threads/T-15fd58fa-5673-40c5-87e1-3ea0057599ca

Co-authored-by: Amp <amp@ampcode.com>
Eric Bower  ·  2025-10-11
refactor: move detach key to Config struct
Eric Bower  ·  2025-10-11
fix: segfault on daemon after existing pty
Eric Bower  ·  2025-10-11
fix: memory leak
Eric Bower  ·  2025-10-11
refactor: use new protocol module
Eric Bower  ·  2025-10-11
refactor: move unix ipc protocol to separate module
Eric Bower  ·  2025-10-11
refactor: test
Eric Bower  ·  2025-10-11
fix: use xev for pty writes
Eric Bower  ·  2025-10-11
chore: pass config struct to all subcommands
Eric Bower  ·  2025-10-11
feat: config toml
Eric Bower  ·  2025-10-11
feat: systemd unit file

docs: agents+readme
Eric Bower  ·  2025-10-10
fix: detach shortcut
Eric Bower  ·  2025-10-10
fix: alt-screen
Eric Bower  ·  2025-10-10
fix: more cleanup
Eric Bower  ·  2025-10-10
feat: version
Eric Bower  ·  2025-10-10
fix: allow zig optimization

fix: help text
Eric Bower  ·  2025-10-10
fix: initial attach clear terminal
Eric Bower  ·  2025-10-10
feat: terminal restore works!
Eric Bower  ·  2025-10-10
fix: restore session
Eric Bower  ·  2025-10-10
fix: ghostty impl
Eric Bower  ·  2025-10-10
docs: readme
Eric Bower  ·  2025-10-10
docs: readme
Eric Bower  ·  2025-10-10
docs: readme
Eric Bower  ·  2025-10-10
feat: init