+1
build.zig
+1
build.zig
+7
-7
build.zig.zon
+7
-7
build.zig.zon
···
3
.version = "1.0.0",
4
.fingerprint = 0x495d173f6002e86, // Changing this has security and trust implications.
5
6
-
.minimum_zig_version = "0.14.0",
7
8
.dependencies = .{
9
.ourio = .{
10
-
.url = "git+https://github.com/rockorager/ourio#ed8a67650e5dbb0a6dca811c9d769187e306ad94",
11
-
.hash = "ourio-0.0.0-_s-z0dAWAgD3XNod2pTh0H8X-a3CjtpAwduh7jcgBz0G",
12
},
13
.zeit = .{
14
-
.url = "git+https://github.com/rockorager/zeit#4496d1c40b2223c22a1341e175fc2ecd94cc0de9",
15
-
.hash = "zeit-0.6.0-5I6bk1J1AgA13rteb6E0steXiOUKBYTzJZMMIuK9oEmb",
16
},
17
.zzdoc = .{
18
-
.url = "git+https://github.com/rockorager/zzdoc#57e86eb4e621bc4a96fbe0dd89ad0986db6d0483",
19
-
.hash = "zzdoc-0.0.0-tzT1PuPZAACr1jIJxjTrdOsLbfXS6idWFGfTq0gwxJiv",
20
},
21
},
22
.paths = .{
···
3
.version = "1.0.0",
4
.fingerprint = 0x495d173f6002e86, // Changing this has security and trust implications.
5
6
+
.minimum_zig_version = "0.15.1",
7
8
.dependencies = .{
9
.ourio = .{
10
+
.url = "git+https://github.com/rockorager/ourio#07bf94db87a9aea70d6e1a1dd99cac6fb9d38b35",
11
+
.hash = "ourio-0.0.0-_s-z0Z0XAgBU_BFjdY8QjGhJ8vcdIONPSErlYRwLoxfg",
12
},
13
.zeit = .{
14
+
.url = "git+https://github.com/rockorager/zeit#74be5a2afb346b2a6a6349abbb609e89ec7e65a6",
15
+
.hash = "zeit-0.6.0-5I6bk4t8AgCP0UGGHVF_khlmWZkAF5XtfQWEKCyLoptU",
16
},
17
.zzdoc = .{
18
+
.url = "git+https://github.com/rockorager/zzdoc#a54223bdc13a80839ccf9f473edf3a171e777946",
19
+
.hash = "zzdoc-0.0.0-tzT1Ph7cAAC5YmXQXiBJHAg41_A5AUAC5VOm7ShnUxlz",
20
},
21
},
22
.paths = .{
+1
-1
flake.nix
+1
-1
flake.nix
+2
-2
nix/cache.nix
+2
-2
nix/cache.nix
···
2
3
pkgs.stdenv.mkDerivation {
4
pname = "lsr-cache";
5
-
version = "0.2.0";
6
doCheck = false;
7
src = ../.;
8
···
17
mv $ZIG_GLOBAL_CACHE_DIR/p $out
18
'';
19
20
-
outputHash = "sha256-lnOow40km0mcj21i2mTQiDGXLhcSxQ2kJoAgUhkQiEg=";
21
outputHashMode = "recursive";
22
outputHashAlgo = "sha256";
23
}
···
2
3
pkgs.stdenv.mkDerivation {
4
pname = "lsr-cache";
5
+
version = "1.0.0";
6
doCheck = false;
7
src = ../.;
8
···
17
mv $ZIG_GLOBAL_CACHE_DIR/p $out
18
'';
19
20
+
outputHash = "sha256-bfc2dlQa1VGq9S6OBeQawAJuvfxU4kgFtQ13fuKhdZc=";
21
outputHashMode = "recursive";
22
outputHashAlgo = "sha256";
23
}
+304
-96
src/main.zig
+304
-96
src/main.zig
···
4
const zeit = @import("zeit");
5
const natord = @import("natord.zig");
6
const build_options = @import("build_options");
7
8
const posix = std.posix;
9
10
const usage =
11
-
\\Usage:
12
-
\\ lsr [options] [path]
13
\\
14
\\ --help Print this message and exit
15
\\ --version Print the version string
···
26
\\ -l, --long Display extended file metadata
27
\\ -r, --reverse Reverse the sort order
28
\\ -t, --time Sort the entries by modification time, most recent first
29
\\
30
;
31
···
42
long: bool = false,
43
sort_by_mod_time: bool = false,
44
reverse_sort: bool = false,
45
46
-
directory: [:0]const u8 = ".",
47
file: ?[]const u8 = null,
48
49
winsize: ?posix.winsize = null,
···
146
147
var cmd: Command = .{ .arena = allocator };
148
149
-
cmd.opts.winsize = getWinsize(std.io.getStdOut().handle);
150
151
cmd.opts.shortview = if (cmd.opts.isatty()) .columns else .oneline;
152
153
-
const stdout = std.io.getStdOut().writer();
154
-
const stderr = std.io.getStdErr().writer();
155
-
var bw = std.io.bufferedWriter(stdout);
156
157
var args = std.process.args();
158
// skip binary
···
172
'r' => cmd.opts.reverse_sort = true,
173
't' => cmd.opts.sort_by_mod_time = true,
174
else => {
175
-
try stderr.print("Invalid opt: '{c}'", .{b});
176
std.process.exit(1);
177
},
178
}
···
184
const val = split.rest();
185
if (eql(opt, "all")) {
186
cmd.opts.all = parseArgBool(val) orelse {
187
-
try stderr.print("Invalid boolean: '{s}'", .{val});
188
std.process.exit(1);
189
};
190
} else if (eql(opt, "long")) {
191
cmd.opts.long = parseArgBool(val) orelse {
192
-
try stderr.print("Invalid boolean: '{s}'", .{val});
193
std.process.exit(1);
194
};
195
} else if (eql(opt, "almost-all")) {
196
cmd.opts.@"almost-all" = parseArgBool(val) orelse {
197
-
try stderr.print("Invalid boolean: '{s}'", .{val});
198
std.process.exit(1);
199
};
200
} else if (eql(opt, "group-directories-first")) {
201
cmd.opts.@"group-directories-first" = parseArgBool(val) orelse {
202
-
try stderr.print("Invalid boolean: '{s}'", .{val});
203
std.process.exit(1);
204
};
205
} else if (eql(opt, "color")) {
206
cmd.opts.color = std.meta.stringToEnum(Options.When, val) orelse {
207
-
try stderr.print("Invalid color option: '{s}'", .{val});
208
std.process.exit(1);
209
};
210
} else if (eql(opt, "human-readable")) {
211
// no-op: present for compatibility
212
} else if (eql(opt, "hyperlinks")) {
213
cmd.opts.hyperlinks = std.meta.stringToEnum(Options.When, val) orelse {
214
-
try stderr.print("Invalid hyperlinks option: '{s}'", .{val});
215
std.process.exit(1);
216
};
217
} else if (eql(opt, "icons")) {
218
cmd.opts.icons = std.meta.stringToEnum(Options.When, val) orelse {
219
-
try stderr.print("Invalid color option: '{s}'", .{val});
220
std.process.exit(1);
221
};
222
} else if (eql(opt, "columns")) {
223
const c = parseArgBool(val) orelse {
224
-
try stderr.print("Invalid columns option: '{s}'", .{val});
225
std.process.exit(1);
226
};
227
cmd.opts.shortview = if (c) .columns else .oneline;
228
} else if (eql(opt, "oneline")) {
229
const o = parseArgBool(val) orelse {
230
-
try stderr.print("Invalid oneline option: '{s}'", .{val});
231
std.process.exit(1);
232
};
233
cmd.opts.shortview = if (o) .oneline else .columns;
234
} else if (eql(opt, "time")) {
235
cmd.opts.sort_by_mod_time = parseArgBool(val) orelse {
236
-
try stderr.print("Invalid boolean: '{s}'", .{val});
237
std.process.exit(1);
238
};
239
} else if (eql(opt, "reverse")) {
240
cmd.opts.reverse_sort = parseArgBool(val) orelse {
241
-
try stderr.print("Invalid boolean: '{s}'", .{val});
242
std.process.exit(1);
243
};
244
} else if (eql(opt, "help")) {
245
return stderr.writeAll(usage);
246
} else if (eql(opt, "version")) {
247
-
try bw.writer().print("lsr {s}\r\n", .{build_options.version});
248
-
try bw.flush();
249
return;
250
} else {
251
-
try stderr.print("Invalid opt: '{s}'", .{opt});
252
std.process.exit(1);
253
}
254
},
255
.positional => {
256
-
cmd.opts.directory = arg;
257
},
258
}
259
}
···
262
cmd.opts.colors = .default;
263
}
264
265
-
var ring: ourio.Ring = try .init(allocator, queue_size);
266
-
defer ring.deinit();
267
268
-
_ = try ring.open(cmd.opts.directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{
269
-
.ptr = &cmd,
270
-
.cb = onCompletion,
271
-
.msg = @intFromEnum(Msg.cwd),
272
-
});
273
274
-
if (cmd.opts.long) {
275
-
_ = try ring.open("/etc/localtime", .{ .CLOEXEC = true }, 0, .{
276
-
.ptr = &cmd,
277
-
.cb = onCompletion,
278
-
.msg = @intFromEnum(Msg.localtime),
279
-
});
280
-
_ = try ring.open("/etc/passwd", .{ .CLOEXEC = true }, 0, .{
281
-
.ptr = &cmd,
282
-
.cb = onCompletion,
283
-
.msg = @intFromEnum(Msg.passwd),
284
-
});
285
-
_ = try ring.open("/etc/group", .{ .CLOEXEC = true }, 0, .{
286
.ptr = &cmd,
287
.cb = onCompletion,
288
-
.msg = @intFromEnum(Msg.group),
289
});
290
-
}
291
292
-
try ring.run(.until_done);
293
294
-
if (cmd.entries.len == 0) return;
295
296
-
std.sort.pdq(Entry, cmd.entries, cmd.opts, Entry.lessThan);
297
298
-
if (cmd.opts.reverse_sort) {
299
-
std.mem.reverse(Entry, cmd.entries);
300
-
}
301
302
-
if (cmd.opts.long) {
303
-
try printLong(cmd, bw.writer());
304
-
} else switch (cmd.opts.shortview) {
305
-
.columns => try printShortColumns(cmd, bw.writer()),
306
-
.oneline => try printShortOnePerLine(cmd, bw.writer()),
307
}
308
-
try bw.flush();
309
}
310
311
fn printShortColumns(cmd: Command, writer: anytype) !void {
···
382
383
if (i < columns.items.len - 1) {
384
const spaces = column.width - (icon_width + entry.name.len);
385
-
try writer.writeByteNTimes(' ', spaces);
386
}
387
}
388
try writer.writeAll("\r\n");
···
397
const opts = cmd.opts;
398
const colors = opts.colors;
399
if (opts.useIcons()) {
400
-
const icon = Icon.get(entry, opts);
401
402
if (opts.useColor()) {
403
try writer.writeAll(icon.color);
···
420
}
421
422
if (opts.useHyperlinks()) {
423
-
const path = try std.fs.path.join(cmd.arena, &.{ opts.directory, entry.name });
424
try writer.print("\x1b]8;;file://{s}\x1b\\", .{path});
425
try writer.writeAll(entry.name);
426
try writer.writeAll("\x1b]8;;\x1b\\");
···
446
}
447
}
448
449
-
fn printLong(cmd: Command, writer: anytype) !void {
450
const tz = cmd.tz.?;
451
const now = zeit.instant(.{}) catch unreachable;
452
const one_year_ago = try now.subtract(.{ .days = 365 });
···
458
var n_size: usize = 0;
459
var n_suff: usize = 0;
460
for (cmd.entries) |entry| {
461
-
const group = cmd.getGroup(entry.statx.gid);
462
-
const user = cmd.getUser(entry.statx.uid);
463
464
var buf: [16]u8 = undefined;
465
const size = try entry.humanReadableSize(&buf);
···
490
};
491
492
for (cmd.entries) |entry| {
493
-
const user: User = cmd.getUser(entry.statx.uid) orelse
494
.{
495
.uid = entry.statx.uid,
496
.name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.uid}),
497
};
498
-
const group: Group = cmd.getGroup(entry.statx.gid) orelse
499
.{
500
.gid = entry.statx.gid,
501
.name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.gid}),
···
509
try writer.writeAll(&mode);
510
try writer.writeByte(' ');
511
try writer.writeAll(user.name);
512
-
try writer.writeByteNTimes(' ', longest_user - user.name.len);
513
try writer.writeByte(' ');
514
try writer.writeAll(group.name);
515
-
try writer.writeByteNTimes(' ', longest_group - group.name.len);
516
try writer.writeByte(' ');
517
518
var size_buf: [16]u8 = undefined;
519
const size = try entry.humanReadableSize(&size_buf);
520
const suffix = entry.humanReadableSuffix();
521
522
-
try writer.writeByteNTimes(' ', longest_size - size.len);
523
try writer.writeAll(size);
524
try writer.writeByte(' ');
525
try writer.writeAll(suffix);
526
-
try writer.writeByteNTimes(' ', longest_suffix - suffix.len);
527
try writer.writeByte(' ');
528
529
try writer.print("{d: >2} {s} ", .{
···
538
}
539
540
if (cmd.opts.useIcons()) {
541
-
const icon = Icon.get(entry, cmd.opts);
542
543
if (cmd.opts.useColor()) {
544
try writer.writeAll(icon.color);
···
562
}
563
564
if (cmd.opts.useHyperlinks()) {
565
-
const path = try std.fs.path.join(cmd.arena, &.{ cmd.opts.directory, entry.name });
566
try writer.print("\x1b]8;;file://{s}\x1b\\", .{path});
567
try writer.writeAll(entry.name);
568
try writer.writeAll("\x1b]8;;\x1b\\");
···
606
entries: []Entry = &.{},
607
entry_idx: usize = 0,
608
symlinks: std.StringHashMapUnmanaged(Symlink) = .empty,
609
610
tz: ?zeit.TimeZone = null,
611
groups: std.ArrayListUnmanaged(Group) = .empty,
612
users: std.ArrayListUnmanaged(User) = .empty,
613
614
-
fn getUser(self: Command, uid: posix.uid_t) ?User {
615
for (self.users.items) |user| {
616
if (user.uid == uid) return user;
617
}
618
return null;
619
}
620
621
-
fn getGroup(self: Command, gid: posix.gid_t) ?Group {
622
for (self.groups.items) |group| {
623
if (group.gid == gid) return group;
624
}
625
return null;
626
}
627
};
···
683
statx: ourio.Statx,
684
685
fn lessThan(opts: Options, lhs: Entry, rhs: Entry) bool {
686
-
if (opts.@"group-directories-first" and
687
-
lhs.kind != rhs.kind and
688
-
(lhs.kind == .directory or rhs.kind == .directory))
689
-
{
690
-
return lhs.kind == .directory;
691
}
692
693
if (opts.sort_by_mod_time) {
···
786
787
// if the user specified a file (or something that couldn't be opened as a
788
// directory), then we open it's parent and apply a filter
789
-
const dirname = std.fs.path.dirname(cmd.opts.directory) orelse ".";
790
-
cmd.opts.file = std.fs.path.basename(cmd.opts.directory);
791
-
cmd.opts.directory = try cmd.arena.dupeZ(u8, dirname);
792
_ = try io.open(
793
-
cmd.opts.directory,
794
.{ .DIRECTORY = true, .CLOEXEC = true },
795
0,
796
.{
···
811
if (cmd.opts.useHyperlinks()) {
812
var buf: [std.fs.max_path_bytes]u8 = undefined;
813
const cwd = try std.os.getFdPath(fd, &buf);
814
-
cmd.opts.directory = try cmd.arena.dupeZ(u8, cwd);
815
}
816
817
var temp_results: std.ArrayListUnmanaged(MinimalEntry) = .empty;
···
873
}
874
const path = try std.fs.path.joinZ(
875
cmd.arena,
876
-
&.{ cmd.opts.directory, entry.name },
877
);
878
879
if (entry.kind == .sym_link) {
···
909
const n = try result.read;
910
_ = try io.close(task.req.read.fd, .{});
911
const bytes = task.req.read.buffer[0..n];
912
-
var fbs = std.io.fixedBufferStream(bytes);
913
-
const tz = try zeit.timezone.TZInfo.parse(cmd.arena, fbs.reader());
914
cmd.tz = .{ .tzinfo = tz };
915
},
916
···
1040
cmd.entry_idx += 1;
1041
const path = try std.fs.path.joinZ(
1042
cmd.arena,
1043
-
&.{ cmd.opts.directory, entry.name },
1044
);
1045
1046
if (entry.kind == .sym_link) {
···
1128
.{ "zon", Icon.zig },
1129
});
1130
1131
-
fn get(entry: Entry, opts: Options) Icon {
1132
// 1. By name
1133
-
// 2. By extension
1134
-
// 3. By type
1135
if (by_name.get(entry.name)) |icon| return icon;
1136
1137
-
const ext = std.fs.path.extension(entry.name);
1138
-
if (ext.len > 0) {
1139
-
const ft = ext[1..];
1140
-
if (by_extension.get(ft)) |icon| return icon;
1141
-
}
1142
-
1143
switch (entry.kind) {
1144
.block_device => return drive,
1145
.character_device => return drive,
1146
.directory => return directory,
1147
.file => {
1148
if (entry.isExecutable()) {
1149
return executable;
1150
}
···
1152
},
1153
.named_pipe => return pipe,
1154
.sym_link => {
1155
-
if (opts.long and posix.S.ISDIR(entry.statx.mode)) {
1156
return symlink_dir;
1157
}
1158
return symlink;
···
4
const zeit = @import("zeit");
5
const natord = @import("natord.zig");
6
const build_options = @import("build_options");
7
+
const grp = @cImport({
8
+
@cInclude("grp.h");
9
+
});
10
11
const posix = std.posix;
12
13
const usage =
14
+
\\Usage:
15
+
\\ lsr [options] [path...]
16
\\
17
\\ --help Print this message and exit
18
\\ --version Print the version string
···
29
\\ -l, --long Display extended file metadata
30
\\ -r, --reverse Reverse the sort order
31
\\ -t, --time Sort the entries by modification time, most recent first
32
+
\\ --tree[=DEPTH] Display entries in a tree format (optional limit depth)
33
\\
34
;
35
···
46
long: bool = false,
47
sort_by_mod_time: bool = false,
48
reverse_sort: bool = false,
49
+
tree: bool = false,
50
+
tree_depth: ?usize = null,
51
52
+
directories: std.ArrayListUnmanaged([:0]const u8) = .empty,
53
file: ?[]const u8 = null,
54
55
winsize: ?posix.winsize = null,
···
152
153
var cmd: Command = .{ .arena = allocator };
154
155
+
cmd.opts.winsize = getWinsize(std.fs.File.stdout().handle);
156
157
cmd.opts.shortview = if (cmd.opts.isatty()) .columns else .oneline;
158
159
+
var stdout_buf: [4096]u8 = undefined;
160
+
var stderr_buf: [4096]u8 = undefined;
161
+
var stdout_writer = std.fs.File.stdout().writer(&stdout_buf);
162
+
var stderr_writer = std.fs.File.stderr().writer(&stderr_buf);
163
+
var stdout = &stdout_writer.interface;
164
+
var stderr = &stderr_writer.interface;
165
166
var args = std.process.args();
167
// skip binary
···
181
'r' => cmd.opts.reverse_sort = true,
182
't' => cmd.opts.sort_by_mod_time = true,
183
else => {
184
+
try stderr.print("Invalid opt: '{c}'\n", .{b});
185
std.process.exit(1);
186
},
187
}
···
193
const val = split.rest();
194
if (eql(opt, "all")) {
195
cmd.opts.all = parseArgBool(val) orelse {
196
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
197
std.process.exit(1);
198
};
199
} else if (eql(opt, "long")) {
200
cmd.opts.long = parseArgBool(val) orelse {
201
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
202
std.process.exit(1);
203
};
204
} else if (eql(opt, "almost-all")) {
205
cmd.opts.@"almost-all" = parseArgBool(val) orelse {
206
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
207
std.process.exit(1);
208
};
209
} else if (eql(opt, "group-directories-first")) {
210
cmd.opts.@"group-directories-first" = parseArgBool(val) orelse {
211
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
212
std.process.exit(1);
213
};
214
} else if (eql(opt, "color")) {
215
cmd.opts.color = std.meta.stringToEnum(Options.When, val) orelse {
216
+
try stderr.print("Invalid color option: '{s}'\n", .{val});
217
std.process.exit(1);
218
};
219
} else if (eql(opt, "human-readable")) {
220
// no-op: present for compatibility
221
} else if (eql(opt, "hyperlinks")) {
222
cmd.opts.hyperlinks = std.meta.stringToEnum(Options.When, val) orelse {
223
+
try stderr.print("Invalid hyperlinks option: '{s}'\n", .{val});
224
std.process.exit(1);
225
};
226
} else if (eql(opt, "icons")) {
227
cmd.opts.icons = std.meta.stringToEnum(Options.When, val) orelse {
228
+
try stderr.print("Invalid color option: '{s}'\n", .{val});
229
std.process.exit(1);
230
};
231
} else if (eql(opt, "columns")) {
232
const c = parseArgBool(val) orelse {
233
+
try stderr.print("Invalid columns option: '{s}'\n", .{val});
234
std.process.exit(1);
235
};
236
cmd.opts.shortview = if (c) .columns else .oneline;
237
} else if (eql(opt, "oneline")) {
238
const o = parseArgBool(val) orelse {
239
+
try stderr.print("Invalid oneline option: '{s}'\n", .{val});
240
std.process.exit(1);
241
};
242
cmd.opts.shortview = if (o) .oneline else .columns;
243
} else if (eql(opt, "time")) {
244
cmd.opts.sort_by_mod_time = parseArgBool(val) orelse {
245
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
246
std.process.exit(1);
247
};
248
} else if (eql(opt, "reverse")) {
249
cmd.opts.reverse_sort = parseArgBool(val) orelse {
250
+
try stderr.print("Invalid boolean: '{s}'\n", .{val});
251
std.process.exit(1);
252
};
253
+
} else if (eql(opt, "tree")) {
254
+
if (val.len == 0) {
255
+
cmd.opts.tree = true;
256
+
cmd.opts.tree_depth = null; // unlimited depth
257
+
} else {
258
+
cmd.opts.tree = true;
259
+
cmd.opts.tree_depth = std.fmt.parseInt(usize, val, 10) catch {
260
+
try stderr.print("Invalid tree depth: '{s}'\n", .{val});
261
+
std.process.exit(1);
262
+
};
263
+
}
264
} else if (eql(opt, "help")) {
265
return stderr.writeAll(usage);
266
} else if (eql(opt, "version")) {
267
+
try stdout.print("lsr {s}\r\n", .{build_options.version});
268
+
try stdout.flush();
269
return;
270
} else {
271
+
try stderr.print("Invalid opt: '{s}'\n", .{opt});
272
std.process.exit(1);
273
}
274
},
275
.positional => {
276
+
try cmd.opts.directories.append(allocator, arg);
277
},
278
}
279
}
···
282
cmd.opts.colors = .default;
283
}
284
285
+
if (cmd.opts.directories.items.len == 0) {
286
+
try cmd.opts.directories.append(allocator, ".");
287
+
}
288
289
+
const multiple_dirs = cmd.opts.directories.items.len > 1;
290
+
291
+
for (cmd.opts.directories.items, 0..) |directory, dir_idx| {
292
+
cmd.entries = &.{};
293
+
cmd.entry_idx = 0;
294
+
cmd.symlinks.clearRetainingCapacity();
295
+
cmd.groups.clearRetainingCapacity();
296
+
cmd.users.clearRetainingCapacity();
297
+
cmd.tz = null;
298
+
cmd.opts.file = null;
299
+
cmd.current_directory = directory;
300
301
+
var ring: ourio.Ring = try .init(allocator, queue_size);
302
+
defer ring.deinit();
303
+
304
+
_ = try ring.open(directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{
305
.ptr = &cmd,
306
.cb = onCompletion,
307
+
.msg = @intFromEnum(Msg.cwd),
308
});
309
310
+
if (cmd.opts.long) {
311
+
_ = try ring.open("/etc/localtime", .{ .CLOEXEC = true }, 0, .{
312
+
.ptr = &cmd,
313
+
.cb = onCompletion,
314
+
.msg = @intFromEnum(Msg.localtime),
315
+
});
316
+
_ = try ring.open("/etc/passwd", .{ .CLOEXEC = true }, 0, .{
317
+
.ptr = &cmd,
318
+
.cb = onCompletion,
319
+
.msg = @intFromEnum(Msg.passwd),
320
+
});
321
+
_ = try ring.open("/etc/group", .{ .CLOEXEC = true }, 0, .{
322
+
.ptr = &cmd,
323
+
.cb = onCompletion,
324
+
.msg = @intFromEnum(Msg.group),
325
+
});
326
+
}
327
328
+
try ring.run(.until_done);
329
330
+
if (cmd.entries.len == 0) {
331
+
if (multiple_dirs and dir_idx < cmd.opts.directories.items.len - 1) {
332
+
try stdout.writeAll("\r\n");
333
+
}
334
+
continue;
335
+
}
336
+
337
+
std.sort.pdq(Entry, cmd.entries, cmd.opts, Entry.lessThan);
338
339
+
if (cmd.opts.reverse_sort) {
340
+
std.mem.reverse(Entry, cmd.entries);
341
+
}
342
343
+
if (multiple_dirs and !cmd.opts.tree) {
344
+
if (dir_idx > 0) try stdout.writeAll("\r\n");
345
+
try stdout.print("{s}:\r\n", .{directory});
346
+
}
347
+
348
+
if (cmd.opts.tree) {
349
+
if (multiple_dirs and dir_idx > 0) try stdout.writeAll("\r\n");
350
+
try printTree(cmd, stdout);
351
+
} else if (cmd.opts.long) {
352
+
try printLong(&cmd, stdout);
353
+
} else switch (cmd.opts.shortview) {
354
+
.columns => try printShortColumns(cmd, stdout),
355
+
.oneline => try printShortOnePerLine(cmd, stdout),
356
+
}
357
}
358
+
try stdout.flush();
359
}
360
361
fn printShortColumns(cmd: Command, writer: anytype) !void {
···
432
433
if (i < columns.items.len - 1) {
434
const spaces = column.width - (icon_width + entry.name.len);
435
+
var space_buf = [_][]const u8{" "};
436
+
try writer.writeSplatAll(&space_buf, spaces);
437
}
438
}
439
try writer.writeAll("\r\n");
···
448
const opts = cmd.opts;
449
const colors = opts.colors;
450
if (opts.useIcons()) {
451
+
const icon = Icon.get(entry);
452
453
if (opts.useColor()) {
454
try writer.writeAll(icon.color);
···
471
}
472
473
if (opts.useHyperlinks()) {
474
+
const path = try std.fs.path.join(cmd.arena, &.{ cmd.current_directory, entry.name });
475
try writer.print("\x1b]8;;file://{s}\x1b\\", .{path});
476
try writer.writeAll(entry.name);
477
try writer.writeAll("\x1b]8;;\x1b\\");
···
497
}
498
}
499
500
+
fn drawTreePrefix(writer: anytype, prefix_list: []const bool, is_last: bool) !void {
501
+
for (prefix_list) |is_last_at_level| {
502
+
if (is_last_at_level) {
503
+
try writer.writeAll(" ");
504
+
} else {
505
+
try writer.writeAll("โ ");
506
+
}
507
+
}
508
+
509
+
if (is_last) {
510
+
try writer.writeAll("โโโ ");
511
+
} else {
512
+
try writer.writeAll("โโโ ");
513
+
}
514
+
}
515
+
516
+
fn printTree(cmd: Command, writer: anytype) !void {
517
+
const dir_name = if (std.mem.eql(u8, cmd.current_directory, ".")) blk: {
518
+
var buf: [std.fs.max_path_bytes]u8 = undefined;
519
+
const cwd = try std.process.getCwd(&buf);
520
+
break :blk std.fs.path.basename(cwd);
521
+
} else std.fs.path.basename(cmd.current_directory);
522
+
523
+
try writer.print("{s}\n", .{dir_name});
524
+
525
+
const max_depth = cmd.opts.tree_depth orelse std.math.maxInt(usize);
526
+
var prefix_list: std.ArrayList(bool) = .{};
527
+
528
+
for (cmd.entries, 0..) |entry, i| {
529
+
const is_last = i == cmd.entries.len - 1;
530
+
531
+
try drawTreePrefix(writer, prefix_list.items, is_last);
532
+
try printShortEntry(entry, cmd, writer);
533
+
try writer.writeAll("\r\n");
534
+
535
+
if (entry.kind == .directory and max_depth > 0) {
536
+
const full_path = try std.fs.path.joinZ(cmd.arena, &.{ cmd.current_directory, entry.name });
537
+
538
+
try prefix_list.append(cmd.arena, is_last);
539
+
try recurseTree(cmd, writer, full_path, &prefix_list, 1, max_depth);
540
+
541
+
_ = prefix_list.pop();
542
+
}
543
+
}
544
+
}
545
+
546
+
fn recurseTree(cmd: Command, writer: anytype, dir_path: [:0]const u8, prefix_list: *std.ArrayList(bool), depth: usize, max_depth: usize) !void {
547
+
var dir = std.fs.cwd().openDir(dir_path, .{ .iterate = true }) catch {
548
+
return;
549
+
};
550
+
defer dir.close();
551
+
552
+
var entries: std.ArrayList(Entry) = .{};
553
+
var iter = dir.iterate();
554
+
555
+
while (try iter.next()) |dirent| {
556
+
if (!cmd.opts.showDotfiles() and std.mem.startsWith(u8, dirent.name, ".")) continue;
557
+
558
+
const nameZ = try cmd.arena.dupeZ(u8, dirent.name);
559
+
try entries.append(cmd.arena, .{
560
+
.name = nameZ,
561
+
.kind = dirent.kind,
562
+
.statx = undefined,
563
+
});
564
+
}
565
+
566
+
std.sort.pdq(Entry, entries.items, cmd.opts, Entry.lessThan);
567
+
568
+
if (cmd.opts.reverse_sort) {
569
+
std.mem.reverse(Entry, entries.items);
570
+
}
571
+
572
+
for (entries.items, 0..) |entry, i| {
573
+
const is_last = i == entries.items.len - 1;
574
+
575
+
try drawTreePrefix(writer, prefix_list.items, is_last);
576
+
try printTreeEntry(entry, cmd, writer, dir_path);
577
+
try writer.writeAll("\r\n");
578
+
579
+
if (entry.kind == .directory and depth < max_depth) {
580
+
const full_path = try std.fs.path.joinZ(cmd.arena, &.{ dir_path, entry.name });
581
+
582
+
try prefix_list.append(cmd.arena, is_last);
583
+
try recurseTree(cmd, writer, full_path, prefix_list, depth + 1, max_depth);
584
+
585
+
_ = prefix_list.pop();
586
+
}
587
+
}
588
+
}
589
+
590
+
fn printTreeEntry(entry: Entry, cmd: Command, writer: anytype, dir_path: [:0]const u8) !void {
591
+
const opts = cmd.opts;
592
+
const colors = opts.colors;
593
+
594
+
if (opts.useIcons()) {
595
+
const icon = Icon.get(entry);
596
+
597
+
if (opts.useColor()) {
598
+
try writer.writeAll(icon.color);
599
+
try writer.writeAll(icon.icon);
600
+
try writer.writeAll(colors.reset);
601
+
} else {
602
+
try writer.writeAll(icon.icon);
603
+
}
604
+
605
+
try writer.writeByte(' ');
606
+
}
607
+
608
+
switch (entry.kind) {
609
+
.directory => try writer.writeAll(colors.dir),
610
+
.sym_link => try writer.writeAll(colors.symlink),
611
+
else => {
612
+
const full_path = try std.fs.path.join(cmd.arena, &.{ dir_path, entry.name });
613
+
const stat_result = std.fs.cwd().statFile(full_path) catch null;
614
+
if (stat_result) |stat| {
615
+
if (stat.mode & (std.posix.S.IXUSR | std.posix.S.IXGRP | std.posix.S.IXOTH) != 0) {
616
+
try writer.writeAll(colors.executable);
617
+
}
618
+
}
619
+
},
620
+
}
621
+
622
+
if (opts.useHyperlinks()) {
623
+
const path = try std.fs.path.join(cmd.arena, &.{ dir_path, entry.name });
624
+
try writer.print("\x1b]8;;file://{s}\x1b\\", .{path});
625
+
try writer.writeAll(entry.name);
626
+
try writer.writeAll("\x1b]8;;\x1b\\");
627
+
try writer.writeAll(colors.reset);
628
+
} else {
629
+
try writer.writeAll(entry.name);
630
+
try writer.writeAll(colors.reset);
631
+
}
632
+
}
633
+
634
+
fn printLong(cmd: *Command, writer: anytype) !void {
635
const tz = cmd.tz.?;
636
const now = zeit.instant(.{}) catch unreachable;
637
const one_year_ago = try now.subtract(.{ .days = 365 });
···
643
var n_size: usize = 0;
644
var n_suff: usize = 0;
645
for (cmd.entries) |entry| {
646
+
const group = try cmd.getGroup(entry.statx.gid);
647
+
const user = try cmd.getUser(entry.statx.uid);
648
649
var buf: [16]u8 = undefined;
650
const size = try entry.humanReadableSize(&buf);
···
675
};
676
677
for (cmd.entries) |entry| {
678
+
const user: User = try cmd.getUser(entry.statx.uid) orelse
679
.{
680
.uid = entry.statx.uid,
681
.name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.uid}),
682
};
683
+
const group: Group = try cmd.getGroup(entry.statx.gid) orelse
684
.{
685
.gid = entry.statx.gid,
686
.name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.gid}),
···
694
try writer.writeAll(&mode);
695
try writer.writeByte(' ');
696
try writer.writeAll(user.name);
697
+
var space_buf1 = [_][]const u8{" "};
698
+
try writer.writeSplatAll(&space_buf1, longest_user - user.name.len);
699
try writer.writeByte(' ');
700
try writer.writeAll(group.name);
701
+
var space_buf2 = [_][]const u8{" "};
702
+
try writer.writeSplatAll(&space_buf2, longest_group - group.name.len);
703
try writer.writeByte(' ');
704
705
var size_buf: [16]u8 = undefined;
706
const size = try entry.humanReadableSize(&size_buf);
707
const suffix = entry.humanReadableSuffix();
708
709
+
var space_buf3 = [_][]const u8{" "};
710
+
try writer.writeSplatAll(&space_buf3, longest_size - size.len);
711
try writer.writeAll(size);
712
try writer.writeByte(' ');
713
try writer.writeAll(suffix);
714
+
var space_buf4 = [_][]const u8{" "};
715
+
try writer.writeSplatAll(&space_buf4, longest_suffix - suffix.len);
716
try writer.writeByte(' ');
717
718
try writer.print("{d: >2} {s} ", .{
···
727
}
728
729
if (cmd.opts.useIcons()) {
730
+
const icon = Icon.get(entry);
731
732
if (cmd.opts.useColor()) {
733
try writer.writeAll(icon.color);
···
751
}
752
753
if (cmd.opts.useHyperlinks()) {
754
+
const path = try std.fs.path.join(cmd.arena, &.{ cmd.current_directory, entry.name });
755
try writer.print("\x1b]8;;file://{s}\x1b\\", .{path});
756
try writer.writeAll(entry.name);
757
try writer.writeAll("\x1b]8;;\x1b\\");
···
795
entries: []Entry = &.{},
796
entry_idx: usize = 0,
797
symlinks: std.StringHashMapUnmanaged(Symlink) = .empty,
798
+
current_directory: [:0]const u8 = ".",
799
800
tz: ?zeit.TimeZone = null,
801
groups: std.ArrayListUnmanaged(Group) = .empty,
802
users: std.ArrayListUnmanaged(User) = .empty,
803
804
+
fn getUser(self: *Command, uid: posix.uid_t) !?User {
805
for (self.users.items) |user| {
806
if (user.uid == uid) return user;
807
}
808
+
if (std.c.getpwuid(uid)) |user| {
809
+
if (user.name) |name| {
810
+
const new_user = User{
811
+
.uid = uid,
812
+
.name = std.mem.span(name),
813
+
};
814
+
try self.users.append(self.arena, new_user);
815
+
return new_user;
816
+
}
817
+
}
818
return null;
819
}
820
821
+
fn getGroup(self: *Command, gid: posix.gid_t) !?Group {
822
for (self.groups.items) |group| {
823
if (group.gid == gid) return group;
824
}
825
+
if (grp.getgrgid(gid)) |group| {
826
+
const new_group = Group{
827
+
.gid = gid,
828
+
.name = std.mem.span(group.*.gr_name),
829
+
};
830
+
try self.groups.append(self.arena, new_group);
831
+
return new_group;
832
+
}
833
return null;
834
}
835
};
···
891
statx: ourio.Statx,
892
893
fn lessThan(opts: Options, lhs: Entry, rhs: Entry) bool {
894
+
if (opts.@"group-directories-first") {
895
+
const lhs_is_dir = posix.S.ISDIR(lhs.statx.mode);
896
+
const rhs_is_dir = posix.S.ISDIR(rhs.statx.mode);
897
+
898
+
if (lhs_is_dir != rhs_is_dir) return lhs_is_dir;
899
}
900
901
if (opts.sort_by_mod_time) {
···
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
.{
···
1019
if (cmd.opts.useHyperlinks()) {
1020
var buf: [std.fs.max_path_bytes]u8 = undefined;
1021
const cwd = try std.os.getFdPath(fd, &buf);
1022
+
cmd.current_directory = try cmd.arena.dupeZ(u8, cwd);
1023
}
1024
1025
var temp_results: std.ArrayListUnmanaged(MinimalEntry) = .empty;
···
1081
}
1082
const path = try std.fs.path.joinZ(
1083
cmd.arena,
1084
+
&.{ cmd.current_directory, entry.name },
1085
);
1086
1087
if (entry.kind == .sym_link) {
···
1117
const n = try result.read;
1118
_ = try io.close(task.req.read.fd, .{});
1119
const bytes = task.req.read.buffer[0..n];
1120
+
var reader = std.Io.Reader.fixed(bytes);
1121
+
const tz = try zeit.timezone.TZInfo.parse(cmd.arena, &reader);
1122
cmd.tz = .{ .tzinfo = tz };
1123
},
1124
···
1248
cmd.entry_idx += 1;
1249
const path = try std.fs.path.joinZ(
1250
cmd.arena,
1251
+
&.{ cmd.current_directory, entry.name },
1252
);
1253
1254
if (entry.kind == .sym_link) {
···
1336
.{ "zon", Icon.zig },
1337
});
1338
1339
+
fn get(entry: Entry) Icon {
1340
// 1. By name
1341
+
// 2. By type
1342
+
// 3. By extension
1343
if (by_name.get(entry.name)) |icon| return icon;
1344
1345
switch (entry.kind) {
1346
.block_device => return drive,
1347
.character_device => return drive,
1348
.directory => return directory,
1349
.file => {
1350
+
const ext = std.fs.path.extension(entry.name);
1351
+
if (ext.len > 0) {
1352
+
const ft = ext[1..];
1353
+
if (by_extension.get(ft)) |icon| return icon;
1354
+
}
1355
+
1356
if (entry.isExecutable()) {
1357
return executable;
1358
}
···
1360
},
1361
.named_pipe => return pipe,
1362
.sym_link => {
1363
+
if (posix.S.ISDIR(entry.statx.mode)) {
1364
return symlink_dir;
1365
}
1366
return symlink;