1b4ed4a8
refactor: one owner turns a spelling into a Target
a73x 2026-08-29 10:01
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -268,7 +268,7 @@ const mod_table = [_]ModSpec{ | |||
| 268 | // attach records its own tile (the wall is attach history), and the | 268 | // attach records its own tile (the wall is attach history), and the |
| 269 | // chord switches that re-dial from inside client.attach have to record | 269 | // chord switches that re-dial from inside client.attach have to record |
| 270 | // theirs too, so the writer cannot live up in mux_main. | 270 | // theirs too, so the writer cannot live up in mux_main. |
| 271 | .{ .name = "client", .path = "src/client/client.zig", .layer = 3, .link_libc = true, .imports = &.{ "protocol", "replica", "keymap", "quic", "handoff", "wall" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 271 | .{ .name = "client", .path = "src/client/client.zig", .layer = 3, .link_libc = true, .imports = &.{ "protocol", "replica", "keymap", "quic", "handoff", "wall", "xdg", "sockpath" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 272 | // The daemon entrypoint loads the key and constructs the listener, so | 272 | // The daemon entrypoint loads the key and constructs the listener, so |
| 273 | // it needs quic/quic_server directly rather than through the server. | 273 | // it needs quic/quic_server directly rather than through the server. |
| 274 | // `muxd endpoint` prints the announce line handoff spells; sockpath is | 274 | // `muxd endpoint` prints the announce line handoff spells; sockpath is |
src/client/client.zig
| Old | New | ||
|---|---|---|---|
| @@ -22,6 +22,9 @@ const proto = @import("protocol"); | |||
| 22 | const TmpDir = @import("testtmp").TmpDir; | 22 | const TmpDir = @import("testtmp").TmpDir; |
| 23 | const quic = @import("quic"); | 23 | const quic = @import("quic"); |
| 24 | const handoff = @import("handoff"); | 24 | const handoff = @import("handoff"); |
| 25 | const wall = @import("wall"); | ||
| 26 | const xdg = @import("xdg"); | ||
| 27 | const sockpath = @import("sockpath"); | ||
| 25 | 28 | ||
| 26 | /// A session name held by value. The names a switch travels on are decoded | 29 | /// A session name held by value. The names a switch travels on are decoded |
| 27 | /// out of a frame payload that is freed before the re-dial, so they cannot | 30 | /// out of a frame payload that is freed before the re-dial, so they cannot |
| @@ -233,6 +236,21 @@ pub const HandoffTarget = struct { | |||
| 233 | /// exists and where the user is owed the progress of a wait they are | 236 | /// exists and where the user is owed the progress of a wait they are |
| 234 | /// sitting through. | 237 | /// sitting through. |
| 235 | quiet: bool = false, | 238 | quiet: bool = false, |
| 239 | |||
| 240 | /// The recipe→target literal: a field added above is added here, not | ||
| 241 | /// at every dial. `asked` is a parameter with NO default though | ||
| 242 | /// the field has one — Zig cannot omit it, and that compile error is | ||
| 243 | /// the pin the field's default is not. | ||
| 244 | pub fn fromRecipe(host: []const u8, r: handoff.Recipe, idle_ms: u32, asked: bool) HandoffTarget { | ||
| 245 | return .{ | ||
| 246 | .host = host, | ||
| 247 | .ssh_argv = r.ssh_argv, | ||
| 248 | .start_argv = r.start_argv, | ||
| 249 | .cache_path = r.cache_path, | ||
| 250 | .idle_ms = idle_ms, | ||
| 251 | .asked = asked, | ||
| 252 | }; | ||
| 253 | } | ||
| 236 | }; | 254 | }; |
| 237 | 255 | ||
| 238 | /// A union, not four nullable fields — "exactly one is set" stops being a | 256 | /// A union, not four nullable fields — "exactly one is set" stops being a |
| @@ -242,8 +260,65 @@ pub const Target = union(enum) { | |||
| 242 | via: []const u8, | 260 | via: []const u8, |
| 243 | quic: QuicTarget, | 261 | quic: QuicTarget, |
| 244 | hand: HandoffTarget, | 262 | hand: HandoffTarget, |
| 263 | |||
| 264 | /// The one road from a spelling's `wall.Spec` to the dial it names: | ||
| 265 | /// the wall's host lines and the hub's tiles resolve here, and the | ||
| 266 | /// CLI's `.host` arm shares `fromRecipe`. What can fail is | ||
| 267 | /// `SpecError`; the word for it is each door's own. | ||
| 268 | /// | ||
| 269 | /// Every slice is OWNED by `alloc` — the spelling a caller parsed may | ||
| 270 | /// be a scratch buffer, and a target outlives the read that made it. | ||
| 271 | /// | ||
| 272 | /// `asked` is required, never defaulted: see `HandoffTarget.fromRecipe`. | ||
| 273 | pub fn fromSpec(alloc: std.mem.Allocator, spec: wall.Spec, key: ?[]const u8, idle_ms: u32, asked: bool) SpecError!Target { | ||
| 274 | return switch (spec) { | ||
| 275 | // sun_path is a fixed array in the kernel's struct: a longer | ||
| 276 | // path cannot be dialed at all, so it is refused here, at | ||
| 277 | // usage altitude, rather than at a connect that fails with a | ||
| 278 | // truncated name nobody typed. | ||
| 279 | .sock => |path| if (path.len > sockpath.max_sun_path) | ||
| 280 | error.SockPathTooLong | ||
| 281 | else | ||
| 282 | .{ .sock = try alloc.dupe(u8, path) }, | ||
| 283 | .host => |h| blk: { | ||
| 284 | const hd = try alloc.dupe(u8, h); | ||
| 285 | const r = try handoff.recipeFor(alloc, hd, false); | ||
| 286 | break :blk .{ .hand = HandoffTarget.fromRecipe(hd, r, idle_ms, asked) }; | ||
| 287 | }, | ||
| 288 | .quic => |hp| blk: { | ||
| 289 | const key_path = switch (xdg.resolveKeyPath(alloc, key) catch |err| switch (err) { | ||
| 290 | // No HOME is no default key path, which is the same | ||
| 291 | // outcome for this caller as a default that isn't | ||
| 292 | // there: nothing to authenticate the dial with. | ||
| 293 | error.NoHome => return error.MissingKey, | ||
| 294 | else => |e| return e, | ||
| 295 | }) { | ||
| 296 | // `.given` borrows from argv/env, which outlives | ||
| 297 | // nothing in particular from the target's point of view. | ||
| 298 | .given => |kp| try alloc.dupe(u8, kp), | ||
| 299 | .default => |kp| kp, | ||
| 300 | // `.missing` is an ALLOCATED path too — the refusal is | ||
| 301 | // the one arm that does not keep it, so it is the one | ||
| 302 | // arm that has to free it. | ||
| 303 | .missing => |kp| { | ||
| 304 | alloc.free(kp); | ||
| 305 | return error.MissingKey; | ||
| 306 | }, | ||
| 307 | }; | ||
| 308 | break :blk .{ .quic = .{ | ||
| 309 | .host_port = try alloc.dupe(u8, hp), | ||
| 310 | .key_path = key_path, | ||
| 311 | .idle_ms = idle_ms, | ||
| 312 | } }; | ||
| 313 | }, | ||
| 314 | }; | ||
| 315 | } | ||
| 245 | }; | 316 | }; |
| 246 | 317 | ||
| 318 | /// What resolving a spelling can fail at: a key that is not there to prove | ||
| 319 | /// the dial with, and a socket path the kernel cannot hold. | ||
| 320 | pub const SpecError = error{ MissingKey, SockPathTooLong, OutOfMemory }; | ||
| 321 | |||
| 247 | /// What the open produced — the live wire. Distinct from `Target` because a | 322 | /// What the open produced — the live wire. Distinct from `Target` because a |
| 248 | /// `hand` recipe yields either a quic or a pipe link, and which one is | 323 | /// `hand` recipe yields either a quic or a pipe link, and which one is |
| 249 | /// decided inside `openHandoff` at runtime. | 324 | /// decided inside `openHandoff` at runtime. |
| @@ -1715,9 +1790,8 @@ fn shimMade(dir: []const u8, name: []const u8) !bool { | |||
| 1715 | } | 1790 | } |
| 1716 | 1791 | ||
| 1717 | test "openHandoff: a HandoffTarget nobody configured starts nothing" { | 1792 | test "openHandoff: a HandoffTarget nobody configured starts nothing" { |
| 1718 | // Seven dial paths spell `.asked = false` and one spells `true`. The | 1793 | // The default is the value a NEW path inherits by forgetting the line, |
| 1719 | // default is the value a NEW path inherits by forgetting the line, and | 1794 | // and nothing fails loudly when it does: the symptom is a daemon (and a |
| 1720 | // nothing fails loudly when it does: the symptom is a daemon (and a | ||
| 1721 | // shell in session 0) appearing on someone else's box. So the default | 1795 | // shell in session 0) appearing on someone else's box. So the default |
| 1722 | // is the harmless half, and the ask is what has to be written down. | 1796 | // is the harmless half, and the ask is what has to be written down. |
| 1723 | const alloc = std.testing.allocator; | 1797 | const alloc = std.testing.allocator; |
| @@ -2283,7 +2357,7 @@ test "client: every spelling this writes, the wall grammar reads back the same" | |||
| 2283 | }; | 2357 | }; |
| 2284 | inline for (cases) |c| { | 2358 | inline for (cases) |c| { |
| 2285 | const spelling = try wallSpelling(&buf, c[0], c[1]); | 2359 | const spelling = try wallSpelling(&buf, c[0], c[1]); |
| 2286 | const p = try @import("wall").parseSpelling(spelling); | 2360 | const p = try wall.parseSpelling(spelling); |
| 2287 | try std.testing.expectEqualStrings(c[1], p.session); | 2361 | try std.testing.expectEqualStrings(c[1], p.session); |
| 2288 | switch (c[0]) { | 2362 | switch (c[0]) { |
| 2289 | .sock => |path| try std.testing.expectEqualStrings(path, p.spec.sock), | 2363 | .sock => |path| try std.testing.expectEqualStrings(path, p.spec.sock), |
| @@ -2537,3 +2611,90 @@ test "listSessions: a poll that failed still reports the login it paid for" { | |||
| 2537 | try std.testing.expectError(error.Transport, listSessions(alloc, .{ .sock = "/nonexistent/mux.sock" }, &out, 200, &link)); | 2611 | try std.testing.expectError(error.Transport, listSessions(alloc, .{ .sock = "/nonexistent/mux.sock" }, &out, 200, &link)); |
| 2538 | try std.testing.expectEqual(std.meta.Tag(Link).fd, link); | 2612 | try std.testing.expectEqual(std.meta.Tag(Link).fd, link); |
| 2539 | } | 2613 | } |
| 2614 | |||
| 2615 | test "Target.fromSpec: asked is the caller's word, never a default" { | ||
| 2616 | // The COMPILE-TIME half of this claim cannot be written as a runtime | ||
| 2617 | // assertion: `fromSpec` and `HandoffTarget.fromRecipe` take `asked` as | ||
| 2618 | // a parameter with no default, so a call that omits it does not build. | ||
| 2619 | // That is the pin — the field's `= false` default stays for a road | ||
| 2620 | // that never heard of the field, and this door has no such road. | ||
| 2621 | // What is runtime-checkable is that the word is CARRIED, not dropped | ||
| 2622 | // and re-defaulted somewhere between here and the recipe. | ||
| 2623 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | ||
| 2624 | defer arena.deinit(); | ||
| 2625 | const alloc = arena.allocator(); | ||
| 2626 | |||
| 2627 | // Both values, off one spelling: a fromSpec that hard-coded either one | ||
| 2628 | // would pass a test that only ever asked for the other. | ||
| 2629 | for ([_]bool{ true, false }) |asked| { | ||
| 2630 | const t = try Target.fromSpec(alloc, .{ .host = "box" }, null, 30_000, asked); | ||
| 2631 | try std.testing.expectEqual(asked, t.hand.asked); | ||
| 2632 | try std.testing.expectEqual(@as(u32, 30_000), t.hand.idle_ms); | ||
| 2633 | // The start line is the recipe's, and it is what `asked` gates. | ||
| 2634 | try std.testing.expect(t.hand.start_argv.len > 0); | ||
| 2635 | } | ||
| 2636 | } | ||
| 2637 | |||
| 2638 | test "Target.fromSpec: the target owns every slice, so a scratch spelling may be reused" { | ||
| 2639 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | ||
| 2640 | defer arena.deinit(); | ||
| 2641 | const alloc = arena.allocator(); | ||
| 2642 | |||
| 2643 | // The hub parses a POST body and the wall parses a filter's buffer; | ||
| 2644 | // both are overwritten before the dial the target describes. | ||
| 2645 | var scratch: [15]u8 = "box ".*; | ||
| 2646 | const t = try Target.fromSpec(alloc, .{ .host = scratch[0..3] }, null, 30_000, false); | ||
| 2647 | @memset(&scratch, 'z'); | ||
| 2648 | try std.testing.expectEqualStrings("box", t.hand.host); | ||
| 2649 | |||
| 2650 | var kbuf: [7]u8 = "/k ".*; | ||
| 2651 | const q = try Target.fromSpec(alloc, .{ .quic = "h:1" }, kbuf[0..2], 30_000, false); | ||
| 2652 | @memset(&kbuf, 'z'); | ||
| 2653 | try std.testing.expectEqualStrings("/k", q.quic.key_path); | ||
| 2654 | try std.testing.expectEqualStrings("h:1", q.quic.host_port); | ||
| 2655 | } | ||
| 2656 | |||
| 2657 | test "Target.fromSpec: a quic spelling with no key frees the path it refused" { | ||
| 2658 | // `std.testing.allocator` IS the assertion: `xdg.resolveKeyPath`'s | ||
| 2659 | // `.missing` arm hands back an allocated path, and the refusal that | ||
| 2660 | // does not keep it must free it. The two live callers pass arenas or a | ||
| 2661 | // long-lived allocator, so nothing else in the product would ever say | ||
| 2662 | // so out loud. | ||
| 2663 | var tmp = try TmpDir.make(); | ||
| 2664 | defer tmp.cleanup(); | ||
| 2665 | |||
| 2666 | // Pointed at an empty directory rather than the developer's own config: | ||
| 2667 | // whether ~/.config/mux/key exists on the machine running the suite | ||
| 2668 | // decides which arm this takes, and a test that grades a different arm | ||
| 2669 | // per box grades nothing. | ||
| 2670 | const libc = @cImport({ | ||
| 2671 | @cInclude("stdlib.h"); | ||
| 2672 | }); | ||
| 2673 | const alloc = std.testing.allocator; | ||
| 2674 | const prior = std.posix.getenv("XDG_CONFIG_HOME"); | ||
| 2675 | const prior_z = if (prior) |p| try alloc.dupeZ(u8, p) else null; | ||
| 2676 | defer if (prior_z) |p| alloc.free(p); | ||
| 2677 | const cfg = try alloc.dupeZ(u8, tmp.path()); | ||
| 2678 | defer alloc.free(cfg); | ||
| 2679 | _ = libc.setenv("XDG_CONFIG_HOME", cfg.ptr, 1); | ||
| 2680 | defer if (prior_z) |p| { | ||
| 2681 | _ = libc.setenv("XDG_CONFIG_HOME", p.ptr, 1); | ||
| 2682 | } else { | ||
| 2683 | _ = libc.unsetenv("XDG_CONFIG_HOME"); | ||
| 2684 | }; | ||
| 2685 | |||
| 2686 | try std.testing.expectError( | ||
| 2687 | error.MissingKey, | ||
| 2688 | Target.fromSpec(alloc, .{ .quic = "h:1" }, null, 30_000, false), | ||
| 2689 | ); | ||
| 2690 | } | ||
| 2691 | |||
| 2692 | test "Target.fromSpec: a sun_path-overflowing socket is refused at usage altitude" { | ||
| 2693 | // Refused where it was typed, not at a connect(2) that would bind a | ||
| 2694 | // truncated name nobody asked for. | ||
| 2695 | const long = "/" ++ "x" ** 200; | ||
| 2696 | try std.testing.expectError( | ||
| 2697 | error.SockPathTooLong, | ||
| 2698 | Target.fromSpec(std.testing.allocator, .{ .sock = long }, null, 30_000, false), | ||
| 2699 | ); | ||
| 2700 | } | ||
src/client/hosts.zig
| Old | New | ||
|---|---|---|---|
| @@ -12,7 +12,10 @@ const std = @import("std"); | |||
| 12 | const wall = @import("wall"); | 12 | const wall = @import("wall"); |
| 13 | const xdg = @import("xdg"); | 13 | const xdg = @import("xdg"); |
| 14 | 14 | ||
| 15 | pub const Spec = union(enum) { sock: []const u8, host: []const u8, quic: []const u8 }; | 15 | /// The wall grammar's, not a second copy of it: `hosts.parse` is the |
| 16 | /// strict door onto the same three spellings `wall.parseSpelling` reads, | ||
| 17 | /// and one type is what lets `client.Target.fromSpec` answer both. | ||
| 18 | pub const Spec = wall.Spec; | ||
| 16 | pub const ParseError = error{ HasSession, EmptySpec, BadByte, BadSpelling }; | 19 | pub const ParseError = error{ HasSession, EmptySpec, BadByte, BadSpelling }; |
| 17 | 20 | ||
| 18 | pub fn parse(line: []const u8) ParseError!Spec { | 21 | pub fn parse(line: []const u8) ParseError!Spec { |
src/tui/wall_host.zig
| Old | New | ||
|---|---|---|---|
| @@ -9,8 +9,6 @@ const client = @import("client"); | |||
| 9 | const wall = @import("wall"); | 9 | const wall = @import("wall"); |
| 10 | const hosts = @import("hosts"); | 10 | const hosts = @import("hosts"); |
| 11 | const handoff = @import("handoff"); | 11 | const handoff = @import("handoff"); |
| 12 | const xdg = @import("xdg"); | ||
| 13 | const sockpath = @import("sockpath"); | ||
| 14 | const wall_layout = @import("wall_layout.zig"); | 12 | const wall_layout = @import("wall_layout.zig"); |
| 15 | const wv = @import("wallview.zig"); | 13 | const wv = @import("wallview.zig"); |
| 16 | const Shared = wv.Shared; | 14 | const Shared = wv.Shared; |
| @@ -30,7 +28,7 @@ pub const Resolved = struct { | |||
| 30 | agent: bool = false, | 28 | agent: bool = false, |
| 31 | }; | 29 | }; |
| 32 | 30 | ||
| 33 | pub const ResolveError = hosts.ParseError || error{ MissingKey, SockPathTooLong, OutOfMemory }; | 31 | pub const ResolveError = hosts.ParseError || client.SpecError; |
| 34 | 32 | ||
| 35 | /// A daemon on the wall: what to dial, and the line that named it. The | 33 | /// A daemon on the wall: what to dial, and the line that named it. The |
| 36 | /// spelling is the sidecar's key and what `mux hosts` prints back, so it | 34 | /// spelling is the sidecar's key and what `mux hosts` prints back, so it |
| @@ -44,47 +42,11 @@ pub fn resolveHost( | |||
| 44 | key: ?[]const u8, | 42 | key: ?[]const u8, |
| 45 | idle_ms: u32, | 43 | idle_ms: u32, |
| 46 | ) ResolveError!HostSpec { | 44 | ) ResolveError!HostSpec { |
| 47 | const target: client.Target = switch (try hosts.parse(spelling)) { | 45 | // A host line is a listing, not an attach anyone waited for. The |
| 48 | // Refused here, at usage altitude, not at a connect that fails | 46 | // POLLER runs off this spec once a second: an asked copy would print |
| 49 | // with a truncated sun_path nobody typed. | 47 | // the fallback line onto the wall's alternate screen every cycle, and |
| 50 | .sock => |path| if (path.len > sockpath.max_sun_path) | 48 | // would start a daemon on a box whose owner just stopped one. |
| 51 | return error.SockPathTooLong | 49 | const target = try client.Target.fromSpec(alloc, try hosts.parse(spelling), key, idle_ms, false); |
| 52 | else | ||
| 53 | .{ .sock = path }, | ||
| 54 | .host => |h| blk: { | ||
| 55 | const r = try handoff.recipeFor(alloc, h, false); | ||
| 56 | break :blk .{ | ||
| 57 | .hand = .{ | ||
| 58 | .host = h, | ||
| 59 | .ssh_argv = r.ssh_argv, | ||
| 60 | .start_argv = r.start_argv, | ||
| 61 | .cache_path = r.cache_path, | ||
| 62 | .idle_ms = idle_ms, | ||
| 63 | // A host line is a listing, not an attach anyone waited | ||
| 64 | // for. The POLLER runs off this spec once a second: an | ||
| 65 | // asked copy would print the fallback line onto the | ||
| 66 | // wall's alternate screen every cycle, and would start | ||
| 67 | // a daemon on a box whose owner just stopped one. | ||
| 68 | .asked = false, | ||
| 69 | }, | ||
| 70 | }; | ||
| 71 | }, | ||
| 72 | .quic => |hp| blk: { | ||
| 73 | const key_path = switch (xdg.resolveKeyPath(alloc, key) catch |err| switch (err) { | ||
| 74 | error.NoHome => return error.MissingKey, | ||
| 75 | else => |e| return e, | ||
| 76 | }) { | ||
| 77 | .given => |kp| kp, | ||
| 78 | .default => |kp| kp, | ||
| 79 | .missing => return error.MissingKey, | ||
| 80 | }; | ||
| 81 | break :blk .{ .quic = .{ | ||
| 82 | .host_port = hp, | ||
| 83 | .key_path = key_path, | ||
| 84 | .idle_ms = idle_ms, | ||
| 85 | } }; | ||
| 86 | }, | ||
| 87 | }; | ||
| 88 | return .{ .spelling = spelling, .target = target, .poll_target = try pollTargetFor(alloc, target) }; | 50 | return .{ .spelling = spelling, .target = target, .poll_target = try pollTargetFor(alloc, target) }; |
| 89 | } | 51 | } |
| 90 | 52 | ||
| @@ -100,14 +62,7 @@ pub fn pollTargetFor(alloc: std.mem.Allocator, target: client.Target) !client.Ta | |||
| 100 | else => return target, | 62 | else => return target, |
| 101 | }; | 63 | }; |
| 102 | const r = try handoff.recipeFor(alloc, h.host, true); | 64 | const r = try handoff.recipeFor(alloc, h.host, true); |
| 103 | return .{ .hand = .{ | 65 | return .{ .hand = client.HandoffTarget.fromRecipe(h.host, r, h.idle_ms, false) }; |
| 104 | .host = h.host, | ||
| 105 | .ssh_argv = r.ssh_argv, | ||
| 106 | .start_argv = r.start_argv, | ||
| 107 | .cache_path = r.cache_path, | ||
| 108 | .idle_ms = h.idle_ms, | ||
| 109 | .asked = false, | ||
| 110 | } }; | ||
| 111 | } | 66 | } |
| 112 | 67 | ||
| 113 | /// One wording for every spelling the wall will not take. | 68 | /// One wording for every spelling the wall will not take. |
src/tui/wall_test_host.zig
| Old | New | ||
|---|---|---|---|
| @@ -493,7 +493,10 @@ test "addHost: a forgotten host's slot comes back, so a and x cannot fill the ta | |||
| 493 | }; | 493 | }; |
| 494 | // `run`'s allocator is an ARENA that never returns, so a reused slot's | 494 | // `run`'s allocator is an ARENA that never returns, so a reused slot's |
| 495 | // old spec is deliberately not freed there; a test outlives its own. | 495 | // old spec is deliberately not freed there; a test outlives its own. |
| 496 | // The target owns its own path: `Target.fromSpec` copies every slice, | ||
| 497 | // because the spelling a caller parsed may be a scratch buffer. | ||
| 496 | defer alloc.free(table[at].spec.spelling); | 498 | defer alloc.free(table[at].spec.spelling); |
| 499 | defer alloc.free(table[at].spec.target.sock); | ||
| 497 | try std.testing.expectEqual(@as(usize, 0), at); | 500 | try std.testing.expectEqual(@as(usize, 0), at); |
| 498 | try std.testing.expectEqual(@as(usize, 2), n); | 501 | try std.testing.expectEqual(@as(usize, 2), n); |
| 499 | try std.testing.expectEqualStrings("/c", table[0].spec.target.sock); | 502 | try std.testing.expectEqualStrings("/c", table[0].spec.target.sock); |
| @@ -538,6 +541,7 @@ test "addHost: the prompt adds a DAEMON — a flag, a session and a host already | |||
| 538 | // `run`'s allocator never returns, so the table's copy is deliberately | 541 | // `run`'s allocator never returns, so the table's copy is deliberately |
| 539 | // never freed there; a test outlives its allocator's bookkeeping. | 542 | // never freed there; a test outlives its allocator's bookkeeping. |
| 540 | defer alloc.free(table[at].spec.spelling); | 543 | defer alloc.free(table[at].spec.spelling); |
| 544 | defer alloc.free(table[at].spec.target.sock); | ||
| 541 | try std.testing.expectEqual(@as(usize, 2), at); | 545 | try std.testing.expectEqual(@as(usize, 2), at); |
| 542 | try std.testing.expectEqual(@as(usize, 3), n); | 546 | try std.testing.expectEqual(@as(usize, 3), n); |
| 543 | try std.testing.expectEqualStrings("/c", table[2].spec.target.sock); | 547 | try std.testing.expectEqualStrings("/c", table[2].spec.target.sock); |
src/tui/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -23,8 +23,6 @@ const client = @import("client"); | |||
| 23 | const wall = @import("wall"); | 23 | const wall = @import("wall"); |
| 24 | const hosts = @import("hosts"); | 24 | const hosts = @import("hosts"); |
| 25 | const handoff = @import("handoff"); | 25 | const handoff = @import("handoff"); |
| 26 | const xdg = @import("xdg"); | ||
| 27 | const sockpath = @import("sockpath"); | ||
| 28 | const proxy = @import("proxy"); | 26 | const proxy = @import("proxy"); |
| 29 | const Engine = @import("engine").Engine; | 27 | const Engine = @import("engine").Engine; |
| 30 | const paint = @import("paint"); | 28 | const paint = @import("paint"); |