╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ command.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
49 break :blk indent; 50 }; 51 52 if (cmd_kind_maybe) |cmd_kind| { 53 const help = commandHelp(cmd_kind); 54 // name and description 55 try writer.print("{s}", .{help.name}); 56 for (0..print_indent - help.name.len) |_| try writer.print(" ", .{}); 57 var split_iter = std.mem.splitScalar(u8, help.descrip, '\n'); 58 try writer.print("{s}\n", .{split_iter.first()}); 59 while (split_iter.next()) |line| { 60 for (0..print_indent) |_| try writer.print(" ", .{}); 61 try writer.print("{s}\n", .{line}); 62 } 63 try writer.print("\n", .{}); 64 // example 65 split_iter = std.mem.splitScalar(u8, help.example, '\n'); 66 while (split_iter.next()) |line| { 67 for (0..print_indent) |_| try writer.print(" ", .{}); 68 try writer.print("{s}\n", .{line}); 69 } 70 } else { 71 try writer.print("help: haxy <command> [<args>]\n\n", .{}); 72 inline for (@typeInfo(CommandKind).@"enum".field_values) |value| { 73 const help = commandHelp(@fromBackingInt(@intCast(value))); 74 // name and description 75 try writer.print("{s}", .{help.name}); 76 for (0..print_indent - help.name.len) |_| try writer.print(" ", .{}); 77 var split_iter = std.mem.splitScalar(u8, help.descrip, '\n'); 78 try writer.print("{s}\n", .{split_iter.first()}); 79 while (split_iter.next()) |line| { 80 for (0..print_indent) |_| try writer.print(" ", .{}); 81 try writer.print("{s}\n", .{line}); 82 } 83 } 84 } 85 } 86 87 pub const CommandArgs = struct { 88 allocator: std.mem.Allocator, 89 arena: *std.heap.ArenaAllocator, 90 command_kind: ?CommandKind, 91 command_name: ?[]const u8, 92 positional_args: []const []const u8, 93 map_args: std.StringArrayHashMapUnmanaged(?[]const u8), 94 unused_args: std.StringArrayHashMapUnmanaged(void), 95 96 // flags that can have a value associated with them 97 // must be included here 98 const value_flags = std.StaticStringMap(void).initComptime(.{ 99 .{"--web-listen"}, 100 .{"--ssh-listen"}, 101 .{"--web-url"}, 102 .{"--ssh-url"}, 103 .{"--max-repo-size"}, 104 .{"--data-dir"}, 105 }); 106 107 pub fn init(allocator: std.mem.Allocator, args: []const []const u8) !CommandArgs { 108 const arena = try allocator.create(std.heap.ArenaAllocator); 109 arena.* = std.heap.ArenaAllocator.init(allocator); 110 errdefer { 111 arena.deinit(); 112 allocator.destroy(arena); 113 } 114 115 var positional_args: std.ArrayList([]const u8) = .empty; 116 var map_args: std.StringArrayHashMapUnmanaged(?[]const u8) = .empty; 117 var unused_args: std.StringArrayHashMapUnmanaged(void) = .empty; 118 119 for (args) |arg| { 120 if (arg.len > 1 and arg[0] == '-') { 121 try map_args.put(arena.allocator(), arg, null); 122 try unused_args.put(arena.allocator(), arg, {}); 123 } else { 124 // if the last key is a value flag and doesn't have a value yet, 125 // set this arg as its value 126 const keys = map_args.keys(); 127 if (keys.len > 0) { 128 const last_key = keys[keys.len - 1]; 129 if (map_args.get(last_key)) |last_val| { 130 if (value_flags.has(last_key) and last_val == null) { 131 try map_args.put(arena.allocator(), last_key, arg); 132 continue; 133 } 134 } 135 } 136 137 // in any other case, just consider it a positional arg 138 try positional_args.append(arena.allocator(), arg); 139 } 140 } 141 142 const args_slice = try positional_args.toOwnedSlice(arena.allocator()); 143 if (args_slice.len == 0) { 144 return .{ 145 .allocator = allocator, 146 .arena = arena, 147 .command_kind = null, 148 .command_name = null, 149 .positional_args = args_slice, 150 .map_args = map_args, 151 .unused_args = unused_args, 152 }; 153 } else { 154 const command_name = args_slice[0]; 155 const extra_args = args_slice[1..]; 156 157 const command_kind: ?CommandKind = inline for (0..@typeInfo(CommandKind).@"enum".field_names.len) |i| { 158 if (std.mem.eql(u8, command_name, commandHelp(@fromBackingInt(@intCast(i))).name)) { 159 break @fromBackingInt(@intCast(i)); 160 } 161 } else null; 162 163 return .{ 164 .allocator = allocator, 165 .arena = arena, 166 .command_kind = command_kind, 167 .command_name = command_name, 168 .positional_args = extra_args, 169 .map_args = map_args, 170 .unused_args = unused_args, 171 }; 172 } 173 } 174 175 pub fn deinit(self: *CommandArgs) void { 176 self.arena.deinit(); 177 self.allocator.destroy(self.arena); 178 } 179 180 pub fn contains(self: *CommandArgs, arg: []const u8) bool { 181 _ = self.unused_args.orderedRemove(arg); 182 return self.map_args.contains(arg); 183 } 184 185 pub fn get(self: *CommandArgs, comptime arg: []const u8) ??[]const u8 { 186 comptime std.debug.assert(value_flags.has(arg)); // can only call `get` with flags included in `value_flags` 187 _ = self.unused_args.orderedRemove(arg); 188 return self.map_args.get(arg); 189 } 190 }; 191 192 /// parses the args into a format that can be directly used by a repo. 193 /// if any additional allocation needs to be done, the arena inside the cmd args will be used. 194 pub const Command = union(CommandKind) { 195 serve: srv.Options, 196 delete_user: srv.DeleteUserOptions, 197 198 pub fn initMaybe(cmd_args: *CommandArgs) !?Command { 199 const command_kind = cmd_args.command_kind orelse return null; 200 switch (command_kind) { 201 .serve => { 202 return .{ .serve = try serveOptions(cmd_args) orelse return null }; 203 }, 204 .delete_user => { 205 if (cmd_args.positional_args.len != 1) return null; 206 var options: srv.DeleteUserOptions = .{ .name = cmd_args.positional_args[0] }; 207 if (cmd_args.get("--data-dir")) |value_maybe| options.data_dir = value_maybe orelse return error.DataDirNeedsValue; 208 return .{ .delete_user = options }; 209 }, 210 } 211 } 212 }; 213 214 pub fn parseServeOptions(allocator: std.mem.Allocator, args: []const []const u8) !srv.Options { 215 var cmd_args = try CommandArgs.init(allocator, args); 216 defer cmd_args.deinit(); 217 const options = try serveOptions(&cmd_args) orelse return error.InvalidServeArgument; 218 if (cmd_args.unused_args.count() != 0) return error.InvalidServeArgument; 219 return options; 220 } 221 222 fn serveOptions(cmd_args: *CommandArgs) !?srv.Options { 223 if (cmd_args.positional_args.len != 0) return null; 224 225 var options: srv.Options = .{}; 226 if (cmd_args.get("--web-listen")) |value_maybe| options.web_listen = value_maybe orelse return error.WebListenNeedsValue; 227 if (cmd_args.get("--ssh-listen")) |value_maybe| { 228 const value = value_maybe orelse return error.SshListenNeedsValue; 229 options.ssh_listen = if (std.mem.eql(u8, value, "none")) null else value; 230 } 231 // urls are joined with "/" + a path 232 if (cmd_args.get("--web-url")) |value_maybe| options.web_url = std.mem.trimEnd(u8, value_maybe orelse return error.WebUrlNeedsValue, "/"); 233 if (cmd_args.get("--ssh-url")) |value_maybe| options.ssh_url = std.mem.trimEnd(u8, value_maybe orelse return error.SshUrlNeedsValue, "/"); 234 if (cmd_args.get("--max-repo-size")) |value_maybe| options.max_repo_size = try parseSize(value_maybe orelse return error.MaxRepoSizeNeedsValue); 235 if (cmd_args.get("--data-dir")) |value_maybe| options.data_dir = value_maybe orelse return error.DataDirNeedsValue; 236 return options; 237 } 238 239 // a byte count with an optional K, M or G suffix 240 fn parseSize(value: []const u8) !u64 { 241 if (value.len == 0) return error.InvalidSize; 242 const shift: u6 = switch (value[value.len - 1]) { 243 'K', 'k' => 10, 244 'M', 'm' => 20, 245 'G', 'g' => 30, 246 else => 0, 247 }; 248 const digits = if (shift == 0) value else value[0 .. value.len - 1]; 249 const count = std.fmt.parseInt(u64, digits, 10) catch return error.InvalidSize; 250 return std.math.shlExact(u64, count, shift) catch error.InvalidSize; 251 } 252 253 /// parses the given args into a command if valid, and determines how it should be run 254 /// (via the TUI or CLI). 255 pub const CommandDispatch = union(enum) { 256 invalid: union(enum) { 257 command: []const u8, 258 argument: struct { 259 command: ?CommandKind, 260 value: []const u8, 261 }, 262 }, 263 help: ?CommandKind, 264 local, 265 cli: Command, 266 267 pub fn init(cmd_args: *CommandArgs) !CommandDispatch { 268 const dispatch = try initIgnoreUnused(cmd_args); 269 if (cmd_args.unused_args.count() > 0) { 270 return .{ 271 .invalid = .{ 272 .argument = .{ 273 .command = switch (dispatch) { 274 .invalid => return dispatch, // if there was already an error, return it instead 275 .help => |cmd_kind_maybe| cmd_kind_maybe, 276 .local => null, 277 .cli => |command| command, 278 }, 279 .value = cmd_args.unused_args.keys()[0], 280 }, 281 }, 282 }; 283 } 284 return dispatch; 285 } 286 287 pub fn initIgnoreUnused(cmd_args: *CommandArgs) !CommandDispatch { 288 const show_help = cmd_args.contains("--help"); 289 290 if (cmd_args.command_kind) |command_kind| { 291 if (show_help) { 292 return .{ .help = command_kind }; 293 } else if (try Command.initMaybe(cmd_args)) |cmd| { 294 return .{ .cli = cmd }; 295 } else { 296 return .{ .help = command_kind }; 297 } 298 } else if (cmd_args.command_name) |command_name| { 299 return .{ .invalid = .{ .command = command_name } }; 300 } else if (show_help) { 301 return .{ .help = null }; 302 } else { 303 return .local; 304 } 305 } 306 }; 307