repos / zmx

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

commit
98fa966
parent
fac9795
author
Eric Bower
date
2026-03-23 22:49:44 -0400 EDT
feat(kill): accepts multiple args and matches session prefixes

Examples:
- zmx kill one
- zmx kill one two three
- zmx kill d.
- ZMX_SESSION_PREFIX="d." zmx kill
7 files changed,  +74, -17
M .dockerignore
+2, -0
1@@ -1,2 +1,4 @@
2 .git
3 zig-out
4+.zig-cache
5+.jj
M CHANGELOG.md
+5, -0
 1@@ -4,6 +4,11 @@ Use spec: https://common-changelog.org/
 2 
 3 ## Staged
 4 
 5+### Changed
 6+
 7+- `zmx kill` now supports multiple args and it will kill sessions that match a prefix
 8+  - e.g. `zmx kill d.` will kill all sessions that match that prefix
 9+
10 ### Fixed
11 
12 - `zmx list` will send "no sessions found" to stderr instead of stdout
M Dockerfile
+1, -1
1@@ -13,7 +13,7 @@ ENV PATH=/usr/local/zig:$PATH
2 
3 WORKDIR /app
4 
5-COPY build.zig build.zig.zon src/ /app/
6+COPY . /app/
7 
8 RUN zig build
9 
M README.md
+1, -1
1@@ -73,7 +73,7 @@ Commands:
2   [r]un <name> [command...]      Send command without attaching, creating session if needed
3   [d]etach                       Detach all clients from current session  (ctrl+\ for current client)
4   [l]ist [--short]               List active sessions
5-  [k]ill <name>                  Kill a session and all attached clients
6+  [k]ill <name>...               Kill a session and all attached clients
7   [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences)
8   [w]ait <name>...               Wait for session tasks to complete
9   [c]ompletions <shell>          Completion scripts for shell integration (bash, zsh, or fish)
M pico.sh
+3, -5
 1@@ -6,14 +6,12 @@ set -xeo pipefail
 2 export ZMX_SESSION_PREFIX="ci-"
 3 
 4 zmx run build podman build -t zig .
 5-zmx wait build
 6+zmx wait
 7 
 8 zmx run fmt podman run --rm -it -v "$(pwd)":/app zig zig fmt --check .
 9 zmx run test podman run --rm -it -v "$(pwd)":/app zig zig build test --summary all
