repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
622a781
parent
0dd41e3
author
Eric Bower
date
2025-10-11 15:53:37 -0400 EDT
feat: config toml
9 files changed,  +193, -39
M build.zig
+6, -0
 1@@ -38,6 +38,12 @@ pub fn build(b: *std.Build) void {
 2     });
 3     exe_mod.addImport("clap", clap_dep.module("clap"));
 4 
 5+    const toml_dep = b.dependency("toml", .{
 6+        .target = target,
 7+        .optimize = optimize,
 8+    });
 9+    exe_mod.addImport("toml", toml_dep.module("toml"));
10+
11     // Exe
12     const exe = b.addExecutable(.{
13         .name = "zmx",
M build.zig.zon
+4, -0
 1@@ -44,6 +44,10 @@
 2             .url = "git+https://github.com/Hejsil/zig-clap?ref=HEAD#b7e3348ed60f99ba32c75aa707ff7c87adc31b36",
 3             .hash = "clap-0.11.0-oBajB-TnAQC7yPLnZRT5WzHZ_4Ly4dX2OILskli74b9H",
 4         },
 5+        .toml = .{
 6+            .url = "git+https://github.com/sam701/zig-toml#3aaeb0bf14935eabae118196f7949b404a42f8ea",
 7+            .hash = "toml-0.3.0-bV14BTd8AQA-wZERtB3dvRE3eSZ-m48AyXFUGkZ_Tm3d",
 8+        },
 9     },
