- commit
- 3b0e4e8
- parent
- 89e2728
- author
- Eric Bower
- date
- 2026-03-22 22:12:51 -0400 EDT
docs: doc strings
1 files changed,
+47,
-15
+47,
-15
1@@ -187,6 +187,9 @@ pub fn main() !void {
2 }
3 }
4
5+/// Client represents each terminal that has connected to a session.
6+///
7+/// Multiple Clients can connect to a single session.
8 const Client = struct {
9 alloc: std.mem.Allocator,
10 socket_fd: i32,
11@@ -201,23 +204,16 @@ const Client = struct {
12 }
13 };
14
15+/// Cfg is zmx's configuration container.
16+///
17+/// The purpose of this container is to hold anything that can be modified by the user.
18 const Cfg = struct {
19 socket_dir: []const u8,
20 log_dir: []const u8,
21 max_scrollback: usize = 10_000_000,
22
23 pub fn init(alloc: std.mem.Allocator) !Cfg {
24- const tmpdir = std.mem.trimRight(u8, posix.getenv("TMPDIR") orelse "/tmp", "/");
25- const uid = posix.getuid();
26-
27- const socket_dir: []const u8 = if (posix.getenv("ZMX_DIR")) |zmxdir|
28- try alloc.dupe(u8, zmxdir)
29- else if (posix.getenv("XDG_RUNTIME_DIR")) |xdg_runtime|
30- try std.fmt.allocPrint(alloc, "{s}/zmx", .{xdg_runtime})
31- else
32- try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid });
33- errdefer alloc.free(socket_dir);
34-
35+ const socket_dir = try socketDir(alloc);
36 const log_dir = try std.fmt.allocPrint(alloc, "{s}/logs", .{socket_dir});
37 errdefer alloc.free(log_dir);
38
39@@ -231,6 +227,21 @@ const Cfg = struct {
40 return cfg;
41 }
42
43+ fn socketDir(alloc: std.mem.Allocator) ![]const u8 {
44+ const tmpdir = std.mem.trimRight(u8, posix.getenv("TMPDIR") orelse "/tmp", "/");
45+ const uid = posix.getuid();
46+
47+ const socket_dir: []const u8 = if (posix.getenv("ZMX_DIR")) |zmxdir|
48+ try alloc.dupe(u8, zmxdir)
49+ else if (posix.getenv("XDG_RUNTIME_DIR")) |xdg_runtime|
50+ try std.fmt.allocPrint(alloc, "{s}/zmx", .{xdg_runtime})
51+ else
52+ try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid });
53+ errdefer alloc.free(socket_dir);
54+
55+ return socket_dir;
56+ }
57+
58 pub fn deinit(self: *Cfg, alloc: std.mem.Allocator) void {
59 if (self.socket_dir.len > 0) alloc.free(self.socket_dir);
60 if (self.log_dir.len > 0) alloc.free(self.log_dir);
61@@ -254,6 +265,14 @@ const EnsureSessionResult = struct {
62 is_daemon: bool,
63 };
64
65+/// Daemon is responsible for managing a zmx session.
66+///
67+/// It holds all the state for a running session. Instead of a single daemon for all sessions, we
68+/// create a daemon for every session. This has some benefits. The ipc communication between
69+/// session clients and the daemon doesn't need to be tagged with the session name. If a daemon
70+/// crashes for one session won't crash all the other sessions.
71+///
72+/// Conceptually it's also much simpler to reason about.
73 const Daemon = struct {
74 cfg: *Cfg,
75 alloc: std.mem.Allocator,
76@@ -343,6 +362,7 @@ const Daemon = struct {
77 std.posix.exit(1);
78 }
79
80+ /// spawnPty runs forkpty() and executes the shell or shell command the user provides.
81 fn spawnPty(self: *Daemon) !c_int {
82 const size = ipc.getTerminalSize(posix.STDOUT_FILENO);
83 var ws: cross.c.struct_winsize = .{
84@@ -379,6 +399,8 @@ const Daemon = struct {
85 return master_fd;
86 }
87
88+ /// ensureSession "upserts" a session by checking if the unix socket exists already.
89+ /// If not it creates one and spawns the daemon.
90 fn ensureSession(self: *Daemon) !EnsureSessionResult {
91 var dir = try std.fs.openDirAbsolute(self.cfg.socket_dir, .{});
92 defer dir.close();
93@@ -411,8 +433,10 @@ const Daemon = struct {
94 std.log.info("creating session={s}", .{self.session_name});
95 const server_sock_fd = try socket.createSocket(self.socket_path);
96
97+ // creates the daemon
98 const pid = try posix.fork();
99 if (pid == 0) { // child (daemon)
100+ // becomes the session leader and detaches process from its controlling terminal
101 _ = try posix.setsid();
102
103 log_system.deinit();
104@@ -695,10 +719,10 @@ fn help() !void {
105 \\ [r]un <name> [command...] Send command without attaching, creating session if needed
106 \\ [d]etach Detach all clients from current session (ctrl+\ for current client)
107 \\ [l]ist [--short] List active sessions
108- \\ [c]ompletions <shell> Completion scripts for shell integration (bash, zsh, or fish)
109 \\ [k]ill <name> Kill a session and all attached clients
110 \\ [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences)
111 \\ [w]ait <name>... Wait for session tasks to complete
112+ \\ [c]ompletions <shell> Completion scripts for shell integration (bash, zsh, or fish)
113 \\ [v]ersion Show version information
114 \\ [h]elp Show this help message
115 \\
116@@ -707,7 +731,7 @@ fn help() !void {
117 \\ - ZMX_DIR Controls which folder is used to store unix socket files (prio: 1)
118 \\ - XDG_RUNTIME_DIR Controls which folder is used to store unix socket files (prio: 2)
119 \\ - TMPDIR Controls which folder is used to store unix socket files (prio: 3)
120- \\ - ZMX_SESSION This variable is injected into every zmx session automatically
121+ \\ - ZMX_SESSION The session name we inject into every zmx session automatically
122 \\ - ZMX_SESSION_PREFIX Adds this value to the start of every session name for all commands
123 \\
124 ;
125@@ -1001,7 +1025,7 @@ fn attach(daemon: *Daemon) !void {
126
127 const client_sock = try socket.sessionConnect(daemon.socket_path);
128 std.log.info("attached session={s}", .{daemon.session_name});
129- // this is typically used with tcsetattr() to modify terminal settings.
130+ // This is typically used with tcsetattr() to modify terminal settings.
131 // - you first get the current settings with tcgetattr()
132 // - modify the desired attributes in the termios structure
133 // - then apply the changes with tcsetattr().
134@@ -1079,6 +1103,10 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void {
135
136 const shell = util.detectShell();
137 const shell_basename = std.fs.path.basename(shell);
138+ // We append a task marker so we can:
139+ // - know when the command finishes
140+ // - capture its exit status
141+ // This information is retrived when running `zmx list`
142 const inline_task_marker = if (std.mem.eql(u8, shell_basename, "fish"))
143 "; echo ZMX_TASK_COMPLETED:$status"
144 else
145@@ -1185,6 +1213,8 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void {
146 return error.NoAckReceived;
147 }
148
149+/// clientLoop sends ipc commands to its corresponding daemon. It uses poll() as its non-blocking
150+/// mechanism. It will send stdin to the daemon and receive stdout from the daemon.
151 fn clientLoop(client_sock_fd: i32) !void {
152 // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking
153 const alloc = std.heap.c_allocator;
154@@ -1343,6 +1373,8 @@ fn clientLoop(client_sock_fd: i32) !void {
155 }
156 }
157
158+/// dameonLoop is what the daemon runs to send and receive ipc commands from its corresponding
159+/// clients. It uses poll() as its non-blocking mechanism.
160 fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
161 std.log.info("daemon started session={s} pty_fd={d}", .{ daemon.session_name, pty_fd });
162 setupSigtermHandler();
163@@ -1554,7 +1586,7 @@ fn handleSigterm(_: i32, _: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c)
164 sigterm_received.store(true, .release);
165 }
166
167-// No SA_RESTART on these: we WANT the signal to interrupt poll() so the
168+// No SA_RESTART: we want the signal to interrupt poll() so the
169 // loop can check the flag. On BSD/macOS, SA_RESTART makes poll restartable,
170 // which would leave an idle daemon deaf to SIGTERM until other I/O wakes it.
171 fn setupSigwinchHandler() void {