10-zmx wait fmt test
11+zmx wait
12 
13-zmx kill build
14-zmx kill fmt
15-zmx kill test
16+zmx kill
17 
18 echo "success!"
M src/main.zig
+60, -8
  1@@ -75,11 +75,6 @@ pub fn main() !void {
  2         return printCompletions(shell);
  3     } else if (std.mem.eql(u8, cmd, "detach") or std.mem.eql(u8, cmd, "d")) {
  4         return detachAll(&cfg);
  5-    } else if (std.mem.eql(u8, cmd, "kill") or std.mem.eql(u8, cmd, "k")) {
  6-        const session_name = args.next() orelse "";
  7-        const sesh = try socket.getSeshName(alloc, session_name);
  8-        defer alloc.free(sesh);
  9-        return kill(&cfg, sesh);
 10     } else if (std.mem.eql(u8, cmd, "history") or std.mem.eql(u8, cmd, "hi")) {
 11         var session_name: ?[]const u8 = null;
 12         var format: util.HistoryFormat = .plain;
 13@@ -169,6 +164,53 @@ pub fn main() !void {
 14         };
 15         std.log.info("socket path={s}", .{daemon.socket_path});
 16         return run(&daemon, cmd_args_raw.items);
 17+    } else if (std.mem.eql(u8, cmd, "kill") or std.mem.eql(u8, cmd, "k")) {
 18+        var stderr_buffer: [1024]u8 = undefined;
 19+        var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
 20+        const stderr = &stderr_writer.interface;
 21+
 22+        var args_raw: std.ArrayList([]const u8) = .empty;
 23+        defer {
 24+            for (args_raw.items) |sesh| {
 25+                alloc.free(sesh);
 26+            }
 27+            args_raw.deinit(alloc);
 28+        }
 29+        while (args.next()) |session_name| {
 30+            const sesh = try socket.getSeshName(alloc, session_name);
 31+            try args_raw.append(alloc, sesh);
 32+        }
 33+        // if no args are provided we assume they want to wait for all sessions matching the
 34+        // prefix.
 35+        if (args_raw.items.len == 0) {
 36+            const prefix = socket.getSeshPrefix();
 37+            if (prefix.len == 0) {
 38+                return error.SessionNameRequired;
 39+            }
 40+            try args_raw.append(alloc, try alloc.dupe(u8, prefix));
 41+        }
 42+        var sessions = try util.get_session_entries(alloc, cfg.socket_dir);
 43+        defer {
 44+            for (sessions.items) |session| {
 45+                session.deinit(alloc);
 46+            }
 47+            sessions.deinit(alloc);
 48+        }
 49+        for (sessions.items) |session| {
 50+            for (args_raw.items) |prefix| {
 51+                if (std.mem.startsWith(u8, session.name, prefix)) {
 52+                    kill(&cfg, session.name) catch |err| {
 53+                        try stderr.print(
 54+                            "failed to kill session={s}: {s}\n",
 55+                            .{ session.name, @errorName(err) },
 56+                        );
 57+                        try stderr.flush();
 58+                    };
 59+                    break;
 60+                }
 61+            }
 62+        }
 63+        return;
 64     } else if (std.mem.eql(u8, cmd, "wait") or std.mem.eql(u8, cmd, "w")) {
 65         var args_raw: std.ArrayList([]const u8) = .empty;
 66         defer {
 67@@ -181,6 +223,15 @@ pub fn main() !void {
 68             const sesh = try socket.getSeshName(alloc, session_name);
 69             try args_raw.append(alloc, sesh);
 70         }
 71+        // if no args are provided we assume they want to wait for all sessions matching the
 72+        // prefix.
 73+        if (args_raw.items.len == 0) {
 74+            const prefix = socket.getSeshPrefix();
 75+            if (prefix.len == 0) {
 76+                return error.SessionNameRequired;
 77+            }
 78+            try args_raw.append(alloc, prefix);
 79+        }
 80         return wait(&cfg, args_raw);
 81     } else {
 82         return help();
 83@@ -760,7 +811,7 @@ fn help() !void {
 84         \\  [r]un <name> [command...]      Send command without attaching, creating session if needed
 85         \\  [d]etach                       Detach all clients from current session (ctrl+\ for current client)
 86         \\  [l]ist [--short]               List active sessions
 87-        \\  [k]ill <name>                  Kill a session and all attached clients
 88+        \\  [k]ill <name>...               Kill a session and all attached clients
 89         \\  [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences)
 90         \\  [w]ait <name>...               Wait for session tasks to complete
 91         \\  [c]ompletions <shell>          Completion scripts for shell integration (bash, zsh, or fish)
 92@@ -891,7 +942,7 @@ fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void {
 93             }
 94         }
 95 
 96-        std.Thread.sleep(1000 * std.time.ns_per_ms);
 97+        std.Thread.sleep(3000 * std.time.ns_per_ms);
 98     }
 99 }
100 
101@@ -1004,13 +1055,14 @@ fn kill(cfg: *Cfg, session_name: []const u8) !void {
102         w.interface.flush() catch {};
103         return;
104     };
105+
106     defer posix.close(result.fd);
107     ipc.send(result.fd, .Kill, "") catch |err| switch (err) {
108         error.BrokenPipe, error.ConnectionResetByPeer => return,
109         else => return err,
110     };
111 
112-    var buf: [4096]u8 = undefined;
113+    var buf: [100]u8 = undefined;
114     var w = std.fs.File.stdout().writer(&buf);
115     try w.interface.print("killed session {s}\n", .{session_name});
116     try w.interface.flush();
M src/socket.zig
+2, -2
 1@@ -1,7 +1,7 @@
 2 const std = @import("std");
 3 const posix = std.posix;
 4 
 5-pub fn seshPrefix() []const u8 {
 6+pub fn getSeshPrefix() []const u8 {
 7     return std.posix.getenv("ZMX_SESSION_PREFIX") orelse "";
 8 }
 9 
10@@ -10,7 +10,7 @@ pub fn getSeshNameFromEnv() []const u8 {
11 }
12 
13 pub fn getSeshName(alloc: std.mem.Allocator, sesh: []const u8) ![]const u8 {
14-    const prefix = seshPrefix();
15+    const prefix = getSeshPrefix();
16     if (prefix.len == 0 and sesh.len == 0) {
17         return error.SessionNameRequired;
18     }