10     .paths = .{
11         "build.zig",
M src/attach.zig
+26, -17
 1@@ -2,7 +2,7 @@ const std = @import("std");
 2 const posix = std.posix;
 3 const xevg = @import("xev");
 4 const xev = xevg.Dynamic;
 5-const socket_path = "/tmp/zmx.sock";
 6+const clap = @import("clap");
 7 
 8 const c = @cImport({
 9     @cInclude("termios.h");
10@@ -22,21 +22,37 @@ const Context = struct {
11     read_ctx: ?*ReadContext = null,
12 };
13 
14-pub fn main() !void {
15+const params = clap.parseParamsComptime(
16+    \\-s, --socket-path <str>  Path to the Unix socket file
17+    \\<str>
18+    \\
19+);
20+
21+pub fn main(socket_path_default: []const u8, iter: *std.process.ArgIterator) !void {
22     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
23     defer _ = gpa.deinit();
24     const allocator = gpa.allocator();
25 
26-    // Get session name from command-line arguments
27-    const args = try std.process.argsAlloc(allocator);
28-    defer std.process.argsFree(allocator, args);
29+    var diag = clap.Diagnostic{};
30+    var res = clap.parseEx(clap.Help, &params, clap.parsers.default, iter, .{
31+        .diagnostic = &diag,
32+        .allocator = allocator,
33+    }) catch |err| {
34+        var buf: [1024]u8 = undefined;
35+        var stderr_file = std.fs.File{ .handle = posix.STDERR_FILENO };
36+        var writer = stderr_file.writer(&buf);
37+        diag.report(&writer.interface, err) catch {};
38+        writer.interface.flush() catch {};
39+        return err;
40+    };
41+    defer res.deinit();
42+
43+    const socket_path = res.args.@"socket-path" orelse socket_path_default;
44 
45-    if (args.len < 3) {
46+    const session_name = res.positionals[0] orelse {
47         std.debug.print("Usage: zmx attach <session-name>\n", .{});
48         std.process.exit(1);
49-    }
50-
51-    const session_name = args[2];
52+    };
53 
54     var thread_pool = xevg.ThreadPool.init(.{});
55     defer thread_pool.deinit();
56@@ -57,15 +73,10 @@ pub fn main() !void {
57     posix.connect(socket_fd, &unix_addr.any, unix_addr.getOsSockLen()) catch |err| {
58         if (err == error.ConnectionRefused) {
59             std.debug.print("Error: Unable to connect to zmx daemon at {s}\nPlease start the daemon first with: zmx daemon\n", .{socket_path});
60-            std.process.exit(1);
61         }
62-        return err;
63+        std.process.exit(1);
64     };
65 
66-    _ = posix.write(posix.STDERR_FILENO, "Attaching to session: ") catch {};
67-    _ = posix.write(posix.STDERR_FILENO, session_name) catch {};
68-    _ = posix.write(posix.STDERR_FILENO, "\n") catch {};
69-
70     // Set raw mode after successful connection
71     defer _ = c.tcsetattr(posix.STDIN_FILENO, c.TCSANOW, &orig_termios);
72 
73@@ -234,8 +245,6 @@ fn readCallback(
74         } else if (std.mem.eql(u8, msg_type, "pty_out")) {
75             const text = payload.get("text").?.string;
76             _ = posix.write(posix.STDOUT_FILENO, text) catch {};
77-        } else {
78-            std.debug.print("Unknown message type: {s}\n", .{msg_type});
79         }
80 
81         return .rearm;
A src/config.zig
+49, -0
 1@@ -0,0 +1,49 @@
 2+const std = @import("std");
 3+const toml = @import("toml");
 4+
 5+pub const Config = struct {
 6+    socket_path: []const u8 = "/tmp/zmx.sock",
 7+
 8+    pub fn load(allocator: std.mem.Allocator) !Config {
 9+        const config_path = getConfigPath(allocator) catch |err| {
10+            if (err == error.FileNotFound) {
11+                return Config{};
12+            }
13+            return err;
14+        };
15+        defer allocator.free(config_path);
16+
17+        var parser = toml.Parser(Config).init(allocator);
18+        defer parser.deinit();
19+
20+        var result = parser.parseFile(config_path) catch |err| {
21+            if (err == error.FileNotFound) {
22+                return Config{};
23+            }
24+            return err;
25+        };
26+        defer result.deinit();
27+
28+        const config = Config{
29+            .socket_path = try allocator.dupe(u8, result.value.socket_path),
30+        };
31+        return config;
32+    }
33+
34+    pub fn deinit(self: *Config, allocator: std.mem.Allocator) void {
35+        if (!std.mem.eql(u8, self.socket_path, "/tmp/zmx.sock")) {
36+            allocator.free(self.socket_path);
37+        }
38+    }
39+};
40+
41+fn getConfigPath(allocator: std.mem.Allocator) ![]const u8 {
42+    const home = std.posix.getenv("HOME") orelse return error.FileNotFound;
43+    const xdg_config_home = std.posix.getenv("XDG_CONFIG_HOME");
44+
45+    if (xdg_config_home) |config_home| {
46+        return try std.fs.path.join(allocator, &.{ config_home, "zmx", "config.toml" });
47+    } else {
48+        return try std.fs.path.join(allocator, &.{ home, ".config", "zmx", "config.toml" });
49+    }
50+}
M src/daemon.zig
+29, -3
 1@@ -2,7 +2,7 @@ const std = @import("std");
 2 const posix = std.posix;
 3 const xevg = @import("xev");
 4 const xev = xevg.Dynamic;
 5-const socket_path = "/tmp/zmx.sock";
 6+const clap = @import("clap");
 7 
 8 const ghostty = @import("ghostty-vt");
 9 
10@@ -94,11 +94,32 @@ const ServerContext = struct {
11     allocator: std.mem.Allocator,
12 };
13 
14-pub fn main() !void {
15+const params = clap.parseParamsComptime(
16+    \\-s, --socket-path <str>  Path to the Unix socket file
17+    \\
18+);
19+
20+pub fn main(socket_path_default: []const u8, iter: *std.process.ArgIterator) !void {
21     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
22     defer _ = gpa.deinit();
23     const allocator = gpa.allocator();
24 
25+    var diag = clap.Diagnostic{};
26+    var res = clap.parseEx(clap.Help, &params, clap.parsers.default, iter, .{
27+        .diagnostic = &diag,
28+        .allocator = allocator,
29+    }) catch |err| {
30+        var buf: [1024]u8 = undefined;
31+        var stderr_file = std.fs.File{ .handle = posix.STDERR_FILENO };
32+        var writer = stderr_file.writer(&buf);
33+        diag.report(&writer.interface, err) catch {};
34+        writer.interface.flush() catch {};
35+        return err;
36+    };
37+    defer res.deinit();
38+
39+    const socket_path = res.args.@"socket-path" orelse socket_path_default;
40+
41     var thread_pool = xevg.ThreadPool.init(.{});
42     defer thread_pool.deinit();
43     defer thread_pool.shutdown();
44@@ -107,6 +128,8 @@ pub fn main() !void {
45     defer loop.deinit();
46 
47     std.debug.print("zmx daemon starting...\n", .{});
48+    std.debug.print("Configuration:\n", .{});
49+    std.debug.print("  socket_path: {s}\n", .{socket_path});
50 
51     _ = std.fs.cwd().deleteFile(socket_path) catch {};
52 
53@@ -114,7 +137,10 @@ pub fn main() !void {
54     // SOCK.STREAM: Reliable, bidirectional communication for JSON protocol messages
55     // SOCK.NONBLOCK: Prevents blocking to work with libxev's async event loop
56     const server_fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM | posix.SOCK.NONBLOCK, 0);
57-    defer posix.close(server_fd);
58+    defer {
59+        posix.close(server_fd);
60+        std.fs.cwd().deleteFile(socket_path) catch {};
61+    }
62 
63     var unix_addr = std.net.Address.initUnix(socket_path) catch |err| {
64         std.debug.print("initUnix failed: {s}\n", .{@errorName(err)});
M src/detach.zig
+24, -4
 1@@ -1,13 +1,33 @@
 2 const std = @import("std");
 3 const posix = std.posix;
 4+const clap = @import("clap");
 5 
 6-const socket_path = "/tmp/zmx.sock";
 7+const params = clap.parseParamsComptime(
 8+    \\-s, --socket-path <str>  Path to the Unix socket file
 9+    \\
10+);
11 
12-pub fn main() !void {
13+pub fn main(socket_path_default: []const u8, iter: *std.process.ArgIterator) !void {
14     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
15     defer _ = gpa.deinit();
16     const allocator = gpa.allocator();
17 
18+    var diag = clap.Diagnostic{};
19+    var res = clap.parseEx(clap.Help, &params, clap.parsers.default, iter, .{
20+        .diagnostic = &diag,
21+        .allocator = allocator,
22+    }) catch |err| {
23+        var buf: [1024]u8 = undefined;
24+        var stderr_file = std.fs.File{ .handle = posix.STDERR_FILENO };
25+        var writer = stderr_file.writer(&buf);
26+        diag.report(&writer.interface, err) catch {};
27+        writer.interface.flush() catch {};
28+        return err;
29+    };
30+    defer res.deinit();
31+
32+    const socket_path = res.args.@"socket-path" orelse socket_path_default;
33+
34     // Find the client_fd file in home directory
35     const home_dir = posix.getenv("HOME") orelse "/tmp";
36 
37@@ -21,8 +41,8 @@ pub fn main() !void {
38     };
39     defer dir.close();
40 
41-    var iter = dir.iterate();
42-    while (iter.next() catch null) |entry| {
43+    var dir_iter = dir.iterate();
44+    while (dir_iter.next() catch null) |entry| {
45         if (entry.kind != .file) continue;
46         if (!std.mem.startsWith(u8, entry.name, ".zmx_client_fd_")) continue;
47 
M src/kill.zig
+24, -8
 1@@ -1,22 +1,38 @@
 2 const std = @import("std");
 3 const posix = std.posix;
 4+const clap = @import("clap");
 5 
 6-const socket_path = "/tmp/zmx.sock";
 7+const params = clap.parseParamsComptime(
 8+    \\-s, --socket-path <str>  Path to the Unix socket file
 9+    \\<str>
10+    \\
11+);
12 
13-pub fn main() !void {
14+pub fn main(socket_path_default: []const u8, iter: *std.process.ArgIterator) !void {
15     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
16     defer _ = gpa.deinit();
17     const allocator = gpa.allocator();
18 
19-    const args = try std.process.argsAlloc(allocator);
20-    defer std.process.argsFree(allocator, args);
21+    var diag = clap.Diagnostic{};
22+    var res = clap.parseEx(clap.Help, &params, clap.parsers.default, iter, .{
23+        .diagnostic = &diag,
24+        .allocator = allocator,
25+    }) catch |err| {
26+        var buf: [1024]u8 = undefined;
27+        var stderr_file = std.fs.File{ .handle = posix.STDERR_FILENO };
28+        var writer = stderr_file.writer(&buf);
29+        diag.report(&writer.interface, err) catch {};
30+        writer.interface.flush() catch {};
31+        return err;
32+    };
33+    defer res.deinit();
34+
35+    const socket_path = res.args.@"socket-path" orelse socket_path_default;
36 
37-    if (args.len < 3) {
38+    const session_name = res.positionals[0] orelse {
39         std.debug.print("Usage: zmx kill <session-name>\n", .{});
40         std.process.exit(1);
41-    }
42-
43-    const session_name = args[2];
44+    };
45 
46     const unix_addr = try std.net.Address.initUnix(socket_path);
47     const socket_fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM, 0);
M src/list.zig
+22, -2
 1@@ -1,13 +1,33 @@
 2 const std = @import("std");
 3 const posix = std.posix;
 4+const clap = @import("clap");
 5 
 6-const socket_path = "/tmp/zmx.sock";
 7+const params = clap.parseParamsComptime(
 8+    \\-s, --socket-path <str>  Path to the Unix socket file
 9+    \\
10+);
11 
12-pub fn main() !void {
13+pub fn main(socket_path_default: []const u8, iter: *std.process.ArgIterator) !void {
14     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
15     defer _ = gpa.deinit();
16     const allocator = gpa.allocator();
17 
18+    var diag = clap.Diagnostic{};
19+    var res = clap.parseEx(clap.Help, &params, clap.parsers.default, iter, .{
20+        .diagnostic = &diag,
21+        .allocator = allocator,
22+    }) catch |err| {
23+        var buf: [1024]u8 = undefined;
24+        var stderr_file = std.fs.File{ .handle = posix.STDERR_FILENO };
25+        var writer = stderr_file.writer(&buf);
26+        diag.report(&writer.interface, err) catch {};
27+        writer.interface.flush() catch {};
28+        return err;
29+    };
30+    defer res.deinit();
31+
32+    const socket_path = res.args.@"socket-path" orelse socket_path_default;
33+
34     const unix_addr = try std.net.Address.initUnix(socket_path);
35     const socket_fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM, 0);
36     defer posix.close(socket_fd);
M src/main.zig
+9, -5
 1@@ -1,5 +1,6 @@
 2 const std = @import("std");
 3 const cli = @import("cli.zig");
 4+const config_mod = @import("config.zig");
 5 const daemon = @import("daemon.zig");
 6 const attach = @import("attach.zig");
 7 const detach = @import("detach.zig");
 8@@ -29,12 +30,15 @@ pub fn main() !void {
 9         return;
10     };
11 
12+    var config = try config_mod.Config.load(allocator);
13+    defer config.deinit(allocator);
14+
15     switch (command) {
16         .help => try cli.help(),
17-        .daemon => try daemon.main(),
18-        .list => try list.main(),
19-        .attach => try attach.main(),
20-        .detach => try detach.main(),
21-        .kill => try kill.main(),
22+        .daemon => try daemon.main(config.socket_path, &iter),
23+        .list => try list.main(config.socket_path, &iter),
24+        .attach => try attach.main(config.socket_path, &iter),
25+        .detach => try detach.main(config.socket_path, &iter),
26+        .kill => try kill.main(config.socket_path, &iter),
27     }
28 }