01b6afb8
refactor: one owner for "where under XDG does this mux file live"
a73x 2026-08-29 10:01
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -194,9 +194,9 @@ const mod_table = [_]ModSpec{ | |||
| 194 | .{ .name = "cmd", .path = "src/server/cmd.zig", .layer = 1, .imports = &.{ "engine", "protocol" } }, | 194 | .{ .name = "cmd", .path = "src/server/cmd.zig", .layer = 1, .imports = &.{ "engine", "protocol" } }, |
| 195 | // The wall: the TARGET spelling grammar, the ordered list, and the | 195 | // The wall: the TARGET spelling grammar, the ordered list, and the |
| 196 | // state file behind them. One owner for a grammar the hub, the state | 196 | // state file behind them. One owner for a grammar the hub, the state |
| 197 | // file and later the CLI all have to agree on — protocol and nothing | 197 | // file and later the CLI all have to agree on — protocol plus xdg for |
| 198 | // else, so its tests need no hub, no daemon and no socket. | 198 | // where that file lives, so its tests need no hub, no daemon and no socket. |
| 199 | .{ .name = "wall", .path = "src/client/wall.zig", .layer = 1, .imports = &.{"protocol"}, .test_imports = &.{"testtmp"} }, | 199 | .{ .name = "wall", .path = "src/client/wall.zig", .layer = 1, .imports = &.{ "protocol", "xdg" }, .test_imports = &.{"testtmp"} }, |
| 200 | // The upgrade vocabulary: the version skew rule and the manifest an | 200 | // The upgrade vocabulary: the version skew rule and the manifest an |
| 201 | // exec-ing daemon leaves for its replacement. Protocol only — the | 201 | // exec-ing daemon leaves for its replacement. Protocol only — the |
| 202 | // manifest is a stranger's format (length-prefixed sections, unknown | 202 | // manifest is a stranger's format (length-prefixed sections, unknown |
| @@ -243,7 +243,7 @@ const mod_table = [_]ModSpec{ | |||
| 243 | // grammar rather than forking a second one, and wall is layer 1 — so | 243 | // grammar rather than forking a second one, and wall is layer 1 — so |
| 244 | // this sits at 2, above its lender and below every consumer, instead | 244 | // this sits at 2, above its lender and below every consumer, instead |
| 245 | // of flattening the stratum it depends on. | 245 | // of flattening the stratum it depends on. |
| 246 | .{ .name = "hosts", .path = "src/client/hosts.zig", .layer = 2, .imports = &.{"wall"}, .test_imports = &.{"testtmp"} }, | 246 | .{ .name = "hosts", .path = "src/client/hosts.zig", .layer = 2, .imports = &.{ "wall", "xdg" }, .test_imports = &.{"testtmp"} }, |
| 247 | // Everything that happens between a user at a terminal and one already | 247 | // Everything that happens between a user at a terminal and one already |
| 248 | // open session: the chord table, the wheel splitter, prediction, the | 248 | // open session: the chord table, the wheel splitter, prediction, the |
| 249 | // side channels, terminal ownership. It sits BELOW client because it | 249 | // side channels, terminal ownership. It sits BELOW client because it |
src/client/hosts.zig
| Old | New | ||
|---|---|---|---|
| @@ -10,6 +10,7 @@ | |||
| 10 | //! a `mux hosts add` elsewhere drops one of the two lines. | 10 | //! a `mux hosts add` elsewhere drops one of the two lines. |
| 11 | const std = @import("std"); | 11 | const std = @import("std"); |
| 12 | const wall = @import("wall"); | 12 | const wall = @import("wall"); |
| 13 | const xdg = @import("xdg"); | ||
| 13 | 14 | ||
| 14 | pub const Spec = union(enum) { sock: []const u8, host: []const u8, quic: []const u8 }; | 15 | pub const Spec = union(enum) { sock: []const u8, host: []const u8, quic: []const u8 }; |
| 15 | pub const ParseError = error{ HasSession, EmptySpec, BadByte, BadSpelling }; | 16 | pub const ParseError = error{ HasSession, EmptySpec, BadByte, BadSpelling }; |
| @@ -154,13 +155,11 @@ pub fn forgetMany( | |||
| 154 | } | 155 | } |
| 155 | 156 | ||
| 156 | pub fn statePath(alloc: std.mem.Allocator) ![]const u8 { | 157 | pub fn statePath(alloc: std.mem.Allocator) ![]const u8 { |
| 157 | return statePathFrom(alloc, std.posix.getenv("XDG_STATE_HOME"), std.posix.getenv("HOME")); | 158 | return xdg.statePath(alloc, "hosts"); |
| 158 | } | 159 | } |
| 159 | 160 | ||
| 160 | pub fn statePathFrom(alloc: std.mem.Allocator, xdg_state_home: ?[]const u8, home: ?[]const u8) ![]const u8 { | 161 | pub fn statePathFrom(alloc: std.mem.Allocator, xdg_state_home: ?[]const u8, home: ?[]const u8) ![]const u8 { |
| 161 | if (xdg_state_home) |x| if (x.len > 0) return std.fmt.allocPrint(alloc, "{s}/mux/hosts", .{x}); | 162 | return xdg.pathFrom(alloc, xdg_state_home, home, ".local/state", "hosts"); |
| 162 | const h = home orelse return error.NoHome; | ||
| 163 | return std.fmt.allocPrint(alloc, "{s}/.local/state/mux/hosts", .{h}); | ||
| 164 | } | 163 | } |
| 165 | 164 | ||
| 166 | /// cliflags hooks for `mux hosts add|rm SPELLING...`, each word validated | 165 | /// cliflags hooks for `mux hosts add|rm SPELLING...`, each word validated |
src/client/wall.zig
| Old | New | ||
|---|---|---|---|
| @@ -18,6 +18,7 @@ | |||
| 18 | //! claim is made here. | 18 | //! claim is made here. |
| 19 | const std = @import("std"); | 19 | const std = @import("std"); |
| 20 | const proto = @import("protocol"); | 20 | const proto = @import("protocol"); |
| 21 | const xdg = @import("xdg"); | ||
| 21 | 22 | ||
| 22 | pub const Spec = union(enum) { | 23 | pub const Spec = union(enum) { |
| 23 | sock: []const u8, | 24 | sock: []const u8, |
| @@ -302,39 +303,22 @@ pub fn record(alloc: std.mem.Allocator, path: []const u8, spelling: []const u8) | |||
| 302 | return true; | 303 | return true; |
| 303 | } | 304 | } |
| 304 | 305 | ||
| 305 | /// The *From split is xdg.zig's pattern for the same reason: setenv is | 306 | /// The *From split is xdg.zig's pattern: setenv is unsafe in Zig tests. |
| 306 | /// unsafe in-process for Zig tests. | ||
| 307 | pub fn statePath(alloc: std.mem.Allocator) ![]const u8 { | 307 | pub fn statePath(alloc: std.mem.Allocator) ![]const u8 { |
| 308 | return statePathFrom(alloc, std.posix.getenv("XDG_STATE_HOME"), std.posix.getenv("HOME")); | 308 | return xdg.statePath(alloc, "wall"); |
| 309 | } | 309 | } |
| 310 | 310 | ||
| 311 | pub fn statePathFrom( | 311 | pub fn statePathFrom(alloc: std.mem.Allocator, xdg_state_home: ?[]const u8, home: ?[]const u8) ![]const u8 { |
| 312 | alloc: std.mem.Allocator, | 312 | return xdg.pathFrom(alloc, xdg_state_home, home, ".local/state", "wall"); |
| 313 | xdg_state_home: ?[]const u8, | 313 | } |
| 314 | home: ?[]const u8, | 314 | |
| 315 | ) ![]const u8 { | 315 | /// The layout sidecar sits beside the wall file; same *From split. |
| 316 | if (xdg_state_home) |d| if (d.len > 0) | ||
| 317 | return std.fmt.allocPrint(alloc, "{s}/mux/wall", .{d}); | ||
| 318 | const h = home orelse return error.NoHome; | ||
| 319 | return std.fmt.allocPrint(alloc, "{s}/.local/state/mux/wall", .{h}); | ||
| 320 | } | ||
| 321 | |||
| 322 | /// The layout sidecar sits beside the wall file. Same *For-tests split as | ||
| 323 | /// `statePathFrom` for the same reason: setenv is unsafe in-process for Zig | ||
| 324 | /// tests. | ||
| 325 | pub fn layoutPath(alloc: std.mem.Allocator) ![]const u8 { | 316 | pub fn layoutPath(alloc: std.mem.Allocator) ![]const u8 { |
| 326 | return layoutPathFrom(alloc, std.posix.getenv("XDG_STATE_HOME"), std.posix.getenv("HOME")); | 317 | return xdg.statePath(alloc, "layout"); |
| 327 | } | 318 | } |
| 328 | 319 | ||
| 329 | pub fn layoutPathFrom( | 320 | pub fn layoutPathFrom(alloc: std.mem.Allocator, xdg_state_home: ?[]const u8, home: ?[]const u8) ![]const u8 { |
| 330 | alloc: std.mem.Allocator, | 321 | return xdg.pathFrom(alloc, xdg_state_home, home, ".local/state", "layout"); |
| 331 | xdg_state_home: ?[]const u8, | ||
| 332 | home: ?[]const u8, | ||
| 333 | ) ![]const u8 { | ||
| 334 | if (xdg_state_home) |d| if (d.len > 0) | ||
| 335 | return std.fmt.allocPrint(alloc, "{s}/mux/layout", .{d}); | ||
| 336 | const h = home orelse return error.NoHome; | ||
| 337 | return std.fmt.allocPrint(alloc, "{s}/.local/state/mux/layout", .{h}); | ||
| 338 | } | 322 | } |
| 339 | 323 | ||
| 340 | /// One atomic writer for state files: `saveLines` and the layout sidecar | 324 | /// One atomic writer for state files: `saveLines` and the layout sidecar |
src/xdg.zig
| Old | New | ||
|---|---|---|---|
| @@ -51,32 +51,33 @@ pub fn pickKey(flag: ?[]const u8, env: ?[]const u8) ?[]const u8 { | |||
| 51 | return if (k.len == 0) null else k; | 51 | return if (k.len == 0) null else k; |
| 52 | } | 52 | } |
| 53 | 53 | ||
| 54 | pub fn keyPathFrom( | 54 | /// An empty XDG spelling is unset, not the root: `XDG_STATE_HOME=` |
| 55 | alloc: std.mem.Allocator, | 55 | /// would otherwise put the hosts file at `/mux/hosts`. |
| 56 | xdg_config_home: ?[]const u8, | 56 | pub fn pathFrom(alloc: std.mem.Allocator, xdg_dir: ?[]const u8, home: ?[]const u8, home_sub: []const u8, tail: []const u8) ![]const u8 { |
| 57 | home: ?[]const u8, | 57 | if (xdg_dir) |d| if (d.len > 0) |
| 58 | ) ![]const u8 { | 58 | return std.fmt.allocPrint(alloc, "{s}/mux/{s}", .{ d, tail }); |
| 59 | if (xdg_config_home) |d| if (d.len > 0) | ||
| 60 | return std.fmt.allocPrint(alloc, "{s}/mux/key", .{d}); | ||
| 61 | const h = home orelse return error.NoHome; | 59 | const h = home orelse return error.NoHome; |
| 62 | return std.fmt.allocPrint(alloc, "{s}/.config/mux/key", .{h}); | 60 | return std.fmt.allocPrint(alloc, "{s}/{s}/mux/{s}", .{ h, home_sub, tail }); |
| 61 | } | ||
| 62 | |||
| 63 | /// The state directory's share of `pathFrom`, read from the real | ||
| 64 | /// environment. | ||
| 65 | pub fn statePath(alloc: std.mem.Allocator, tail: []const u8) ![]const u8 { | ||
| 66 | return pathFrom(alloc, std.posix.getenv("XDG_STATE_HOME"), std.posix.getenv("HOME"), ".local/state", tail); | ||
| 67 | } | ||
| 68 | |||
| 69 | pub fn keyPathFrom(alloc: std.mem.Allocator, xdg_config_home: ?[]const u8, home: ?[]const u8) ![]const u8 { | ||
| 70 | return pathFrom(alloc, xdg_config_home, home, ".config", "key"); | ||
| 63 | } | 71 | } |
| 64 | 72 | ||
| 65 | /// The daemon's stdout+stderr. Truncated only by `mux d start`; the attach | 73 | /// Truncated by `mux d start`, appended by auto-starts |
| 66 | /// auto-starts append (spawn.zig owns why an attach is not a restart). | 74 | /// (spawn.zig owns why). |
| 67 | pub fn logPath(alloc: std.mem.Allocator) ![]const u8 { | 75 | pub fn logPath(alloc: std.mem.Allocator) ![]const u8 { |
| 68 | return logPathFrom(alloc, std.posix.getenv("XDG_STATE_HOME"), std.posix.getenv("HOME")); | 76 | return statePath(alloc, "muxd.log"); |
| 69 | } | 77 | } |
| 70 | 78 | ||
| 71 | pub fn logPathFrom( | 79 | pub fn logPathFrom(alloc: std.mem.Allocator, xdg_state_home: ?[]const u8, home: ?[]const u8) ![]const u8 { |
| 72 | alloc: std.mem.Allocator, | 80 | return pathFrom(alloc, xdg_state_home, home, ".local/state", "muxd.log"); |
| 73 | xdg_state_home: ?[]const u8, | ||
| 74 | home: ?[]const u8, | ||
| 75 | ) ![]const u8 { | ||
| 76 | if (xdg_state_home) |d| if (d.len > 0) | ||
| 77 | return std.fmt.allocPrint(alloc, "{s}/mux/muxd.log", .{d}); | ||
| 78 | const h = home orelse return error.NoHome; | ||
| 79 | return std.fmt.allocPrint(alloc, "{s}/.local/state/mux/muxd.log", .{h}); | ||
| 80 | } | 81 | } |
| 81 | 82 | ||
| 82 | /// Where `mux HOST` remembers the last announce. A host containing a path | 83 | /// Where `mux HOST` remembers the last announce. A host containing a path |
| @@ -94,10 +95,9 @@ pub fn hostCachePathFrom( | |||
| 94 | // Checked before the environment, so the refusal does not depend on | 95 | // Checked before the environment, so the refusal does not depend on |
| 95 | // which of the two spellings the caller's box happens to take. | 96 | // which of the two spellings the caller's box happens to take. |
| 96 | if (std.mem.indexOfScalar(u8, host, '/') != null) return error.UncacheableHost; | 97 | if (std.mem.indexOfScalar(u8, host, '/') != null) return error.UncacheableHost; |
| 97 | if (xdg_cache_home) |d| if (d.len > 0) | 98 | const tail = try std.fmt.allocPrint(alloc, "hosts/{s}", .{host}); |
| 98 | return std.fmt.allocPrint(alloc, "{s}/mux/hosts/{s}", .{ d, host }); | 99 | defer alloc.free(tail); |
| 99 | const h = home orelse return error.NoHome; | 100 | return pathFrom(alloc, xdg_cache_home, home, ".cache", tail); |
| 100 | return std.fmt.allocPrint(alloc, "{s}/.cache/mux/hosts/{s}", .{ h, host }); | ||
| 101 | } | 101 | } |
| 102 | 102 | ||
| 103 | /// Create `dir` and everything above it, then tighten `dir` itself to | 103 | /// Create `dir` and everything above it, then tighten `dir` itself to |