+29
-23
README.md
+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
+2
build.zig
+2
-2
build.zig.zon
+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
+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
+1
-1
nix/cache.nix
+200
-32
src/main.zig
+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
+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
+
}