a73x

67ee88ff

refactor: mux wall parses its flags with cliflags

a73x   2026-08-27 06:51

Commit message
refactor: mux wall parses its flags with cliflags

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) {