Commit 98fa966

Eric Bower  ·  2026-03-23 22:49:44 -0400 EDT
parent fac9795
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
+2, -0
......@@ -1,2 +1,4 @@
11 .git
22 zig-out
3+.zig-cache
4+.jj
+5, -0
......@@ -4,6 +4,11 @@ Use spec: https://common-changelog.org/
44
55 ## Staged
66
7+### Changed
8+
9+- `zmx kill` now supports multiple args and it will kill sessions that match a prefix
10+ - e.g. `zmx kill d.` will kill all sessions that match that prefix
11+
712 ### Fixed
813
914 - `zmx list` will send "no sessions found" to stderr instead of stdout
+1, -1
......@@ -13,7 +13,7 @@ ENV PATH=/usr/local/zig:$PATH
1313
1414 WORKDIR /app
1515
16-COPY build.zig build.zig.zon src/ /app/
16+COPY . /app/
1717
1818 RUN zig build
1919
+1, -1
......@@ -73,7 +73,7 @@ Commands:
7373 [r]un <name> [command...] Send command without attaching, creating session if needed
7474 [d]etach Detach all clients from current session (ctrl+\ for current client)
7575 [l]ist [--short] List active sessions
76- [k]ill <name> Kill a session and all attached clients
76+ [k]ill <name>... Kill a session and all attached clients
7777 [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences)
7878 [w]ait <name>... Wait for session tasks to complete
7979 [c]ompletions <shell> Completion scripts for shell integration (bash, zsh, or fish)
+3, -5
......@@ -6,14 +6,12 @@ set -xeo pipefail
66 export ZMX_SESSION_PREFIX="ci-"
77
88 zmx run build podman build -t zig .
9-zmx wait build
9+zmx wait
1010
1111 zmx run fmt podman run --rm -it -v "$(pwd)":/app zig zig fmt --check .
1212 zmx run test podman run --rm -it -v "$(pwd)":/app zig zig build test --summary all
13-zmx wait fmt test
13+zmx wait
1414
15-zmx kill build
16-zmx kill fmt
17-zmx kill test
15+zmx kill
1816
1917 echo "success!"
+60, -8
......@@ -75,11 +75,6 @@ pub fn main() !void {
7575 return printCompletions(shell);
7676 } else if (std.mem.eql(u8, cmd, "detach") or std.mem.eql(u8, cmd, "d")) {
7777 return detachAll(&cfg);
78- } else if (std.mem.eql(u8, cmd, "kill") or std.mem.eql(u8, cmd, "k")) {
79- const session_name = args.next() orelse "";
80- const sesh = try socket.getSeshName(alloc, session_name);
81- defer alloc.free(sesh);
82- return kill(&cfg, sesh);
8378 } else if (std.mem.eql(u8, cmd, "history") or std.mem.eql(u8, cmd, "hi")) {
8479 var session_name: ?[]const u8 = null;
8580 var format: util.HistoryFormat = .plain;
......@@ -169,6 +164,53 @@ pub fn main() !void {
169164 };
170165 std.log.info("socket path={s}", .{daemon.socket_path});
171166 return run(&daemon, cmd_args_raw.items);
167+ } else if (std.mem.eql(u8, cmd, "kill") or std.mem.eql(u8, cmd, "k")) {
168+ var stderr_buffer: [1024]u8 = undefined;
169+ var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
170+ const stderr = &stderr_writer.interface;
171+
172+ var args_raw: std.ArrayList([]const u8) = .empty;
173+ defer {
174+ for (args_raw.items) |sesh| {
175+ alloc.free(sesh);
176+ }
177+ args_raw.deinit(alloc);
178+ }
179+ while (args.next()) |session_name| {
180+ const sesh = try socket.getSeshName(alloc, session_name);
181+ try args_raw.append(alloc, sesh);
182+ }
183+ // if no args are provided we assume they want to wait for all sessions matching the
184+ // prefix.
185+ if (args_raw.items.len == 0) {
186+ const prefix = socket.getSeshPrefix();
187+ if (prefix.len == 0) {
188+ return error.SessionNameRequired;
189+ }
190+ try args_raw.append(alloc, try alloc.dupe(u8, prefix));
191+ }
192+ var sessions = try util.get_session_entries(alloc, cfg.socket_dir);
193+ defer {
194+ for (sessions.items) |session| {
195+ session.deinit(alloc);
196+ }
197+ sessions.deinit(alloc);
198+ }
199+ for (sessions.items) |session| {
200+ for (args_raw.items) |prefix| {
201+ if (std.mem.startsWith(u8, session.name, prefix)) {
202+ kill(&cfg, session.name) catch |err| {
203+ try stderr.print(
204+ "failed to kill session={s}: {s}\n",
205+ .{ session.name, @errorName(err) },
206+ );
207+ try stderr.flush();
208+ };
209+ break;
210+ }
211+ }
212+ }
213+ return;
172214 } else if (std.mem.eql(u8, cmd, "wait") or std.mem.eql(u8, cmd, "w")) {
173215 var args_raw: std.ArrayList([]const u8) = .empty;
174216 defer {
......@@ -181,6 +223,15 @@ pub fn main() !void {
181223 const sesh = try socket.getSeshName(alloc, session_name);
182224 try args_raw.append(alloc, sesh);
183225 }
226+ // if no args are provided we assume they want to wait for all sessions matching the
227+ // prefix.
228+ if (args_raw.items.len == 0) {
229+ const prefix = socket.getSeshPrefix();
230+ if (prefix.len == 0) {
231+ return error.SessionNameRequired;
232+ }
233+ try args_raw.append(alloc, prefix);
234+ }
184235 return wait(&cfg, args_raw);
185236 } else {
186237 return help();
......@@ -760,7 +811,7 @@ fn help() !void {
760811 \\ [r]un <name> [command...] Send command without attaching, creating session if needed
761812 \\ [d]etach Detach all clients from current session (ctrl+\ for current client)
762813 \\ [l]ist [--short] List active sessions
763- \\ [k]ill <name> Kill a session and all attached clients
814+ \\ [k]ill <name>... Kill a session and all attached clients
764815 \\ [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences)
765816 \\ [w]ait <name>... Wait for session tasks to complete
766817 \\ [c]ompletions <shell> Completion scripts for shell integration (bash, zsh, or fish)
......@@ -891,7 +942,7 @@ fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void {
891942 }
892943 }
893944
894- std.Thread.sleep(1000 * std.time.ns_per_ms);
945+ std.Thread.sleep(3000 * std.time.ns_per_ms);
895946 }
896947 }
897948
......@@ -1004,13 +1055,14 @@ fn kill(cfg: *Cfg, session_name: []const u8) !void {
10041055 w.interface.flush() catch {};
10051056 return;
10061057 };
1058+
10071059 defer posix.close(result.fd);
10081060 ipc.send(result.fd, .Kill, "") catch |err| switch (err) {
10091061 error.BrokenPipe, error.ConnectionResetByPeer => return,
10101062 else => return err,
10111063 };
10121064
1013- var buf: [4096]u8 = undefined;
1065+ var buf: [100]u8 = undefined;
10141066 var w = std.fs.File.stdout().writer(&buf);
10151067 try w.interface.print("killed session {s}\n", .{session_name});
10161068 try w.interface.flush();
+2, -2
......@@ -1,7 +1,7 @@
11 const std = @import("std");
22 const posix = std.posix;
33
4-pub fn seshPrefix() []const u8 {
4+pub fn getSeshPrefix() []const u8 {
55 return std.posix.getenv("ZMX_SESSION_PREFIX") orelse "";
66 }
77
......@@ -10,7 +10,7 @@ pub fn getSeshNameFromEnv() []const u8 {
1010 }
1111
1212 pub fn getSeshName(alloc: std.mem.Allocator, sesh: []const u8) ![]const u8 {
13- const prefix = seshPrefix();
13+ const prefix = getSeshPrefix();
1414 if (prefix.len == 0 and sesh.len == 0) {
1515 return error.SessionNameRequired;
1616 }