this repo has no description

Compare changes

Choose any two refs to compare.

+29 -23
README.md
··· 19 19 ## Usage 20 20 21 21 ``` 22 - lsr [options] [directory] 22 + lsr [options] [path] 23 23 24 24 --help Print this message and exit 25 25 --version Print the version string ··· 30 30 -A, --almost-all Like --all, but skips implicit "." and ".." directories 31 31 -C, --columns Print the output in columns 32 32 --color=WHEN When to use colors (always, auto, never) 33 - --group-directories-first 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) 34 35 --icons=WHEN When to display icons (always, auto, never) 35 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 36 39 37 40 ``` 38 41 ··· 43 46 (because io_uring). `lsr` does work on macOS/BSD as well, but will not see the 44 47 syscall batching benefits that are available with io_uring. 45 48 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 | 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 | 53 57 54 58 ### Time 55 59 56 60 Data gathered with `hyperfine` on a directory of `n` plain files. 57 61 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 | 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 | 65 70 66 71 ### Syscalls 67 72 68 73 Data gathered with `strace -c` on a directory of `n` plain files. (Lower is better) 69 74 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 | 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 |
+2
build.zig
··· 82 82 "-C", 83 83 b.build_root.path orelse ".", 84 84 "describe", 85 + "--match", 86 + "*.*.*", 85 87 "--tags", 86 88 "--abbrev=9", 87 89 }, &code, .Ignore) catch {
+2 -2
build.zig.zon
··· 7 7 8 8 .dependencies = .{ 9 9 .ourio = .{ 10 - .url = "git+https://github.com/rockorager/ourio#54c1a1ed8d0994636770e5185ecdb59fe6d8535e", 11 - .hash = "ourio-0.0.0-_s-z0asOAgAhpi7gSpLLvWGj_4XURez4W9TWN6SGs5BP", 10 + .url = "git+https://github.com/rockorager/ourio#17280493cff33a4713d7df39933557792789f002", 11 + .hash = "ourio-0.0.0-_s-z0fsOAgBBgWaFDe0-yxAFdOJYN0ySemeXbEghPUh9", 12 12 }, 13 13 .zeit = .{ 14 14 .url = "git+https://github.com/rockorager/zeit#4496d1c40b2223c22a1341e175fc2ecd94cc0de9",
+11 -2
docs/lsr.1.scd
··· 6 6 7 7 # SYNOPSIS 8 8 9 - *lsr* [options...] [directory] 9 + *lsr* [options...] [path] 10 10 11 11 # DESCRIPTION 12 12 ··· 31 31 When to use colors (always, auto, never) 32 32 33 33 *--group-directories-first* 34 - When to use colors (always, auto, never) 34 + Print all directories before printing regular files 35 35 36 36 *--help* 37 37 Print the help menu and exit 38 38 39 + *--hyperlinks=WHEN* 40 + When to use OSC 8 hyperlinks (always, auto, never) 41 + 39 42 *--icons=WHEN* 40 43 When to display icons (always, auto, never) 41 44 42 45 *-l*, *--long* 43 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 44 53 45 54 *--version* 46 55 Print the version and exit
+1 -1
nix/cache.nix
··· 17 17 mv $ZIG_GLOBAL_CACHE_DIR/p $out 18 18 ''; 19 19 20 - outputHash = "sha256-hAq1/uE9eu/82+e079y+v9EnN0ViXX7k3GwkgQkxOyo="; 20 + outputHash = "sha256-UeZOnpZ5iFF8f2WtO8qavzzau06/z/jPgYjcP9kHmWc="; 21 21 outputHashMode = "recursive"; 22 22 outputHashAlgo = "sha256"; 23 23 }
+200 -32
src/main.zig
··· 2 2 const builtin = @import("builtin"); 3 3 const ourio = @import("ourio"); 4 4 const zeit = @import("zeit"); 5 + const natord = @import("natord.zig"); 5 6 const build_options = @import("build_options"); 6 7 7 8 const posix = std.posix; 8 9 9 10 const usage = 10 11 \\Usage: 11 - \\ lsr [options] [directory] 12 + \\ lsr [options] [path] 12 13 \\ 13 14 \\ --help Print this message and exit 14 15 \\ --version Print the version string ··· 19 20 \\ -A, --almost-all Like --all, but skips implicit "." and ".." directories 20 21 \\ -C, --columns Print the output in columns 21 22 \\ --color=WHEN When to use colors (always, auto, never) 22 - \\ --group-directories-first When to use colors (always, auto, never) 23 + \\ --group-directories-first Print all directories before printing regular files 24 + \\ --hyperlinks=WHEN When to use OSC 8 hyperlinks (always, auto, never) 23 25 \\ --icons=WHEN When to display icons (always, auto, never) 24 26 \\ -l, --long Display extended file metadata 27 + \\ -r, --reverse Reverse the sort order 28 + \\ -t, --time Sort the entries by modification time, most recent first 25 29 \\ 26 30 ; 27 31 ··· 33 37 color: When = .auto, 34 38 shortview: enum { columns, oneline } = .oneline, 35 39 @"group-directories-first": bool = true, 40 + hyperlinks: When = .auto, 36 41 icons: When = .auto, 37 42 long: bool = false, 43 + sort_by_mod_time: bool = false, 44 + reverse_sort: bool = false, 38 45 39 46 directory: [:0]const u8 = ".", 47 + file: ?[]const u8 = null, 40 48 41 49 winsize: ?posix.winsize = null, 42 50 colors: Colors = .none, ··· 101 109 } 102 110 } 103 111 112 + fn useHyperlinks(self: Options) bool { 113 + switch (self.hyperlinks) { 114 + .never => return false, 115 + .always => return true, 116 + .auto => return self.isatty(), 117 + } 118 + } 119 + 120 + fn showDotfiles(self: Options) bool { 121 + return self.@"almost-all" or self.all; 122 + } 123 + 104 124 fn isatty(self: Options) bool { 105 125 return self.winsize != null; 106 126 } ··· 148 168 'C' => cmd.opts.shortview = .columns, 149 169 'a' => cmd.opts.all = true, 150 170 'l' => cmd.opts.long = true, 171 + 'r' => cmd.opts.reverse_sort = true, 172 + 't' => cmd.opts.sort_by_mod_time = true, 151 173 else => { 152 174 try stderr.print("Invalid opt: '{c}'", .{b}); 153 175 std.process.exit(1); ··· 184 206 try stderr.print("Invalid color option: '{s}'", .{val}); 185 207 std.process.exit(1); 186 208 }; 209 + } else if (eql(opt, "hyperlinks")) { 210 + cmd.opts.hyperlinks = std.meta.stringToEnum(Options.When, val) orelse { 211 + try stderr.print("Invalid hyperlinks option: '{s}'", .{val}); 212 + std.process.exit(1); 213 + }; 187 214 } else if (eql(opt, "icons")) { 188 215 cmd.opts.icons = std.meta.stringToEnum(Options.When, val) orelse { 189 216 try stderr.print("Invalid color option: '{s}'", .{val}); ··· 201 228 std.process.exit(1); 202 229 }; 203 230 cmd.opts.shortview = if (o) .oneline else .columns; 231 + } else if (eql(opt, "time")) { 232 + cmd.opts.sort_by_mod_time = parseArgBool(val) orelse { 233 + try stderr.print("Invalid boolean: '{s}'", .{val}); 234 + std.process.exit(1); 235 + }; 236 + } else if (eql(opt, "reverse")) { 237 + cmd.opts.reverse_sort = parseArgBool(val) orelse { 238 + try stderr.print("Invalid boolean: '{s}'", .{val}); 239 + std.process.exit(1); 240 + }; 204 241 } else if (eql(opt, "help")) { 205 242 return stderr.writeAll(usage); 206 243 } else if (eql(opt, "version")) { ··· 253 290 254 291 if (cmd.entries.len == 0) return; 255 292 293 + std.sort.pdq(Entry, cmd.entries, cmd.opts, Entry.lessThan); 294 + 295 + if (cmd.opts.reverse_sort) { 296 + std.mem.reverse(Entry, cmd.entries); 297 + } 298 + 256 299 if (cmd.opts.long) { 257 300 try printLong(cmd, bw.writer()); 258 301 } else switch (cmd.opts.shortview) { ··· 332 375 for (columns.items, 0..) |column, i| { 333 376 if (row >= column.entries.len) continue; 334 377 const entry = column.entries[row]; 335 - try printShortEntry(column.entries[row], cmd.opts, writer); 378 + try printShortEntry(column.entries[row], cmd, writer); 336 379 337 380 if (i < columns.items.len - 1) { 338 381 const spaces = column.width - (icon_width + entry.name.len); ··· 347 390 return idx + n_short_cols >= n_cols; 348 391 } 349 392 350 - fn printShortEntry(entry: Entry, opts: Options, writer: anytype) !void { 393 + fn printShortEntry(entry: Entry, cmd: Command, writer: anytype) !void { 394 + const opts = cmd.opts; 351 395 const colors = opts.colors; 352 396 if (opts.useIcons()) { 353 397 const icon = Icon.get(entry, opts); ··· 371 415 } 372 416 }, 373 417 } 374 - try writer.writeAll(entry.name); 375 - try writer.writeAll(colors.reset); 418 + 419 + if (opts.useHyperlinks()) { 420 + const path = try std.fs.path.join(cmd.arena, &.{ opts.directory, entry.name }); 421 + try writer.print("\x1b]8;;file://{s}\x1b\\", .{path}); 422 + try writer.writeAll(entry.name); 423 + try writer.writeAll("\x1b]8;;\x1b\\"); 424 + try writer.writeAll(colors.reset); 425 + } else { 426 + try writer.writeAll(entry.name); 427 + try writer.writeAll(colors.reset); 428 + } 376 429 } 377 430 378 431 fn printShortOneRow(cmd: Command, writer: anytype) !void { ··· 385 438 386 439 fn printShortOnePerLine(cmd: Command, writer: anytype) !void { 387 440 for (cmd.entries) |entry| { 388 - try printShortEntry(entry, cmd.opts, writer); 441 + try printShortEntry(entry, cmd, writer); 389 442 try writer.writeAll("\r\n"); 390 443 } 391 444 } ··· 504 557 } 505 558 }, 506 559 } 507 - try writer.writeAll(entry.name); 560 + 561 + if (cmd.opts.useHyperlinks()) { 562 + const path = try std.fs.path.join(cmd.arena, &.{ cmd.opts.directory, entry.name }); 563 + try writer.print("\x1b]8;;file://{s}\x1b\\", .{path}); 564 + try writer.writeAll(entry.name); 565 + try writer.writeAll("\x1b]8;;\x1b\\"); 566 + } else { 567 + try writer.writeAll(entry.name); 568 + } 508 569 try writer.writeAll(colors.reset); 509 570 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); 571 + switch (entry.kind) { 572 + .sym_link => { 573 + try writer.writeAll(" -> "); 574 + 575 + const symlink: Symlink = cmd.symlinks.get(entry.name) orelse .{ 576 + .name = "[missing]", 577 + .exists = false, 578 + }; 579 + 580 + const color = if (symlink.exists) colors.symlink_target else colors.symlink_missing; 581 + 582 + try writer.writeAll(color); 583 + if (cmd.opts.useHyperlinks() and symlink.exists) { 584 + try writer.print("\x1b]8;;file://{s}\x1b\\", .{symlink.name}); 585 + try writer.writeAll(symlink.name); 586 + try writer.writeAll("\x1b]8;;\x1b\\"); 587 + } else { 588 + try writer.writeAll(symlink.name); 589 + } 590 + try writer.writeAll(colors.reset); 591 + }, 592 + 593 + else => {}, 519 594 } 520 595 521 596 try writer.writeAll("\r\n"); ··· 527 602 opts: Options = .{}, 528 603 entries: []Entry = &.{}, 529 604 entry_idx: usize = 0, 605 + symlinks: std.StringHashMapUnmanaged(Symlink) = .empty, 530 606 531 607 tz: ?zeit.TimeZone = null, 532 608 groups: std.ArrayListUnmanaged(Group) = .empty, ··· 560 636 }; 561 637 562 638 const User = struct { 563 - uid: posix.uid_t, 639 + uid: if (builtin.os.tag == .macos) i33 else posix.uid_t, 564 640 name: []const u8, 565 641 566 642 fn lessThan(_: void, lhs: User, rhs: User) bool { ··· 569 645 }; 570 646 571 647 const Group = struct { 572 - gid: posix.gid_t, 648 + gid: if (builtin.os.tag == .macos) i33 else posix.gid_t, 573 649 name: []const u8, 574 650 575 651 fn lessThan(_: void, lhs: Group, rhs: Group) bool { ··· 593 669 } 594 670 }; 595 671 672 + const Symlink = struct { 673 + name: [:0]const u8, 674 + exists: bool = true, 675 + }; 676 + 596 677 const Entry = struct { 597 678 name: [:0]const u8, 598 679 kind: std.fs.File.Kind, 599 680 statx: ourio.Statx, 600 - link_name: [:0]const u8 = "", 601 - symlink_missing: bool = false, 681 + 682 + fn lessThan(opts: Options, lhs: Entry, rhs: Entry) bool { 683 + if (opts.@"group-directories-first" and 684 + lhs.kind != rhs.kind and 685 + (lhs.kind == .directory or rhs.kind == .directory)) 686 + { 687 + return lhs.kind == .directory; 688 + } 689 + 690 + if (opts.sort_by_mod_time) { 691 + if (lhs.statx.mtime.sec == rhs.statx.mtime.sec) { 692 + return lhs.statx.mtime.nsec > rhs.statx.mtime.nsec; 693 + } 694 + return lhs.statx.mtime.sec > rhs.statx.mtime.sec; 695 + } 696 + 697 + return natord.orderIgnoreCase(lhs.name, rhs.name) == .lt; 698 + } 602 699 603 700 fn modeStr(self: Entry) [10]u8 { 604 701 var mode = [_]u8{'-'} ** 10; ··· 678 775 679 776 switch (msg) { 680 777 .cwd => { 681 - const fd = try result.open; 778 + const fd = result.open catch |err| { 779 + switch (err) { 780 + error.NotDir => { 781 + // Guard against infinite recursion 782 + if (cmd.opts.file != null) return err; 783 + 784 + // if the user specified a file (or something that couldn't be opened as a 785 + // directory), then we open it's parent and apply a filter 786 + const dirname = std.fs.path.dirname(cmd.opts.directory) orelse "."; 787 + cmd.opts.file = std.fs.path.basename(cmd.opts.directory); 788 + cmd.opts.directory = try cmd.arena.dupeZ(u8, dirname); 789 + _ = try io.open( 790 + cmd.opts.directory, 791 + .{ .DIRECTORY = true, .CLOEXEC = true }, 792 + 0, 793 + .{ 794 + .ptr = cmd, 795 + .cb = onCompletion, 796 + .msg = @intFromEnum(Msg.cwd), 797 + }, 798 + ); 799 + return; 800 + }, 801 + else => return err, 802 + } 803 + }; 682 804 // we are async, no need to defer! 683 805 _ = try io.close(fd, .{}); 684 806 const dir: std.fs.Dir = .{ .fd = fd }; 807 + 808 + if (cmd.opts.useHyperlinks()) { 809 + var buf: [std.fs.max_path_bytes]u8 = undefined; 810 + const cwd = try std.os.getFdPath(fd, &buf); 811 + cmd.opts.directory = try cmd.arena.dupeZ(u8, cwd); 812 + } 685 813 686 814 var temp_results: std.ArrayListUnmanaged(MinimalEntry) = .empty; 687 815 ··· 702 830 703 831 var iter = dir.iterate(); 704 832 while (try iter.next()) |dirent| { 705 - if (!cmd.opts.@"almost-all" and std.mem.startsWith(u8, dirent.name, ".")) continue; 833 + if (!cmd.opts.showDotfiles() and std.mem.startsWith(u8, dirent.name, ".")) continue; 834 + if (cmd.opts.file) |file| { 835 + if (eql(file, dirent.name)) { 836 + const nameZ = try cmd.arena.dupeZ(u8, dirent.name); 837 + try temp_results.append(cmd.arena, .{ 838 + .name = nameZ, 839 + .kind = dirent.kind, 840 + }); 841 + } 842 + continue; 843 + } 706 844 const nameZ = try cmd.arena.dupeZ(u8, dirent.name); 707 845 try temp_results.append(cmd.arena, .{ 708 846 .name = nameZ, ··· 740 878 741 879 // NOTE: Sadly, we can't do readlink via io_uring 742 880 const link = try posix.readlink(path, &buf); 743 - entry.link_name = try cmd.arena.dupeZ(u8, link); 881 + const symlink: Symlink = .{ .name = try cmd.arena.dupeZ(u8, link) }; 882 + try cmd.symlinks.put(cmd.arena, entry.name, symlink); 744 883 } 745 884 _ = try io.stat(path, &entry.statx, .{ 746 885 .cb = onCompletion, ··· 801 940 // <name>:<throwaway>:<uid><...garbage> 802 941 while (lines.next()) |line| { 803 942 if (line.len == 0) continue; 943 + if (std.mem.startsWith(u8, line, "#")) continue; 944 + 804 945 var iter = std.mem.splitScalar(u8, line, ':'); 805 946 const name = iter.first(); 806 947 _ = iter.next(); ··· 808 949 809 950 const user: User = .{ 810 951 .name = name, 811 - .uid = try std.fmt.parseInt(u32, uid, 10), 952 + .uid = try std.fmt.parseInt( 953 + if (builtin.os.tag == .macos) i33 else u32, 954 + uid, 955 + 10, 956 + ), 812 957 }; 813 958 814 959 cmd.users.appendAssumeCapacity(user); ··· 843 988 // <name>:<throwaway>:<uid><...garbage> 844 989 while (lines.next()) |line| { 845 990 if (line.len == 0) continue; 991 + if (std.mem.startsWith(u8, line, "#")) continue; 992 + 846 993 var iter = std.mem.splitScalar(u8, line, ':'); 847 994 const name = iter.first(); 848 995 _ = iter.next(); ··· 850 997 851 998 const group: Group = .{ 852 999 .name = name, 853 - .gid = try std.fmt.parseInt(u32, gid, 10), 1000 + .gid = try std.fmt.parseInt( 1001 + if (builtin.os.tag == .macos) i33 else u32, 1002 + gid, 1003 + 10, 1004 + ), 854 1005 }; 855 1006 856 1007 cmd.groups.appendAssumeCapacity(group); ··· 859 1010 }, 860 1011 861 1012 .stat => { 862 - _ = result.statx catch { 1013 + _ = result.statx catch |err| { 863 1014 const entry: *Entry = @fieldParentPtr("statx", task.req.statx.result); 864 - if (entry.symlink_missing) { 865 - // we already got here. Just zero out the statx; 1015 + const symlink = cmd.symlinks.getPtr(entry.name) orelse return err; 1016 + 1017 + if (!symlink.exists) { 1018 + // We already lstated this and found an error. Just zero out statx and move 1019 + // along 866 1020 entry.statx = std.mem.zeroInit(ourio.Statx, entry.statx); 867 1021 return; 868 1022 } 869 1023 870 - entry.symlink_missing = true; 1024 + symlink.exists = false; 1025 + 871 1026 _ = try io.lstat(task.req.statx.path, task.req.statx.result, .{ 872 1027 .cb = onCompletion, 873 1028 .ptr = cmd, ··· 890 1045 891 1046 // NOTE: Sadly, we can't do readlink via io_uring 892 1047 const link = try posix.readlink(path, &buf); 893 - entry.link_name = try cmd.arena.dupeZ(u8, link); 1048 + const symlink: Symlink = .{ .name = try cmd.arena.dupeZ(u8, link) }; 1049 + try cmd.symlinks.put(cmd.arena, entry.name, symlink); 894 1050 } 895 1051 _ = try io.stat(path, &entry.statx, .{ 896 1052 .cb = onCompletion, ··· 928 1084 const json: Icon = .{ .icon = "๎˜‹", .color = Options.Colors.blue }; 929 1085 const lua: Icon = .{ .icon = "๓ฐขฑ", .color = Options.Colors.blue }; 930 1086 const markdown: Icon = .{ .icon = "๎˜‰", .color = "" }; 1087 + const nix: Icon = .{ .icon = "๓ฑ„…", .color = "\x1b[38:2:127:185:228m" }; 931 1088 const python: Icon = .{ .icon = "๎œผ", .color = Options.Colors.yellow }; 932 1089 const rust: Icon = .{ .icon = "๎žจ", .color = "" }; 933 1090 const typescript: Icon = .{ .icon = "๎ฃŠ", .color = Options.Colors.blue }; 934 1091 const zig: Icon = .{ .icon = "๎šฉ", .color = "\x1b[38:2:247:164:29m" }; 935 1092 936 - const by_name: std.StaticStringMap(Icon) = .initComptime(.{}); 1093 + const by_name: std.StaticStringMap(Icon) = .initComptime(.{ 1094 + .{ "flake.lock", Icon.nix }, 1095 + .{ "go.mod", Icon.go }, 1096 + .{ "go.sum", Icon.go }, 1097 + }); 937 1098 938 1099 const by_extension: std.StaticStringMap(Icon) = .initComptime(.{ 939 1100 .{ "cjs", Icon.javascript }, 940 1101 .{ "css", Icon.css }, 1102 + .{ "drv", Icon.nix }, 941 1103 .{ "gif", Icon.image }, 942 1104 .{ "go", Icon.go }, 943 1105 .{ "html", Icon.html }, ··· 951 1113 .{ "mjs", Icon.javascript }, 952 1114 .{ "mkv", Icon.video }, 953 1115 .{ "mp4", Icon.video }, 1116 + .{ "nar", Icon.nix }, 1117 + .{ "nix", Icon.nix }, 954 1118 .{ "png", Icon.image }, 955 1119 .{ "py", Icon.python }, 956 1120 .{ "rs", Icon.rust }, ··· 1032 1196 if (std.mem.startsWith(u8, a, "-")) return .short; 1033 1197 return .positional; 1034 1198 } 1199 + 1200 + test "ref" { 1201 + _ = natord; 1202 + }
+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 + }