+29
-23
README.md
+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
+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
+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
+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
+1
-1
flake.nix
+2
-2
nix/cache.nix
+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
+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
+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
+
}