this repo has no description

Compare changes

Choose any two refs to compare.

+29 -23
README.md
··· 19 ## Usage 20 21 ``` 22 - lsr [options] [directory] 23 24 --help Print this message and exit 25 --version Print the version string ··· 30 -A, --almost-all Like --all, but skips implicit "." and ".." directories 31 -C, --columns Print the output in columns 32 --color=WHEN When to use colors (always, auto, never) 33 - --group-directories-first When to use colors (always, auto, never) 34 --icons=WHEN When to display icons (always, auto, never) 35 -l, --long Display extended file metadata 36 37 ``` 38 ··· 43 (because io_uring). `lsr` does work on macOS/BSD as well, but will not see the 44 syscall batching benefits that are available with io_uring. 45 46 - | Program | Version | 47 - |:-------:|:-------:| 48 - | lsr | 0.1.0 | 49 - | ls | 9.7 | 50 - | eza | 0.21.3 | 51 - | lsd | 1.1.5 | 52 - | uutils | 0.0.30 | 53 54 ### Time 55 56 Data gathered with `hyperfine` on a directory of `n` plain files. 57 58 - | Program | n=10 | n=100 | n=1,000 | n=10,000 | 59 - |:-------------:|:--------:|:--------:|:-------:|:--------:| 60 - | lsr -al | 372.6 µs | 634.3 µs | 2.7 ms | 22.1 ms | 61 - | ls -al | 1.4 ms | 1.7 ms | 4.7 ms | 38.0 ms | 62 - | eza -al | 2.9 ms | 3.3 ms | 6.6 ms | 40.2 ms | 63 - | lsd -al | 2.1 ms | 3.5 ms | 17.0 ms | 153.4 ms | 64 - | uutils ls -al | 2.9 ms | 3.6 ms | 11.3 ms | 89.6 ms | 65 66 ### Syscalls 67 68 Data gathered with `strace -c` on a directory of `n` plain files. (Lower is better) 69 70 - | Program | n=10 | n=100 | n=1,000 | n=10,000 | 71 - |:-------------:|:----:|:-----:|:-------:|:--------:| 72 - | lsr -al | 20 | 28 | 105 | 848 | 73 - | ls -al | 405 | 675 | 3,377 | 30,396 | 74 - | eza -al | 319 | 411 | 1,320 | 10,364 | 75 - | lsd -al | 508 | 1,408 | 10,423 | 100,512 | 76 - | uutils ls -al | 445 | 986 | 6,397 | 10,005 |
··· 19 ## Usage 20 21 ``` 22 + lsr [options] [path] 23 24 --help Print this message and exit 25 --version Print the version string ··· 30 -A, --almost-all Like --all, but skips implicit "." and ".." directories 31 -C, --columns Print the output in columns 32 --color=WHEN When to use colors (always, auto, never) 33 + --group-directories-first Print all directories before printing regular files 34 + --hyperlinks=WHEN When to use OSC 8 hyperlinks (always, auto, never) 35 --icons=WHEN When to display icons (always, auto, never) 36 -l, --long Display extended file metadata 37 + -r, --reverse Reverse the sort order 38 + -t, --time Sort the entries by modification time, most recent first 39 40 ``` 41 ··· 46 (because io_uring). `lsr` does work on macOS/BSD as well, but will not see the 47 syscall batching benefits that are available with io_uring. 48 49 + | Program | Version | 50 + |:--------:|:-------:| 51 + | lsr | 0.1.0 | 52 + | ls | 9.7 | 53 + | eza | 0.21.3 | 54 + | lsd | 1.1.5 | 55 + | uutils | 0.0.30 | 56 + | busybox | 1.36.1 | 57 58 ### Time 59 60 Data gathered with `hyperfine` on a directory of `n` plain files. 61 62 + | Program | n=10 | n=100 | n=1,000 | n=10,000 | 63 + |:--------------:|:--------:|:--------:|:-------:|:--------:| 64 + | lsr -al | 372.6 µs | 634.3 µs | 2.7 ms | 22.1 ms | 65 + | busybox ls -al | 403.8 µs | 1.1 ms | 3.5 ms | 32.5 ms | 66 + | ls -al | 1.4 ms | 1.7 ms | 4.7 ms | 38.0 ms | 67 + | eza -al | 2.9 ms | 3.3 ms | 6.6 ms | 40.2 ms | 68 + | lsd -al | 2.1 ms | 3.5 ms | 17.0 ms | 153.4 ms | 69 + | uutils ls -al | 2.9 ms | 3.6 ms | 11.3 ms | 89.6 ms | 70 71 ### Syscalls 72 73 Data gathered with `strace -c` on a directory of `n` plain files. (Lower is better) 74 75 + | Program | n=10 | n=100 | n=1,000 | n=10,000 | 76 + |:--------------:|:----:|:-----:|:-------:|:--------:| 77 + | lsr -al | 20 | 28 | 105 | 848 | 78 + | busybox ls -al | 84 | 410 | 2,128 | 20,383 | 79 + | ls -al | 405 | 675 | 3,377 | 30,396 | 80 + | eza -al | 319 | 411 | 1,320 | 10,364 | 81 + | lsd -al | 508 | 1,408 | 10,423 | 100,512 | 82 + | uutils ls -al | 445 | 986 | 6,397 | 10,005 |
+4 -1
build.zig
··· 2 const zzdoc = @import("zzdoc"); 3 4 /// Must be kept in sync with git tags 5 - const version: std.SemanticVersion = .{ .major = 0, .minor = 2, .patch = 0 }; 6 7 pub fn build(b: *std.Build) void { 8 const target = b.standardTargetOptions(.{}); ··· 45 .name = "lsr", 46 .root_module = exe_mod, 47 }); 48 49 b.installArtifact(exe); 50 ··· 82 "-C", 83 b.build_root.path orelse ".", 84 "describe", 85 "--tags", 86 "--abbrev=9", 87 }, &code, .Ignore) catch {
··· 2 const zzdoc = @import("zzdoc"); 3 4 /// Must be kept in sync with git tags 5 + const version: std.SemanticVersion = .{ .major = 1, .minor = 0, .patch = 0 }; 6 7 pub fn build(b: *std.Build) void { 8 const target = b.standardTargetOptions(.{}); ··· 45 .name = "lsr", 46 .root_module = exe_mod, 47 }); 48 + exe.linkLibC(); 49 50 b.installArtifact(exe); 51 ··· 83 "-C", 84 b.build_root.path orelse ".", 85 "describe", 86 + "--match", 87 + "*.*.*", 88 "--tags", 89 "--abbrev=9", 90 }, &code, .Ignore) catch {
+8 -8
build.zig.zon
··· 1 .{ 2 .name = .lsr, 3 - .version = "0.2.0", 4 .fingerprint = 0x495d173f6002e86, // Changing this has security and trust implications. 5 6 - .minimum_zig_version = "0.14.0", 7 8 .dependencies = .{ 9 .ourio = .{ 10 - .url = "git+https://github.com/rockorager/ourio#54c1a1ed8d0994636770e5185ecdb59fe6d8535e", 11 - .hash = "ourio-0.0.0-_s-z0asOAgAhpi7gSpLLvWGj_4XURez4W9TWN6SGs5BP", 12 }, 13 .zeit = .{ 14 - .url = "git+https://github.com/rockorager/zeit#4496d1c40b2223c22a1341e175fc2ecd94cc0de9", 15 - .hash = "zeit-0.6.0-5I6bk1J1AgA13rteb6E0steXiOUKBYTzJZMMIuK9oEmb", 16 }, 17 .zzdoc = .{ 18 - .url = "git+https://github.com/rockorager/zzdoc#57e86eb4e621bc4a96fbe0dd89ad0986db6d0483", 19 - .hash = "zzdoc-0.0.0-tzT1PuPZAACr1jIJxjTrdOsLbfXS6idWFGfTq0gwxJiv", 20 }, 21 }, 22 .paths = .{
··· 1 .{ 2 .name = .lsr, 3 + .version = "1.0.0", 4 .fingerprint = 0x495d173f6002e86, // Changing this has security and trust implications. 5 6 + .minimum_zig_version = "0.15.1", 7 8 .dependencies = .{ 9 .ourio = .{ 10 + .url = "git+https://github.com/rockorager/ourio#07bf94db87a9aea70d6e1a1dd99cac6fb9d38b35", 11 + .hash = "ourio-0.0.0-_s-z0Z0XAgBU_BFjdY8QjGhJ8vcdIONPSErlYRwLoxfg", 12 }, 13 .zeit = .{ 14 + .url = "git+https://github.com/rockorager/zeit#74be5a2afb346b2a6a6349abbb609e89ec7e65a6", 15 + .hash = "zeit-0.6.0-5I6bk4t8AgCP0UGGHVF_khlmWZkAF5XtfQWEKCyLoptU", 16 }, 17 .zzdoc = .{ 18 + .url = "git+https://github.com/rockorager/zzdoc#a54223bdc13a80839ccf9f473edf3a171e777946", 19 + .hash = "zzdoc-0.0.0-tzT1Ph7cAAC5YmXQXiBJHAg41_A5AUAC5VOm7ShnUxlz", 20 }, 21 }, 22 .paths = .{
+11 -2
docs/lsr.1.scd
··· 6 7 # SYNOPSIS 8 9 - *lsr* [options...] [directory] 10 11 # DESCRIPTION 12 ··· 31 When to use colors (always, auto, never) 32 33 *--group-directories-first* 34 - When to use colors (always, auto, never) 35 36 *--help* 37 Print the help menu and exit 38 39 *--icons=WHEN* 40 When to display icons (always, auto, never) 41 42 *-l*, *--long* 43 Display extended file metadata 44 45 *--version* 46 Print the version and exit
··· 6 7 # SYNOPSIS 8 9 + *lsr* [options...] [path] 10 11 # DESCRIPTION 12 ··· 31 When to use colors (always, auto, never) 32 33 *--group-directories-first* 34 + Print all directories before printing regular files 35 36 *--help* 37 Print the help menu and exit 38 39 + *--hyperlinks=WHEN* 40 + When to use OSC 8 hyperlinks (always, auto, never) 41 + 42 *--icons=WHEN* 43 When to display icons (always, auto, never) 44 45 *-l*, *--long* 46 Display extended file metadata 47 + 48 + *-r*, *--reverse* 49 + Reverse the sort order 50 + 51 + *-t*, *--time* 52 + Sort the entries by modification time, most recent first 53 54 *--version* 55 Print the version and exit
+1 -1
flake.nix
··· 16 17 packages.default = pkgs.stdenv.mkDerivation { 18 pname = "lsr"; 19 - version = "0.1.0"; 20 doCheck = false; 21 src = ./.; 22
··· 16 17 packages.default = pkgs.stdenv.mkDerivation { 18 pname = "lsr"; 19 + version = "1.0.0"; 20 doCheck = false; 21 src = ./.; 22
+2 -2
nix/cache.nix
··· 2 3 pkgs.stdenv.mkDerivation { 4 pname = "lsr-cache"; 5 - version = "0.1.0"; 6 doCheck = false; 7 src = ../.; 8 ··· 17 mv $ZIG_GLOBAL_CACHE_DIR/p $out 18 ''; 19 20 - outputHash = "sha256-hAq1/uE9eu/82+e079y+v9EnN0ViXX7k3GwkgQkxOyo="; 21 outputHashMode = "recursive"; 22 outputHashAlgo = "sha256"; 23 }
··· 2 3 pkgs.stdenv.mkDerivation { 4 pname = "lsr-cache"; 5 + version = "1.0.0"; 6 doCheck = false; 7 src = ../.; 8 ··· 17 mv $ZIG_GLOBAL_CACHE_DIR/p $out 18 ''; 19 20 + outputHash = "sha256-bfc2dlQa1VGq9S6OBeQawAJuvfxU4kgFtQ13fuKhdZc="; 21 outputHashMode = "recursive"; 22 outputHashAlgo = "sha256"; 23 }
+490 -111
src/main.zig
··· 2 const builtin = @import("builtin"); 3 const ourio = @import("ourio"); 4 const zeit = @import("zeit"); 5 const build_options = @import("build_options"); 6 7 const posix = std.posix; 8 9 const usage = 10 - \\Usage: 11 - \\ lsr [options] [directory] 12 \\ 13 \\ --help Print this message and exit 14 \\ --version Print the version string ··· 19 \\ -A, --almost-all Like --all, but skips implicit "." and ".." directories 20 \\ -C, --columns Print the output in columns 21 \\ --color=WHEN When to use colors (always, auto, never) 22 - \\ --group-directories-first When to use colors (always, auto, never) 23 \\ --icons=WHEN When to display icons (always, auto, never) 24 \\ -l, --long Display extended file metadata 25 \\ 26 ; 27 ··· 33 color: When = .auto, 34 shortview: enum { columns, oneline } = .oneline, 35 @"group-directories-first": bool = true, 36 icons: When = .auto, 37 long: bool = false, 38 39 - directory: [:0]const u8 = ".", 40 41 winsize: ?posix.winsize = null, 42 colors: Colors = .none, ··· 101 } 102 } 103 104 fn isatty(self: Options) bool { 105 return self.winsize != null; 106 } ··· 126 127 var cmd: Command = .{ .arena = allocator }; 128 129 - cmd.opts.winsize = getWinsize(std.io.getStdOut().handle); 130 131 cmd.opts.shortview = if (cmd.opts.isatty()) .columns else .oneline; 132 133 - const stdout = std.io.getStdOut().writer(); 134 - const stderr = std.io.getStdErr().writer(); 135 - var bw = std.io.bufferedWriter(stdout); 136 137 var args = std.process.args(); 138 // skip binary ··· 147 'A' => cmd.opts.@"almost-all" = true, 148 'C' => cmd.opts.shortview = .columns, 149 'a' => cmd.opts.all = true, 150 'l' => cmd.opts.long = true, 151 else => { 152 - try stderr.print("Invalid opt: '{c}'", .{b}); 153 std.process.exit(1); 154 }, 155 } ··· 161 const val = split.rest(); 162 if (eql(opt, "all")) { 163 cmd.opts.all = parseArgBool(val) orelse { 164 - try stderr.print("Invalid boolean: '{s}'", .{val}); 165 std.process.exit(1); 166 }; 167 } else if (eql(opt, "long")) { 168 cmd.opts.long = parseArgBool(val) orelse { 169 - try stderr.print("Invalid boolean: '{s}'", .{val}); 170 std.process.exit(1); 171 }; 172 } else if (eql(opt, "almost-all")) { 173 cmd.opts.@"almost-all" = parseArgBool(val) orelse { 174 - try stderr.print("Invalid boolean: '{s}'", .{val}); 175 std.process.exit(1); 176 }; 177 } else if (eql(opt, "group-directories-first")) { 178 cmd.opts.@"group-directories-first" = parseArgBool(val) orelse { 179 - try stderr.print("Invalid boolean: '{s}'", .{val}); 180 std.process.exit(1); 181 }; 182 } else if (eql(opt, "color")) { 183 cmd.opts.color = std.meta.stringToEnum(Options.When, val) orelse { 184 - try stderr.print("Invalid color option: '{s}'", .{val}); 185 std.process.exit(1); 186 }; 187 } else if (eql(opt, "icons")) { 188 cmd.opts.icons = std.meta.stringToEnum(Options.When, val) orelse { 189 - try stderr.print("Invalid color option: '{s}'", .{val}); 190 std.process.exit(1); 191 }; 192 } else if (eql(opt, "columns")) { 193 const c = parseArgBool(val) orelse { 194 - try stderr.print("Invalid columns option: '{s}'", .{val}); 195 std.process.exit(1); 196 }; 197 cmd.opts.shortview = if (c) .columns else .oneline; 198 } else if (eql(opt, "oneline")) { 199 const o = parseArgBool(val) orelse { 200 - try stderr.print("Invalid oneline option: '{s}'", .{val}); 201 std.process.exit(1); 202 }; 203 cmd.opts.shortview = if (o) .oneline else .columns; 204 } else if (eql(opt, "help")) { 205 return stderr.writeAll(usage); 206 } else if (eql(opt, "version")) { 207 - try bw.writer().print("lsr {s}\r\n", .{build_options.version}); 208 - try bw.flush(); 209 return; 210 } else { 211 - try stderr.print("Invalid opt: '{s}'", .{opt}); 212 std.process.exit(1); 213 } 214 }, 215 .positional => { 216 - cmd.opts.directory = arg; 217 }, 218 } 219 } ··· 222 cmd.opts.colors = .default; 223 } 224 225 - var ring: ourio.Ring = try .init(allocator, queue_size); 226 - defer ring.deinit(); 227 228 - _ = try ring.open(cmd.opts.directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{ 229 - .ptr = &cmd, 230 - .cb = onCompletion, 231 - .msg = @intFromEnum(Msg.cwd), 232 - }); 233 234 - if (cmd.opts.long) { 235 - _ = try ring.open("/etc/localtime", .{ .CLOEXEC = true }, 0, .{ 236 .ptr = &cmd, 237 .cb = onCompletion, 238 - .msg = @intFromEnum(Msg.localtime), 239 }); 240 - _ = try ring.open("/etc/passwd", .{ .CLOEXEC = true }, 0, .{ 241 - .ptr = &cmd, 242 - .cb = onCompletion, 243 - .msg = @intFromEnum(Msg.passwd), 244 - }); 245 - _ = try ring.open("/etc/group", .{ .CLOEXEC = true }, 0, .{ 246 - .ptr = &cmd, 247 - .cb = onCompletion, 248 - .msg = @intFromEnum(Msg.group), 249 - }); 250 - } 251 252 - try ring.run(.until_done); 253 254 - if (cmd.entries.len == 0) return; 255 256 - if (cmd.opts.long) { 257 - try printLong(cmd, bw.writer()); 258 - } else switch (cmd.opts.shortview) { 259 - .columns => try printShortColumns(cmd, bw.writer()), 260 - .oneline => try printShortOnePerLine(cmd, bw.writer()), 261 } 262 - try bw.flush(); 263 } 264 265 fn printShortColumns(cmd: Command, writer: anytype) !void { ··· 332 for (columns.items, 0..) |column, i| { 333 if (row >= column.entries.len) continue; 334 const entry = column.entries[row]; 335 - try printShortEntry(column.entries[row], cmd.opts, writer); 336 337 if (i < columns.items.len - 1) { 338 const spaces = column.width - (icon_width + entry.name.len); 339 - try writer.writeByteNTimes(' ', spaces); 340 } 341 } 342 try writer.writeAll("\r\n"); ··· 347 return idx + n_short_cols >= n_cols; 348 } 349 350 - fn printShortEntry(entry: Entry, opts: Options, writer: anytype) !void { 351 const colors = opts.colors; 352 if (opts.useIcons()) { 353 - const icon = Icon.get(entry, opts); 354 355 if (opts.useColor()) { 356 try writer.writeAll(icon.color); ··· 371 } 372 }, 373 } 374 - try writer.writeAll(entry.name); 375 - try writer.writeAll(colors.reset); 376 } 377 378 fn printShortOneRow(cmd: Command, writer: anytype) !void { ··· 385 386 fn printShortOnePerLine(cmd: Command, writer: anytype) !void { 387 for (cmd.entries) |entry| { 388 - try printShortEntry(entry, cmd.opts, writer); 389 try writer.writeAll("\r\n"); 390 } 391 } 392 393 - fn printLong(cmd: Command, writer: anytype) !void { 394 const tz = cmd.tz.?; 395 const now = zeit.instant(.{}) catch unreachable; 396 const one_year_ago = try now.subtract(.{ .days = 365 }); ··· 402 var n_size: usize = 0; 403 var n_suff: usize = 0; 404 for (cmd.entries) |entry| { 405 - const group = cmd.getGroup(entry.statx.gid); 406 - const user = cmd.getUser(entry.statx.uid); 407 408 var buf: [16]u8 = undefined; 409 const size = try entry.humanReadableSize(&buf); ··· 434 }; 435 436 for (cmd.entries) |entry| { 437 - const user: User = cmd.getUser(entry.statx.uid) orelse 438 .{ 439 .uid = entry.statx.uid, 440 .name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.uid}), 441 }; 442 - const group: Group = cmd.getGroup(entry.statx.gid) orelse 443 .{ 444 .gid = entry.statx.gid, 445 .name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.gid}), ··· 453 try writer.writeAll(&mode); 454 try writer.writeByte(' '); 455 try writer.writeAll(user.name); 456 - try writer.writeByteNTimes(' ', longest_user - user.name.len); 457 try writer.writeByte(' '); 458 try writer.writeAll(group.name); 459 - try writer.writeByteNTimes(' ', longest_group - group.name.len); 460 try writer.writeByte(' '); 461 462 var size_buf: [16]u8 = undefined; 463 const size = try entry.humanReadableSize(&size_buf); 464 const suffix = entry.humanReadableSuffix(); 465 466 - try writer.writeByteNTimes(' ', longest_size - size.len); 467 try writer.writeAll(size); 468 try writer.writeByte(' '); 469 try writer.writeAll(suffix); 470 - try writer.writeByteNTimes(' ', longest_suffix - suffix.len); 471 try writer.writeByte(' '); 472 473 try writer.print("{d: >2} {s} ", .{ ··· 482 } 483 484 if (cmd.opts.useIcons()) { 485 - const icon = Icon.get(entry, cmd.opts); 486 487 if (cmd.opts.useColor()) { 488 try writer.writeAll(icon.color); ··· 504 } 505 }, 506 } 507 - try writer.writeAll(entry.name); 508 try writer.writeAll(colors.reset); 509 510 - if (entry.kind == .sym_link) { 511 - try writer.writeAll(" -> "); 512 - const color = if (entry.symlink_missing) 513 - colors.symlink_missing 514 - else 515 - colors.symlink_target; 516 - try writer.writeAll(color); 517 - try writer.writeAll(entry.link_name); 518 - try writer.writeAll(colors.reset); 519 } 520 521 try writer.writeAll("\r\n"); ··· 527 opts: Options = .{}, 528 entries: []Entry = &.{}, 529 entry_idx: usize = 0, 530 531 tz: ?zeit.TimeZone = null, 532 groups: std.ArrayListUnmanaged(Group) = .empty, 533 users: std.ArrayListUnmanaged(User) = .empty, 534 535 - fn getUser(self: Command, uid: posix.uid_t) ?User { 536 for (self.users.items) |user| { 537 if (user.uid == uid) return user; 538 } 539 return null; 540 } 541 542 - fn getGroup(self: Command, gid: posix.gid_t) ?Group { 543 for (self.groups.items) |group| { 544 if (group.gid == gid) return group; 545 } 546 return null; 547 } 548 }; ··· 560 }; 561 562 const User = struct { 563 - uid: posix.uid_t, 564 name: []const u8, 565 566 fn lessThan(_: void, lhs: User, rhs: User) bool { ··· 569 }; 570 571 const Group = struct { 572 - gid: posix.gid_t, 573 name: []const u8, 574 575 fn lessThan(_: void, lhs: Group, rhs: Group) bool { ··· 593 } 594 }; 595 596 const Entry = struct { 597 name: [:0]const u8, 598 kind: std.fs.File.Kind, 599 statx: ourio.Statx, 600 - link_name: [:0]const u8 = "", 601 - symlink_missing: bool = false, 602 603 fn modeStr(self: Entry) [10]u8 { 604 var mode = [_]u8{'-'} ** 10; ··· 678 679 switch (msg) { 680 .cwd => { 681 - const fd = try result.open; 682 // we are async, no need to defer! 683 _ = try io.close(fd, .{}); 684 const dir: std.fs.Dir = .{ .fd = fd }; 685 686 var temp_results: std.ArrayListUnmanaged(MinimalEntry) = .empty; 687 ··· 702 703 var iter = dir.iterate(); 704 while (try iter.next()) |dirent| { 705 - if (!cmd.opts.@"almost-all" and std.mem.startsWith(u8, dirent.name, ".")) continue; 706 const nameZ = try cmd.arena.dupeZ(u8, dirent.name); 707 try temp_results.append(cmd.arena, .{ 708 .name = nameZ, ··· 732 } 733 const path = try std.fs.path.joinZ( 734 cmd.arena, 735 - &.{ cmd.opts.directory, entry.name }, 736 ); 737 738 if (entry.kind == .sym_link) { ··· 740 741 // NOTE: Sadly, we can't do readlink via io_uring 742 const link = try posix.readlink(path, &buf); 743 - entry.link_name = try cmd.arena.dupeZ(u8, link); 744 } 745 _ = try io.stat(path, &entry.statx, .{ 746 .cb = onCompletion, ··· 756 // Largest TZ file on my system is Asia/Hebron at 4791 bytes. We allocate an amount 757 // sufficiently more than that to make sure we do this in a single pass 758 const buffer = try cmd.arena.alloc(u8, 8192); 759 - _ = try io.read(fd, buffer, .{ 760 .cb = onCompletion, 761 .ptr = cmd, 762 .msg = @intFromEnum(Msg.read_localtime), ··· 767 const n = try result.read; 768 _ = try io.close(task.req.read.fd, .{}); 769 const bytes = task.req.read.buffer[0..n]; 770 - var fbs = std.io.fixedBufferStream(bytes); 771 - const tz = try zeit.timezone.TZInfo.parse(cmd.arena, fbs.reader()); 772 cmd.tz = .{ .tzinfo = tz }; 773 }, 774 ··· 778 // TODO: stat this or do multiple reads. We'll never know a good bound unless we go 779 // really big 780 const buffer = try cmd.arena.alloc(u8, 8192 * 2); 781 - _ = try io.read(fd, buffer, .{ 782 .cb = onCompletion, 783 .ptr = cmd, 784 .msg = @intFromEnum(Msg.read_passwd), ··· 801 // <name>:<throwaway>:<uid><...garbage> 802 while (lines.next()) |line| { 803 if (line.len == 0) continue; 804 var iter = std.mem.splitScalar(u8, line, ':'); 805 const name = iter.first(); 806 _ = iter.next(); ··· 808 809 const user: User = .{ 810 .name = name, 811 - .uid = try std.fmt.parseInt(u32, uid, 10), 812 }; 813 814 cmd.users.appendAssumeCapacity(user); ··· 820 const fd = try result.open; 821 822 const buffer = try cmd.arena.alloc(u8, 8192); 823 - _ = try io.read(fd, buffer, .{ 824 .cb = onCompletion, 825 .ptr = cmd, 826 .msg = @intFromEnum(Msg.read_group), ··· 843 // <name>:<throwaway>:<uid><...garbage> 844 while (lines.next()) |line| { 845 if (line.len == 0) continue; 846 var iter = std.mem.splitScalar(u8, line, ':'); 847 const name = iter.first(); 848 _ = iter.next(); ··· 850 851 const group: Group = .{ 852 .name = name, 853 - .gid = try std.fmt.parseInt(u32, gid, 10), 854 }; 855 856 cmd.groups.appendAssumeCapacity(group); ··· 859 }, 860 861 .stat => { 862 - _ = result.statx catch { 863 const entry: *Entry = @fieldParentPtr("statx", task.req.statx.result); 864 - if (entry.symlink_missing) { 865 - // we already got here. Just zero out the statx; 866 entry.statx = std.mem.zeroInit(ourio.Statx, entry.statx); 867 return; 868 } 869 870 - entry.symlink_missing = true; 871 _ = try io.lstat(task.req.statx.path, task.req.statx.result, .{ 872 .cb = onCompletion, 873 .ptr = cmd, ··· 882 cmd.entry_idx += 1; 883 const path = try std.fs.path.joinZ( 884 cmd.arena, 885 - &.{ cmd.opts.directory, entry.name }, 886 ); 887 888 if (entry.kind == .sym_link) { ··· 890 891 // NOTE: Sadly, we can't do readlink via io_uring 892 const link = try posix.readlink(path, &buf); 893 - entry.link_name = try cmd.arena.dupeZ(u8, link); 894 } 895 _ = try io.stat(path, &entry.statx, .{ 896 .cb = onCompletion, ··· 928 const json: Icon = .{ .icon = "", .color = Options.Colors.blue }; 929 const lua: Icon = .{ .icon = "󰢱", .color = Options.Colors.blue }; 930 const markdown: Icon = .{ .icon = "", .color = "" }; 931 const python: Icon = .{ .icon = "", .color = Options.Colors.yellow }; 932 const rust: Icon = .{ .icon = "", .color = "" }; 933 const typescript: Icon = .{ .icon = "", .color = Options.Colors.blue }; 934 const zig: Icon = .{ .icon = "", .color = "\x1b[38:2:247:164:29m" }; 935 936 - const by_name: std.StaticStringMap(Icon) = .initComptime(.{}); 937 938 const by_extension: std.StaticStringMap(Icon) = .initComptime(.{ 939 .{ "cjs", Icon.javascript }, 940 .{ "css", Icon.css }, 941 .{ "gif", Icon.image }, 942 .{ "go", Icon.go }, 943 .{ "html", Icon.html }, ··· 951 .{ "mjs", Icon.javascript }, 952 .{ "mkv", Icon.video }, 953 .{ "mp4", Icon.video }, 954 .{ "png", Icon.image }, 955 .{ "py", Icon.python }, 956 .{ "rs", Icon.rust }, ··· 961 .{ "zon", Icon.zig }, 962 }); 963 964 - fn get(entry: Entry, opts: Options) Icon { 965 // 1. By name 966 - // 2. By extension 967 - // 3. By type 968 if (by_name.get(entry.name)) |icon| return icon; 969 970 - const ext = std.fs.path.extension(entry.name); 971 - if (ext.len > 0) { 972 - const ft = ext[1..]; 973 - if (by_extension.get(ft)) |icon| return icon; 974 - } 975 - 976 switch (entry.kind) { 977 .block_device => return drive, 978 .character_device => return drive, 979 .directory => return directory, 980 .file => { 981 if (entry.isExecutable()) { 982 return executable; 983 } ··· 985 }, 986 .named_pipe => return pipe, 987 .sym_link => { 988 - if (opts.long and posix.S.ISDIR(entry.statx.mode)) { 989 return symlink_dir; 990 } 991 return symlink; ··· 1032 if (std.mem.startsWith(u8, a, "-")) return .short; 1033 return .positional; 1034 }
··· 2 const builtin = @import("builtin"); 3 const ourio = @import("ourio"); 4 const zeit = @import("zeit"); 5 + const natord = @import("natord.zig"); 6 const build_options = @import("build_options"); 7 + const grp = @cImport({ 8 + @cInclude("grp.h"); 9 + }); 10 11 const posix = std.posix; 12 13 const usage = 14 + \\Usage: 15 + \\ lsr [options] [path...] 16 \\ 17 \\ --help Print this message and exit 18 \\ --version Print the version string ··· 23 \\ -A, --almost-all Like --all, but skips implicit "." and ".." directories 24 \\ -C, --columns Print the output in columns 25 \\ --color=WHEN When to use colors (always, auto, never) 26 + \\ --group-directories-first Print all directories before printing regular files 27 + \\ --hyperlinks=WHEN When to use OSC 8 hyperlinks (always, auto, never) 28 \\ --icons=WHEN When to display icons (always, auto, never) 29 \\ -l, --long Display extended file metadata 30 + \\ -r, --reverse Reverse the sort order 31 + \\ -t, --time Sort the entries by modification time, most recent first 32 + \\ --tree[=DEPTH] Display entries in a tree format (optional limit depth) 33 \\ 34 ; 35 ··· 41 color: When = .auto, 42 shortview: enum { columns, oneline } = .oneline, 43 @"group-directories-first": bool = true, 44 + hyperlinks: When = .auto, 45 icons: When = .auto, 46 long: bool = false, 47 + sort_by_mod_time: bool = false, 48 + reverse_sort: bool = false, 49 + tree: bool = false, 50 + tree_depth: ?usize = null, 51 52 + directories: std.ArrayListUnmanaged([:0]const u8) = .empty, 53 + file: ?[]const u8 = null, 54 55 winsize: ?posix.winsize = null, 56 colors: Colors = .none, ··· 115 } 116 } 117 118 + fn useHyperlinks(self: Options) bool { 119 + switch (self.hyperlinks) { 120 + .never => return false, 121 + .always => return true, 122 + .auto => return self.isatty(), 123 + } 124 + } 125 + 126 + fn showDotfiles(self: Options) bool { 127 + return self.@"almost-all" or self.all; 128 + } 129 + 130 fn isatty(self: Options) bool { 131 return self.winsize != null; 132 } ··· 152 153 var cmd: Command = .{ .arena = allocator }; 154 155 + cmd.opts.winsize = getWinsize(std.fs.File.stdout().handle); 156 157 cmd.opts.shortview = if (cmd.opts.isatty()) .columns else .oneline; 158 159 + var stdout_buf: [4096]u8 = undefined; 160 + var stderr_buf: [4096]u8 = undefined; 161 + var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); 162 + var stderr_writer = std.fs.File.stderr().writer(&stderr_buf); 163 + var stdout = &stdout_writer.interface; 164 + var stderr = &stderr_writer.interface; 165 166 var args = std.process.args(); 167 // skip binary ··· 176 'A' => cmd.opts.@"almost-all" = true, 177 'C' => cmd.opts.shortview = .columns, 178 'a' => cmd.opts.all = true, 179 + 'h' => {}, // human-readable: present for compatibility 180 'l' => cmd.opts.long = true, 181 + 'r' => cmd.opts.reverse_sort = true, 182 + 't' => cmd.opts.sort_by_mod_time = true, 183 else => { 184 + try stderr.print("Invalid opt: '{c}'\n", .{b}); 185 std.process.exit(1); 186 }, 187 } ··· 193 const val = split.rest(); 194 if (eql(opt, "all")) { 195 cmd.opts.all = parseArgBool(val) orelse { 196 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 197 std.process.exit(1); 198 }; 199 } else if (eql(opt, "long")) { 200 cmd.opts.long = parseArgBool(val) orelse { 201 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 202 std.process.exit(1); 203 }; 204 } else if (eql(opt, "almost-all")) { 205 cmd.opts.@"almost-all" = parseArgBool(val) orelse { 206 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 207 std.process.exit(1); 208 }; 209 } else if (eql(opt, "group-directories-first")) { 210 cmd.opts.@"group-directories-first" = parseArgBool(val) orelse { 211 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 212 std.process.exit(1); 213 }; 214 } else if (eql(opt, "color")) { 215 cmd.opts.color = std.meta.stringToEnum(Options.When, val) orelse { 216 + try stderr.print("Invalid color option: '{s}'\n", .{val}); 217 + std.process.exit(1); 218 + }; 219 + } else if (eql(opt, "human-readable")) { 220 + // no-op: present for compatibility 221 + } else if (eql(opt, "hyperlinks")) { 222 + cmd.opts.hyperlinks = std.meta.stringToEnum(Options.When, val) orelse { 223 + try stderr.print("Invalid hyperlinks option: '{s}'\n", .{val}); 224 std.process.exit(1); 225 }; 226 } else if (eql(opt, "icons")) { 227 cmd.opts.icons = std.meta.stringToEnum(Options.When, val) orelse { 228 + try stderr.print("Invalid color option: '{s}'\n", .{val}); 229 std.process.exit(1); 230 }; 231 } else if (eql(opt, "columns")) { 232 const c = parseArgBool(val) orelse { 233 + try stderr.print("Invalid columns option: '{s}'\n", .{val}); 234 std.process.exit(1); 235 }; 236 cmd.opts.shortview = if (c) .columns else .oneline; 237 } else if (eql(opt, "oneline")) { 238 const o = parseArgBool(val) orelse { 239 + try stderr.print("Invalid oneline option: '{s}'\n", .{val}); 240 std.process.exit(1); 241 }; 242 cmd.opts.shortview = if (o) .oneline else .columns; 243 + } else if (eql(opt, "time")) { 244 + cmd.opts.sort_by_mod_time = parseArgBool(val) orelse { 245 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 246 + std.process.exit(1); 247 + }; 248 + } else if (eql(opt, "reverse")) { 249 + cmd.opts.reverse_sort = parseArgBool(val) orelse { 250 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 251 + std.process.exit(1); 252 + }; 253 + } else if (eql(opt, "tree")) { 254 + if (val.len == 0) { 255 + cmd.opts.tree = true; 256 + cmd.opts.tree_depth = null; // unlimited depth 257 + } else { 258 + cmd.opts.tree = true; 259 + cmd.opts.tree_depth = std.fmt.parseInt(usize, val, 10) catch { 260 + try stderr.print("Invalid tree depth: '{s}'\n", .{val}); 261 + std.process.exit(1); 262 + }; 263 + } 264 } else if (eql(opt, "help")) { 265 return stderr.writeAll(usage); 266 } else if (eql(opt, "version")) { 267 + try stdout.print("lsr {s}\r\n", .{build_options.version}); 268 + try stdout.flush(); 269 return; 270 } else { 271 + try stderr.print("Invalid opt: '{s}'\n", .{opt}); 272 std.process.exit(1); 273 } 274 }, 275 .positional => { 276 + try cmd.opts.directories.append(allocator, arg); 277 }, 278 } 279 } ··· 282 cmd.opts.colors = .default; 283 } 284 285 + if (cmd.opts.directories.items.len == 0) { 286 + try cmd.opts.directories.append(allocator, "."); 287 + } 288 + 289 + const multiple_dirs = cmd.opts.directories.items.len > 1; 290 + 291 + for (cmd.opts.directories.items, 0..) |directory, dir_idx| { 292 + cmd.entries = &.{}; 293 + cmd.entry_idx = 0; 294 + cmd.symlinks.clearRetainingCapacity(); 295 + cmd.groups.clearRetainingCapacity(); 296 + cmd.users.clearRetainingCapacity(); 297 + cmd.tz = null; 298 + cmd.opts.file = null; 299 + cmd.current_directory = directory; 300 301 + var ring: ourio.Ring = try .init(allocator, queue_size); 302 + defer ring.deinit(); 303 304 + _ = try ring.open(directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{ 305 .ptr = &cmd, 306 .cb = onCompletion, 307 + .msg = @intFromEnum(Msg.cwd), 308 }); 309 + 310 + if (cmd.opts.long) { 311 + _ = try ring.open("/etc/localtime", .{ .CLOEXEC = true }, 0, .{ 312 + .ptr = &cmd, 313 + .cb = onCompletion, 314 + .msg = @intFromEnum(Msg.localtime), 315 + }); 316 + _ = try ring.open("/etc/passwd", .{ .CLOEXEC = true }, 0, .{ 317 + .ptr = &cmd, 318 + .cb = onCompletion, 319 + .msg = @intFromEnum(Msg.passwd), 320 + }); 321 + _ = try ring.open("/etc/group", .{ .CLOEXEC = true }, 0, .{ 322 + .ptr = &cmd, 323 + .cb = onCompletion, 324 + .msg = @intFromEnum(Msg.group), 325 + }); 326 + } 327 + 328 + try ring.run(.until_done); 329 330 + if (cmd.entries.len == 0) { 331 + if (multiple_dirs and dir_idx < cmd.opts.directories.items.len - 1) { 332 + try stdout.writeAll("\r\n"); 333 + } 334 + continue; 335 + } 336 337 + std.sort.pdq(Entry, cmd.entries, cmd.opts, Entry.lessThan); 338 339 + if (cmd.opts.reverse_sort) { 340 + std.mem.reverse(Entry, cmd.entries); 341 + } 342 + 343 + if (multiple_dirs and !cmd.opts.tree) { 344 + if (dir_idx > 0) try stdout.writeAll("\r\n"); 345 + try stdout.print("{s}:\r\n", .{directory}); 346 + } 347 + 348 + if (cmd.opts.tree) { 349 + if (multiple_dirs and dir_idx > 0) try stdout.writeAll("\r\n"); 350 + try printTree(cmd, stdout); 351 + } else if (cmd.opts.long) { 352 + try printLong(&cmd, stdout); 353 + } else switch (cmd.opts.shortview) { 354 + .columns => try printShortColumns(cmd, stdout), 355 + .oneline => try printShortOnePerLine(cmd, stdout), 356 + } 357 } 358 + try stdout.flush(); 359 } 360 361 fn printShortColumns(cmd: Command, writer: anytype) !void { ··· 428 for (columns.items, 0..) |column, i| { 429 if (row >= column.entries.len) continue; 430 const entry = column.entries[row]; 431 + try printShortEntry(column.entries[row], cmd, writer); 432 433 if (i < columns.items.len - 1) { 434 const spaces = column.width - (icon_width + entry.name.len); 435 + var space_buf = [_][]const u8{" "}; 436 + try writer.writeSplatAll(&space_buf, spaces); 437 } 438 } 439 try writer.writeAll("\r\n"); ··· 444 return idx + n_short_cols >= n_cols; 445 } 446 447 + fn printShortEntry(entry: Entry, cmd: Command, writer: anytype) !void { 448 + const opts = cmd.opts; 449 const colors = opts.colors; 450 if (opts.useIcons()) { 451 + const icon = Icon.get(entry); 452 453 if (opts.useColor()) { 454 try writer.writeAll(icon.color); ··· 469 } 470 }, 471 } 472 + 473 + if (opts.useHyperlinks()) { 474 + const path = try std.fs.path.join(cmd.arena, &.{ cmd.current_directory, entry.name }); 475 + try writer.print("\x1b]8;;file://{s}\x1b\\", .{path}); 476 + try writer.writeAll(entry.name); 477 + try writer.writeAll("\x1b]8;;\x1b\\"); 478 + try writer.writeAll(colors.reset); 479 + } else { 480 + try writer.writeAll(entry.name); 481 + try writer.writeAll(colors.reset); 482 + } 483 } 484 485 fn printShortOneRow(cmd: Command, writer: anytype) !void { ··· 492 493 fn printShortOnePerLine(cmd: Command, writer: anytype) !void { 494 for (cmd.entries) |entry| { 495 + try printShortEntry(entry, cmd, writer); 496 + try writer.writeAll("\r\n"); 497 + } 498 + } 499 + 500 + fn drawTreePrefix(writer: anytype, prefix_list: []const bool, is_last: bool) !void { 501 + for (prefix_list) |is_last_at_level| { 502 + if (is_last_at_level) { 503 + try writer.writeAll(" "); 504 + } else { 505 + try writer.writeAll("│ "); 506 + } 507 + } 508 + 509 + if (is_last) { 510 + try writer.writeAll("└── "); 511 + } else { 512 + try writer.writeAll("├── "); 513 + } 514 + } 515 + 516 + fn printTree(cmd: Command, writer: anytype) !void { 517 + const dir_name = if (std.mem.eql(u8, cmd.current_directory, ".")) blk: { 518 + var buf: [std.fs.max_path_bytes]u8 = undefined; 519 + const cwd = try std.process.getCwd(&buf); 520 + break :blk std.fs.path.basename(cwd); 521 + } else std.fs.path.basename(cmd.current_directory); 522 + 523 + try writer.print("{s}\n", .{dir_name}); 524 + 525 + const max_depth = cmd.opts.tree_depth orelse std.math.maxInt(usize); 526 + var prefix_list: std.ArrayList(bool) = .{}; 527 + 528 + for (cmd.entries, 0..) |entry, i| { 529 + const is_last = i == cmd.entries.len - 1; 530 + 531 + try drawTreePrefix(writer, prefix_list.items, is_last); 532 + try printShortEntry(entry, cmd, writer); 533 + try writer.writeAll("\r\n"); 534 + 535 + if (entry.kind == .directory and max_depth > 0) { 536 + const full_path = try std.fs.path.joinZ(cmd.arena, &.{ cmd.current_directory, entry.name }); 537 + 538 + try prefix_list.append(cmd.arena, is_last); 539 + try recurseTree(cmd, writer, full_path, &prefix_list, 1, max_depth); 540 + 541 + _ = prefix_list.pop(); 542 + } 543 + } 544 + } 545 + 546 + fn recurseTree(cmd: Command, writer: anytype, dir_path: [:0]const u8, prefix_list: *std.ArrayList(bool), depth: usize, max_depth: usize) !void { 547 + var dir = std.fs.cwd().openDir(dir_path, .{ .iterate = true }) catch { 548 + return; 549 + }; 550 + defer dir.close(); 551 + 552 + var entries: std.ArrayList(Entry) = .{}; 553 + var iter = dir.iterate(); 554 + 555 + while (try iter.next()) |dirent| { 556 + if (!cmd.opts.showDotfiles() and std.mem.startsWith(u8, dirent.name, ".")) continue; 557 + 558 + const nameZ = try cmd.arena.dupeZ(u8, dirent.name); 559 + try entries.append(cmd.arena, .{ 560 + .name = nameZ, 561 + .kind = dirent.kind, 562 + .statx = undefined, 563 + }); 564 + } 565 + 566 + std.sort.pdq(Entry, entries.items, cmd.opts, Entry.lessThan); 567 + 568 + if (cmd.opts.reverse_sort) { 569 + std.mem.reverse(Entry, entries.items); 570 + } 571 + 572 + for (entries.items, 0..) |entry, i| { 573 + const is_last = i == entries.items.len - 1; 574 + 575 + try drawTreePrefix(writer, prefix_list.items, is_last); 576 + try printTreeEntry(entry, cmd, writer, dir_path); 577 try writer.writeAll("\r\n"); 578 + 579 + if (entry.kind == .directory and depth < max_depth) { 580 + const full_path = try std.fs.path.joinZ(cmd.arena, &.{ dir_path, entry.name }); 581 + 582 + try prefix_list.append(cmd.arena, is_last); 583 + try recurseTree(cmd, writer, full_path, prefix_list, depth + 1, max_depth); 584 + 585 + _ = prefix_list.pop(); 586 + } 587 } 588 } 589 590 + fn printTreeEntry(entry: Entry, cmd: Command, writer: anytype, dir_path: [:0]const u8) !void { 591 + const opts = cmd.opts; 592 + const colors = opts.colors; 593 + 594 + if (opts.useIcons()) { 595 + const icon = Icon.get(entry); 596 + 597 + if (opts.useColor()) { 598 + try writer.writeAll(icon.color); 599 + try writer.writeAll(icon.icon); 600 + try writer.writeAll(colors.reset); 601 + } else { 602 + try writer.writeAll(icon.icon); 603 + } 604 + 605 + try writer.writeByte(' '); 606 + } 607 + 608 + switch (entry.kind) { 609 + .directory => try writer.writeAll(colors.dir), 610 + .sym_link => try writer.writeAll(colors.symlink), 611 + else => { 612 + const full_path = try std.fs.path.join(cmd.arena, &.{ dir_path, entry.name }); 613 + const stat_result = std.fs.cwd().statFile(full_path) catch null; 614 + if (stat_result) |stat| { 615 + if (stat.mode & (std.posix.S.IXUSR | std.posix.S.IXGRP | std.posix.S.IXOTH) != 0) { 616 + try writer.writeAll(colors.executable); 617 + } 618 + } 619 + }, 620 + } 621 + 622 + if (opts.useHyperlinks()) { 623 + const path = try std.fs.path.join(cmd.arena, &.{ dir_path, entry.name }); 624 + try writer.print("\x1b]8;;file://{s}\x1b\\", .{path}); 625 + try writer.writeAll(entry.name); 626 + try writer.writeAll("\x1b]8;;\x1b\\"); 627 + try writer.writeAll(colors.reset); 628 + } else { 629 + try writer.writeAll(entry.name); 630 + try writer.writeAll(colors.reset); 631 + } 632 + } 633 + 634 + fn printLong(cmd: *Command, writer: anytype) !void { 635 const tz = cmd.tz.?; 636 const now = zeit.instant(.{}) catch unreachable; 637 const one_year_ago = try now.subtract(.{ .days = 365 }); ··· 643 var n_size: usize = 0; 644 var n_suff: usize = 0; 645 for (cmd.entries) |entry| { 646 + const group = try cmd.getGroup(entry.statx.gid); 647 + const user = try cmd.getUser(entry.statx.uid); 648 649 var buf: [16]u8 = undefined; 650 const size = try entry.humanReadableSize(&buf); ··· 675 }; 676 677 for (cmd.entries) |entry| { 678 + const user: User = try cmd.getUser(entry.statx.uid) orelse 679 .{ 680 .uid = entry.statx.uid, 681 .name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.uid}), 682 }; 683 + const group: Group = try cmd.getGroup(entry.statx.gid) orelse 684 .{ 685 .gid = entry.statx.gid, 686 .name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.gid}), ··· 694 try writer.writeAll(&mode); 695 try writer.writeByte(' '); 696 try writer.writeAll(user.name); 697 + var space_buf1 = [_][]const u8{" "}; 698 + try writer.writeSplatAll(&space_buf1, longest_user - user.name.len); 699 try writer.writeByte(' '); 700 try writer.writeAll(group.name); 701 + var space_buf2 = [_][]const u8{" "}; 702 + try writer.writeSplatAll(&space_buf2, longest_group - group.name.len); 703 try writer.writeByte(' '); 704 705 var size_buf: [16]u8 = undefined; 706 const size = try entry.humanReadableSize(&size_buf); 707 const suffix = entry.humanReadableSuffix(); 708 709 + var space_buf3 = [_][]const u8{" "}; 710 + try writer.writeSplatAll(&space_buf3, longest_size - size.len); 711 try writer.writeAll(size); 712 try writer.writeByte(' '); 713 try writer.writeAll(suffix); 714 + var space_buf4 = [_][]const u8{" "}; 715 + try writer.writeSplatAll(&space_buf4, longest_suffix - suffix.len); 716 try writer.writeByte(' '); 717 718 try writer.print("{d: >2} {s} ", .{ ··· 727 } 728 729 if (cmd.opts.useIcons()) { 730 + const icon = Icon.get(entry); 731 732 if (cmd.opts.useColor()) { 733 try writer.writeAll(icon.color); ··· 749 } 750 }, 751 } 752 + 753 + if (cmd.opts.useHyperlinks()) { 754 + const path = try std.fs.path.join(cmd.arena, &.{ cmd.current_directory, entry.name }); 755 + try writer.print("\x1b]8;;file://{s}\x1b\\", .{path}); 756 + try writer.writeAll(entry.name); 757 + try writer.writeAll("\x1b]8;;\x1b\\"); 758 + } else { 759 + try writer.writeAll(entry.name); 760 + } 761 try writer.writeAll(colors.reset); 762 763 + switch (entry.kind) { 764 + .sym_link => { 765 + try writer.writeAll(" -> "); 766 + 767 + const symlink: Symlink = cmd.symlinks.get(entry.name) orelse .{ 768 + .name = "[missing]", 769 + .exists = false, 770 + }; 771 + 772 + const color = if (symlink.exists) colors.symlink_target else colors.symlink_missing; 773 + 774 + try writer.writeAll(color); 775 + if (cmd.opts.useHyperlinks() and symlink.exists) { 776 + try writer.print("\x1b]8;;file://{s}\x1b\\", .{symlink.name}); 777 + try writer.writeAll(symlink.name); 778 + try writer.writeAll("\x1b]8;;\x1b\\"); 779 + } else { 780 + try writer.writeAll(symlink.name); 781 + } 782 + try writer.writeAll(colors.reset); 783 + }, 784 + 785 + else => {}, 786 } 787 788 try writer.writeAll("\r\n"); ··· 794 opts: Options = .{}, 795 entries: []Entry = &.{}, 796 entry_idx: usize = 0, 797 + symlinks: std.StringHashMapUnmanaged(Symlink) = .empty, 798 + current_directory: [:0]const u8 = ".", 799 800 tz: ?zeit.TimeZone = null, 801 groups: std.ArrayListUnmanaged(Group) = .empty, 802 users: std.ArrayListUnmanaged(User) = .empty, 803 804 + fn getUser(self: *Command, uid: posix.uid_t) !?User { 805 for (self.users.items) |user| { 806 if (user.uid == uid) return user; 807 } 808 + if (std.c.getpwuid(uid)) |user| { 809 + if (user.name) |name| { 810 + const new_user = User{ 811 + .uid = uid, 812 + .name = std.mem.span(name), 813 + }; 814 + try self.users.append(self.arena, new_user); 815 + return new_user; 816 + } 817 + } 818 return null; 819 } 820 821 + fn getGroup(self: *Command, gid: posix.gid_t) !?Group { 822 for (self.groups.items) |group| { 823 if (group.gid == gid) return group; 824 } 825 + if (grp.getgrgid(gid)) |group| { 826 + const new_group = Group{ 827 + .gid = gid, 828 + .name = std.mem.span(group.*.gr_name), 829 + }; 830 + try self.groups.append(self.arena, new_group); 831 + return new_group; 832 + } 833 return null; 834 } 835 }; ··· 847 }; 848 849 const User = struct { 850 + uid: if (builtin.os.tag == .macos) i33 else posix.uid_t, 851 name: []const u8, 852 853 fn lessThan(_: void, lhs: User, rhs: User) bool { ··· 856 }; 857 858 const Group = struct { 859 + gid: if (builtin.os.tag == .macos) i33 else posix.gid_t, 860 name: []const u8, 861 862 fn lessThan(_: void, lhs: Group, rhs: Group) bool { ··· 880 } 881 }; 882 883 + const Symlink = struct { 884 + name: [:0]const u8, 885 + exists: bool = true, 886 + }; 887 + 888 const Entry = struct { 889 name: [:0]const u8, 890 kind: std.fs.File.Kind, 891 statx: ourio.Statx, 892 + 893 + fn lessThan(opts: Options, lhs: Entry, rhs: Entry) bool { 894 + if (opts.@"group-directories-first") { 895 + const lhs_is_dir = posix.S.ISDIR(lhs.statx.mode); 896 + const rhs_is_dir = posix.S.ISDIR(rhs.statx.mode); 897 + 898 + if (lhs_is_dir != rhs_is_dir) return lhs_is_dir; 899 + } 900 + 901 + if (opts.sort_by_mod_time) { 902 + if (lhs.statx.mtime.sec == rhs.statx.mtime.sec) { 903 + return lhs.statx.mtime.nsec > rhs.statx.mtime.nsec; 904 + } 905 + return lhs.statx.mtime.sec > rhs.statx.mtime.sec; 906 + } 907 + 908 + return natord.orderIgnoreCase(lhs.name, rhs.name) == .lt; 909 + } 910 911 fn modeStr(self: Entry) [10]u8 { 912 var mode = [_]u8{'-'} ** 10; ··· 986 987 switch (msg) { 988 .cwd => { 989 + const fd = result.open catch |err| { 990 + switch (err) { 991 + error.NotDir => { 992 + // Guard against infinite recursion 993 + if (cmd.opts.file != null) return err; 994 + 995 + // if the user specified a file (or something that couldn't be opened as a 996 + // directory), then we open it's parent and apply a filter 997 + const dirname = std.fs.path.dirname(cmd.current_directory) orelse "."; 998 + cmd.opts.file = std.fs.path.basename(cmd.current_directory); 999 + cmd.current_directory = try cmd.arena.dupeZ(u8, dirname); 1000 + _ = try io.open( 1001 + cmd.current_directory, 1002 + .{ .DIRECTORY = true, .CLOEXEC = true }, 1003 + 0, 1004 + .{ 1005 + .ptr = cmd, 1006 + .cb = onCompletion, 1007 + .msg = @intFromEnum(Msg.cwd), 1008 + }, 1009 + ); 1010 + return; 1011 + }, 1012 + else => return err, 1013 + } 1014 + }; 1015 // we are async, no need to defer! 1016 _ = try io.close(fd, .{}); 1017 const dir: std.fs.Dir = .{ .fd = fd }; 1018 + 1019 + if (cmd.opts.useHyperlinks()) { 1020 + var buf: [std.fs.max_path_bytes]u8 = undefined; 1021 + const cwd = try std.os.getFdPath(fd, &buf); 1022 + cmd.current_directory = try cmd.arena.dupeZ(u8, cwd); 1023 + } 1024 1025 var temp_results: std.ArrayListUnmanaged(MinimalEntry) = .empty; 1026 ··· 1041 1042 var iter = dir.iterate(); 1043 while (try iter.next()) |dirent| { 1044 + if (!cmd.opts.showDotfiles() and std.mem.startsWith(u8, dirent.name, ".")) continue; 1045 + if (cmd.opts.file) |file| { 1046 + if (eql(file, dirent.name)) { 1047 + const nameZ = try cmd.arena.dupeZ(u8, dirent.name); 1048 + try temp_results.append(cmd.arena, .{ 1049 + .name = nameZ, 1050 + .kind = dirent.kind, 1051 + }); 1052 + } 1053 + continue; 1054 + } 1055 const nameZ = try cmd.arena.dupeZ(u8, dirent.name); 1056 try temp_results.append(cmd.arena, .{ 1057 .name = nameZ, ··· 1081 } 1082 const path = try std.fs.path.joinZ( 1083 cmd.arena, 1084 + &.{ cmd.current_directory, entry.name }, 1085 ); 1086 1087 if (entry.kind == .sym_link) { ··· 1089 1090 // NOTE: Sadly, we can't do readlink via io_uring 1091 const link = try posix.readlink(path, &buf); 1092 + const symlink: Symlink = .{ .name = try cmd.arena.dupeZ(u8, link) }; 1093 + try cmd.symlinks.put(cmd.arena, entry.name, symlink); 1094 } 1095 _ = try io.stat(path, &entry.statx, .{ 1096 .cb = onCompletion, ··· 1106 // Largest TZ file on my system is Asia/Hebron at 4791 bytes. We allocate an amount 1107 // sufficiently more than that to make sure we do this in a single pass 1108 const buffer = try cmd.arena.alloc(u8, 8192); 1109 + _ = try io.read(fd, buffer, .file, .{ 1110 .cb = onCompletion, 1111 .ptr = cmd, 1112 .msg = @intFromEnum(Msg.read_localtime), ··· 1117 const n = try result.read; 1118 _ = try io.close(task.req.read.fd, .{}); 1119 const bytes = task.req.read.buffer[0..n]; 1120 + var reader = std.Io.Reader.fixed(bytes); 1121 + const tz = try zeit.timezone.TZInfo.parse(cmd.arena, &reader); 1122 cmd.tz = .{ .tzinfo = tz }; 1123 }, 1124 ··· 1128 // TODO: stat this or do multiple reads. We'll never know a good bound unless we go 1129 // really big 1130 const buffer = try cmd.arena.alloc(u8, 8192 * 2); 1131 + _ = try io.read(fd, buffer, .file, .{ 1132 .cb = onCompletion, 1133 .ptr = cmd, 1134 .msg = @intFromEnum(Msg.read_passwd), ··· 1151 // <name>:<throwaway>:<uid><...garbage> 1152 while (lines.next()) |line| { 1153 if (line.len == 0) continue; 1154 + if (std.mem.startsWith(u8, line, "#")) continue; 1155 + 1156 var iter = std.mem.splitScalar(u8, line, ':'); 1157 const name = iter.first(); 1158 _ = iter.next(); ··· 1160 1161 const user: User = .{ 1162 .name = name, 1163 + .uid = try std.fmt.parseInt( 1164 + if (builtin.os.tag == .macos) i33 else u32, 1165 + uid, 1166 + 10, 1167 + ), 1168 }; 1169 1170 cmd.users.appendAssumeCapacity(user); ··· 1176 const fd = try result.open; 1177 1178 const buffer = try cmd.arena.alloc(u8, 8192); 1179 + _ = try io.read(fd, buffer, .file, .{ 1180 .cb = onCompletion, 1181 .ptr = cmd, 1182 .msg = @intFromEnum(Msg.read_group), ··· 1199 // <name>:<throwaway>:<uid><...garbage> 1200 while (lines.next()) |line| { 1201 if (line.len == 0) continue; 1202 + if (std.mem.startsWith(u8, line, "#")) continue; 1203 + 1204 var iter = std.mem.splitScalar(u8, line, ':'); 1205 const name = iter.first(); 1206 _ = iter.next(); ··· 1208 1209 const group: Group = .{ 1210 .name = name, 1211 + .gid = try std.fmt.parseInt( 1212 + if (builtin.os.tag == .macos) i33 else u32, 1213 + gid, 1214 + 10, 1215 + ), 1216 }; 1217 1218 cmd.groups.appendAssumeCapacity(group); ··· 1221 }, 1222 1223 .stat => { 1224 + _ = result.statx catch |err| { 1225 const entry: *Entry = @fieldParentPtr("statx", task.req.statx.result); 1226 + const symlink = cmd.symlinks.getPtr(entry.name) orelse return err; 1227 + 1228 + if (!symlink.exists) { 1229 + // We already lstated this and found an error. Just zero out statx and move 1230 + // along 1231 entry.statx = std.mem.zeroInit(ourio.Statx, entry.statx); 1232 return; 1233 } 1234 1235 + symlink.exists = false; 1236 + 1237 _ = try io.lstat(task.req.statx.path, task.req.statx.result, .{ 1238 .cb = onCompletion, 1239 .ptr = cmd, ··· 1248 cmd.entry_idx += 1; 1249 const path = try std.fs.path.joinZ( 1250 cmd.arena, 1251 + &.{ cmd.current_directory, entry.name }, 1252 ); 1253 1254 if (entry.kind == .sym_link) { ··· 1256 1257 // NOTE: Sadly, we can't do readlink via io_uring 1258 const link = try posix.readlink(path, &buf); 1259 + const symlink: Symlink = .{ .name = try cmd.arena.dupeZ(u8, link) }; 1260 + try cmd.symlinks.put(cmd.arena, entry.name, symlink); 1261 } 1262 _ = try io.stat(path, &entry.statx, .{ 1263 .cb = onCompletion, ··· 1295 const json: Icon = .{ .icon = "", .color = Options.Colors.blue }; 1296 const lua: Icon = .{ .icon = "󰢱", .color = Options.Colors.blue }; 1297 const markdown: Icon = .{ .icon = "", .color = "" }; 1298 + const nix: Icon = .{ .icon = "󱄅", .color = "\x1b[38:2:127:185:228m" }; 1299 const python: Icon = .{ .icon = "", .color = Options.Colors.yellow }; 1300 const rust: Icon = .{ .icon = "", .color = "" }; 1301 const typescript: Icon = .{ .icon = "", .color = Options.Colors.blue }; 1302 const zig: Icon = .{ .icon = "", .color = "\x1b[38:2:247:164:29m" }; 1303 1304 + const by_name: std.StaticStringMap(Icon) = .initComptime(.{ 1305 + .{ "flake.lock", Icon.nix }, 1306 + .{ "go.mod", Icon.go }, 1307 + .{ "go.sum", Icon.go }, 1308 + }); 1309 1310 const by_extension: std.StaticStringMap(Icon) = .initComptime(.{ 1311 .{ "cjs", Icon.javascript }, 1312 .{ "css", Icon.css }, 1313 + .{ "drv", Icon.nix }, 1314 .{ "gif", Icon.image }, 1315 .{ "go", Icon.go }, 1316 .{ "html", Icon.html }, ··· 1324 .{ "mjs", Icon.javascript }, 1325 .{ "mkv", Icon.video }, 1326 .{ "mp4", Icon.video }, 1327 + .{ "nar", Icon.nix }, 1328 + .{ "nix", Icon.nix }, 1329 .{ "png", Icon.image }, 1330 .{ "py", Icon.python }, 1331 .{ "rs", Icon.rust }, ··· 1336 .{ "zon", Icon.zig }, 1337 }); 1338 1339 + fn get(entry: Entry) Icon { 1340 // 1. By name 1341 + // 2. By type 1342 + // 3. By extension 1343 if (by_name.get(entry.name)) |icon| return icon; 1344 1345 switch (entry.kind) { 1346 .block_device => return drive, 1347 .character_device => return drive, 1348 .directory => return directory, 1349 .file => { 1350 + const ext = std.fs.path.extension(entry.name); 1351 + if (ext.len > 0) { 1352 + const ft = ext[1..]; 1353 + if (by_extension.get(ft)) |icon| return icon; 1354 + } 1355 + 1356 if (entry.isExecutable()) { 1357 return executable; 1358 } ··· 1360 }, 1361 .named_pipe => return pipe, 1362 .sym_link => { 1363 + if (posix.S.ISDIR(entry.statx.mode)) { 1364 return symlink_dir; 1365 } 1366 return symlink; ··· 1407 if (std.mem.startsWith(u8, a, "-")) return .short; 1408 return .positional; 1409 } 1410 + 1411 + test "ref" { 1412 + _ = natord; 1413 + }
+347
src/natord.zig
···
··· 1 + //! This file is a port of C implementaion that can be found here 2 + //! https://github.com/sourcefrog/natsort. 3 + const std = @import("std"); 4 + const isSpace = std.ascii.isWhitespace; 5 + const isDigit = std.ascii.isDigit; 6 + const Order = std.math.Order; 7 + const testing = std.testing; 8 + 9 + pub fn order(a: []const u8, b: []const u8) Order { 10 + return natOrder(a, b, false); 11 + } 12 + 13 + pub fn orderIgnoreCase(a: []const u8, b: []const u8) Order { 14 + return natOrder(a, b, true); 15 + } 16 + 17 + fn natOrder(a: []const u8, b: []const u8, comptime fold_case: bool) Order { 18 + var ai: usize = 0; 19 + var bi: usize = 0; 20 + 21 + while (true) : ({ 22 + ai += 1; 23 + bi += 1; 24 + }) { 25 + var ca = if (ai == a.len) 0 else a[ai]; 26 + var cb = if (bi == b.len) 0 else b[bi]; 27 + 28 + while (isSpace(ca)) { 29 + ai += 1; 30 + ca = if (ai == a.len) 0 else a[ai]; 31 + } 32 + 33 + while (isSpace(cb)) { 34 + bi += 1; 35 + cb = if (bi == b.len) 0 else b[bi]; 36 + } 37 + 38 + if (isDigit(ca) and isDigit(cb)) { 39 + const fractional = ca == '0' or cb == '0'; 40 + 41 + if (fractional) { 42 + const result = compareLeft(a[ai..], b[bi..]); 43 + if (result != .eq) return result; 44 + } else { 45 + const result = compareRight(a[ai..], b[bi..]); 46 + if (result != .eq) return result; 47 + } 48 + } 49 + 50 + if (ca == 0 and cb == 0) { 51 + return .eq; 52 + } 53 + 54 + if (fold_case) { 55 + ca = std.ascii.toUpper(ca); 56 + cb = std.ascii.toUpper(cb); 57 + } 58 + 59 + if (ca < cb) { 60 + return .lt; 61 + } 62 + 63 + if (ca > cb) { 64 + return .gt; 65 + } 66 + } 67 + } 68 + 69 + fn compareLeft(a: []const u8, b: []const u8) Order { 70 + var i: usize = 0; 71 + while (true) : (i += 1) { 72 + const ca = if (i == a.len) 0 else a[i]; 73 + const cb = if (i == b.len) 0 else b[i]; 74 + 75 + if (!isDigit(ca) and !isDigit(cb)) { 76 + return .eq; 77 + } 78 + if (!isDigit(ca)) { 79 + return .lt; 80 + } 81 + if (!isDigit(cb)) { 82 + return .gt; 83 + } 84 + if (ca < cb) { 85 + return .lt; 86 + } 87 + if (ca > cb) { 88 + return .gt; 89 + } 90 + } 91 + 92 + return .eq; 93 + } 94 + 95 + fn compareRight(a: []const u8, b: []const u8) Order { 96 + var bias = Order.eq; 97 + 98 + var i: usize = 0; 99 + while (true) : (i += 1) { 100 + const ca = if (i == a.len) 0 else a[i]; 101 + const cb = if (i == b.len) 0 else b[i]; 102 + 103 + if (!isDigit(ca) and !isDigit(cb)) { 104 + return bias; 105 + } 106 + if (!isDigit(ca)) { 107 + return .lt; 108 + } 109 + if (!isDigit(cb)) { 110 + return .gt; 111 + } 112 + 113 + if (ca < cb) { 114 + if (bias != .eq) { 115 + bias = .lt; 116 + } 117 + } else if (ca > cb) { 118 + if (bias != .eq) { 119 + bias = .gt; 120 + } 121 + } else if (ca == 0 and cb == 0) { 122 + return bias; 123 + } 124 + } 125 + 126 + return .eq; 127 + } 128 + 129 + const SortContext = struct { 130 + ignore_case: bool = false, 131 + reverse: bool = false, 132 + 133 + fn compare(self: @This(), a: []const u8, b: []const u8) bool { 134 + const ord: std.math.Order = if (self.reverse) .gt else .lt; 135 + if (self.ignore_case) { 136 + return orderIgnoreCase(a, b) == ord; 137 + } else { 138 + return order(a, b) == ord; 139 + } 140 + } 141 + }; 142 + 143 + test "lt" { 144 + try testing.expectEqual(Order.lt, order("a_1", "a_10")); 145 + } 146 + 147 + test "eq" { 148 + try testing.expectEqual(Order.eq, order("a_1", "a_1")); 149 + } 150 + 151 + test "gt" { 152 + try testing.expectEqual(Order.gt, order("a_10", "a_1")); 153 + } 154 + 155 + fn sortAndAssert(context: SortContext, input: [][]const u8, want: []const []const u8) !void { 156 + std.sort.pdq([]const u8, input, context, SortContext.compare); 157 + 158 + for (input, want) |actual, expected| { 159 + try testing.expectEqualStrings(expected, actual); 160 + } 161 + } 162 + 163 + test "sorting" { 164 + const context = SortContext{}; 165 + var items = [_][]const u8{ 166 + "item100", 167 + "item10", 168 + "item1", 169 + }; 170 + const want = [_][]const u8{ 171 + "item1", 172 + "item10", 173 + "item100", 174 + }; 175 + 176 + try sortAndAssert(context, &items, &want); 177 + } 178 + 179 + test "sorting 2" { 180 + const context = SortContext{}; 181 + var items = [_][]const u8{ 182 + "item_30", 183 + "item_15", 184 + "item_3", 185 + "item_2", 186 + "item_10", 187 + }; 188 + const want = [_][]const u8{ 189 + "item_2", 190 + "item_3", 191 + "item_10", 192 + "item_15", 193 + "item_30", 194 + }; 195 + 196 + try sortAndAssert(context, &items, &want); 197 + } 198 + 199 + test "leading zeros" { 200 + const context = SortContext{}; 201 + var items = [_][]const u8{ 202 + "item100", 203 + "item999", 204 + "item001", 205 + "item010", 206 + "item000", 207 + }; 208 + const want = [_][]const u8{ 209 + "item000", 210 + "item001", 211 + "item010", 212 + "item100", 213 + "item999", 214 + }; 215 + 216 + try sortAndAssert(context, &items, &want); 217 + } 218 + 219 + test "dates" { 220 + const context = SortContext{}; 221 + var items = [_][]const u8{ 222 + "2000-1-10", 223 + "2000-1-2", 224 + "1999-12-25", 225 + "2000-3-23", 226 + "1999-3-3", 227 + }; 228 + const want = [_][]const u8{ 229 + "1999-3-3", 230 + "1999-12-25", 231 + "2000-1-2", 232 + "2000-1-10", 233 + "2000-3-23", 234 + }; 235 + 236 + try sortAndAssert(context, &items, &want); 237 + } 238 + 239 + test "fractions" { 240 + const context = SortContext{}; 241 + var items = [_][]const u8{ 242 + "Fractional release numbers", 243 + "1.011.02", 244 + "1.010.12", 245 + "1.009.02", 246 + "1.009.20", 247 + "1.009.10", 248 + "1.002.08", 249 + "1.002.03", 250 + "1.002.01", 251 + }; 252 + const want = [_][]const u8{ 253 + "1.002.01", 254 + "1.002.03", 255 + "1.002.08", 256 + "1.009.02", 257 + "1.009.10", 258 + "1.009.20", 259 + "1.010.12", 260 + "1.011.02", 261 + "Fractional release numbers", 262 + }; 263 + 264 + try sortAndAssert(context, &items, &want); 265 + } 266 + 267 + test "words" { 268 + const context = SortContext{}; 269 + var items = [_][]const u8{ 270 + "fred", 271 + "pic2", 272 + "pic100a", 273 + "pic120", 274 + "pic121", 275 + "jane", 276 + "tom", 277 + "pic02a", 278 + "pic3", 279 + "pic4", 280 + "1-20", 281 + "pic100", 282 + "pic02000", 283 + "10-20", 284 + "1-02", 285 + "1-2", 286 + "x2-y7", 287 + "x8-y8", 288 + "x2-y08", 289 + "x2-g8", 290 + "pic01", 291 + "pic02", 292 + "pic 6", 293 + "pic 7", 294 + "pic 5", 295 + "pic05", 296 + "pic 5 ", 297 + "pic 5 something", 298 + "pic 4 else", 299 + }; 300 + const want = [_][]const u8{ 301 + "1-02", 302 + "1-2", 303 + "1-20", 304 + "10-20", 305 + "fred", 306 + "jane", 307 + "pic01", 308 + "pic02", 309 + "pic02a", 310 + "pic02000", 311 + "pic05", 312 + "pic2", 313 + "pic3", 314 + "pic4", 315 + "pic 4 else", 316 + "pic 5", 317 + "pic 5 ", 318 + "pic 5 something", 319 + "pic 6", 320 + "pic 7", 321 + "pic100", 322 + "pic100a", 323 + "pic120", 324 + "pic121", 325 + "tom", 326 + "x2-g8", 327 + "x2-y08", 328 + "x2-y7", 329 + "x8-y8", 330 + }; 331 + 332 + try sortAndAssert(context, &items, &want); 333 + } 334 + 335 + test "fuzz" { 336 + const Context = struct { 337 + fn testOne(context: @This(), input: []const u8) anyerror!void { 338 + _ = context; 339 + 340 + const a = input[0..(input.len / 2)]; 341 + const b = input[(input.len / 2)..]; 342 + _ = order(a, b); 343 + } 344 + }; 345 + 346 + try std.testing.fuzz(Context{}, Context.testOne, .{}); 347 + }