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
+5,
-0
| ... | ... | @@ -4,6 +4,11 @@ Use spec: https://common-changelog.org/ | |
| 4 | 4 | ||
| 5 | 5 | ## Staged | |
| 6 | 6 | ||
| 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 | + | ||
| 7 | 12 | ### Fixed | |
| 8 | 13 | ||
| 9 | 14 | - `zmx list` will send "no sessions found" to stderr instead of stdout |
+1,
-1
| ... | ... | @@ -13,7 +13,7 @@ ENV PATH=/usr/local/zig:$PATH | |
| 13 | 13 | ||
| 14 | 14 | WORKDIR /app | |
| 15 | 15 | ||
| 16 | - | COPY build.zig build.zig.zon src/ /app/ | |
| 16 | + | COPY . /app/ | |
| 17 | 17 | ||
| 18 | 18 | RUN zig build | |
| 19 | 19 |
+1,
-1
| ... | ... | @@ -73,7 +73,7 @@ Commands: | |
| 73 | 73 | [r]un <name> [command...] Send command without attaching, creating session if needed | |
| 74 | 74 | [d]etach Detach all clients from current session (ctrl+\ for current client) | |
| 75 | 75 | [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 | |
| 77 | 77 | [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences) | |
| 78 | 78 | [w]ait <name>... Wait for session tasks to complete | |
| 79 | 79 | [c]ompletions <shell> Completion scripts for shell integration (bash, zsh, or fish) |
M
pico.sh
+3,
-5
| ... | ... | @@ -6,14 +6,12 @@ set -xeo pipefail | |
| 6 | 6 | export ZMX_SESSION_PREFIX="ci-" | |
| 7 | 7 | ||
| 8 | 8 | zmx run build podman build -t zig . | |
| 9 | - | zmx wait build | |
| 9 | + | zmx wait | |
| 10 | 10 | ||
| 11 | 11 | zmx run fmt podman run --rm -it -v "$(pwd)":/app zig zig fmt --check . | |
| 12 | 12 | zmx run test podman run --rm -it -v "$(pwd)":/app zig zig build test --summary all | |
| 13 | - | zmx wait fmt test | |
| 13 | + | zmx wait | |
| 14 | 14 | ||
| 15 | - | zmx kill build | |
| 16 | - | zmx kill fmt | |
| 17 | - | zmx kill test | |
| 15 | + | zmx kill | |
| 18 | 16 | ||
| 19 | 17 | echo "success!" |
+60,
-8
| ... | ... | @@ -75,11 +75,6 @@ pub fn main() !void { | |
| 75 | 75 | return printCompletions(shell); | |
| 76 | 76 | } else if (std.mem.eql(u8, cmd, "detach") or std.mem.eql(u8, cmd, "d")) { | |
| 77 | 77 | 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); | |
| 83 | 78 | } else if (std.mem.eql(u8, cmd, "history") or std.mem.eql(u8, cmd, "hi")) { | |
| 84 | 79 | var session_name: ?[]const u8 = null; | |
| 85 | 80 | var format: util.HistoryFormat = .plain; |
| ... | ... | @@ -169,6 +164,53 @@ pub fn main() !void { | |
| 169 | 164 | }; | |
| 170 | 165 | std.log.info("socket path={s}", .{daemon.socket_path}); | |
| 171 | 166 | 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; | |
| 172 | 214 | } else if (std.mem.eql(u8, cmd, "wait") or std.mem.eql(u8, cmd, "w")) { | |
| 173 | 215 | var args_raw: std.ArrayList([]const u8) = .empty; | |
| 174 | 216 | defer { |
| ... | ... | @@ -181,6 +223,15 @@ pub fn main() !void { | |
| 181 | 223 | const sesh = try socket.getSeshName(alloc, session_name); | |
| 182 | 224 | try args_raw.append(alloc, sesh); | |
| 183 | 225 | } | |
| 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 | + | } | |
| 184 | 235 | return wait(&cfg, args_raw); | |
| 185 | 236 | } else { | |
| 186 | 237 | return help(); |
| ... | ... | @@ -760,7 +811,7 @@ fn help() !void { | |
| 760 | 811 | \\ [r]un <name> [command...] Send command without attaching, creating session if needed | |
| 761 | 812 | \\ [d]etach Detach all clients from current session (ctrl+\ for current client) | |
| 762 | 813 | \\ [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 | |
| 764 | 815 | \\ [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences) | |
| 765 | 816 | \\ [w]ait <name>... Wait for session tasks to complete | |
| 766 | 817 | \\ [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 { | |
| 891 | 942 | } | |
| 892 | 943 | } | |
| 893 | 944 | ||
| 894 | - | std.Thread.sleep(1000 * std.time.ns_per_ms); | |
| 945 | + | std.Thread.sleep(3000 * std.time.ns_per_ms); | |
| 895 | 946 | } | |
| 896 | 947 | } | |
| 897 | 948 |
| ... | ... | @@ -1004,13 +1055,14 @@ fn kill(cfg: *Cfg, session_name: []const u8) !void { | |
| 1004 | 1055 | w.interface.flush() catch {}; | |
| 1005 | 1056 | return; | |
| 1006 | 1057 | }; | |
| 1058 | + | ||
| 1007 | 1059 | defer posix.close(result.fd); | |
| 1008 | 1060 | ipc.send(result.fd, .Kill, "") catch |err| switch (err) { | |
| 1009 | 1061 | error.BrokenPipe, error.ConnectionResetByPeer => return, | |
| 1010 | 1062 | else => return err, | |
| 1011 | 1063 | }; | |
| 1012 | 1064 | ||
| 1013 | - | var buf: [4096]u8 = undefined; | |
| 1065 | + | var buf: [100]u8 = undefined; | |
| 1014 | 1066 | var w = std.fs.File.stdout().writer(&buf); | |
| 1015 | 1067 | try w.interface.print("killed session {s}\n", .{session_name}); | |
| 1016 | 1068 | try w.interface.flush(); |
+2,
-2
| ... | ... | @@ -1,7 +1,7 @@ | |
| 1 | 1 | const std = @import("std"); | |
| 2 | 2 | const posix = std.posix; | |
| 3 | 3 | ||
| 4 | - | pub fn seshPrefix() []const u8 { | |
| 4 | + | pub fn getSeshPrefix() []const u8 { | |
| 5 | 5 | return std.posix.getenv("ZMX_SESSION_PREFIX") orelse ""; | |
| 6 | 6 | } | |
| 7 | 7 |
| ... | ... | @@ -10,7 +10,7 @@ pub fn getSeshNameFromEnv() []const u8 { | |
| 10 | 10 | } | |
| 11 | 11 | ||
| 12 | 12 | pub fn getSeshName(alloc: std.mem.Allocator, sesh: []const u8) ![]const u8 { | |
| 13 | - | const prefix = seshPrefix(); | |
| 13 | + | const prefix = getSeshPrefix(); | |
| 14 | 14 | if (prefix.len == 0 and sesh.len == 0) { | |
| 15 | 15 | return error.SessionNameRequired; | |
| 16 | 16 | } |