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