3827724d
refactor: layers count components, and the test order bands by touch
a73x 2026-08-30 13:19
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -42,7 +42,7 @@ fn quicDeps(b: *std.Build, target: std.Build.ResolvedTarget) struct { | |||
| 42 | 42 | ||
| 43 | /// One wasm-side twin of a native module: same source, the wasm32 target, | 43 | /// One wasm-side twin of a native module: same source, the wasm32 target, |
| 44 | /// and ReleaseSmall — never `optimize`, because the artifact is embedded | 44 | /// and ReleaseSmall — never `optimize`, because the artifact is embedded |
| 45 | /// into muxweb and its Debug build is 3.7MB against ReleaseSmall's 345KB. | 45 | /// into the one binary and its Debug build is 3.7MB against ReleaseSmall's 345KB. |
| 46 | /// They differ only in path, and the optimize mode is the one field that | 46 | /// They differ only in path, and the optimize mode is the one field that |
| 47 | /// must not drift between them. | 47 | /// must not drift between them. |
| 48 | fn wasmMod(b: *std.Build, wasm_target: std.Build.ResolvedTarget, path: []const u8) *std.Build.Module { | 48 | fn wasmMod(b: *std.Build, wasm_target: std.Build.ResolvedTarget, path: []const u8) *std.Build.Module { |
| @@ -59,7 +59,7 @@ fn wasmMod(b: *std.Build, wasm_target: std.Build.ResolvedTarget, path: []const u | |||
| 59 | /// Linking an archive whose symbols nothing references pulls in no objects, | 59 | /// Linking an archive whose symbols nothing references pulls in no objects, |
| 60 | /// which is why this is safe to apply before any QUIC code exists. Measured | 60 | /// which is why this is safe to apply before any QUIC code exists. Measured |
| 61 | /// rather than assumed at 2b: `nm` finds **zero** ngtcp2 or wolfSSL symbols | 61 | /// rather than assumed at 2b: `nm` finds **zero** ngtcp2 or wolfSSL symbols |
| 62 | /// in the resulting muxd. The binary does grow by 80 bytes — deterministic | 62 | /// in the resulting binary. The binary does grow by 80 bytes — deterministic |
| 63 | /// across rebuilds, and it is link metadata, not code, since no object from | 63 | /// across rebuilds, and it is link metadata, not code, since no object from |
| 64 | /// those archives is present. Stated exactly because "unchanged" would have | 64 | /// those archives is present. Stated exactly because "unchanged" would have |
| 65 | /// been the easy sentence and it would have been false. | 65 | /// been the easy sentence and it would have been false. |
| @@ -87,11 +87,12 @@ fn fatal(comptime fmt: []const u8, args: anytype) noreturn { | |||
| 87 | /// are impossible, not detected. (Dependency edges — ghostty — and | 87 | /// are impossible, not detected. (Dependency edges — ghostty — and |
| 88 | /// build_options are explicitly outside the table's jurisdiction and | 88 | /// build_options are explicitly outside the table's jurisdiction and |
| 89 | /// stay wired by hand.) Layers are the topological strata of the production | 89 | /// stay wired by hand.) Layers are the topological strata of the production |
| 90 | /// graph, computed 2026-08-14, re-stratified 2026-08-20 (the interact | 90 | /// graph, computed 2026-08-14 and re-stratified 2026-08-30, when the table |
| 91 | /// extraction pushed client to 3 and everything above it up one), and | 91 | /// became one row per owned COMPONENT rather than one per file: the strata |
| 92 | /// FROZEN: a new import that would flatten or invert a stratum fails at | 92 | /// are four deep because the components are, and a row's children no longer |
| 93 | /// comptime, and re-stratifying requires editing this table, which is | 93 | /// buy it a layer. FROZEN either way — a new import that would flatten or |
| 94 | /// the point. | 94 | /// invert a stratum fails at comptime, and re-stratifying requires editing |
| 95 | /// this table, which is the point. | ||
| 95 | const ModSpec = struct { | 96 | const ModSpec = struct { |
| 96 | name: []const u8, | 97 | name: []const u8, |
| 97 | path: []const u8, | 98 | path: []const u8, |
| @@ -105,7 +106,7 @@ const ModSpec = struct { | |||
| 105 | /// compile error, not a convention. Excluded from the strata computation. | 106 | /// compile error, not a convention. Excluded from the strata computation. |
| 106 | test_imports: []const []const u8 = &.{}, | 107 | test_imports: []const []const u8 = &.{}, |
| 107 | link_libc: bool = false, | 108 | link_libc: bool = false, |
| 108 | /// Also instantiated against wasm32 (the muxweb core's twins). | 109 | /// Also instantiated against wasm32 (the browser core's twins). |
| 109 | wasm: bool = false, | 110 | wasm: bool = false, |
| 110 | /// This module's test binary needs the QUIC archives. | 111 | /// This module's test binary needs the QUIC archives. |
| 111 | quic_tests: bool = false, | 112 | quic_tests: bool = false, |
| @@ -159,13 +160,13 @@ const mod_table = [_]ModSpec{ | |||
| 159 | // Reflection over a caller's options struct, so it imports nothing: the | 160 | // Reflection over a caller's options struct, so it imports nothing: the |
| 160 | // struct is the flag table and the parser learns it at comptime. | 161 | // struct is the flag table and the parser learns it at comptime. |
| 161 | .{ .name = "cliflags", .path = "src/cli/flags.zig", .layer = 0 }, | 162 | .{ .name = "cliflags", .path = "src/cli/flags.zig", .layer = 0 }, |
| 162 | // ---- layer 1: single-hop over the leaves ---- | ||
| 163 | // This image, as a path something can exec. Under src/cli/ because it | 163 | // This image, as a path something can exec. Under src/cli/ because it |
| 164 | // asks the OS about the process it is in — a question no headless | 164 | // asks the OS about the process it is in — a question no headless |
| 165 | // client may spell. | 165 | // client may spell. |
| 166 | .{ .name = "spawn", .path = "src/cli/spawn.zig", .layer = 1, .link_libc = true }, | 166 | .{ .name = "spawn", .path = "src/cli/spawn.zig", .layer = 0, .link_libc = true }, |
| 167 | // ---- layer 1: single-hop over the leaves ---- | ||
| 167 | // Replays a captured client stdout stream and prints the final grid in | 168 | // Replays a captured client stdout stream and prints the final grid in |
| 168 | // `muxd dump`'s formats — the client half of the M11 render-vs-dump | 169 | // `mux d dump`'s formats — the client half of the M11 render-vs-dump |
| 169 | // convergence check. Imports term so both sides of the diff go through | 170 | // convergence check. Imports term so both sides of the diff go through |
| 170 | // the same ghostty-vt and the same formatter. | 171 | // the same ghostty-vt and the same formatter. |
| 171 | .{ .name = "render", .path = "test/render.zig", .layer = 1, .imports = &.{"term"} }, | 172 | .{ .name = "render", .path = "test/render.zig", .layer = 1, .imports = &.{"term"} }, |
| @@ -173,7 +174,6 @@ const mod_table = [_]ModSpec{ | |||
| 173 | // from stdin (M12). Imports pty so the product's own module is the one | 174 | // from stdin (M12). Imports pty so the product's own module is the one |
| 174 | // under it. | 175 | // under it. |
| 175 | .{ .name = "ptyclient", .path = "test/ptyclient.zig", .layer = 1, .link_libc = true, .imports = &.{ "pty", "script" } }, | 176 | .{ .name = "ptyclient", .path = "test/ptyclient.zig", .layer = 1, .link_libc = true, .imports = &.{ "pty", "script" } }, |
| 176 | // ---- layer 2 ---- | ||
| 177 | // The command state machine, shell integration, the upgrade vocabulary | 177 | // The command state machine, shell integration, the upgrade vocabulary |
| 178 | // and the QUIC listener are CHILD FILES of this root rather than modules | 178 | // and the QUIC listener are CHILD FILES of this root rather than modules |
| 179 | // of their own, so nothing outside src/server/ can name one: a second | 179 | // of their own, so nothing outside src/server/ can name one: a second |
| @@ -185,7 +185,7 @@ const mod_table = [_]ModSpec{ | |||
| 185 | // daemon itself when nobody handed it a --key — and for the shim | 185 | // daemon itself when nobody handed it a --key — and for the shim |
| 186 | // directory shell integration writes under the same 0700 policy. | 186 | // directory shell integration writes under the same 0700 policy. |
| 187 | // `pty` stays a row of its own: the ptyclient fixture consumes it. | 187 | // `pty` stays a row of its own: the ptyclient fixture consumes it. |
| 188 | .{ .name = "daemon", .path = "src/server/server.zig", .layer = 2, .link_libc = true, .imports = &.{ "term", "pty", "sockpath", "quic", "xdg", "proxy" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 188 | .{ .name = "daemon", .path = "src/server/server.zig", .layer = 1, .link_libc = true, .imports = &.{ "term", "pty", "sockpath", "quic", "xdg", "proxy" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 189 | // The agent-facing client. It speaks frames and owns no terminal, which | 189 | // The agent-facing client. It speaks frames and owns no terminal, which |
| 190 | // is the whole point — it attaches at 0x0 and never claims the grid. | 190 | // is the whole point — it attaches at 0x0 and never claims the grid. |
| 191 | // The transport modules are the CLI client's, minus everything that | 191 | // The transport modules are the CLI client's, minus everything that |
| @@ -194,9 +194,8 @@ const mod_table = [_]ModSpec{ | |||
| 194 | // wire contract and nothing else — no engine and no replica, muxa having | 194 | // wire contract and nothing else — no engine and no replica, muxa having |
| 195 | // nothing to draw: a fact of muxa.zig itself, which the one-row component | 195 | // nothing to draw: a fact of muxa.zig itself, which the one-row component |
| 196 | // no longer refuses on its behalf. | 196 | // no longer refuses on its behalf. |
| 197 | .{ .name = "agent", .path = "src/cli/muxa.zig", .layer = 2, .link_libc = true, .imports = &.{ "term", "sockpath", "quic", "xdg", "cliflags" }, .quic_tests = true }, | 197 | .{ .name = "agent", .path = "src/cli/muxa.zig", .layer = 1, .link_libc = true, .imports = &.{ "term", "sockpath", "quic", "xdg", "cliflags" }, .quic_tests = true }, |
| 198 | .{ .name = "wsclient", .path = "test/wsclient.zig", .layer = 2, .link_libc = true, .imports = &.{ "term", "script" } }, | 198 | .{ .name = "wsclient", .path = "test/wsclient.zig", .layer = 1, .link_libc = true, .imports = &.{ "term", "script" } }, |
| 199 | // ---- layer 3 ---- | ||
| 200 | // Dialling, and what a chord means. The client is the only thing that | 199 | // Dialling, and what a chord means. The client is the only thing that |
| 201 | // predicts — the overlay is a local display decision and never becomes | 200 | // predicts — the overlay is a local display decision and never becomes |
| 202 | // state anybody else can see — but the predicting itself is interact's | 201 | // state anybody else can see — but the predicting itself is interact's |
| @@ -210,16 +209,16 @@ const mod_table = [_]ModSpec{ | |||
| 210 | // sits here rather than in either front so the CLI wall and the browser | 209 | // sits here rather than in either front so the CLI wall and the browser |
| 211 | // hub resolve a host line the same way. Nothing here WRITES that file — | 210 | // hub resolve a host line the same way. Nothing here WRITES that file — |
| 212 | // `wall_host.recordHost` and `webhub_main` do. | 211 | // `wall_host.recordHost` and `webhub_main` do. |
| 213 | .{ .name = "client", .path = "src/client/client.zig", .layer = 3, .link_libc = true, .imports = &.{ "term", "quic", "xdg", "sockpath" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 212 | .{ .name = "client", .path = "src/client/client.zig", .layer = 1, .link_libc = true, .imports = &.{ "term", "quic", "xdg", "sockpath" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 214 | // ---- layer 4 ---- | 213 | // ---- layer 2 ---- |
| 215 | // The muxweb hub's HTTP/WebSocket decisions: Origin gate, route table, | 214 | // The browser hub's HTTP/WebSocket decisions: Origin gate, route table, |
| 216 | // WS endpoint naming. Assets are injected (the exe root @embedFiles | 215 | // WS endpoint naming. Assets are injected (the exe root @embedFiles |
| 217 | // them), so its tests build no artifacts. It stays a row rather than | 216 | // them), so its tests build no artifacts. It stays a row rather than |
| 218 | // becoming a child file of the dispatcher that @embedFiles for it: a | 217 | // becoming a child file of the dispatcher that @embedFiles for it: a |
| 219 | // module's root directory is the dirname of its root file, and | 218 | // module's root directory is the dirname of its root file, and |
| 220 | // `@import("../client/webhub.zig")` from src/cli is "import of file | 219 | // `@import("../client/webhub.zig")` from src/cli is "import of file |
| 221 | // outside module path" — a compiler rule, not a table choice. | 220 | // outside module path" — a compiler rule, not a table choice. |
| 222 | .{ .name = "webhub", .path = "src/client/webhub.zig", .layer = 4, .imports = &.{ "term", "client" }, .quic_tests = true }, | 221 | .{ .name = "webhub", .path = "src/client/webhub.zig", .layer = 2, .imports = &.{ "term", "client" }, .quic_tests = true }, |
| 223 | // The CLI wall (`mux wall`): multiattach stripes in one terminal, one of | 222 | // The CLI wall (`mux wall`): multiattach stripes in one terminal, one of |
| 224 | // which can be ZOOMED — promoted to the terminal's size and typed | 223 | // which can be ZOOMED — promoted to the terminal's size and typed |
| 225 | // through. Same layer as webhub for the same reason — both sit on | 224 | // through. Same layer as webhub for the same reason — both sit on |
| @@ -229,11 +228,12 @@ const mod_table = [_]ModSpec{ | |||
| 229 | // overlay are CHILD FILES of this root rather than modules of their own, | 228 | // overlay are CHILD FILES of this root rather than modules of their own, |
| 230 | // so nothing outside src/tui/ can name one: a second module claiming any | 229 | // so nothing outside src/tui/ can name one: a second module claiming any |
| 231 | // of those files is a file-in-multiple-modules compile error. `term` is | 230 | // of those files is a file-in-multiple-modules compile error. `term` is |
| 232 | // the children's as much as the root's: `term.replica` is what a painter | 231 | // the children's as much as the root's: `term.replica` is the keyboard |
| 233 | // and a keyboard loop want and the wall itself never spells; the decoder | 232 | // loop's alone, which the root never spells, and the painter takes |
| 234 | // and the key table they also want reach them through `client`'s seams. | 233 | // `term.engine` and `term.protocol`; the decoder and the key table they |
| 235 | .{ .name = "wall", .path = "src/tui/wallview.zig", .layer = 4, .link_libc = true, .imports = &.{ "term", "client", "proxy", "spawn" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 234 | // also want reach them through `client`'s seams. |
| 236 | // ---- layer 6: the one binary ---- | 235 | .{ .name = "wall", .path = "src/tui/wallview.zig", .layer = 2, .link_libc = true, .imports = &.{ "term", "client", "proxy", "spawn" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 236 | // ---- layer 3: the one binary ---- | ||
| 237 | // Four words, one image — and one row: the daemon's entrypoint, the | 237 | // Four words, one image — and one row: the daemon's entrypoint, the |
| 238 | // client's and the hub's are CHILD FILES of the dispatcher, so a second | 238 | // client's and the hub's are CHILD FILES of the dispatcher, so a second |
| 239 | // module claiming any of them is a file-in-multiple-modules compile | 239 | // module claiming any of them is a file-in-multiple-modules compile |
| @@ -251,7 +251,7 @@ const mod_table = [_]ModSpec{ | |||
| 251 | // downstream must refuse) and `sockpath` for the sun_path bound every verb | 251 | // downstream must refuse) and `sockpath` for the sun_path bound every verb |
| 252 | // checks before acting on a path. `testtmp` is the keygen round-trip's: | 252 | // checks before acting on a path. `testtmp` is the keygen round-trip's: |
| 253 | // it needs a directory to generate into, which the daemon never touches. | 253 | // it needs a directory to generate into, which the daemon never touches. |
| 254 | .{ .name = "mux", .path = "src/cli/mux.zig", .layer = 6, .link_libc = true, .imports = &.{ "daemon", "client", "wall", "agent", "webhub", "term", "proxy", "quic", "xdg", "spawn", "sockpath", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 254 | .{ .name = "mux", .path = "src/cli/mux.zig", .layer = 3, .link_libc = true, .imports = &.{ "daemon", "client", "wall", "agent", "webhub", "term", "proxy", "quic", "xdg", "spawn", "sockpath", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 255 | }; | 255 | }; |
| 256 | 256 | ||
| 257 | /// Comptime row lookup. Every hand-written module name in this file goes | 257 | /// Comptime row lookup. Every hand-written module name in this file goes |
| @@ -270,7 +270,8 @@ fn layerOf(comptime name: []const u8) u8 { | |||
| 270 | 270 | ||
| 271 | comptime { | 271 | comptime { |
| 272 | // Both checks are O(edges x rows) name comparisons; the default 1000 | 272 | // Both checks are O(edges x rows) name comparisons; the default 1000 |
| 273 | // backwards branches does not cover a 32-row, 76-edge table. | 273 | // backwards branches does not cover a 21-row, 45-edge table (38 |
| 274 | // production imports and 7 test-only ones). | ||
| 274 | @setEvalBranchQuota(20_000); | 275 | @setEvalBranchQuota(20_000); |
| 275 | for (mod_table) |m| { | 276 | for (mod_table) |m| { |
| 276 | for (m.imports) |dep| { | 277 | for (m.imports) |dep| { |
| @@ -711,7 +712,7 @@ fn zigFilesIn(b: *std.Build, sub: []const u8, paths: *std.ArrayList([]const u8)) | |||
| 711 | /// | 712 | /// |
| 712 | /// The tool is a build tool, not part of the program, so it stays out of the | 713 | /// The tool is a build tool, not part of the program, so it stays out of the |
| 713 | /// module table: that table is the program's import graph, and a row there | 714 | /// module table: that table is the program's import graph, and a row there |
| 714 | /// would claim docscheck is something muxd links. | 715 | /// would claim docscheck is something `mux` links. |
| 715 | /// | 716 | /// |
| 716 | /// Nothing about this step can silently skip, which is the whole hazard with | 717 | /// Nothing about this step can silently skip, which is the whole hazard with |
| 717 | /// a gate: the tool is built from source in this repo, so "not installed" is a | 718 | /// a gate: the tool is built from source in this repo, so "not installed" is a |
| @@ -783,20 +784,23 @@ fn docGate(b: *std.Build, target: std.Build.ResolvedTarget, check_step: *std.Bui | |||
| 783 | } | 784 | } |
| 784 | } | 785 | } |
| 785 | 786 | ||
| 786 | /// Test registration order. Doctrine-laden and deliberately NOT derived | 787 | /// Test registration order — the order failures ARRIVE in, and deliberately |
| 787 | /// from the layers: term and sockpath run BEFORE daemon because their | 788 | /// NOT the layers'. A suite that waits on a socket can wedge, and a wedged |
| 788 | /// tests are seconds-long and socket-free, while a regression in either | 789 | /// step prints nothing at all; whatever runs before it is the only legible |
| 789 | /// can wedge a daemon test that waits on a client forever — and a wedged | 790 | /// catch. So the order is three bands by what a suite TOUCHES, cheapest |
| 790 | /// step prints nothing at all. Failing first is what makes the catch | 791 | /// touch first: no fd at all, then a pty or a fixture binary, then the |
| 791 | /// legible. script leads for the same reason: instant, allocation-only, | 792 | /// suites that bind a unix socket (the `testtmp` set) and can wait forever. |
| 792 | /// and both fixtures inherit its escape pins. `mux` runs last and carries | 793 | /// Inside a band the seconds-long one goes last — `pty` at 5s, `daemon` at |
| 793 | /// every argument parser but muxa's, its mains being child files — a test | 794 | /// 40s, everything else in the milliseconds (`zig build test --summary all`, |
| 794 | /// that is never built is not a test (decisions.md). | 795 | /// Debug, 2026-08-30). `script` leads its band because both fixtures inherit |
| 796 | /// its escape pins, and `mux` runs last of all: it carries every argument | ||
| 797 | /// parser but muxa's, its mains being child files — a test that is never | ||
| 798 | /// built is not a test (decisions.md). | ||
| 795 | const test_order = [_][]const u8{ | 799 | const test_order = [_][]const u8{ |
| 796 | "script", "term", "pty", "webhub", "wall", | 800 | "script", "cliflags", "testtmp", "spawn", "quic", |
| 797 | "sockpath", "agent", "daemon", "client", "proxy", | 801 | "webhub", "agent", "term", "rawmode", "delaypipe", |
| 798 | "quic", "testtmp", "rawmode", "delaypipe", "xdg", | 802 | "render", "wsclient", "ptyclient", "pty", "sockpath", |
| 799 | "spawn", "render", "ptyclient", "wsclient", "cliflags", | 803 | "xdg", "proxy", "wall", "client", "daemon", |
| 800 | "mux", | 804 | "mux", |
| 801 | }; | 805 | }; |
| 802 | 806 | ||
| @@ -957,9 +961,9 @@ pub fn build(b: *std.Build) void { | |||
| 957 | // ---- The wasm core (M-web Task 4) ---- | 961 | // ---- The wasm core (M-web Task 4) ---- |
| 958 | // A SECOND resolved target: modules are target-bound, so the wasm-clean | 962 | // A SECOND resolved target: modules are target-bound, so the wasm-clean |
| 959 | // row — the term component — and the ghostty dependency are | 963 | // row — the term component — and the ghostty dependency are |
| 960 | // instantiated again against wasm32. Always | 964 | // instantiated again against wasm32. Always ReleaseSmall — the artifact |
| 961 | // ReleaseSmall — the artifact is @embedFile'd into muxweb, and its | 965 | // is @embedFile'd into the one binary, and its Debug build is 3.7MB |
| 962 | // Debug build is 3.7MB against ReleaseSmall's 345KB (spike-measured). | 966 | // against ReleaseSmall's 345KB (spike-measured). |
| 963 | // Safety checks are the price; the native test suite runs the same | 967 | // Safety checks are the price; the native test suite runs the same |
| 964 | // code checked. | 968 | // code checked. |
| 965 | const wasm_target = b.resolveTargetQuery(.{ | 969 | const wasm_target = b.resolveTargetQuery(.{ |
| @@ -1012,7 +1016,7 @@ pub fn build(b: *std.Build) void { | |||
| 1012 | wasm_exe.rdynamic = true; | 1016 | wasm_exe.rdynamic = true; |
| 1013 | b.installArtifact(wasm_exe); | 1017 | b.installArtifact(wasm_exe); |
| 1014 | 1018 | ||
| 1015 | // ---- muxweb, the hub binary (M-web Task 7) ---- | 1019 | // ---- the hub's browser stand-in ---- |
| 1016 | const wsclient_exe = b.addExecutable(.{ .name = "wsclient", .root_module = wsclient_mod }); | 1020 | const wsclient_exe = b.addExecutable(.{ .name = "wsclient", .root_module = wsclient_mod }); |
| 1017 | wsclient_exe.use_llvm = true; | 1021 | wsclient_exe.use_llvm = true; |
| 1018 | wsclient_exe.use_lld = true; | 1022 | wsclient_exe.use_lld = true; |
| @@ -1039,7 +1043,7 @@ pub fn build(b: *std.Build) void { | |||
| 1039 | t.use_lld = true; | 1043 | t.use_lld = true; |
| 1040 | // quic_tests is also what makes `make test` build the QUIC deps on | 1044 | // quic_tests is also what makes `make test` build the QUIC deps on |
| 1041 | // a clean checkout — the dependency must reach the test binaries, | 1045 | // a clean checkout — the dependency must reach the test binaries, |
| 1042 | // not only muxd (decisions.md, M8). | 1046 | // not only the binary (decisions.md, M8). |
| 1043 | if (mod_table[i].quic_tests) linkQuic(b, t, quic); | 1047 | if (mod_table[i].quic_tests) linkQuic(b, t, quic); |
| 1044 | test_step.dependOn(&b.addRunArtifact(t).step); | 1048 | test_step.dependOn(&b.addRunArtifact(t).step); |
| 1045 | } | 1049 | } |
| @@ -1135,8 +1139,8 @@ pub fn build(b: *std.Build) void { | |||
| 1135 | // @embedFile of mux_core.wasm drags the wasm build in with it, so the | 1139 | // @embedFile of mux_core.wasm drags the wasm build in with it, so the |
| 1136 | // compile coverage this gate gives is every module in the table plus the | 1140 | // compile coverage this gate gives is every module in the table plus the |
| 1137 | // wasm core. What it does NOT cover is the executable wiring itself — | 1141 | // wasm core. What it does NOT cover is the executable wiring itself — |
| 1138 | // linking muxd/mux/muxa/muxweb, the QUIC archives included — which is | 1142 | // linking `mux` and the test fixtures, the QUIC archives included — which |
| 1139 | // `zig build` (and e2e, which runs the binaries). | 1143 | // is `zig build` (and e2e, which runs the binaries). |
| 1140 | const check_step = b.step("check", "fmt + unit tests + shell syntax — the pre-commit gate"); | 1144 | const check_step = b.step("check", "fmt + unit tests + shell syntax — the pre-commit gate"); |
| 1141 | check_step.dependOn(fmt_step); | 1145 | check_step.dependOn(fmt_step); |
| 1142 | check_step.dependOn(test_step); | 1146 | check_step.dependOn(test_step); |
src/cli/mux_main.zig
| Old | New | ||
|---|---|---|---|
| @@ -8,7 +8,7 @@ | |||
| 8 | //! | 8 | //! |
| 9 | //! Bare `mux` is the WALL: every session every daemon on | 9 | //! Bare `mux` is the WALL: every session every daemon on |
| 10 | //! `$XDG_STATE_HOME/mux/hosts` has live. Naming a transport is the same | 10 | //! `$XDG_STATE_HOME/mux/hosts` has live. Naming a transport is the same |
| 11 | //! wall, entered zoomed on that daemon's session (`wallview.runAttach`). | 11 | //! wall, entered zoomed on that daemon's session (`wall.runAttach`). |
| 12 | //! What lives up here is argv, the refusals that must happen before a dial | 12 | //! What lives up here is argv, the refusals that must happen before a dial |
| 13 | //! (self-attach, an unbindable socket path), the auto-start, and the | 13 | //! (self-attach, an unbindable socket path), the auto-start, and the |
| 14 | //! `hosts` subcommand — everything about the command line rather than | 14 | //! `hosts` subcommand — everything about the command line rather than |
| @@ -21,7 +21,7 @@ const xdg = @import("xdg"); | |||
| 21 | const spawn = @import("spawn"); | 21 | const spawn = @import("spawn"); |
| 22 | const handoff = @import("client").handoff; | 22 | const handoff = @import("client").handoff; |
| 23 | const sockpath = @import("sockpath"); | 23 | const sockpath = @import("sockpath"); |
| 24 | const wallview = @import("wall"); | 24 | const wall = @import("wall"); |
| 25 | const hosts = @import("client").hosts; | 25 | const hosts = @import("client").hosts; |
| 26 | const cliflags = @import("cliflags"); | 26 | const cliflags = @import("cliflags"); |
| 27 | const TmpDir = @import("testtmp").TmpDir; | 27 | const TmpDir = @import("testtmp").TmpDir; |
| @@ -317,7 +317,7 @@ pub fn main(args: []const [:0]const u8) !u8 { | |||
| 317 | return 2; | 317 | return 2; |
| 318 | }, | 318 | }, |
| 319 | }; | 319 | }; |
| 320 | return wallview.runAttach(alloc, .{ .quic = .{ | 320 | return wall.runAttach(alloc, .{ .quic = .{ |
| 321 | .host_port = q.host_port, | 321 | .host_port = q.host_port, |
| 322 | .key_path = key_path, | 322 | .key_path = key_path, |
| 323 | .idle_ms = q.idle_ms, | 323 | .idle_ms = q.idle_ms, |
| @@ -341,10 +341,10 @@ pub fn main(args: []const [:0]const u8) !u8 { | |||
| 341 | // wall yet and no alternate screen to corrupt, and the user is | 341 | // wall yet and no alternate screen to corrupt, and the user is |
| 342 | // sitting in front of the wait those bytes describe. | 342 | // sitting in front of the wait those bytes describe. |
| 343 | target.narrate = true; | 343 | target.narrate = true; |
| 344 | return wallview.runAttach(alloc, .{ .hand = target }, h.session, null, h.idle_ms, h.agent); | 344 | return wall.runAttach(alloc, .{ .hand = target }, h.session, null, h.idle_ms, h.agent); |
| 345 | }, | 345 | }, |
| 346 | .attach => |t| { | 346 | .attach => |t| { |
| 347 | if (t.via) |cmd| return wallview.runAttach( | 347 | if (t.via) |cmd| return wall.runAttach( |
| 348 | alloc, | 348 | alloc, |
| 349 | .{ .via = cmd }, | 349 | .{ .via = cmd }, |
| 350 | t.session, | 350 | t.session, |
| @@ -385,8 +385,8 @@ fn attachLocal( | |||
| 385 | // USER's attach only — the Ctrl-\ chords grow their tiles from inside | 385 | // USER's attach only — the Ctrl-\ chords grow their tiles from inside |
| 386 | // the wall and never come back through here, so focusing from session 0 | 386 | // the wall and never come back through here, so focusing from session 0 |
| 387 | // to session 1 keeps working. The picker's `a` is the one chord that | 387 | // to session 1 keeps working. The picker's `a` is the one chord that |
| 388 | // takes a spelling, and it runs `wallview.showsSelf` itself. | 388 | // takes a spelling, and it runs `wall.showsSelf` itself. |
| 389 | if (wallview.showsSelf( | 389 | if (wall.showsSelf( |
| 390 | .{ .sock = sock_path }, | 390 | .{ .sock = sock_path }, |
| 391 | session, | 391 | session, |
| 392 | std.posix.getenv(proto.sock_env), | 392 | std.posix.getenv(proto.sock_env), |
| @@ -397,7 +397,7 @@ fn attachLocal( | |||
| 397 | } | 397 | } |
| 398 | 398 | ||
| 399 | if (!sockpath.answers(sock_path) and !try startLocalDaemon(alloc, sock_path)) return 1; | 399 | if (!sockpath.answers(sock_path) and !try startLocalDaemon(alloc, sock_path)) return 1; |
| 400 | return wallview.runAttach( | 400 | return wall.runAttach( |
| 401 | alloc, | 401 | alloc, |
| 402 | .{ .sock = sock_path }, | 402 | .{ .sock = sock_path }, |
| 403 | session, | 403 | session, |
| @@ -447,7 +447,7 @@ comptime { | |||
| 447 | /// `mux`: the wall of daemons. An empty file is the local one, so a first | 447 | /// `mux`: the wall of daemons. An empty file is the local one, so a first |
| 448 | /// run is still just a shell. | 448 | /// run is still just a shell. |
| 449 | /// | 449 | /// |
| 450 | /// Resolution allocates into an arena because `wallview.run` never returns | 450 | /// Resolution allocates into an arena because `wall.run` never returns |
| 451 | /// on the success path; only the early refusals come back through the | 451 | /// on the success path; only the early refusals come back through the |
| 452 | /// defers here. | 452 | /// defers here. |
| 453 | fn wallOfHosts(alloc: std.mem.Allocator) !u8 { | 453 | fn wallOfHosts(alloc: std.mem.Allocator) !u8 { |
| @@ -461,7 +461,7 @@ fn wallOfHosts(alloc: std.mem.Allocator) !u8 { | |||
| 461 | // The local daemon is a host like any other; what is special about | 461 | // The local daemon is a host like any other; what is special about |
| 462 | // it is only that `mux` reaches for it when nothing is listed. The | 462 | // it is only that `mux` reaches for it when nothing is listed. The |
| 463 | // line itself is written once the attach answers, by | 463 | // line itself is written once the attach answers, by |
| 464 | // `wallview.runAttach`. | 464 | // `wall.runAttach`. |
| 465 | const sock_path = try sockpath.defaultOrExplain(arena, "mux") orelse return 1; | 465 | const sock_path = try sockpath.defaultOrExplain(arena, "mux") orelse return 1; |
| 466 | return attachLocal(alloc, sock_path, "", false); | 466 | return attachLocal(alloc, sock_path, "", false); |
| 467 | } | 467 | } |
| @@ -478,18 +478,18 @@ fn wallOfHosts(alloc: std.mem.Allocator) !u8 { | |||
| 478 | } | 478 | } |
| 479 | 479 | ||
| 480 | const key = std.posix.getenv(xdg.key_env); | 480 | const key = std.posix.getenv(xdg.key_env); |
| 481 | const specs = try arena.alloc(wallview.HostSpec, h.lines.items.len); | 481 | const specs = try arena.alloc(wall.HostSpec, h.lines.items.len); |
| 482 | for (specs, h.lines.items) |*s, line| { | 482 | for (specs, h.lines.items) |*s, line| { |
| 483 | // Refused outright, unlike the same bad line under `mux HOST`: here | 483 | // Refused outright, unlike the same bad line under `mux HOST`: here |
| 484 | // the wall IS what was asked for, and a wall silently missing one of | 484 | // the wall IS what was asked for, and a wall silently missing one of |
| 485 | // the machines the user wrote down is the lie this whole file exists | 485 | // the machines the user wrote down is the lie this whole file exists |
| 486 | // to stop telling. | 486 | // to stop telling. |
| 487 | s.* = wallview.resolveHost(arena, line, key, client.quic_idle_ms_default) catch |err| { | 487 | s.* = wall.resolveHost(arena, line, key, client.quic_idle_ms_default) catch |err| { |
| 488 | std.debug.print("mux: bad host '{s}': {s}\n", .{ line, hosts.reason(err) }); | 488 | std.debug.print("mux: bad host '{s}': {s}\n", .{ line, hosts.reason(err) }); |
| 489 | return 2; | 489 | return 2; |
| 490 | }; | 490 | }; |
| 491 | } | 491 | } |
| 492 | return wallview.run(arena, specs, .{ .key = key }); | 492 | return wall.run(arena, specs, .{ .key = key }); |
| 493 | } | 493 | } |
| 494 | 494 | ||
| 495 | /// What a count is worth waiting for. It bounds the DAEMON's answer, not | 495 | /// What a count is worth waiting for. It bounds the DAEMON's answer, not |
| @@ -536,7 +536,7 @@ fn hostsList(arena: std.mem.Allocator, path: []const u8, out_fd: std.posix.fd_t) | |||
| 536 | const lines = hosts.loadLines(arena, path) catch |err| return refuseFile(arena, "mux hosts", path, err); | 536 | const lines = hosts.loadLines(arena, path) catch |err| return refuseFile(arena, "mux hosts", path, err); |
| 537 | const key = std.posix.getenv(xdg.key_env); | 537 | const key = std.posix.getenv(xdg.key_env); |
| 538 | for (lines.items) |line| { | 538 | for (lines.items) |line| { |
| 539 | const spec = wallview.resolveHost(arena, line, key, client.quic_idle_ms_default) catch |err| { | 539 | const spec = wall.resolveHost(arena, line, key, client.quic_idle_ms_default) catch |err| { |
| 540 | printRow(out_fd, line, "\t[bad host: {s}]\n", .{hosts.reason(err)}); | 540 | printRow(out_fd, line, "\t[bad host: {s}]\n", .{hosts.reason(err)}); |
| 541 | continue; | 541 | continue; |
| 542 | }; | 542 | }; |
src/client/client.zig
| Old | New | ||
|---|---|---|---|
| @@ -25,10 +25,10 @@ const sockpath = @import("sockpath"); | |||
| 25 | pub const hosts = @import("hosts.zig"); | 25 | pub const hosts = @import("hosts.zig"); |
| 26 | pub const handoff = @import("handoff.zig"); | 26 | pub const handoff = @import("handoff.zig"); |
| 27 | pub const layout = @import("layout.zig"); | 27 | pub const layout = @import("layout.zig"); |
| 28 | // This file uses `keymap.detach_key` only — the abort byte both opening waits | 28 | // The seam carries `keymap.detach_key` and nothing else: the dial here and |
| 29 | // watch for. The dial has no session to command and no terminal to drive, | 29 | // the prefix filter in interact.zig both watch for that one abort byte. It |
| 30 | // which is why the byte comes from keymap rather than from the filter that | 30 | // is keymap's rather than the filter's because a dial has no session to |
| 31 | // consumes it. | 31 | // command and no terminal to drive. |
| 32 | pub const keymap = @import("keymap.zig"); | 32 | pub const keymap = @import("keymap.zig"); |
| 33 | pub const askpass = @import("askpass.zig"); | 33 | pub const askpass = @import("askpass.zig"); |
| 34 | pub const core = @import("client_core.zig"); | 34 | pub const core = @import("client_core.zig"); |