- commit
- fd0874e
- parent
- 15c4a9c
- author
- Eric Bower
- date
- 2026-02-19 15:32:39 -0500 EST
feat: wait command This command will wait for all tasks to be completed and return an "aggregate" exit code.
3 files changed,
+122,
-44
+1,
-0
1@@ -66,6 +66,7 @@ Commands:
2 [c]ompletions <shell> Completion scripts for shell integration (bash, zsh, or fish)
3 [k]ill <name> Kill a session and all attached clients
4 [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences)
5+ [w]ait <name>... Wait for session tasks to complete
6 [v]ersion Show version information
7 [h]elp Show this help message
8 ```
+1,
-1
1@@ -37,7 +37,7 @@ pub const Info = extern struct {
2 cwd: [MAX_CWD_LEN]u8,
3 created_at: u64,
4 task_ended_at: u64,
5- task_exit_code: i32,
6+ task_exit_code: u8,
7 };
8
9 pub fn expectedLength(data: []const u8) ?usize {
+120,
-43
1@@ -142,7 +142,7 @@ const Daemon = struct {
2 has_had_client: bool = false,
3 created_at: u64, // unix timestamp (ns)
4 is_task_mode: bool = false, // flag for when session is run as a task
5- task_exit_code: ?i32 = null, // null = running or n/a, set when task completes
6+ task_exit_code: ?u8 = null, // null = running or n/a, set when task completes
7 task_ended_at: ?u64 = null, // timestamp when task exited
8 task_command: ?[]const []const u8 = null,
9
10@@ -373,9 +373,7 @@ pub fn main() !void {
11 } else if (std.mem.eql(u8, cmd, "detach") or std.mem.eql(u8, cmd, "d")) {
12 return detachAll(&cfg);
13 } else if (std.mem.eql(u8, cmd, "kill") or std.mem.eql(u8, cmd, "k")) {
14- const session_name = args.next() orelse {
15- return error.SessionNameRequired;
16- };
17+ const session_name = args.next() orelse "";
18 const sesh = try getSeshName(alloc, session_name);
19 defer alloc.free(sesh);
20 return kill(&cfg, sesh);
21@@ -391,16 +389,11 @@ pub fn main() !void {
22 session_name = arg;
23 }
24 }
25- if (session_name == null) {
26- return error.SessionNameRequired;
27- }
28 const sesh = try getSeshName(alloc, session_name.?);
29 defer alloc.free(sesh);
30 return history(&cfg, sesh, format);
31 } else if (std.mem.eql(u8, cmd, "attach") or std.mem.eql(u8, cmd, "a")) {
32- const session_name = args.next() orelse {
33- return error.SessionNameRequired;
34- };
35+ const session_name = args.next() orelse "";
36
37 var command_args: std.ArrayList([]const u8) = .empty;
38 defer command_args.deinit(alloc);
39@@ -435,9 +428,7 @@ pub fn main() !void {
40 std.log.info("socket path={s}", .{daemon.socket_path});
41 return attach(&daemon);
42 } else if (std.mem.eql(u8, cmd, "run") or std.mem.eql(u8, cmd, "r")) {
43- const session_name = args.next() orelse {
44- return error.SessionNameRequired;
45- };
46+ const session_name = args.next() orelse "";
47
48 var cmd_args_raw: std.ArrayList([]const u8) = .empty;
49 defer cmd_args_raw.deinit(alloc);
50@@ -480,6 +471,19 @@ pub fn main() !void {
51 daemon.socket_path = try getSocketPath(alloc, cfg.socket_dir, sesh);
52 std.log.info("socket path={s}", .{daemon.socket_path});
53 return run(&daemon, cmd_args.items);
54+ } else if (std.mem.eql(u8, cmd, "wait")) {
55+ var args_raw: std.ArrayList([]const u8) = .empty;
56+ defer {
57+ args_raw.deinit(alloc);
58+ for (args_raw.items) |sesh| {
59+ alloc.free(sesh);
60+ }
61+ }
62+ while (args.next()) |session_name| {
63+ const sesh = try getSeshName(alloc, session_name);
64+ try args_raw.append(alloc, sesh);
65+ }
66+ return wait(&cfg, args_raw);
67 } else {
68 return help();
69 }
70@@ -514,15 +518,24 @@ fn help() !void {
71 \\Usage: zmx <command> [args]
72 \\
73 \\Commands:
74- \\ [a]ttach <name> [command...] Attach to session, creating session if needed
75- \\ [r]un <name> [command...] Send command without attaching, creating session if needed
76- \\ [d]etach Detach all clients from current session (ctrl+\ for current client)
77- \\ [l]ist [--short] List active sessions
78- \\ [c]ompletions <shell> Completion scripts for shell integration (bash, zsh, or fish)
79- \\ [k]ill <name> Kill a session and all attached clients
80+ \\ [a]ttach <name> [command...] Attach to session, creating session if needed
81+ \\ [r]un <name> [command...] Send command without attaching, creating session if needed
82+ \\ [d]etach Detach all clients from current session (ctrl+\ for current client)
83+ \\ [l]ist [--short] List active sessions
84+ \\ [c]ompletions <shell> Completion scripts for shell integration (bash, zsh, or fish)
85+ \\ [k]ill <name> Kill a session and all attached clients
86 \\ [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences)
87- \\ [v]ersion Show version information
88- \\ [h]elp Show this help message
89+ \\ [w]ait <name>... Wait for session tasks to complete
90+ \\ [v]ersion Show version information
91+ \\ [h]elp Show this help message
92+ \\
93+ \\Environment variables:
94+ \\ - SHELL Determines which shell is used when creating a session
95+ \\ - ZMX_DIR Controls which folder is used to store unix socket files (prio: 1)
96+ \\ - XDG_RUNTIME_DIR Controls which folder is used to store unix socket files (prio: 2)
97+ \\ - TMPDIR Controls which folder is used to store unix socket files (prio: 3)
98+ \\ - ZMX_SESSION This variable is injected into every zmx session automatically
99+ \\ - ZMX_SESSION_PREFIX Adds this value to the start of every session name for all commands
100 \\
101 ;
102 var buf: [4096]u8 = undefined;
103@@ -541,41 +554,80 @@ const SessionEntry = struct {
104 cwd: ?[]const u8 = null,
105 created_at: u64,
106 task_ended_at: ?u64,
107- task_exit_code: ?i32,
108+ task_exit_code: ?u8,
109
110 fn lessThan(_: void, a: SessionEntry, b: SessionEntry) bool {
111 return std.mem.order(u8, a.name, b.name) == .lt;
112 }
113 };
114
115-const current_arrow = "→";
116-
117-fn list(cfg: *Cfg, short: bool) !void {
118+fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void {
119 var gpa = std.heap.GeneralPurposeAllocator(.{}){};
120 defer _ = gpa.deinit();
121 const alloc = gpa.allocator();
122
123- const current_session = std.process.getEnvVarOwned(alloc, "ZMX_SESSION") catch |err| switch (err) {
124- error.EnvironmentVariableNotFound => null,
125- else => return err,
126- };
127- defer if (current_session) |name| alloc.free(name);
128+ var stdout_buffer: [1024]u8 = undefined;
129+ var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
130+ const stdout = &stdout_writer.interface;
131
132- var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{ .iterate = true });
133- defer dir.close();
134- var iter = dir.iterate();
135- var buf: [4096]u8 = undefined;
136- var w = std.fs.File.stdout().writer(&buf);
137+ while (true) {
138+ var sessions = try get_session_entries(alloc, cfg);
139+ var total: i32 = 0;
140+ var done: i32 = 0;
141+ var agg_exit_code: u8 = 0;
142
143- var sessions = try std.ArrayList(SessionEntry).initCapacity(alloc, 16);
144- defer {
145 for (sessions.items) |session| {
146- alloc.free(session.name);
147- if (session.cmd) |cmd| alloc.free(cmd);
148- if (session.cwd) |cwd| alloc.free(cwd);
149+ var found = false;
150+ for (session_names.items) |prefix| {
151+ if (std.mem.startsWith(u8, session.name, prefix)) {
152+ found = true;
153+ break;
154+ }
155+ }
156+ if (!found) {
157+ continue;
158+ }
159+
160+ total += 1;
161+ if (session.task_ended_at == 0) {
162+ try stdout.print("still waiting task={s}\n", .{session.name});
163+ try stdout.flush();
164+ continue;
165+ }
166+ if (session.task_exit_code != 0) {
167+ agg_exit_code = session.task_exit_code orelse 0;
168+ }
169+ done += 1;
170 }
171- sessions.deinit(alloc);
172+
173+ session_entries_deinit(alloc, &sessions);
174+
175+ if (total == done) {
176+ try stdout.print("tasks completed!\n", .{});
177+ try stdout.flush();
178+ std.process.exit(agg_exit_code);
179+ return;
180+ }
181+
182+ std.Thread.sleep(1000 * std.time.ns_per_ms);
183+ }
184+}
185+
186+fn session_entries_deinit(alloc: std.mem.Allocator, sessions: *std.ArrayList(SessionEntry)) void {
187+ for (sessions.items) |session| {
188+ alloc.free(session.name);
189+ if (session.cmd) |cmd| alloc.free(cmd);
190+ if (session.cwd) |cwd| alloc.free(cwd);
191 }
192+ sessions.deinit(alloc);
193+}
194+
195+fn get_session_entries(alloc: std.mem.Allocator, cfg: *Cfg) !std.ArrayList(SessionEntry) {
196+ var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{ .iterate = true });
197+ defer dir.close();
198+ var iter = dir.iterate();
199+
200+ var sessions = try std.ArrayList(SessionEntry).initCapacity(alloc, 30);
201
202 while (try iter.next()) |entry| {
203 const exists = sessionExists(dir, entry.name) catch continue;
204@@ -627,6 +679,27 @@ fn list(cfg: *Cfg, short: bool) !void {
205 }
206 }
207
208+ return sessions;
209+}
210+
211+const current_arrow = "→";
212+
213+fn list(cfg: *Cfg, short: bool) !void {
214+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
215+ defer _ = gpa.deinit();
216+ const alloc = gpa.allocator();
217+
218+ const current_session = std.process.getEnvVarOwned(alloc, "ZMX_SESSION") catch |err| switch (err) {
219+ error.EnvironmentVariableNotFound => null,
220+ else => return err,
221+ };
222+ defer if (current_session) |name| alloc.free(name);
223+ var buf: [4096]u8 = undefined;
224+ var w = std.fs.File.stdout().writer(&buf);
225+
226+ var sessions = try get_session_entries(alloc, cfg);
227+ defer session_entries_deinit(alloc, &sessions);
228+
229 if (sessions.items.len == 0) {
230 if (short) return;
231 try w.interface.print("no sessions found in {s}\n", .{cfg.socket_dir});
232@@ -1146,7 +1219,7 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
233 }
234 }
235
236-fn findTaskExitMarker(output: []const u8) ?i32 {
237+fn findTaskExitMarker(output: []const u8) ?u8 {
238 const marker = "ZMX_TASK_COMPLETED:";
239
240 // Search for marker in output
241@@ -1162,7 +1235,7 @@ fn findTaskExitMarker(output: []const u8) ?i32 {
242 const exit_code_str = after_marker[0..end_idx];
243
244 // Parse exit code
245- if (std.fmt.parseInt(i32, exit_code_str, 10)) |exit_code| {
246+ if (std.fmt.parseInt(u8, exit_code_str, 10)) |exit_code| {
247 return exit_code;
248 } else |_| {
249 std.log.warn("failed to parse task exit code from: {s}", .{exit_code_str});
250@@ -1427,6 +1500,10 @@ fn seshPrefix() []const u8 {
251 }
252
253 fn getSeshName(alloc: std.mem.Allocator, sesh: []const u8) ![]const u8 {
254+ const prefix = seshPrefix();
255+ if (std.mem.eql(u8, prefix, "") and std.mem.eql(u8, sesh, "")) {
256+ return error.SessionNameRequired;
257+ }
258 return std.fmt.allocPrint(alloc, "{s}{s}", .{ seshPrefix(), sesh });
259 }
260