+63
-23
README.md
+63
-23
README.md
···
4
4
5
5

6
6
7
+
## Installation
8
+
9
+
`lsr` uses the zig build system. To install, you will need zig 0.14.0. To
10
+
install for the local user (assuming `$HOME/.local/bin` is in `$PATH`), run:
11
+
12
+
```sh
13
+
zig build -Doptimize=ReleaseSmall --prefix $HOME/.local
14
+
```
15
+
16
+
which will install `lsr` and the associated manpage appropriately. Replace
17
+
`$HOME/.local` with your preferred installation directory.
18
+
19
+
## Usage
20
+
21
+
```
22
+
lsr [options] [path]
23
+
24
+
--help Print this message and exit
25
+
--version Print the version string
26
+
27
+
DISPLAY OPTIONS
28
+
-1, --oneline Print entries one per line
29
+
-a, --all Show files that start with a dot (ASCII 0x2E)
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
+
7
42
## Benchmarks
8
43
9
44
Benchmarks were all gathered on the same set of directories, using the latest
10
-
releases of each program (versions are shown below).
45
+
releases of each program (versions are shown below). All benchmarks run on Linux
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.
11
48
12
-
| Program | Version |
13
-
|:-------:|:-------:|
14
-
| lsr | 0.1.0 |
15
-
| ls | 9.7 |
16
-
| eza | 0.21.3 |
17
-
| lsd | 1.1.5 |
18
-
| 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 |
19
57
20
58
### Time
21
59
22
60
Data gathered with `hyperfine` on a directory of `n` plain files.
23
61
24
-
| Program | n=10 | n=100 | n=1,000 | n=10,000 |
25
-
|:-------------:|:--------:|:--------:|:-------:|:--------:|
26
-
| lsr -al | 372.6 ยตs | 634.3 ยตs | 2.7 ms | 22.1 ms |
27
-
| ls -al | 1.4 ms | 1.7 ms | 4.7 ms | 38.0 ms |
28
-
| eza -al | 2.9 ms | 3.3 ms | 6.6 ms | 40.2 ms |
29
-
| lsd -al | 2.1 ms | 3.5 ms | 17.0 ms | 153.4 ms |
30
-
| 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 |
31
70
32
71
### Syscalls
33
72
34
-
Data gathered with `strace -c` on a directory of `n` plain files.
73
+
Data gathered with `strace -c` on a directory of `n` plain files. (Lower is better)
35
74
36
-
| Program | n=10 | n=100 | n=1,000 | n=10,000 |
37
-
|:-------------:|:----:|:-----:|:-------:|:--------:|
38
-
| lsr -al | 20 | 28 | 105 | 848 |
39
-
| ls -al | 405 | 675 | 3,377 | 30,396 |
40
-
| eza -al | 319 | 411 | 1,320 | 10,364 |
41
-
| lsd -al | 508 | 1,408 | 10,423 | 100,512 |
42
-
| 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 |
+15
-1
build.zig
+15
-1
build.zig
···
1
1
const std = @import("std");
2
+
const zzdoc = @import("zzdoc");
2
3
3
4
/// Must be kept in sync with git tags
4
-
const version: std.SemanticVersion = .{ .major = 0, .minor = 1, .patch = 0 };
5
+
const version: std.SemanticVersion = .{ .major = 1, .minor = 0, .patch = 0 };
5
6
6
7
pub fn build(b: *std.Build) void {
7
8
const target = b.standardTargetOptions(.{});
8
9
const optimize = b.standardOptimizeOption(.{});
9
10
11
+
// manpages
12
+
{
13
+
var man_step = zzdoc.addManpageStep(b, .{
14
+
.root_doc_dir = b.path("docs/"),
15
+
});
16
+
17
+
const install_step = man_step.addInstallStep(.{});
18
+
b.default_step.dependOn(&install_step.step);
19
+
}
20
+
10
21
const exe_mod = b.createModule(.{
11
22
.root_source_file = b.path("src/main.zig"),
12
23
.target = target,
···
34
45
.name = "lsr",
35
46
.root_module = exe_mod,
36
47
});
48
+
exe.linkLibC();
37
49
38
50
b.installArtifact(exe);
39
51
···
71
83
"-C",
72
84
b.build_root.path orelse ".",
73
85
"describe",
86
+
"--match",
87
+
"*.*.*",
74
88
"--tags",
75
89
"--abbrev=9",
76
90
}, &code, .Ignore) catch {
+10
-6
build.zig.zon
+10
-6
build.zig.zon
···
1
1
.{
2
2
.name = .lsr,
3
-
.version = "0.1.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#1afffffe2424f9c5923271ac764e950efbdde696",
11
-
.hash = "ourio-0.0.0-_s-z0cALAgCdk4d-uZk0B07uX47Lf64fJDJ9L4Ejj3Rs",
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
+
},
17
+
.zzdoc = .{
18
+
.url = "git+https://github.com/rockorager/zzdoc#a54223bdc13a80839ccf9f473edf3a171e777946",
19
+
.hash = "zzdoc-0.0.0-tzT1Ph7cAAC5YmXQXiBJHAg41_A5AUAC5VOm7ShnUxlz",
16
20
},
17
21
},
18
22
.paths = .{
+60
docs/lsr.1.scd
+60
docs/lsr.1.scd
···
1
+
lsr(1)
2
+
3
+
# NAME
4
+
5
+
lsr - list directory contents, but with io_uring
6
+
7
+
# SYNOPSIS
8
+
9
+
*lsr* [options...] [path]
10
+
11
+
# DESCRIPTION
12
+
13
+
lsr is an implementation of ls(1) which utilizes io_uring to perform syscall
14
+
batching.
15
+
16
+
# OPTIONS
17
+
18
+
*-1*, *--oneline*
19
+
Print entries one per line
20
+
21
+
*-a*, *--all*
22
+
Show files that start with a dot (ASCII 0x2E)
23
+
24
+
*-A*, *--almost-all*
25
+
Like --all, but skips implicit "." and ".." directories
26
+
27
+
*-C*, *--columns*
28
+
Print the output in columns
29
+
30
+
*--color=WHEN*
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
56
+
57
+
# AUTHORS
58
+
59
+
Written and maintained by Tim Culverhouse <tim@timculverhouse.com>, assisted by
60
+
open source contributors.
+62
flake.lock
+62
flake.lock
···
1
+
{
2
+
"nodes": {
3
+
"nixpkgs": {
4
+
"locked": {
5
+
"lastModified": 1746397377,
6
+
"narHash": "sha256-5oLdRa3vWSRbuqPIFFmQBGGUqaYZBxX+GGtN9f/n4lU=",
7
+
"owner": "nixos",
8
+
"repo": "nixpkgs",
9
+
"rev": "ed30f8aba41605e3ab46421e3dcb4510ec560ff8",
10
+
"type": "github"
11
+
},
12
+
"original": {
13
+
"owner": "nixos",
14
+
"ref": "nixpkgs-unstable",
15
+
"repo": "nixpkgs",
16
+
"type": "github"
17
+
}
18
+
},
19
+
"root": {
20
+
"inputs": {
21
+
"nixpkgs": "nixpkgs",
22
+
"utils": "utils"
23
+
}
24
+
},
25
+
"systems": {
26
+
"locked": {
27
+
"lastModified": 1681028828,
28
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
29
+
"owner": "nix-systems",
30
+
"repo": "default",
31
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
32
+
"type": "github"
33
+
},
34
+
"original": {
35
+
"owner": "nix-systems",
36
+
"repo": "default",
37
+
"type": "github"
38
+
}
39
+
},
40
+
"utils": {
41
+
"inputs": {
42
+
"systems": "systems"
43
+
},
44
+
"locked": {
45
+
"lastModified": 1731533236,
46
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
47
+
"owner": "numtide",
48
+
"repo": "flake-utils",
49
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
50
+
"type": "github"
51
+
},
52
+
"original": {
53
+
"owner": "numtide",
54
+
"ref": "main",
55
+
"repo": "flake-utils",
56
+
"type": "github"
57
+
}
58
+
}
59
+
},
60
+
"root": "root",
61
+
"version": 7
62
+
}
+44
flake.nix
+44
flake.nix
···
1
+
{
2
+
inputs = {
3
+
utils.url = "github:numtide/flake-utils/main";
4
+
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
5
+
};
6
+
7
+
outputs = { self, nixpkgs, utils }:
8
+
utils.lib.eachDefaultSystem(system:
9
+
let
10
+
pkgs = import nixpkgs { inherit system; };
11
+
cache = import ./nix/cache.nix { inherit pkgs; };
12
+
in {
13
+
devshells.default = pkgs.mkShell {
14
+
nativeBuildInputs = with pkgs; [ zig zls ];
15
+
};
16
+
17
+
packages.default = pkgs.stdenv.mkDerivation {
18
+
pname = "lsr";
19
+
version = "1.0.0";
20
+
doCheck = false;
21
+
src = ./.;
22
+
23
+
nativeBuildInputs = with pkgs; [ zig ];
24
+
25
+
buildPhase = ''
26
+
export ZIG_GLOBAL_CACHE_DIR=$(mktemp -d)
27
+
ln -sf ${cache} $ZIG_GLOBAL_CACHE_DIR/p
28
+
zig build -Doptimize=ReleaseFast --summary all
29
+
'';
30
+
31
+
installPhase = ''
32
+
install -Ds -m755 zig-out/bin/lsr $out/bin/lsr
33
+
'';
34
+
35
+
meta = with pkgs.lib; {
36
+
description = "ls(1) but with io_uring";
37
+
homepage = "https://tangled.sh/@rockorager.dev/lsr";
38
+
maintainers = with maintainers; [ rockorager ];
39
+
platforms = platforms.linux;
40
+
license = licenses.mit;
41
+
};
42
+
};
43
+
});
44
+
}
+23
nix/cache.nix
+23
nix/cache.nix
···
1
+
{ pkgs, ... }:
2
+
3
+
pkgs.stdenv.mkDerivation {
4
+
pname = "lsr-cache";
5
+
version = "1.0.0";
6
+
doCheck = false;
7
+
src = ../.;
8
+
9
+
nativeBuildInputs = with pkgs; [ zig ];
10
+
11
+
buildPhase = ''
12
+
export ZIG_GLOBAL_CACHE_DIR=$(mktemp -d)
13
+
zig build --fetch --summary none
14
+
'';
15
+
16
+
installPhase = ''
17
+
mv $ZIG_GLOBAL_CACHE_DIR/p $out
18
+
'';
19
+
20
+
outputHash = "sha256-bfc2dlQa1VGq9S6OBeQawAJuvfxU4kgFtQ13fuKhdZc=";
21
+
outputHashMode = "recursive";
22
+
outputHashAlgo = "sha256";
23
+
}
+530
-113
src/main.zig
+530
-113
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
18
+
\\ --version Print the version string
14
19
\\
15
20
\\DISPLAY OPTIONS
16
21
\\ -1, --oneline Print entries one per line
···
18
23
\\ -A, --almost-all Like --all, but skips implicit "." and ".." directories
19
24
\\ -C, --columns Print the output in columns
20
25
\\ --color=WHEN When to use colors (always, auto, never)
21
-
\\ --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)
22
28
\\ --icons=WHEN When to display icons (always, auto, never)
23
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
+
\\
24
34
;
25
35
26
36
const queue_size = 256;
···
31
41
color: When = .auto,
32
42
shortview: enum { columns, oneline } = .oneline,
33
43
@"group-directories-first": bool = true,
44
+
hyperlinks: When = .auto,
34
45
icons: When = .auto,
35
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,
36
51
37
-
directory: [:0]const u8 = ".",
52
+
directories: std.ArrayListUnmanaged([:0]const u8) = .empty,
53
+
file: ?[]const u8 = null,
38
54
39
55
winsize: ?posix.winsize = null,
40
56
colors: Colors = .none,
···
99
115
}
100
116
}
101
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
+
102
130
fn isatty(self: Options) bool {
103
131
return self.winsize != null;
104
132
}
···
124
152
125
153
var cmd: Command = .{ .arena = allocator };
126
154
127
-
cmd.opts.winsize = getWinsize(std.io.getStdOut().handle);
155
+
cmd.opts.winsize = getWinsize(std.fs.File.stdout().handle);
128
156
129
157
cmd.opts.shortview = if (cmd.opts.isatty()) .columns else .oneline;
130
158
131
-
const stdout = std.io.getStdOut().writer();
132
-
const stderr = std.io.getStdErr().writer();
133
-
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;
134
165
135
166
var args = std.process.args();
136
167
// skip binary
···
145
176
'A' => cmd.opts.@"almost-all" = true,
146
177
'C' => cmd.opts.shortview = .columns,
147
178
'a' => cmd.opts.all = true,
179
+
'h' => {}, // human-readable: present for compatibility
148
180
'l' => cmd.opts.long = true,
181
+
'r' => cmd.opts.reverse_sort = true,
182
+
't' => cmd.opts.sort_by_mod_time = true,
149
183
else => {
150
-
try stderr.print("Invalid opt: '{c}'", .{b});
184
+
try stderr.print("Invalid opt: '{c}'\n", .{b});
151
185
std.process.exit(1);
152
186
},
153
187
}
···
159
193
const val = split.rest();
160
194
if (eql(opt, "all")) {
161
195
cmd.opts.all = parseArgBool(val) orelse {
162
-
try stderr.print("Invalid boolean: '{s}'", .{val});
196
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
163
197
std.process.exit(1);
164
198
};
165
199
} else if (eql(opt, "long")) {
166
200
cmd.opts.long = parseArgBool(val) orelse {
167
-
try stderr.print("Invalid boolean: '{s}'", .{val});
201
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
168
202
std.process.exit(1);
169
203
};
170
204
} else if (eql(opt, "almost-all")) {
171
205
cmd.opts.@"almost-all" = parseArgBool(val) orelse {
172
-
try stderr.print("Invalid boolean: '{s}'", .{val});
206
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
173
207
std.process.exit(1);
174
208
};
175
209
} else if (eql(opt, "group-directories-first")) {
176
210
cmd.opts.@"group-directories-first" = parseArgBool(val) orelse {
177
-
try stderr.print("Invalid boolean: '{s}'", .{val});
211
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
178
212
std.process.exit(1);
179
213
};
180
214
} else if (eql(opt, "color")) {
181
215
cmd.opts.color = std.meta.stringToEnum(Options.When, val) orelse {
182
-
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});
183
224
std.process.exit(1);
184
225
};
185
226
} else if (eql(opt, "icons")) {
186
227
cmd.opts.icons = std.meta.stringToEnum(Options.When, val) orelse {
187
-
try stderr.print("Invalid color option: '{s}'", .{val});
228
+
try stderr.print("Invalid color option: '{s}'\n", .{val});
188
229
std.process.exit(1);
189
230
};
190
231
} else if (eql(opt, "columns")) {
191
232
const c = parseArgBool(val) orelse {
192
-
try stderr.print("Invalid columns option: '{s}'", .{val});
233
+
try stderr.print("Invalid columns option: '{s}'\n", .{val});
193
234
std.process.exit(1);
194
235
};
195
236
cmd.opts.shortview = if (c) .columns else .oneline;
196
237
} else if (eql(opt, "oneline")) {
197
238
const o = parseArgBool(val) orelse {
198
-
try stderr.print("Invalid oneline option: '{s}'", .{val});
239
+
try stderr.print("Invalid oneline option: '{s}'\n", .{val});
199
240
std.process.exit(1);
200
241
};
201
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
+
}
202
264
} else if (eql(opt, "help")) {
203
265
return stderr.writeAll(usage);
204
266
} else if (eql(opt, "version")) {
205
-
try bw.writer().print("lsr {s}\r\n", .{build_options.version});
206
-
try bw.flush();
267
+
try stdout.print("lsr {s}\r\n", .{build_options.version});
268
+
try stdout.flush();
207
269
return;
208
270
} else {
209
-
try stderr.print("Invalid opt: '{s}'", .{opt});
271
+
try stderr.print("Invalid opt: '{s}'\n", .{opt});
210
272
std.process.exit(1);
211
273
}
212
274
},
213
275
.positional => {
214
-
cmd.opts.directory = arg;
276
+
try cmd.opts.directories.append(allocator, arg);
215
277
},
216
278
}
217
279
}
···
220
282
cmd.opts.colors = .default;
221
283
}
222
284
223
-
var ring: ourio.Ring = try .init(allocator, queue_size);
224
-
defer ring.deinit();
285
+
if (cmd.opts.directories.items.len == 0) {
286
+
try cmd.opts.directories.append(allocator, ".");
287
+
}
225
288
226
-
_ = try ring.open(cmd.opts.directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{
227
-
.ptr = &cmd,
228
-
.cb = onCompletion,
229
-
.msg = @intFromEnum(Msg.cwd),
230
-
});
289
+
const multiple_dirs = cmd.opts.directories.items.len > 1;
231
290
232
-
if (cmd.opts.long) {
233
-
_ = try ring.open("/etc/localtime", .{ .CLOEXEC = true }, 0, .{
234
-
.ptr = &cmd,
235
-
.cb = onCompletion,
236
-
.msg = @intFromEnum(Msg.localtime),
237
-
});
238
-
_ = try ring.open("/etc/passwd", .{ .CLOEXEC = true }, 0, .{
239
-
.ptr = &cmd,
240
-
.cb = onCompletion,
241
-
.msg = @intFromEnum(Msg.passwd),
242
-
});
243
-
_ = try ring.open("/etc/group", .{ .CLOEXEC = true }, 0, .{
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, .{
244
305
.ptr = &cmd,
245
306
.cb = onCompletion,
246
-
.msg = @intFromEnum(Msg.group),
307
+
.msg = @intFromEnum(Msg.cwd),
247
308
});
248
-
}
249
309
250
-
try ring.run(.until_done);
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
+
}
251
327
252
-
if (cmd.entries.len == 0) return;
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
+
}
253
347
254
-
if (cmd.opts.long) {
255
-
try printLong(cmd, bw.writer());
256
-
} else switch (cmd.opts.shortview) {
257
-
.columns => try printShortColumns(cmd, bw.writer()),
258
-
.oneline => try printShortOnePerLine(cmd, bw.writer()),
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
+
}
259
357
}
260
-
try bw.flush();
358
+
try stdout.flush();
261
359
}
262
360
263
361
fn printShortColumns(cmd: Command, writer: anytype) !void {
···
330
428
for (columns.items, 0..) |column, i| {
331
429
if (row >= column.entries.len) continue;
332
430
const entry = column.entries[row];
333
-
try printShortEntry(column.entries[row], cmd.opts, writer);
431
+
try printShortEntry(column.entries[row], cmd, writer);
334
432
335
433
if (i < columns.items.len - 1) {
336
434
const spaces = column.width - (icon_width + entry.name.len);
337
-
try writer.writeByteNTimes(' ', spaces);
435
+
var space_buf = [_][]const u8{" "};
436
+
try writer.writeSplatAll(&space_buf, spaces);
338
437
}
339
438
}
340
439
try writer.writeAll("\r\n");
···
345
444
return idx + n_short_cols >= n_cols;
346
445
}
347
446
348
-
fn printShortEntry(entry: Entry, opts: Options, writer: anytype) !void {
447
+
fn printShortEntry(entry: Entry, cmd: Command, writer: anytype) !void {
448
+
const opts = cmd.opts;
349
449
const colors = opts.colors;
350
450
if (opts.useIcons()) {
351
-
const icon = Icon.get(entry, opts);
451
+
const icon = Icon.get(entry);
352
452
353
453
if (opts.useColor()) {
354
454
try writer.writeAll(icon.color);
···
369
469
}
370
470
},
371
471
}
372
-
try writer.writeAll(entry.name);
373
-
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
+
}
374
483
}
375
484
376
485
fn printShortOneRow(cmd: Command, writer: anytype) !void {
···
383
492
384
493
fn printShortOnePerLine(cmd: Command, writer: anytype) !void {
385
494
for (cmd.entries) |entry| {
386
-
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);
387
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
+
}
388
543
}
389
544
}
390
545
391
-
fn printLong(cmd: Command, writer: anytype) !void {
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 {
392
635
const tz = cmd.tz.?;
393
636
const now = zeit.instant(.{}) catch unreachable;
394
637
const one_year_ago = try now.subtract(.{ .days = 365 });
···
400
643
var n_size: usize = 0;
401
644
var n_suff: usize = 0;
402
645
for (cmd.entries) |entry| {
403
-
const group = cmd.getGroup(entry.statx.gid).?;
404
-
const user = cmd.getGroup(entry.statx.uid).?;
646
+
const group = try cmd.getGroup(entry.statx.gid);
647
+
const user = try cmd.getUser(entry.statx.uid);
648
+
405
649
var buf: [16]u8 = undefined;
406
650
const size = try entry.humanReadableSize(&buf);
407
-
n_group = @max(n_group, group.name.len);
408
-
n_user = @max(n_user, user.name.len);
651
+
const group_len: usize = if (group) |g| g.name.len else switch (entry.statx.gid) {
652
+
0...9 => 1,
653
+
10...99 => 2,
654
+
100...999 => 3,
655
+
1000...9999 => 4,
656
+
10000...99999 => 5,
657
+
else => 6,
658
+
};
659
+
660
+
const user_len: usize = if (user) |u| u.name.len else switch (entry.statx.uid) {
661
+
0...9 => 1,
662
+
10...99 => 2,
663
+
100...999 => 3,
664
+
1000...9999 => 4,
665
+
10000...99999 => 5,
666
+
else => 6,
667
+
};
668
+
669
+
n_group = @max(n_group, group_len);
670
+
n_user = @max(n_user, user_len);
409
671
n_size = @max(n_size, size.len);
410
672
n_suff = @max(n_suff, entry.humanReadableSuffix().len);
411
673
}
···
413
675
};
414
676
415
677
for (cmd.entries) |entry| {
416
-
const user = cmd.getUser(entry.statx.uid).?;
417
-
const group = cmd.getGroup(entry.statx.gid).?;
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}),
687
+
};
418
688
const ts = @as(i128, entry.statx.mtime.sec) * std.time.ns_per_s;
419
689
const inst: zeit.Instant = .{ .timestamp = ts, .timezone = &tz };
420
690
const time = inst.time();
···
424
694
try writer.writeAll(&mode);
425
695
try writer.writeByte(' ');
426
696
try writer.writeAll(user.name);
427
-
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);
428
699
try writer.writeByte(' ');
429
700
try writer.writeAll(group.name);
430
-
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);
431
703
try writer.writeByte(' ');
432
704
433
705
var size_buf: [16]u8 = undefined;
434
706
const size = try entry.humanReadableSize(&size_buf);
435
707
const suffix = entry.humanReadableSuffix();
436
708
437
-
try writer.writeByteNTimes(' ', longest_size - size.len);
709
+
var space_buf3 = [_][]const u8{" "};
710
+
try writer.writeSplatAll(&space_buf3, longest_size - size.len);
438
711
try writer.writeAll(size);
439
712
try writer.writeByte(' ');
440
713
try writer.writeAll(suffix);
441
-
try writer.writeByteNTimes(' ', longest_suffix - suffix.len);
714
+
var space_buf4 = [_][]const u8{" "};
715
+
try writer.writeSplatAll(&space_buf4, longest_suffix - suffix.len);
442
716
try writer.writeByte(' ');
443
717
444
718
try writer.print("{d: >2} {s} ", .{
···
453
727
}
454
728
455
729
if (cmd.opts.useIcons()) {
456
-
const icon = Icon.get(entry, cmd.opts);
730
+
const icon = Icon.get(entry);
457
731
458
732
if (cmd.opts.useColor()) {
459
733
try writer.writeAll(icon.color);
···
475
749
}
476
750
},
477
751
}
478
-
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
+
}
479
761
try writer.writeAll(colors.reset);
480
762
481
-
if (entry.kind == .sym_link) {
482
-
try writer.writeAll(" -> ");
483
-
const color = if (entry.symlink_missing)
484
-
colors.symlink_missing
485
-
else
486
-
colors.symlink_target;
487
-
try writer.writeAll(color);
488
-
try writer.writeAll(entry.link_name);
489
-
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 => {},
490
786
}
491
787
492
788
try writer.writeAll("\r\n");
···
498
794
opts: Options = .{},
499
795
entries: []Entry = &.{},
500
796
entry_idx: usize = 0,
797
+
symlinks: std.StringHashMapUnmanaged(Symlink) = .empty,
798
+
current_directory: [:0]const u8 = ".",
501
799
502
800
tz: ?zeit.TimeZone = null,
503
801
groups: std.ArrayListUnmanaged(Group) = .empty,
504
802
users: std.ArrayListUnmanaged(User) = .empty,
505
803
506
-
fn getUser(self: Command, uid: posix.uid_t) ?User {
804
+
fn getUser(self: *Command, uid: posix.uid_t) !?User {
507
805
for (self.users.items) |user| {
508
806
if (user.uid == uid) return user;
509
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
+
}
510
818
return null;
511
819
}
512
820
513
-
fn getGroup(self: Command, gid: posix.gid_t) ?Group {
821
+
fn getGroup(self: *Command, gid: posix.gid_t) !?Group {
514
822
for (self.groups.items) |group| {
515
823
if (group.gid == gid) return group;
516
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
+
}
517
833
return null;
518
834
}
519
835
};
···
531
847
};
532
848
533
849
const User = struct {
534
-
uid: posix.uid_t,
850
+
uid: if (builtin.os.tag == .macos) i33 else posix.uid_t,
535
851
name: []const u8,
536
852
537
853
fn lessThan(_: void, lhs: User, rhs: User) bool {
···
540
856
};
541
857
542
858
const Group = struct {
543
-
gid: posix.gid_t,
859
+
gid: if (builtin.os.tag == .macos) i33 else posix.gid_t,
544
860
name: []const u8,
545
861
546
862
fn lessThan(_: void, lhs: Group, rhs: Group) bool {
···
564
880
}
565
881
};
566
882
883
+
const Symlink = struct {
884
+
name: [:0]const u8,
885
+
exists: bool = true,
886
+
};
887
+
567
888
const Entry = struct {
568
889
name: [:0]const u8,
569
890
kind: std.fs.File.Kind,
570
891
statx: ourio.Statx,
571
-
link_name: [:0]const u8 = "",
572
-
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
+
}
573
910
574
911
fn modeStr(self: Entry) [10]u8 {
575
912
var mode = [_]u8{'-'} ** 10;
576
913
switch (self.kind) {
914
+
.block_device => mode[0] = 'b',
915
+
.character_device => mode[0] = 'c',
577
916
.directory => mode[0] = 'd',
917
+
.named_pipe => mode[0] = 'p',
578
918
.sym_link => mode[0] = 'l',
579
919
else => {},
580
920
}
···
646
986
647
987
switch (msg) {
648
988
.cwd => {
649
-
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
+
};
650
1015
// we are async, no need to defer!
651
1016
_ = try io.close(fd, .{});
652
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
+
}
653
1024
654
1025
var temp_results: std.ArrayListUnmanaged(MinimalEntry) = .empty;
655
1026
···
670
1041
671
1042
var iter = dir.iterate();
672
1043
while (try iter.next()) |dirent| {
673
-
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
+
}
674
1055
const nameZ = try cmd.arena.dupeZ(u8, dirent.name);
675
1056
try temp_results.append(cmd.arena, .{
676
1057
.name = nameZ,
···
700
1081
}
701
1082
const path = try std.fs.path.joinZ(
702
1083
cmd.arena,
703
-
&.{ cmd.opts.directory, entry.name },
1084
+
&.{ cmd.current_directory, entry.name },
704
1085
);
705
1086
706
1087
if (entry.kind == .sym_link) {
···
708
1089
709
1090
// NOTE: Sadly, we can't do readlink via io_uring
710
1091
const link = try posix.readlink(path, &buf);
711
-
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);
712
1094
}
713
1095
_ = try io.stat(path, &entry.statx, .{
714
1096
.cb = onCompletion,
···
724
1106
// Largest TZ file on my system is Asia/Hebron at 4791 bytes. We allocate an amount
725
1107
// sufficiently more than that to make sure we do this in a single pass
726
1108
const buffer = try cmd.arena.alloc(u8, 8192);
727
-
_ = try io.read(fd, buffer, .{
1109
+
_ = try io.read(fd, buffer, .file, .{
728
1110
.cb = onCompletion,
729
1111
.ptr = cmd,
730
1112
.msg = @intFromEnum(Msg.read_localtime),
···
735
1117
const n = try result.read;
736
1118
_ = try io.close(task.req.read.fd, .{});
737
1119
const bytes = task.req.read.buffer[0..n];
738
-
var fbs = std.io.fixedBufferStream(bytes);
739
-
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);
740
1122
cmd.tz = .{ .tzinfo = tz };
741
1123
},
742
1124
···
746
1128
// TODO: stat this or do multiple reads. We'll never know a good bound unless we go
747
1129
// really big
748
1130
const buffer = try cmd.arena.alloc(u8, 8192 * 2);
749
-
_ = try io.read(fd, buffer, .{
1131
+
_ = try io.read(fd, buffer, .file, .{
750
1132
.cb = onCompletion,
751
1133
.ptr = cmd,
752
1134
.msg = @intFromEnum(Msg.read_passwd),
···
769
1151
// <name>:<throwaway>:<uid><...garbage>
770
1152
while (lines.next()) |line| {
771
1153
if (line.len == 0) continue;
1154
+
if (std.mem.startsWith(u8, line, "#")) continue;
1155
+
772
1156
var iter = std.mem.splitScalar(u8, line, ':');
773
1157
const name = iter.first();
774
1158
_ = iter.next();
···
776
1160
777
1161
const user: User = .{
778
1162
.name = name,
779
-
.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
+
),
780
1168
};
781
1169
782
1170
cmd.users.appendAssumeCapacity(user);
···
788
1176
const fd = try result.open;
789
1177
790
1178
const buffer = try cmd.arena.alloc(u8, 8192);
791
-
_ = try io.read(fd, buffer, .{
1179
+
_ = try io.read(fd, buffer, .file, .{
792
1180
.cb = onCompletion,
793
1181
.ptr = cmd,
794
1182
.msg = @intFromEnum(Msg.read_group),
···
811
1199
// <name>:<throwaway>:<uid><...garbage>
812
1200
while (lines.next()) |line| {
813
1201
if (line.len == 0) continue;
1202
+
if (std.mem.startsWith(u8, line, "#")) continue;
1203
+
814
1204
var iter = std.mem.splitScalar(u8, line, ':');
815
1205
const name = iter.first();
816
1206
_ = iter.next();
···
818
1208
819
1209
const group: Group = .{
820
1210
.name = name,
821
-
.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
+
),
822
1216
};
823
1217
824
1218
cmd.groups.appendAssumeCapacity(group);
···
827
1221
},
828
1222
829
1223
.stat => {
830
-
_ = result.statx catch {
1224
+
_ = result.statx catch |err| {
831
1225
const entry: *Entry = @fieldParentPtr("statx", task.req.statx.result);
832
-
if (entry.symlink_missing) {
833
-
// 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
834
1231
entry.statx = std.mem.zeroInit(ourio.Statx, entry.statx);
835
1232
return;
836
1233
}
837
1234
838
-
entry.symlink_missing = true;
1235
+
symlink.exists = false;
1236
+
839
1237
_ = try io.lstat(task.req.statx.path, task.req.statx.result, .{
840
1238
.cb = onCompletion,
841
1239
.ptr = cmd,
···
850
1248
cmd.entry_idx += 1;
851
1249
const path = try std.fs.path.joinZ(
852
1250
cmd.arena,
853
-
&.{ cmd.opts.directory, entry.name },
1251
+
&.{ cmd.current_directory, entry.name },
854
1252
);
855
1253
856
1254
if (entry.kind == .sym_link) {
···
858
1256
859
1257
// NOTE: Sadly, we can't do readlink via io_uring
860
1258
const link = try posix.readlink(path, &buf);
861
-
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);
862
1261
}
863
1262
_ = try io.stat(path, &entry.statx, .{
864
1263
.cb = onCompletion,
···
896
1295
const json: Icon = .{ .icon = "๎", .color = Options.Colors.blue };
897
1296
const lua: Icon = .{ .icon = "๓ฐขฑ", .color = Options.Colors.blue };
898
1297
const markdown: Icon = .{ .icon = "๎", .color = "" };
1298
+
const nix: Icon = .{ .icon = "๓ฑ
", .color = "\x1b[38:2:127:185:228m" };
899
1299
const python: Icon = .{ .icon = "๎ผ", .color = Options.Colors.yellow };
1300
+
const rust: Icon = .{ .icon = "๎จ", .color = "" };
900
1301
const typescript: Icon = .{ .icon = "๎ฃ", .color = Options.Colors.blue };
901
1302
const zig: Icon = .{ .icon = "๎ฉ", .color = "\x1b[38:2:247:164:29m" };
902
1303
903
-
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
+
});
904
1309
905
1310
const by_extension: std.StaticStringMap(Icon) = .initComptime(.{
1311
+
.{ "cjs", Icon.javascript },
906
1312
.{ "css", Icon.css },
1313
+
.{ "drv", Icon.nix },
907
1314
.{ "gif", Icon.image },
908
1315
.{ "go", Icon.go },
909
1316
.{ "html", Icon.html },
910
1317
.{ "jpeg", Icon.image },
911
1318
.{ "jpg", Icon.image },
912
1319
.{ "js", Icon.javascript },
1320
+
.{ "jsx", Icon.javascript },
913
1321
.{ "json", Icon.json },
914
1322
.{ "lua", Icon.lua },
915
1323
.{ "md", Icon.markdown },
1324
+
.{ "mjs", Icon.javascript },
916
1325
.{ "mkv", Icon.video },
917
1326
.{ "mp4", Icon.video },
1327
+
.{ "nar", Icon.nix },
1328
+
.{ "nix", Icon.nix },
918
1329
.{ "png", Icon.image },
919
1330
.{ "py", Icon.python },
1331
+
.{ "rs", Icon.rust },
920
1332
.{ "ts", Icon.typescript },
1333
+
.{ "tsx", Icon.typescript },
921
1334
.{ "webp", Icon.image },
922
1335
.{ "zig", Icon.zig },
923
1336
.{ "zon", Icon.zig },
924
1337
});
925
1338
926
-
fn get(entry: Entry, opts: Options) Icon {
1339
+
fn get(entry: Entry) Icon {
927
1340
// 1. By name
928
-
// 2. By extension
929
-
// 3. By type
1341
+
// 2. By type
1342
+
// 3. By extension
930
1343
if (by_name.get(entry.name)) |icon| return icon;
931
-
932
-
const ext = std.fs.path.extension(entry.name);
933
-
if (ext.len > 0) {
934
-
const ft = ext[1..];
935
-
if (by_extension.get(ft)) |icon| return icon;
936
-
}
937
1344
938
1345
switch (entry.kind) {
939
1346
.block_device => return drive,
940
1347
.character_device => return drive,
941
1348
.directory => return directory,
942
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
+
943
1356
if (entry.isExecutable()) {
944
1357
return executable;
945
1358
}
···
947
1360
},
948
1361
.named_pipe => return pipe,
949
1362
.sym_link => {
950
-
if (opts.long and posix.S.ISDIR(entry.statx.mode)) {
1363
+
if (posix.S.ISDIR(entry.statx.mode)) {
951
1364
return symlink_dir;
952
1365
}
953
1366
return symlink;
···
994
1407
if (std.mem.startsWith(u8, a, "-")) return .short;
995
1408
return .positional;
996
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
+
}