Eric Bower
·
2026-08-01
1/// Cfg is zmx's configuration container.
2///
3/// The purpose of this container is to hold anything that can be modified by the user.
4pub const Cfg = @This();
5
6const std = @import("std");
7const lib_posix = @import("posix.zig");
8const cross = @import("cross.zig");
9
10socket_dir: []const u8,
11log_dir: []const u8,
12max_scrollback_lines: usize = 2_000, // same default as tmux
13dir_mode: u32 = 0o750,
14log_mode: u32 = 0o640,
15
16pub fn init(alloc: std.mem.Allocator, io: std.Io) !Cfg {
17 const socket_dir = try socketDir(alloc);
18 errdefer alloc.free(socket_dir);
19 const log_dir = try logDir(alloc);
20 errdefer alloc.free(log_dir);
21
22 const dir_mode = if (lib_posix.getenv("ZMX_DIR_MODE")) |m|
23 std.fmt.parseInt(u32, m, 8) catch 0o750
24 else
25 0o750;
26
27 const log_mode = if (lib_posix.getenv("ZMX_LOG_MODE")) |m|
28 std.fmt.parseInt(u32, m, 8) catch 0o640
29 else
30 0o640;
31
32 var cfg = Cfg{
33 .socket_dir = socket_dir,
34 .log_dir = log_dir,
35 .dir_mode = dir_mode,
36 .log_mode = log_mode,
37 };
38
39 try cfg.mkdir(io);
40
41 return cfg;
42}
43
44fn socketDir(alloc: std.mem.Allocator) ![]const u8 {
45 const tmpdir = std.mem.trimEnd(u8, lib_posix.getenv("TMPDIR") orelse "/tmp", "/");
46 const uid = lib_posix.getuid();
47
48 const socket_dir: []const u8 = if (lib_posix.getenv("ZMX_DIR")) |zmxdir|
49 try alloc.dupe(u8, zmxdir)
50 else if (lib_posix.getenv("XDG_RUNTIME_DIR")) |xdg_runtime|
51 try std.fmt.allocPrint(alloc, "{s}/zmx", .{xdg_runtime})
52 else
53 try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid });
54
55 return socket_dir;
56}
57
58fn logDir(alloc: std.mem.Allocator) ![]const u8 {
59 const log_dir = if (lib_posix.getenv("ZMX_DIR")) |zmxdir|
60 try std.fmt.allocPrint(alloc, "{s}/logs", .{zmxdir})
61 else if (lib_posix.getenv("XDG_STATE_HOME")) |xdg_state_home|
62 try std.fmt.allocPrint(alloc, "{s}/zmx/logs", .{xdg_state_home})
63 else if (lib_posix.getenv("HOME")) |home_dir|
64 try std.fmt.allocPrint(alloc, "{s}/.local/state/zmx/logs", .{home_dir})
65 else fallback: {
66 // This is the last resort: falling back to /tmp/$UID if HOME is unset.
67 const tmpdir = std.mem.trimEnd(u8, lib_posix.getenv("TMPDIR") orelse "/tmp", "/");
68 const uid = lib_posix.getuid();
69 break :fallback try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid });
70 };
71
72 return log_dir;
73}
74
75pub fn deinit(self: *Cfg, alloc: std.mem.Allocator) void {
76 if (self.socket_dir.len > 0) alloc.free(self.socket_dir);
77 if (self.log_dir.len > 0) alloc.free(self.log_dir);
78}
79
80pub fn mkdir(self: *Cfg, io: std.Io) !void {
81 const sock_perms = std.Io.Dir.Permissions.fromMode(@intCast(self.dir_mode));
82 try mkdirAll(io, self.socket_dir, sock_perms);
83 const log_perms = std.Io.Dir.Permissions.fromMode(@intCast(self.dir_mode));
84 try mkdirAll(io, self.log_dir, log_perms);
85}
86
87fn mkdirAll(io: std.Io, sub_dir_path: []const u8, permissions: std.Io.Dir.Permissions) !void {
88 var it = std.fs.path.componentIterator(sub_dir_path);
89 var component = it.last() orelse return error.BadPathName;
90 while (true) {
91 std.Io.Dir.createDirAbsolute(io, component.path, permissions) catch |err| switch (err) {
92 error.PathAlreadyExists => {},
93 error.FileNotFound => |e| {
94 component = it.previous() orelse return e;
95 continue;
96 },
97 else => |e| return e,
98 };
99 component = it.next() orelse return;
100 }
101}
102
103test "Cfg.init uses default modes when env vars are not set" {
104 const alloc = std.testing.allocator;
105
106 // Ensure they are not set
107 _ = cross.c.unsetenv("ZMX_DIR_MODE");
108 _ = cross.c.unsetenv("ZMX_LOG_MODE");
109
110 var cfg = try Cfg.init(alloc, std.testing.io);
111 defer cfg.deinit(alloc);
112
113 try std.testing.expectEqual(@as(u32, 0o750), cfg.dir_mode);
114 try std.testing.expectEqual(@as(u32, 0o640), cfg.log_mode);
115}
116
117test "Cfg.init uses custom modes from env vars" {
118 const alloc = std.testing.allocator;
119
120 // Set custom octal values
121 _ = cross.c.setenv("ZMX_DIR_MODE", "770", 1);
122 _ = cross.c.setenv("ZMX_LOG_MODE", "660", 1);
123 defer {
124 _ = cross.c.unsetenv("ZMX_DIR_MODE");
125 _ = cross.c.unsetenv("ZMX_LOG_MODE");
126 }
127
128 var cfg = try Cfg.init(alloc, std.testing.io);
129 defer cfg.deinit(alloc);
130
131 try std.testing.expectEqual(@as(u32, 0o770), cfg.dir_mode);
132 try std.testing.expectEqual(@as(u32, 0o660), cfg.log_mode);
133}