╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ command.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
16 .serve => .{ 17 .name = "serve", 18 .descrip = 19 \\a long-running server forwarding receive-pack and upload-pack. 20 , 21 .example = 22 \\haxy serve --http-listen 127.0.0.1:8080 --ssh-listen 127.0.0.1:8022 --wui-listen 127.0.0.1:8000 --data-dir /srv/git 23 , 24 }, 25 }; 26 } 27 28 pub fn printHelp(cmd_kind_maybe: ?CommandKind, writer: *std.Io.Writer) !void { 29 const print_indent = comptime blk: { 30 var indent = 0; 31 for (0..@typeInfo(CommandKind).@"enum".fields.len) |i| { 32 indent = @max(commandHelp(@enumFromInt(i)).name.len, indent); 33 } 34 indent += 2; 35 break :blk indent; 36 }; 37 38 if (cmd_kind_maybe) |cmd_kind| { 39 const help = commandHelp(cmd_kind); 40 // name and description 41 try writer.print("{s}", .{help.name}); 42 for (0..print_indent - help.name.len) |_| try writer.print(" ", .{}); 43 var split_iter = std.mem.splitScalar(u8, help.descrip, '\n'); 44 try writer.print("{s}\n", .{split_iter.first()}); 45 while (split_iter.next()) |line| { 46 for (0..print_indent) |_| try writer.print(" ", .{}); 47 try writer.print("{s}\n", .{line}); 48 } 49 try writer.print("\n", .{}); 50 // example 51 split_iter = std.mem.splitScalar(u8, help.example, '\n'); 52 while (split_iter.next()) |line| { 53 for (0..print_indent) |_| try writer.print(" ", .{}); 54 try writer.print("{s}\n", .{line}); 55 } 56 } else { 57 try writer.print("help: xit <command> [<args>]\n\n", .{}); 58 inline for (@typeInfo(CommandKind).@"enum".fields) |field| { 59 const help = commandHelp(@enumFromInt(field.value)); 60 // name and description 61 try writer.print("{s}", .{help.name}); 62 for (0..print_indent - help.name.len) |_| try writer.print(" ", .{}); 63 var split_iter = std.mem.splitScalar(u8, help.descrip, '\n'); 64 try writer.print("{s}\n", .{split_iter.first()}); 65 while (split_iter.next()) |line| { 66 for (0..print_indent) |_| try writer.print(" ", .{}); 67 try writer.print("{s}\n", .{line}); 68 } 69 } 70 } 71 } 72 73 pub const CommandArgs = struct { 74 allocator: std.mem.Allocator, 75 arena: *std.heap.ArenaAllocator, 76 command_kind: ?CommandKind, 77 command_name: ?[]const u8, 78 positional_args: []const []const u8, 79 map_args: std.StringArrayHashMapUnmanaged(?[]const u8), 80 unused_args: std.StringArrayHashMapUnmanaged(void), 81 82 // flags that can have a value associated with them 83 // must be included here 84 const value_flags = std.StaticStringMap(void).initComptime(.{ 85 .{"--http-listen"}, 86 .{"--ssh-listen"}, 87 .{"--wui-listen"}, 88 .{"--data-dir"}, 89 }); 90 91 pub fn init(allocator: std.mem.Allocator, args: []const []const u8) !CommandArgs { 92 const arena = try allocator.create(std.heap.ArenaAllocator); 93 arena.* = std.heap.ArenaAllocator.init(allocator); 94 errdefer { 95 arena.deinit(); 96 allocator.destroy(arena); 97 } 98 99 var positional_args: std.ArrayList([]const u8) = .empty; 100 var map_args: std.StringArrayHashMapUnmanaged(?[]const u8) = .empty; 101 var unused_args: std.StringArrayHashMapUnmanaged(void) = .empty; 102 103 for (args) |arg| { 104 if (arg.len > 1 and arg[0] == '-') { 105 try map_args.put(arena.allocator(), arg, null); 106 try unused_args.put(arena.allocator(), arg, {}); 107 } else { 108 // if the last key is a value flag and doesn't have a value yet, 109 // set this arg as its value 110 const keys = map_args.keys(); 111 if (keys.len > 0) { 112 const last_key = keys[keys.len - 1]; 113 if (map_args.get(last_key)) |last_val| { 114 if (value_flags.has(last_key) and last_val == null) { 115 try map_args.put(arena.allocator(), last_key, arg); 116 continue; 117 } 118 } 119 } 120 121 // in any other case, just consider it a positional arg 122 try positional_args.append(arena.allocator(), arg); 123 } 124 } 125 126 const args_slice = try positional_args.toOwnedSlice(arena.allocator()); 127 if (args_slice.len == 0) { 128 return .{ 129 .allocator = allocator, 130 .arena = arena, 131 .command_kind = null, 132 .command_name = null, 133 .positional_args = args_slice, 134 .map_args = map_args, 135 .unused_args = unused_args, 136 }; 137 } else { 138 const command_name = args_slice[0]; 139 const extra_args = args_slice[1..]; 140 141 const command_kind: ?CommandKind = inline for (0..@typeInfo(CommandKind).@"enum".fields.len) |i| { 142 if (std.mem.eql(u8, command_name, commandHelp(@enumFromInt(i)).name)) { 143 break @enumFromInt(i); 144 } 145 } else null; 146 147 return .{ 148 .allocator = allocator, 149 .arena = arena, 150 .command_kind = command_kind, 151 .command_name = command_name, 152 .positional_args = extra_args, 153 .map_args = map_args, 154 .unused_args = unused_args, 155 }; 156 } 157 } 158 159 pub fn deinit(self: *CommandArgs) void { 160 self.arena.deinit(); 161 self.allocator.destroy(self.arena); 162 } 163 164 pub fn contains(self: *CommandArgs, arg: []const u8) bool { 165 _ = self.unused_args.orderedRemove(arg); 166 return self.map_args.contains(arg); 167 } 168 169 pub fn get(self: *CommandArgs, comptime arg: []const u8) ??[]const u8 { 170 comptime std.debug.assert(value_flags.has(arg)); // can only call `get` with flags included in `value_flags` 171 _ = self.unused_args.orderedRemove(arg); 172 return self.map_args.get(arg); 173 } 174 }; 175 176 /// parses the args into a format that can be directly used by a repo. 177 /// if any additional allocation needs to be done, the arena inside the cmd args will be used. 178 pub const Command = union(CommandKind) { 179 serve: srv.Options, 180 181 pub fn initMaybe(cmd_args: *CommandArgs) !?Command { 182 const command_kind = cmd_args.command_kind orelse return null; 183 switch (command_kind) { 184 .serve => { 185 if (cmd_args.positional_args.len != 0) return null; 186 187 var options: srv.Options = .{}; 188 189 if (cmd_args.get("--http-listen")) |val_maybe| { 190 options.http_listen = val_maybe orelse return error.HttpListenNeedsValue; 191 } 192 if (cmd_args.get("--ssh-listen")) |val_maybe| { 193 options.ssh_listen = val_maybe orelse return error.SshListenNeedsValue; 194 } 195 if (cmd_args.get("--wui-listen")) |val_maybe| { 196 options.wui_listen = val_maybe orelse return error.WuiListenNeedsValue; 197 } 198 if (cmd_args.get("--data-dir")) |val_maybe| { 199 options.data_dir = val_maybe orelse return error.DataDirNeedsValue; 200 } 201 202 return .{ .serve = options }; 203 }, 204 } 205 } 206 }; 207 208 /// parses the given args into a command if valid, and determines how it should be run 209 /// (via the TUI or CLI). 210 pub const CommandDispatch = union(enum) { 211 invalid: union(enum) { 212 command: []const u8, 213 argument: struct { 214 command: ?CommandKind, 215 value: []const u8, 216 }, 217 }, 218 help: ?CommandKind, 219 cli: Command, 220 221 pub fn init(cmd_args: *CommandArgs) !CommandDispatch { 222 const dispatch = try initIgnoreUnused(cmd_args); 223 if (cmd_args.unused_args.count() > 0) { 224 return .{ 225 .invalid = .{ 226 .argument = .{ 227 .command = switch (dispatch) { 228 .invalid => return dispatch, // if there was already an error, return it instead 229 .help => |cmd_kind_maybe| cmd_kind_maybe, 230 .cli => |command| command, 231 }, 232 .value = cmd_args.unused_args.keys()[0], 233 }, 234 }, 235 }; 236 } 237 return dispatch; 238 } 239 240 pub fn initIgnoreUnused(cmd_args: *CommandArgs) !CommandDispatch { 241 const show_help = cmd_args.contains("--help"); 242 243 if (cmd_args.command_kind) |command_kind| { 244 if (show_help) { 245 return .{ .help = command_kind }; 246 } else if (try Command.initMaybe(cmd_args)) |cmd| { 247 return .{ .cli = cmd }; 248 } else { 249 return .{ .help = command_kind }; 250 } 251 } else if (cmd_args.command_name) |command_name| { 252 return .{ .invalid = .{ .command = command_name } }; 253 } else if (show_help) { 254 return .{ .help = null }; 255 } else { 256 return .{ .help = null }; 257 } 258 } 259 }; 260