67ee88ff
refactor: mux wall parses its flags with cliflags
a73x 2026-08-27 06:51
Commit message
src/cli/mux_main.zig
| Old | New | ||
|---|---|---|---|
| @@ -477,6 +477,55 @@ pub fn main() !u8 { | |||
| 477 | /// wallview.run. Resolution allocates into an arena because run() never | 477 | /// wallview.run. Resolution allocates into an arena because run() never |
| 478 | /// returns on the success path (it exits the process — see wallview.run); | 478 | /// returns on the success path (it exits the process — see wallview.run); |
| 479 | /// only the early usage-error paths come back through the defers here. | 479 | /// only the early usage-error paths come back through the defers here. |
| 480 | /// `mux wall`'s own line: two flags, and every other word a tile. Shares | ||
| 481 | /// `usage` with the attach line, so the prose that documents `--key` there | ||
| 482 | /// documents it here. | ||
| 483 | const WallOpts = struct { | ||
| 484 | key: ?[]const u8 = null, | ||
| 485 | quic_idle_ms: u32 = client.quic_idle_ms_default, | ||
| 486 | _arena: std.mem.Allocator, | ||
| 487 | _tiles: std.ArrayList([]const u8) = .empty, | ||
| 488 | /// A hook answers yes or no, so one that failed for a reason has | ||
| 489 | /// nowhere to say so: wallMain owns every message this command prints. | ||
| 490 | _err: ?anyerror = null, | ||
| 491 | |||
| 492 | pub fn positional(self: *WallOpts, word: []const u8) bool { | ||
| 493 | const copy = self._arena.dupe(u8, word) catch |e| { | ||
| 494 | self._err = e; | ||
| 495 | return false; | ||
| 496 | }; | ||
| 497 | self._tiles.append(self._arena, copy) catch |e| { | ||
| 498 | self._err = e; | ||
| 499 | return false; | ||
| 500 | }; | ||
| 501 | return true; | ||
| 502 | } | ||
| 503 | |||
| 504 | /// `--sock` is not one of this command's own flags, so wall may claim | ||
| 505 | /// it and its path as one spelling — muxweb's dialect, accepted here | ||
| 506 | /// too, and joined by wall so both binaries read one grammar. | ||
| 507 | pub fn extra(self: *WallOpts, rest: []const [:0]const u8) usize { | ||
| 508 | const n = wall.spellingFromArgv(self._arena, rest, 0) catch |err| switch (err) { | ||
| 509 | // Not a target this command refuses but one it never saw: | ||
| 510 | // left for cliflags to name as the unknown flag it is. | ||
| 511 | error.FlagLikeTarget => return 0, | ||
| 512 | else => |e| { | ||
| 513 | self._err = e; | ||
| 514 | return 0; | ||
| 515 | }, | ||
| 516 | }; | ||
| 517 | self._tiles.append(self._arena, n.spelling) catch |e| { | ||
| 518 | self._err = e; | ||
| 519 | return 0; | ||
| 520 | }; | ||
| 521 | return n.consumed; | ||
| 522 | } | ||
| 523 | }; | ||
| 524 | |||
| 525 | comptime { | ||
| 526 | cliflags.assertDocumented(WallOpts, usage, &.{}); | ||
| 527 | } | ||
| 528 | |||
| 480 | fn wallMain(alloc: std.mem.Allocator, args: []const [:0]const u8) !u8 { | 529 | fn wallMain(alloc: std.mem.Allocator, args: []const [:0]const u8) !u8 { |
| 481 | var arena_state = std.heap.ArenaAllocator.init(alloc); | 530 | var arena_state = std.heap.ArenaAllocator.init(alloc); |
| 482 | defer arena_state.deinit(); | 531 | defer arena_state.deinit(); |
| @@ -491,46 +540,50 @@ fn wallMain(alloc: std.mem.Allocator, args: []const [:0]const u8) !u8 { | |||
| 491 | (std.mem.eql(u8, args[0], "add") or std.mem.eql(u8, args[0], "rm"))) | 540 | (std.mem.eql(u8, args[0], "add") or std.mem.eql(u8, args[0], "rm"))) |
| 492 | return wallEdit(arena, args[0], args[1..]); | 541 | return wallEdit(arena, args[0], args[1..]); |
| 493 | 542 | ||
| 494 | var key: ?[]const u8 = null; | 543 | var w_opts = WallOpts{ ._arena = arena }; |
| 495 | var idle_ms: u32 = client.quic_idle_ms_default; | 544 | const outcome = cliflags.parse(WallOpts, &w_opts, args); |
| 496 | var spellings: std.ArrayList([]const u8) = .empty; | 545 | // Read before the outcome: a hook that refused for a REASON has already |
| 497 | 546 | // named it, and that reason outranks the bare "unknown word" cliflags | |
| 498 | var i: usize = 0; | 547 | // saw when the hook said no. |
| 499 | while (i < args.len) : (i += 1) { | 548 | if (w_opts._err) |e| switch (e) { |
| 500 | const a = args[i]; | 549 | error.MissingSockPath => { |
| 501 | if (std.mem.eql(u8, a, "--key") and i + 1 < args.len) { | 550 | std.debug.print("mux: wall target '--sock' names no path\n", .{}); |
| 502 | i += 1; | 551 | return 2; |
| 503 | key = args[i]; | 552 | }, |
| 504 | } else if (std.mem.eql(u8, a, "--quic-idle-ms") and i + 1 < args.len) { | 553 | else => return e, |
| 505 | i += 1; | 554 | }; |
| 506 | const n = std.fmt.parseInt(u32, args[i], 10) catch 0; | 555 | switch (outcome) { |
| 507 | if (n == 0) { | 556 | .ok => {}, |
| 508 | std.debug.print("{s}", .{usage}); | 557 | .help => { |
| 509 | return 2; | 558 | // stdout, unlike the refusals below: a usage someone asked for |
| 510 | } | 559 | // is output, and they may well have piped it into a pager. |
| 511 | idle_ms = n; | 560 | _ = std.posix.write(std.posix.STDOUT_FILENO, usage) catch {}; |
| 512 | } else { | 561 | return 0; |
| 513 | // Every other argument is a tile. `--sock` is not one of this | 562 | }, |
| 514 | // command's own flags, so wall may claim it and its path as | 563 | .version => { |
| 515 | // one spelling — muxweb's dialect, accepted here too. | 564 | var vbuf: [64]u8 = undefined; |
| 516 | const n = wall.spellingFromArgv(arena, args, i) catch |err| switch (err) { | 565 | const s = std.fmt.bufPrint(&vbuf, "mux {s}\n", .{build_options.version}) catch unreachable; |
| 517 | error.MissingSockPath => { | 566 | _ = std.posix.write(std.posix.STDOUT_FILENO, s) catch {}; |
| 518 | std.debug.print("mux: wall target '--sock' names no path\n", .{}); | 567 | return 0; |
| 519 | return 2; | 568 | }, |
| 520 | }, | 569 | .missing_value, .bad_number => { |
| 521 | error.FlagLikeTarget => { | 570 | std.debug.print("{s}", .{usage}); |
| 522 | std.debug.print( | 571 | return 2; |
| 523 | "mux: wall takes targets, not flags: '{s}'\n{s}", | 572 | }, |
| 524 | .{ args[i], usage }, | 573 | .unknown_arg => |a| { |
| 525 | ); | 574 | std.debug.print("mux: wall takes targets, not flags: '{s}'\n{s}", .{ a, usage }); |
| 526 | return 2; | 575 | return 2; |
| 527 | }, | 576 | }, |
| 528 | else => |e| return e, | 577 | } |
| 529 | }; | 578 | // Zero means "no idle timeout" to ngtcp2, the inverse of what anyone |
| 530 | i += n.consumed - 1; | 579 | // typing a timeout of zero is asking for. |
| 531 | try spellings.append(arena, n.spelling); | 580 | if (w_opts.quic_idle_ms == 0) { |
| 532 | } | 581 | std.debug.print("{s}", .{usage}); |
| 582 | return 2; | ||
| 533 | } | 583 | } |
| 584 | const key = w_opts.key; | ||
| 585 | const idle_ms = w_opts.quic_idle_ms; | ||
| 586 | var spellings = w_opts._tiles; | ||
| 534 | 587 | ||
| 535 | var from_file = false; | 588 | var from_file = false; |
| 536 | if (spellings.items.len == 0) { | 589 | if (spellings.items.len == 0) { |