- commit
- d3a13c3
- parent
- 3cdcc40
- author
- Eric Bower
- date
- 2025-11-26 15:08:07 -0500 EST
feat: provide attach with a command
2 files changed,
+45,
-12
+10,
-2
1@@ -18,14 +18,22 @@ session persistence for terminal processes
2
3 ## usage
4
5-- `zmx attach {session_name}` - create or attach to a session
6+- `zmx attach {session_name} [command...]` - create or attach to a session, optionally running a command instead of shell
7 - `zmx detach [{session_name}]` (or Ctrl+\\) - detach all connected clients to session, can be used inside session without providing name
8 - `zmx list` - list sessions
9 - `zmx kill {session_name}` kill pty and all clients attached to session
10
11+### examples
12+
13+```bash
14+zmx attach dev # start a shell session
15+zmx attach dev nvim . # start nvim in a persistent session
16+zmx attach build make -j8 # run a build, reattach to check progress
17+zmx attach mux dvtm # run a multiplexer inside zmx
18+```
19+
20 ## todo
21
22-- [ ] Ability to pass a command to attach `zmx attach mux dvtm`
23 - [ ] Integrate with `libghostty` to restore terminal state on re-attach
24 - [ ] Binary distribution (e.g. pkg managers)
25
+35,
-10
1@@ -79,6 +79,7 @@ const Daemon = struct {
2 socket_path: []const u8,
3 running: bool,
4 pid: i32,
5+ command: ?[]const []const u8 = null,
6
7 pub fn deinit(self: *Daemon) void {
8 self.clients.deinit(self.alloc);
9@@ -147,6 +148,13 @@ pub fn main() !void {
10 std.log.err("session name required", .{});
11 return;
12 };
13+
14+ var command_args: std.ArrayList([]const u8) = .empty;
15+ defer command_args.deinit(alloc);
16+ while (args.next()) |arg| {
17+ try command_args.append(alloc, arg);
18+ }
19+
20 const clients = try std.ArrayList(*Client).initCapacity(alloc, 10);
21 var daemon = Daemon{
22 .running = true,
23@@ -156,6 +164,7 @@ pub fn main() !void {
24 .session_name = session_name,
25 .socket_path = undefined,
26 .pid = undefined,
27+ .command = if (command_args.items.len > 0) command_args.items else null,
28 };
29 daemon.socket_path = try getSocketPath(alloc, cfg.socket_dir, session_name);
30 std.log.info("socket path={s}", .{daemon.socket_path});
31@@ -172,11 +181,11 @@ fn help() !void {
32 \\Usage: zmx <command> [args]
33 \\
34 \\Commands:
35- \\ attach <name> Create or attach to a session
36- \\ detach Detach from current session (or Ctrl+\)
37- \\ list List active sessions
38- \\ kill <name> Kill a session and all attached clients
39- \\ help Show this help message
40+ \\ attach <name> [command...] Create or attach to a session
41+ \\ detach Detach from current session (or Ctrl+\)
42+ \\ list List active sessions
43+ \\ kill <name> Kill a session and all attached clients
44+ \\ help Show this help message
45 \\
46 ;
47 try std.fs.File.stdout().writeAll(help_text);
48@@ -315,6 +324,9 @@ fn attach(daemon: *Daemon) !void {
49 };
50 if (fd != -1) {
51 posix.close(fd);
52+ if (daemon.command != null) {
53+ std.log.warn("session already exists, ignoring command session={s}", .{daemon.session_name});
54+ }
55 }
56 }
57
58@@ -753,11 +765,24 @@ fn spawnPty(daemon: *Daemon) !c_int {
59 const session_env = try std.fmt.allocPrint(daemon.alloc, "ZMX_SESSION={s}\x00", .{daemon.session_name});
60 _ = c.putenv(@ptrCast(session_env.ptr));
61
62- const shell = std.posix.getenv("SHELL") orelse "/bin/sh";
63- const argv = [_:null]?[*:0]const u8{ shell, null };
64- const err = std.posix.execveZ(shell, &argv, std.c.environ);
65- std.log.err("execve failed: err={s}", .{@errorName(err)});
66- std.posix.exit(1);
67+ if (daemon.command) |cmd_args| {
68+ const cmd = cmd_args[0];
69+ var argv_buf: [64:null]?[*:0]const u8 = undefined;
70+ for (cmd_args, 0..) |arg, i| {
71+ argv_buf[i] = @ptrCast(arg.ptr);
72+ }
73+ argv_buf[cmd_args.len] = null;
74+ const argv: [*:null]const ?[*:0]const u8 = &argv_buf;
75+ const err = std.posix.execvpeZ(@ptrCast(cmd.ptr), argv, std.c.environ);
76+ std.log.err("execvpe failed: cmd={s} err={s}", .{ cmd, @errorName(err) });
77+ std.posix.exit(1);
78+ } else {
79+ const shell = std.posix.getenv("SHELL") orelse "/bin/sh";
80+ const argv = [_:null]?[*:0]const u8{ shell, null };
81+ const err = std.posix.execveZ(shell, &argv, std.c.environ);
82+ std.log.err("execve failed: err={s}", .{@errorName(err)});
83+ std.posix.exit(1);
84+ }
85 }
86 // master pid code path
87 daemon.pid = pid;