+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 |
+4
-1
build.zig
+4
-1
build.zig
···
2
2
const zzdoc = @import("zzdoc");
3
3
4
4
/// Must be kept in sync with git tags
5
-
const version: std.SemanticVersion = .{ .major = 0, .minor = 2, .patch = 0 };
5
+
const version: std.SemanticVersion = .{ .major = 1, .minor = 0, .patch = 0 };
6
6
7
7
pub fn build(b: *std.Build) void {
8
8
const target = b.standardTargetOptions(.{});
···
45
45
.name = "lsr",
46
46
.root_module = exe_mod,
47
47
});
48
+
exe.linkLibC();
48
49
49
50
b.installArtifact(exe);
50
51
···
82
83
"-C",
83
84
b.build_root.path orelse ".",
84
85
"describe",
86
+
"--match",
87
+
"*.*.*",
85
88
"--tags",
86
89
"--abbrev=9",
87
90
}, &code, .Ignore) catch {
+8
-8
build.zig.zon
+8
-8
build.zig.zon
···
1
1
.{
2
2
.name = .lsr,
3
-
.version = "0.2.0",
3
+
.version = "1.0.0",
4
4
.fingerprint = 0x495d173f6002e86, // Changing this has security and trust implications.
5
5
6
-
.minimum_zig_version = "0.14.0",
6
+
.minimum_zig_version = "0.15.1",
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#07bf94db87a9aea70d6e1a1dd99cac6fb9d38b35",
11
+
.hash = "ourio-0.0.0-_s-z0Z0XAgBU_BFjdY8QjGhJ8vcdIONPSErlYRwLoxfg",
12
12
},
13
13
.zeit = .{
14
-
.url = "git+https://github.com/rockorager/zeit#4496d1c40b2223c22a1341e175fc2ecd94cc0de9",
15
-
.hash = "zeit-0.6.0-5I6bk1J1AgA13rteb6E0steXiOUKBYTzJZMMIuK9oEmb",
14
+
.url = "git+https://github.com/rockorager/zeit#74be5a2afb346b2a6a6349abbb609e89ec7e65a6",
15
+
.hash = "zeit-0.6.0-5I6bk4t8AgCP0UGGHVF_khlmWZkAF5XtfQWEKCyLoptU",
16
16
},
17
17
.zzdoc = .{
18
-
.url = "git+https://github.com/rockorager/zzdoc#57e86eb4e621bc4a96fbe0dd89ad0986db6d0483",
19
-
.hash = "zzdoc-0.0.0-tzT1PuPZAACr1jIJxjTrdOsLbfXS6idWFGfTq0gwxJiv",
18
+
.url = "git+https://github.com/rockorager/zzdoc#a54223bdc13a80839ccf9f473edf3a171e777946",
19
+
.hash = "zzdoc-0.0.0-tzT1Ph7cAAC5YmXQXiBJHAg41_A5AUAC5VOm7ShnUxlz",
20
20
},
21
21
},
22
22
.paths = .{
+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
flake.nix
+1
-1
flake.nix
+2
-2
nix/cache.nix
+2
-2
nix/cache.nix
···
2
2
3
3
pkgs.stdenv.mkDerivation {
4
4
pname = "lsr-cache";
5
-
version = "0.1.0";
5
+
version = "1.0.0";
6
6
doCheck = false;
7
7
src = ../.;
8
8
···
17
17
mv $ZIG_GLOBAL_CACHE_DIR/p $out
18
18
'';
19
19
20
-
outputHash = "sha256-hAq1/uE9eu/82+e079y+v9EnN0ViXX7k3GwkgQkxOyo=";
20
+
outputHash = "sha256-bfc2dlQa1VGq9S6OBeQawAJuvfxU4kgFtQ13fuKhdZc=";
21
21
outputHashMode = "recursive";
22
22
outputHashAlgo = "sha256";
23
23
}
+490
-111
src/main.zig
+490
-111
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");
7
+
const grp = @cImport({
8
+
@cInclude("grp.h");
9
+
});
6
10
7
11
const posix = std.posix;
8
12
9
13
const usage =
10
-
\\Usage:
11
-
\\ lsr [options] [directory]
14
+
\\Usage:
15
+
\\ lsr [options] [path...]
12
16
\\
13
17
\\ --help Print this message and exit
14
18
\\ --version Print the version string
···
19
23
\\ -A, --almost-all Like --all, but skips implicit "." and ".." directories
20
24
\\ -C, --columns Print the output in columns
21
25
\\ --color=WHEN When to use colors (always, auto, never)
22
-
\\ --group-directories-first 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)
23
28
\\ --icons=WHEN When to display icons (always, auto, never)
24
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)
25
33
\\
26
34
;
27
35
···
33
41
color: When = .auto,
34
42
shortview: enum { columns, oneline } = .oneline,
35
43
@"group-directories-first": bool = true,
44
+
hyperlinks: When = .auto,
36
45
icons: When = .auto,
37
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,
38
51
39
-
directory: [:0]const u8 = ".",
52
+
directories: std.ArrayListUnmanaged([:0]const u8) = .empty,
53
+
file: ?[]const u8 = null,
40
54
41
55
winsize: ?posix.winsize = null,
42
56
colors: Colors = .none,
···
101
115
}
102
116
}
103
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
+
104
130
fn isatty(self: Options) bool {
105
131
return self.winsize != null;
106
132
}
···
126
152
127
153
var cmd: Command = .{ .arena = allocator };
128
154
129
-
cmd.opts.winsize = getWinsize(std.io.getStdOut().handle);
155
+
cmd.opts.winsize = getWinsize(std.fs.File.stdout().handle);
130
156
131
157
cmd.opts.shortview = if (cmd.opts.isatty()) .columns else .oneline;
132
158
133
-
const stdout = std.io.getStdOut().writer();
134
-
const stderr = std.io.getStdErr().writer();
135
-
var bw = std.io.bufferedWriter(stdout);
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;
136
165
137
166
var args = std.process.args();
138
167
// skip binary
···
147
176
'A' => cmd.opts.@"almost-all" = true,
148
177
'C' => cmd.opts.shortview = .columns,
149
178
'a' => cmd.opts.all = true,
179
+
'h' => {}, // human-readable: present for compatibility
150
180
'l' => cmd.opts.long = true,
181
+
'r' => cmd.opts.reverse_sort = true,
182
+
't' => cmd.opts.sort_by_mod_time = true,
151
183
else => {
152
-
try stderr.print("Invalid opt: '{c}'", .{b});
184
+
try stderr.print("Invalid opt: '{c}'\n", .{b});
153
185
std.process.exit(1);
154
186
},
155
187
}
···
161
193
const val = split.rest();
162
194
if (eql(opt, "all")) {
163
195
cmd.opts.all = parseArgBool(val) orelse {
164
-
try stderr.print("Invalid boolean: '{s}'", .{val});
196
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
165
197
std.process.exit(1);
166
198
};
167
199
} else if (eql(opt, "long")) {
168
200
cmd.opts.long = parseArgBool(val) orelse {
169
-
try stderr.print("Invalid boolean: '{s}'", .{val});
201
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
170
202
std.process.exit(1);
171
203
};
172
204
} else if (eql(opt, "almost-all")) {
173
205
cmd.opts.@"almost-all" = parseArgBool(val) orelse {
174
-
try stderr.print("Invalid boolean: '{s}'", .{val});
206
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
175
207
std.process.exit(1);
176
208
};
177
209
} else if (eql(opt, "group-directories-first")) {
178
210
cmd.opts.@"group-directories-first" = parseArgBool(val) orelse {
179
-
try stderr.print("Invalid boolean: '{s}'", .{val});
211
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
180
212
std.process.exit(1);
181
213
};
182
214
} else if (eql(opt, "color")) {
183
215
cmd.opts.color = std.meta.stringToEnum(Options.When, val) orelse {
184
-
try stderr.print("Invalid color option: '{s}'", .{val});
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});
185
224
std.process.exit(1);
186
225
};
187
226
} else if (eql(opt, "icons")) {
188
227
cmd.opts.icons = std.meta.stringToEnum(Options.When, val) orelse {
189
-
try stderr.print("Invalid color option: '{s}'", .{val});
228
+
try stderr.print("Invalid color option: '{s}'\n", .{val});
190
229
std.process.exit(1);
191
230
};
192
231
} else if (eql(opt, "columns")) {
193
232
const c = parseArgBool(val) orelse {
194
-
try stderr.print("Invalid columns option: '{s}'", .{val});
233
+
try stderr.print("Invalid columns option: '{s}'\n", .{val});
195
234
std.process.exit(1);
196
235
};
197
236
cmd.opts.shortview = if (c) .columns else .oneline;
198
237
} else if (eql(opt, "oneline")) {
199
238
const o = parseArgBool(val) orelse {
200
-
try stderr.print("Invalid oneline option: '{s}'", .{val});
239
+
try stderr.print("Invalid oneline option: '{s}'\n", .{val});
201
240
std.process.exit(1);
202
241
};
203
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
+
}
204
264
} else if (eql(opt, "help")) {
205
265
return stderr.writeAll(usage);
206
266
} else if (eql(opt, "version")) {
207
-
try bw.writer().print("lsr {s}\r\n", .{build_options.version});
208
-
try bw.flush();
267
+
try stdout.print("lsr {s}\r\n", .{build_options.version});
268
+
try stdout.flush();
209
269
return;
210
270
} else {
211
-
try stderr.print("Invalid opt: '{s}'", .{opt});
271
+
try stderr.print("Invalid opt: '{s}'\n", .{opt});
212
272
std.process.exit(1);
213
273
}
214
274
},
215
275
.positional => {
216
-
cmd.opts.directory = arg;
276
+
try cmd.opts.directories.append(allocator, arg);
217
277
},
218
278
}
219
279
}
···
222
282
cmd.opts.colors = .default;
223
283
}
224
284
225
-
var ring: ourio.Ring = try .init(allocator, queue_size);
226
-
defer ring.deinit();
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;
227
300
228
-
_ = try ring.open(cmd.opts.directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{
229
-
.ptr = &cmd,
230
-
.cb = onCompletion,
231
-
.msg = @intFromEnum(Msg.cwd),
232
-
});
301
+
var ring: ourio.Ring = try .init(allocator, queue_size);
302
+
defer ring.deinit();
233
303
234
-
if (cmd.opts.long) {
235
-
_ = try ring.open("/etc/localtime", .{ .CLOEXEC = true }, 0, .{
304
+
_ = try ring.open(directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{
236
305
.ptr = &cmd,
237
306
.cb = onCompletion,
238
-
.msg = @intFromEnum(Msg.localtime),
307
+
.msg = @intFromEnum(Msg.cwd),
239
308
});
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
-
}
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);
251
329
252
-
try ring.run(.until_done);
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
+
}
253
336
254
-
if (cmd.entries.len == 0) return;
337
+
std.sort.pdq(Entry, cmd.entries, cmd.opts, Entry.lessThan);
255
338
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()),
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
+
}
261
357
}
262
-
try bw.flush();
358
+
try stdout.flush();
263
359
}
264
360
265
361
fn printShortColumns(cmd: Command, writer: anytype) !void {
···
332
428
for (columns.items, 0..) |column, i| {
333
429
if (row >= column.entries.len) continue;
334
430
const entry = column.entries[row];
335
-
try printShortEntry(column.entries[row], cmd.opts, writer);
431
+
try printShortEntry(column.entries[row], cmd, writer);
336
432
337
433
if (i < columns.items.len - 1) {
338
434
const spaces = column.width - (icon_width + entry.name.len);
339
-
try writer.writeByteNTimes(' ', spaces);
435
+
var space_buf = [_][]const u8{" "};
436
+
try writer.writeSplatAll(&space_buf, spaces);
340
437
}
341
438
}
342
439
try writer.writeAll("\r\n");
···
347
444
return idx + n_short_cols >= n_cols;
348
445
}
349
446
350
-
fn printShortEntry(entry: Entry, opts: Options, writer: anytype) !void {
447
+
fn printShortEntry(entry: Entry, cmd: Command, writer: anytype) !void {
448
+
const opts = cmd.opts;
351
449
const colors = opts.colors;
352
450
if (opts.useIcons()) {
353
-
const icon = Icon.get(entry, opts);
451
+
const icon = Icon.get(entry);
354
452
355
453
if (opts.useColor()) {
356
454
try writer.writeAll(icon.color);
···
371
469
}
372
470
},
373
471
}
374
-
try writer.writeAll(entry.name);
375
-
try writer.writeAll(colors.reset);
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
+
}
376
483
}
377
484
378
485
fn printShortOneRow(cmd: Command, writer: anytype) !void {
···
385
492
386
493
fn printShortOnePerLine(cmd: Command, writer: anytype) !void {
387
494
for (cmd.entries) |entry| {
388
-
try printShortEntry(entry, cmd.opts, writer);
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);
389
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
+
}
390
587
}
391
588
}
392
589
393
-
fn printLong(cmd: Command, writer: anytype) !void {
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 {
394
635
const tz = cmd.tz.?;
395
636
const now = zeit.instant(.{}) catch unreachable;
396
637
const one_year_ago = try now.subtract(.{ .days = 365 });
···
402
643
var n_size: usize = 0;
403
644
var n_suff: usize = 0;
404
645
for (cmd.entries) |entry| {
405
-
const group = cmd.getGroup(entry.statx.gid);
406
-
const user = cmd.getUser(entry.statx.uid);
646
+
const group = try cmd.getGroup(entry.statx.gid);
647
+
const user = try cmd.getUser(entry.statx.uid);
407
648
408
649
var buf: [16]u8 = undefined;
409
650
const size = try entry.humanReadableSize(&buf);
···
434
675
};
435
676
436
677
for (cmd.entries) |entry| {
437
-
const user: User = cmd.getUser(entry.statx.uid) orelse
678
+
const user: User = try cmd.getUser(entry.statx.uid) orelse
438
679
.{
439
680
.uid = entry.statx.uid,
440
681
.name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.uid}),
441
682
};
442
-
const group: Group = cmd.getGroup(entry.statx.gid) orelse
683
+
const group: Group = try cmd.getGroup(entry.statx.gid) orelse
443
684
.{
444
685
.gid = entry.statx.gid,
445
686
.name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.gid}),
···
453
694
try writer.writeAll(&mode);
454
695
try writer.writeByte(' ');
455
696
try writer.writeAll(user.name);
456
-
try writer.writeByteNTimes(' ', longest_user - user.name.len);
697
+
var space_buf1 = [_][]const u8{" "};
698
+
try writer.writeSplatAll(&space_buf1, longest_user - user.name.len);
457
699
try writer.writeByte(' ');
458
700
try writer.writeAll(group.name);
459
-
try writer.writeByteNTimes(' ', longest_group - group.name.len);
701
+
var space_buf2 = [_][]const u8{" "};
702
+
try writer.writeSplatAll(&space_buf2, longest_group - group.name.len);
460
703
try writer.writeByte(' ');
461
704
462
705
var size_buf: [16]u8 = undefined;
463
706
const size = try entry.humanReadableSize(&size_buf);
464
707
const suffix = entry.humanReadableSuffix();
465
708
466
-
try writer.writeByteNTimes(' ', longest_size - size.len);
709
+
var space_buf3 = [_][]const u8{" "};
710
+
try writer.writeSplatAll(&space_buf3, longest_size - size.len);
467
711
try writer.writeAll(size);
468
712
try writer.writeByte(' ');
469
713
try writer.writeAll(suffix);
470
-
try writer.writeByteNTimes(' ', longest_suffix - suffix.len);
714
+
var space_buf4 = [_][]const u8{" "};
715
+
try writer.writeSplatAll(&space_buf4, longest_suffix - suffix.len);
471
716
try writer.writeByte(' ');
472
717
473
718
try writer.print("{d: >2} {s} ", .{
···
482
727
}
483
728
484
729
if (cmd.opts.useIcons()) {
485
-
const icon = Icon.get(entry, cmd.opts);
730
+
const icon = Icon.get(entry);
486
731
487
732
if (cmd.opts.useColor()) {
488
733
try writer.writeAll(icon.color);
···
504
749
}
505
750
},
506
751
}
507
-
try writer.writeAll(entry.name);
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
+
}
508
761
try writer.writeAll(colors.reset);
509
762
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);
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 => {},
519
786
}
520
787
521
788
try writer.writeAll("\r\n");
···
527
794
opts: Options = .{},
528
795
entries: []Entry = &.{},
529
796
entry_idx: usize = 0,
797
+
symlinks: std.StringHashMapUnmanaged(Symlink) = .empty,
798
+
current_directory: [:0]const u8 = ".",
530
799
531
800
tz: ?zeit.TimeZone = null,
532
801
groups: std.ArrayListUnmanaged(Group) = .empty,
533
802
users: std.ArrayListUnmanaged(User) = .empty,
534
803
535
-
fn getUser(self: Command, uid: posix.uid_t) ?User {
804
+
fn getUser(self: *Command, uid: posix.uid_t) !?User {
536
805
for (self.users.items) |user| {
537
806
if (user.uid == uid) return user;
538
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
+
}
539
818
return null;
540
819
}
541
820
542
-
fn getGroup(self: Command, gid: posix.gid_t) ?Group {
821
+
fn getGroup(self: *Command, gid: posix.gid_t) !?Group {
543
822
for (self.groups.items) |group| {
544
823
if (group.gid == gid) return group;
545
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
+
}
546
833
return null;
547
834
}
548
835
};
···
560
847
};
561
848
562
849
const User = struct {
563
-
uid: posix.uid_t,
850
+
uid: if (builtin.os.tag == .macos) i33 else posix.uid_t,
564
851
name: []const u8,
565
852
566
853
fn lessThan(_: void, lhs: User, rhs: User) bool {
···
569
856
};
570
857
571
858
const Group = struct {
572
-
gid: posix.gid_t,
859
+
gid: if (builtin.os.tag == .macos) i33 else posix.gid_t,
573
860
name: []const u8,
574
861
575
862
fn lessThan(_: void, lhs: Group, rhs: Group) bool {
···
593
880
}
594
881
};
595
882
883
+
const Symlink = struct {
884
+
name: [:0]const u8,
885
+
exists: bool = true,
886
+
};
887
+
596
888
const Entry = struct {
597
889
name: [:0]const u8,
598
890
kind: std.fs.File.Kind,
599
891
statx: ourio.Statx,
600
-
link_name: [:0]const u8 = "",
601
-
symlink_missing: bool = false,
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
+
}
602
910
603
911
fn modeStr(self: Entry) [10]u8 {
604
912
var mode = [_]u8{'-'} ** 10;
···
678
986
679
987
switch (msg) {
680
988
.cwd => {
681
-
const fd = try result.open;
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
+
};
682
1015
// we are async, no need to defer!
683
1016
_ = try io.close(fd, .{});
684
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
+
}
685
1024
686
1025
var temp_results: std.ArrayListUnmanaged(MinimalEntry) = .empty;
687
1026
···
702
1041
703
1042
var iter = dir.iterate();
704
1043
while (try iter.next()) |dirent| {
705
-
if (!cmd.opts.@"almost-all" and std.mem.startsWith(u8, dirent.name, ".")) continue;
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
+
}
706
1055
const nameZ = try cmd.arena.dupeZ(u8, dirent.name);
707
1056
try temp_results.append(cmd.arena, .{
708
1057
.name = nameZ,
···
732
1081
}
733
1082
const path = try std.fs.path.joinZ(
734
1083
cmd.arena,
735
-
&.{ cmd.opts.directory, entry.name },
1084
+
&.{ cmd.current_directory, entry.name },
736
1085
);
737
1086
738
1087
if (entry.kind == .sym_link) {
···
740
1089
741
1090
// NOTE: Sadly, we can't do readlink via io_uring
742
1091
const link = try posix.readlink(path, &buf);
743
-
entry.link_name = try cmd.arena.dupeZ(u8, link);
1092
+
const symlink: Symlink = .{ .name = try cmd.arena.dupeZ(u8, link) };
1093
+
try cmd.symlinks.put(cmd.arena, entry.name, symlink);
744
1094
}
745
1095
_ = try io.stat(path, &entry.statx, .{
746
1096
.cb = onCompletion,
···
756
1106
// Largest TZ file on my system is Asia/Hebron at 4791 bytes. We allocate an amount
757
1107
// sufficiently more than that to make sure we do this in a single pass
758
1108
const buffer = try cmd.arena.alloc(u8, 8192);
759
-
_ = try io.read(fd, buffer, .{
1109
+
_ = try io.read(fd, buffer, .file, .{
760
1110
.cb = onCompletion,
761
1111
.ptr = cmd,
762
1112
.msg = @intFromEnum(Msg.read_localtime),
···
767
1117
const n = try result.read;
768
1118
_ = try io.close(task.req.read.fd, .{});
769
1119
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());
1120
+
var reader = std.Io.Reader.fixed(bytes);
1121
+
const tz = try zeit.timezone.TZInfo.parse(cmd.arena, &reader);
772
1122
cmd.tz = .{ .tzinfo = tz };
773
1123
},
774
1124
···
778
1128
// TODO: stat this or do multiple reads. We'll never know a good bound unless we go
779
1129
// really big
780
1130
const buffer = try cmd.arena.alloc(u8, 8192 * 2);
781
-
_ = try io.read(fd, buffer, .{
1131
+
_ = try io.read(fd, buffer, .file, .{
782
1132
.cb = onCompletion,
783
1133
.ptr = cmd,
784
1134
.msg = @intFromEnum(Msg.read_passwd),
···
801
1151
// <name>:<throwaway>:<uid><...garbage>
802
1152
while (lines.next()) |line| {
803
1153
if (line.len == 0) continue;
1154
+
if (std.mem.startsWith(u8, line, "#")) continue;
1155
+
804
1156
var iter = std.mem.splitScalar(u8, line, ':');
805
1157
const name = iter.first();
806
1158
_ = iter.next();
···
808
1160
809
1161
const user: User = .{
810
1162
.name = name,
811
-
.uid = try std.fmt.parseInt(u32, uid, 10),
1163
+
.uid = try std.fmt.parseInt(
1164
+
if (builtin.os.tag == .macos) i33 else u32,
1165
+
uid,
1166
+
10,
1167
+
),
812
1168
};
813
1169
814
1170
cmd.users.appendAssumeCapacity(user);
···
820
1176
const fd = try result.open;
821
1177
822
1178
const buffer = try cmd.arena.alloc(u8, 8192);
823
-
_ = try io.read(fd, buffer, .{
1179
+
_ = try io.read(fd, buffer, .file, .{
824
1180
.cb = onCompletion,
825
1181
.ptr = cmd,
826
1182
.msg = @intFromEnum(Msg.read_group),
···
843
1199
// <name>:<throwaway>:<uid><...garbage>
844
1200
while (lines.next()) |line| {
845
1201
if (line.len == 0) continue;
1202
+
if (std.mem.startsWith(u8, line, "#")) continue;
1203
+
846
1204
var iter = std.mem.splitScalar(u8, line, ':');
847
1205
const name = iter.first();
848
1206
_ = iter.next();
···
850
1208
851
1209
const group: Group = .{
852
1210
.name = name,
853
-
.gid = try std.fmt.parseInt(u32, gid, 10),
1211
+
.gid = try std.fmt.parseInt(
1212
+
if (builtin.os.tag == .macos) i33 else u32,
1213
+
gid,
1214
+
10,
1215
+
),
854
1216
};
855
1217
856
1218
cmd.groups.appendAssumeCapacity(group);
···
859
1221
},
860
1222
861
1223
.stat => {
862
-
_ = result.statx catch {
1224
+
_ = result.statx catch |err| {
863
1225
const entry: *Entry = @fieldParentPtr("statx", task.req.statx.result);
864
-
if (entry.symlink_missing) {
865
-
// we already got here. Just zero out the statx;
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
866
1231
entry.statx = std.mem.zeroInit(ourio.Statx, entry.statx);
867
1232
return;
868
1233
}
869
1234
870
-
entry.symlink_missing = true;
1235
+
symlink.exists = false;
1236
+
871
1237
_ = try io.lstat(task.req.statx.path, task.req.statx.result, .{
872
1238
.cb = onCompletion,
873
1239
.ptr = cmd,
···
882
1248
cmd.entry_idx += 1;
883
1249
const path = try std.fs.path.joinZ(
884
1250
cmd.arena,
885
-
&.{ cmd.opts.directory, entry.name },
1251
+
&.{ cmd.current_directory, entry.name },
886
1252
);
887
1253
888
1254
if (entry.kind == .sym_link) {
···
890
1256
891
1257
// NOTE: Sadly, we can't do readlink via io_uring
892
1258
const link = try posix.readlink(path, &buf);
893
-
entry.link_name = try cmd.arena.dupeZ(u8, link);
1259
+
const symlink: Symlink = .{ .name = try cmd.arena.dupeZ(u8, link) };
1260
+
try cmd.symlinks.put(cmd.arena, entry.name, symlink);
894
1261
}
895
1262
_ = try io.stat(path, &entry.statx, .{
896
1263
.cb = onCompletion,
···
928
1295
const json: Icon = .{ .icon = "๎", .color = Options.Colors.blue };
929
1296
const lua: Icon = .{ .icon = "๓ฐขฑ", .color = Options.Colors.blue };
930
1297
const markdown: Icon = .{ .icon = "๎", .color = "" };
1298
+
const nix: Icon = .{ .icon = "๓ฑ
", .color = "\x1b[38:2:127:185:228m" };
931
1299
const python: Icon = .{ .icon = "๎ผ", .color = Options.Colors.yellow };
932
1300
const rust: Icon = .{ .icon = "๎จ", .color = "" };
933
1301
const typescript: Icon = .{ .icon = "๎ฃ", .color = Options.Colors.blue };
934
1302
const zig: Icon = .{ .icon = "๎ฉ", .color = "\x1b[38:2:247:164:29m" };
935
1303
936
-
const by_name: std.StaticStringMap(Icon) = .initComptime(.{});
1304
+
const by_name: std.StaticStringMap(Icon) = .initComptime(.{
1305
+
.{ "flake.lock", Icon.nix },
1306
+
.{ "go.mod", Icon.go },
1307
+
.{ "go.sum", Icon.go },
1308
+
});
937
1309
938
1310
const by_extension: std.StaticStringMap(Icon) = .initComptime(.{
939
1311
.{ "cjs", Icon.javascript },
940
1312
.{ "css", Icon.css },
1313
+
.{ "drv", Icon.nix },
941
1314
.{ "gif", Icon.image },
942
1315
.{ "go", Icon.go },
943
1316
.{ "html", Icon.html },
···
951
1324
.{ "mjs", Icon.javascript },
952
1325
.{ "mkv", Icon.video },
953
1326
.{ "mp4", Icon.video },
1327
+
.{ "nar", Icon.nix },
1328
+
.{ "nix", Icon.nix },
954
1329
.{ "png", Icon.image },
955
1330
.{ "py", Icon.python },
956
1331
.{ "rs", Icon.rust },
···
961
1336
.{ "zon", Icon.zig },
962
1337
});
963
1338
964
-
fn get(entry: Entry, opts: Options) Icon {
1339
+
fn get(entry: Entry) Icon {
965
1340
// 1. By name
966
-
// 2. By extension
967
-
// 3. By type
1341
+
// 2. By type
1342
+
// 3. By extension
968
1343
if (by_name.get(entry.name)) |icon| return icon;
969
1344
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
1345
switch (entry.kind) {
977
1346
.block_device => return drive,
978
1347
.character_device => return drive,
979
1348
.directory => return directory,
980
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
+
981
1356
if (entry.isExecutable()) {
982
1357
return executable;
983
1358
}
···
985
1360
},
986
1361
.named_pipe => return pipe,
987
1362
.sym_link => {
988
-
if (opts.long and posix.S.ISDIR(entry.statx.mode)) {
1363
+
if (posix.S.ISDIR(entry.statx.mode)) {
989
1364
return symlink_dir;
990
1365
}
991
1366
return symlink;
···
1032
1407
if (std.mem.startsWith(u8, a, "-")) return .short;
1033
1408
return .positional;
1034
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
+
}