1const std = @import("std");
2const builtin = @import("builtin");
3const ourio = @import("ourio");
4const zeit = @import("zeit");
5const build_options = @import("build_options");
6
7const posix = std.posix;
8
9const usage =
10 \\Usage:
11 \\ lsr [options] [directory]
12 \\
13 \\ --help Print this message and exit
14 \\
15 \\DISPLAY OPTIONS
16 \\ -1, --oneline Print entries one per line
17 \\ -a, --all Show files that start with a dot (ASCII 0x2E)
18 \\ -A, --almost-all Like --all, but skips implicit "." and ".." directories
19 \\ -C, --columns Print the output in columns
20 \\ --color=WHEN When to use colors (always, auto, never)
21 \\ --group-directories-first When to use colors (always, auto, never)
22 \\ --icons=WHEN When to display icons (always, auto, never)
23 \\ -l, --long Display extended file metadata
24;
25
26const queue_size = 256;
27
28const Options = struct {
29 all: bool = false,
30 @"almost-all": bool = false,
31 color: When = .auto,
32 shortview: enum { columns, oneline } = .oneline,
33 @"group-directories-first": bool = true,
34 icons: When = .auto,
35 long: bool = false,
36
37 directory: [:0]const u8 = ".",
38
39 winsize: ?posix.winsize = null,
40 colors: Colors = .none,
41
42 const When = enum {
43 never,
44 auto,
45 always,
46 };
47
48 const Colors = struct {
49 reset: []const u8,
50 dir: []const u8,
51 executable: []const u8,
52 symlink: []const u8,
53 symlink_target: []const u8,
54 symlink_missing: []const u8,
55
56 const none: Colors = .{
57 .reset = "",
58 .dir = "",
59 .executable = "",
60 .symlink = "",
61 .symlink_target = "",
62 .symlink_missing = "",
63 };
64
65 const default: Colors = .{
66 .reset = _reset,
67 .dir = bold ++ blue,
68 .executable = bold ++ green,
69 .symlink = bold ++ purple,
70 .symlink_target = bold ++ cyan,
71 .symlink_missing = bold ++ red,
72 };
73
74 const _reset = "\x1b[m";
75 const red = "\x1b[31m";
76 const green = "\x1b[32m";
77 const yellow = "\x1b[33m";
78 const blue = "\x1b[34m";
79 const purple = "\x1b[35m";
80 const cyan = "\x1b[36m";
81 const fg = "\x1b[37m";
82
83 const bold = "\x1b[1m";
84 };
85
86 fn useColor(self: Options) bool {
87 switch (self.color) {
88 .never => return false,
89 .always => return true,
90 .auto => return self.isatty(),
91 }
92 }
93
94 fn useIcons(self: Options) bool {
95 switch (self.icons) {
96 .never => return false,
97 .always => return true,
98 .auto => return self.isatty(),
99 }
100 }
101
102 fn isatty(self: Options) bool {
103 return self.winsize != null;
104 }
105};
106
107pub fn main() !void {
108 var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
109 const gpa, const is_debug = gpa: {
110 break :gpa switch (builtin.mode) {
111 .Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true },
112 .ReleaseFast, .ReleaseSmall => .{ std.heap.smp_allocator, false },
113 };
114 };
115 defer if (is_debug) {
116 _ = debug_allocator.deinit();
117 };
118
119 var arena = std.heap.ArenaAllocator.init(gpa);
120 defer arena.deinit();
121
122 var sfb = std.heap.stackFallback(1 << 20, arena.allocator());
123 const allocator = sfb.get();
124
125 var cmd: Command = .{ .arena = allocator };
126
127 cmd.opts.winsize = getWinsize(std.io.getStdOut().handle);
128
129 cmd.opts.shortview = if (cmd.opts.isatty()) .columns else .oneline;
130
131 const stdout = std.io.getStdOut().writer();
132 const stderr = std.io.getStdErr().writer();
133 var bw = std.io.bufferedWriter(stdout);
134
135 var args = std.process.args();
136 // skip binary
137 _ = args.next();
138 while (args.next()) |arg| {
139 switch (optKind(arg)) {
140 .short => {
141 const str = arg[1..];
142 for (str) |b| {
143 switch (b) {
144 '1' => cmd.opts.shortview = .oneline,
145 'A' => cmd.opts.@"almost-all" = true,
146 'C' => cmd.opts.shortview = .columns,
147 'a' => cmd.opts.all = true,
148 'l' => cmd.opts.long = true,
149 else => {
150 try stderr.print("Invalid opt: '{c}'", .{b});
151 std.process.exit(1);
152 },
153 }
154 }
155 },
156 .long => {
157 var split = std.mem.splitScalar(u8, arg[2..], '=');
158 const opt = split.first();
159 const val = split.rest();
160 if (eql(opt, "all")) {
161 cmd.opts.all = parseArgBool(val) orelse {
162 try stderr.print("Invalid boolean: '{s}'", .{val});
163 std.process.exit(1);
164 };
165 } else if (eql(opt, "long")) {
166 cmd.opts.long = parseArgBool(val) orelse {
167 try stderr.print("Invalid boolean: '{s}'", .{val});
168 std.process.exit(1);
169 };
170 } else if (eql(opt, "almost-all")) {
171 cmd.opts.@"almost-all" = parseArgBool(val) orelse {
172 try stderr.print("Invalid boolean: '{s}'", .{val});
173 std.process.exit(1);
174 };
175 } else if (eql(opt, "group-directories-first")) {
176 cmd.opts.@"group-directories-first" = parseArgBool(val) orelse {
177 try stderr.print("Invalid boolean: '{s}'", .{val});
178 std.process.exit(1);
179 };
180 } else if (eql(opt, "color")) {
181 cmd.opts.color = std.meta.stringToEnum(Options.When, val) orelse {
182 try stderr.print("Invalid color option: '{s}'", .{val});
183 std.process.exit(1);
184 };
185 } else if (eql(opt, "icons")) {
186 cmd.opts.icons = std.meta.stringToEnum(Options.When, val) orelse {
187 try stderr.print("Invalid color option: '{s}'", .{val});
188 std.process.exit(1);
189 };
190 } else if (eql(opt, "columns")) {
191 const c = parseArgBool(val) orelse {
192 try stderr.print("Invalid columns option: '{s}'", .{val});
193 std.process.exit(1);
194 };
195 cmd.opts.shortview = if (c) .columns else .oneline;
196 } else if (eql(opt, "oneline")) {
197 const o = parseArgBool(val) orelse {
198 try stderr.print("Invalid oneline option: '{s}'", .{val});
199 std.process.exit(1);
200 };
201 cmd.opts.shortview = if (o) .oneline else .columns;
202 } else if (eql(opt, "help")) {
203 return stderr.writeAll(usage);
204 } else if (eql(opt, "version")) {
205 try bw.writer().print("lsr {s}\r\n", .{build_options.version});
206 try bw.flush();
207 return;
208 } else {
209 try stderr.print("Invalid opt: '{s}'", .{opt});
210 std.process.exit(1);
211 }
212 },
213 .positional => {
214 cmd.opts.directory = arg;
215 },
216 }
217 }
218
219 if (cmd.opts.useColor()) {
220 cmd.opts.colors = .default;
221 }
222
223 var ring: ourio.Ring = try .init(allocator, queue_size);
224 defer ring.deinit();
225
226 _ = try ring.open(cmd.opts.directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{
227 .ptr = &cmd,
228 .cb = onCompletion,
229 .msg = @intFromEnum(Msg.cwd),
230 });
231
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, .{
244 .ptr = &cmd,
245 .cb = onCompletion,
246 .msg = @intFromEnum(Msg.group),
247 });
248 }
249
250 try ring.run(.until_done);
251
252 if (cmd.entries.len == 0) return;
253
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()),
259 }
260 try bw.flush();
261}
262
263fn printShortColumns(cmd: Command, writer: anytype) !void {
264 const win_width = blk: {
265 const ws = cmd.opts.winsize orelse break :blk 80;
266 break :blk ws.col;
267 };
268 if (win_width == 0) return printShortOnePerLine(cmd, writer);
269
270 const icon_width: u2 = if (cmd.opts.useIcons()) 2 else 0;
271
272 var n_cols = @min(win_width, cmd.entries.len);
273
274 const Column = struct {
275 width: usize = 0,
276 entries: []const Entry = &.{},
277 };
278
279 var columns: std.ArrayListUnmanaged(Column) = try .initCapacity(cmd.arena, n_cols);
280
281 outer: while (n_cols > 0) {
282 columns.clearRetainingCapacity();
283 const n_rows = std.math.divCeil(usize, cmd.entries.len, n_cols) catch unreachable;
284 const padding = (n_cols - 1) * 2;
285
286 // The number of columns that are short by one entry
287 const short_cols = n_cols * n_rows - cmd.entries.len;
288
289 var idx: usize = 0;
290 var line_width: usize = padding + icon_width * n_cols;
291
292 if (line_width > win_width) {
293 n_cols -= 1;
294 continue :outer;
295 }
296
297 for (0..n_cols) |i| {
298 const col_entries = if (isShortColumn(i, n_cols, short_cols)) n_rows - 1 else n_rows;
299 const entries = cmd.entries[idx .. idx + col_entries];
300 idx += col_entries;
301
302 var max_width: usize = 0;
303 for (entries) |entry| {
304 max_width = @max(max_width, entry.name.len);
305 }
306
307 // line_width already includes all icons and padding
308 line_width += max_width;
309
310 const col_width = max_width + icon_width + 2;
311
312 columns.appendAssumeCapacity(.{
313 .entries = entries,
314 .width = col_width,
315 });
316
317 if (line_width > win_width) {
318 n_cols -= 1;
319 continue :outer;
320 }
321 }
322
323 break :outer;
324 }
325
326 if (n_cols <= 1) return printShortOnePerLine(cmd, writer);
327
328 const n_rows = std.math.divCeil(usize, cmd.entries.len, columns.items.len) catch unreachable;
329 for (0..n_rows) |row| {
330 for (columns.items, 0..) |column, i| {
331 if (row >= column.entries.len) continue;
332 const entry = column.entries[row];
333 try printShortEntry(column.entries[row], cmd.opts, writer);
334
335 if (i < columns.items.len - 1) {
336 const spaces = column.width - (icon_width + entry.name.len);
337 try writer.writeByteNTimes(' ', spaces);
338 }
339 }
340 try writer.writeAll("\r\n");
341 }
342}
343
344fn isShortColumn(idx: usize, n_cols: usize, n_short_cols: usize) bool {
345 return idx + n_short_cols >= n_cols;
346}
347
348fn printShortEntry(entry: Entry, opts: Options, writer: anytype) !void {
349 const colors = opts.colors;
350 if (opts.useIcons()) {
351 const icon = Icon.get(entry, opts);
352
353 if (opts.useColor()) {
354 try writer.writeAll(icon.color);
355 try writer.writeAll(icon.icon);
356 try writer.writeAll(colors.reset);
357 } else {
358 try writer.writeAll(icon.icon);
359 }
360
361 try writer.writeByte(' ');
362 }
363 switch (entry.kind) {
364 .directory => try writer.writeAll(colors.dir),
365 .sym_link => try writer.writeAll(colors.symlink),
366 else => {
367 if (entry.isExecutable()) {
368 try writer.writeAll(colors.executable);
369 }
370 },
371 }
372 try writer.writeAll(entry.name);
373 try writer.writeAll(colors.reset);
374}
375
376fn printShortOneRow(cmd: Command, writer: anytype) !void {
377 for (cmd.entries) |entry| {
378 try printShortEntry(entry, cmd.opts, writer);
379 try writer.writeAll(" ");
380 }
381 try writer.writeAll("\r\n");
382}
383
384fn printShortOnePerLine(cmd: Command, writer: anytype) !void {
385 for (cmd.entries) |entry| {
386 try printShortEntry(entry, cmd.opts, writer);
387 try writer.writeAll("\r\n");
388 }
389}
390
391fn printLong(cmd: Command, writer: anytype) !void {
392 const tz = cmd.tz.?;
393 const now = zeit.instant(.{}) catch unreachable;
394 const one_year_ago = try now.subtract(.{ .days = 365 });
395 const colors = cmd.opts.colors;
396
397 const longest_group, const longest_user, const longest_size, const longest_suffix = blk: {
398 var n_group: usize = 0;
399 var n_user: usize = 0;
400 var n_size: usize = 0;
401 var n_suff: usize = 0;
402 for (cmd.entries) |entry| {
403 const group = cmd.getGroup(entry.statx.gid).?;
404 const user = cmd.getGroup(entry.statx.uid).?;
405 var buf: [16]u8 = undefined;
406 const size = try entry.humanReadableSize(&buf);
407 n_group = @max(n_group, group.name.len);
408 n_user = @max(n_user, user.name.len);
409 n_size = @max(n_size, size.len);
410 n_suff = @max(n_suff, entry.humanReadableSuffix().len);
411 }
412 break :blk .{ n_group, n_user, n_size, n_suff };
413 };
414
415 for (cmd.entries) |entry| {
416 const user = cmd.getUser(entry.statx.uid).?;
417 const group = cmd.getGroup(entry.statx.gid).?;
418 const ts = @as(i128, entry.statx.mtime.sec) * std.time.ns_per_s;
419 const inst: zeit.Instant = .{ .timestamp = ts, .timezone = &tz };
420 const time = inst.time();
421
422 const mode = entry.modeStr();
423
424 try writer.writeAll(&mode);
425 try writer.writeByte(' ');
426 try writer.writeAll(user.name);
427 try writer.writeByteNTimes(' ', longest_user - user.name.len);
428 try writer.writeByte(' ');
429 try writer.writeAll(group.name);
430 try writer.writeByteNTimes(' ', longest_group - group.name.len);
431 try writer.writeByte(' ');
432
433 var size_buf: [16]u8 = undefined;
434 const size = try entry.humanReadableSize(&size_buf);
435 const suffix = entry.humanReadableSuffix();
436
437 try writer.writeByteNTimes(' ', longest_size - size.len);
438 try writer.writeAll(size);
439 try writer.writeByte(' ');
440 try writer.writeAll(suffix);
441 try writer.writeByteNTimes(' ', longest_suffix - suffix.len);
442 try writer.writeByte(' ');
443
444 try writer.print("{d: >2} {s} ", .{
445 time.day,
446 time.month.shortName(),
447 });
448
449 if (ts > one_year_ago.timestamp) {
450 try writer.print("{d: >2}:{d:0>2} ", .{ time.hour, time.minute });
451 } else {
452 try writer.print("{d: >5} ", .{@as(u32, @intCast(time.year))});
453 }
454
455 if (cmd.opts.useIcons()) {
456 const icon = Icon.get(entry, cmd.opts);
457
458 if (cmd.opts.useColor()) {
459 try writer.writeAll(icon.color);
460 try writer.writeAll(icon.icon);
461 try writer.writeAll(colors.reset);
462 } else {
463 try writer.writeAll(icon.icon);
464 }
465
466 try writer.writeByte(' ');
467 }
468
469 switch (entry.kind) {
470 .directory => try writer.writeAll(colors.dir),
471 .sym_link => try writer.writeAll(colors.symlink),
472 else => {
473 if (entry.isExecutable()) {
474 try writer.writeAll(colors.executable);
475 }
476 },
477 }
478 try writer.writeAll(entry.name);
479 try writer.writeAll(colors.reset);
480
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);
490 }
491
492 try writer.writeAll("\r\n");
493 }
494}
495
496const Command = struct {
497 arena: std.mem.Allocator,
498 opts: Options = .{},
499 entries: []Entry = &.{},
500 entry_idx: usize = 0,
501
502 tz: ?zeit.TimeZone = null,
503 groups: std.ArrayListUnmanaged(Group) = .empty,
504 users: std.ArrayListUnmanaged(User) = .empty,
505
506 fn getUser(self: Command, uid: posix.uid_t) ?User {
507 for (self.users.items) |user| {
508 if (user.uid == uid) return user;
509 }
510 return null;
511 }
512
513 fn getGroup(self: Command, gid: posix.gid_t) ?Group {
514 for (self.groups.items) |group| {
515 if (group.gid == gid) return group;
516 }
517 return null;
518 }
519};
520
521const Msg = enum(u16) {
522 cwd,
523 localtime,
524 passwd,
525 group,
526 stat,
527
528 read_localtime,
529 read_passwd,
530 read_group,
531};
532
533const User = struct {
534 uid: posix.uid_t,
535 name: []const u8,
536
537 fn lessThan(_: void, lhs: User, rhs: User) bool {
538 return lhs.uid < rhs.uid;
539 }
540};
541
542const Group = struct {
543 gid: posix.gid_t,
544 name: []const u8,
545
546 fn lessThan(_: void, lhs: Group, rhs: Group) bool {
547 return lhs.gid < rhs.gid;
548 }
549};
550
551const MinimalEntry = struct {
552 name: [:0]const u8,
553 kind: std.fs.File.Kind,
554
555 fn lessThan(opts: Options, lhs: MinimalEntry, rhs: MinimalEntry) bool {
556 if (opts.@"group-directories-first" and
557 lhs.kind != rhs.kind and
558 (lhs.kind == .directory or rhs.kind == .directory))
559 {
560 return lhs.kind == .directory;
561 }
562
563 return std.ascii.lessThanIgnoreCase(lhs.name, rhs.name);
564 }
565};
566
567const Entry = struct {
568 name: [:0]const u8,
569 kind: std.fs.File.Kind,
570 statx: ourio.Statx,
571 link_name: [:0]const u8 = "",
572 symlink_missing: bool = false,
573
574 fn modeStr(self: Entry) [10]u8 {
575 var mode = [_]u8{'-'} ** 10;
576 switch (self.kind) {
577 .directory => mode[0] = 'd',
578 .sym_link => mode[0] = 'l',
579 else => {},
580 }
581
582 if (self.statx.mode & posix.S.IRUSR != 0) mode[1] = 'r';
583 if (self.statx.mode & posix.S.IWUSR != 0) mode[2] = 'w';
584 if (self.statx.mode & posix.S.IXUSR != 0) mode[3] = 'x';
585
586 if (self.statx.mode & posix.S.IRGRP != 0) mode[4] = 'r';
587 if (self.statx.mode & posix.S.IWGRP != 0) mode[5] = 'w';
588 if (self.statx.mode & posix.S.IXGRP != 0) mode[6] = 'x';
589
590 if (self.statx.mode & posix.S.IROTH != 0) mode[7] = 'r';
591 if (self.statx.mode & posix.S.IWOTH != 0) mode[8] = 'w';
592 if (self.statx.mode & posix.S.IXOTH != 0) mode[9] = 'x';
593 return mode;
594 }
595
596 fn humanReadableSuffix(self: Entry) []const u8 {
597 if (self.kind == .directory) return "-";
598
599 const buckets = [_]u64{
600 1 << 40, // TB
601 1 << 30, // GB
602 1 << 20, // MB
603 1 << 10, // KB
604 };
605
606 const suffixes = [_][]const u8{ "TB", "GB", "MB", "KB" };
607
608 for (buckets, suffixes) |bucket, suffix| {
609 if (self.statx.size >= bucket) {
610 return suffix;
611 }
612 }
613 return "B";
614 }
615
616 fn humanReadableSize(self: Entry, out: []u8) ![]u8 {
617 if (self.kind == .directory) return &.{};
618
619 const buckets = [_]u64{
620 1 << 40, // TB
621 1 << 30, // GB
622 1 << 20, // MB
623 1 << 10, // KB
624 };
625
626 for (buckets) |bucket| {
627 if (self.statx.size >= bucket) {
628 const size_f: f64 = @floatFromInt(self.statx.size);
629 const bucket_f: f64 = @floatFromInt(bucket);
630 const val = size_f / bucket_f;
631 return std.fmt.bufPrint(out, "{d:0.1}", .{val});
632 }
633 }
634 return std.fmt.bufPrint(out, "{d}", .{self.statx.size});
635 }
636
637 fn isExecutable(self: Entry) bool {
638 return self.statx.mode & (posix.S.IXUSR | posix.S.IXGRP | posix.S.IXOTH) != 0;
639 }
640};
641
642fn onCompletion(io: *ourio.Ring, task: ourio.Task) anyerror!void {
643 const cmd = task.userdataCast(Command);
644 const msg = task.msgToEnum(Msg);
645 const result = task.result.?;
646
647 switch (msg) {
648 .cwd => {
649 const fd = try result.open;
650 // we are async, no need to defer!
651 _ = try io.close(fd, .{});
652 const dir: std.fs.Dir = .{ .fd = fd };
653
654 var temp_results: std.ArrayListUnmanaged(MinimalEntry) = .empty;
655
656 // Preallocate some memory
657 try temp_results.ensureUnusedCapacity(cmd.arena, queue_size);
658
659 // zig skips "." and "..", so we manually add them if needed
660 if (cmd.opts.all) {
661 temp_results.appendAssumeCapacity(.{
662 .name = ".",
663 .kind = .directory,
664 });
665 temp_results.appendAssumeCapacity(.{
666 .name = "..",
667 .kind = .directory,
668 });
669 }
670
671 var iter = dir.iterate();
672 while (try iter.next()) |dirent| {
673 if (!cmd.opts.@"almost-all" and std.mem.startsWith(u8, dirent.name, ".")) continue;
674 const nameZ = try cmd.arena.dupeZ(u8, dirent.name);
675 try temp_results.append(cmd.arena, .{
676 .name = nameZ,
677 .kind = dirent.kind,
678 });
679 }
680
681 // sort the entries on the minimal struct. This has better memory locality since it is
682 // much smaller than bringing in the ourio.Statx struct
683 std.sort.pdq(MinimalEntry, temp_results.items, cmd.opts, MinimalEntry.lessThan);
684
685 var results: std.ArrayListUnmanaged(Entry) = .empty;
686 try results.ensureUnusedCapacity(cmd.arena, temp_results.items.len);
687 for (temp_results.items) |tmp| {
688 results.appendAssumeCapacity(.{
689 .name = tmp.name,
690 .kind = tmp.kind,
691 .statx = undefined,
692 });
693 }
694 cmd.entries = results.items;
695
696 for (cmd.entries, 0..) |*entry, i| {
697 if (i >= queue_size) {
698 cmd.entry_idx = i;
699 break;
700 }
701 const path = try std.fs.path.joinZ(
702 cmd.arena,
703 &.{ cmd.opts.directory, entry.name },
704 );
705
706 if (entry.kind == .sym_link) {
707 var buf: [std.fs.max_path_bytes]u8 = undefined;
708
709 // NOTE: Sadly, we can't do readlink via io_uring
710 const link = try posix.readlink(path, &buf);
711 entry.link_name = try cmd.arena.dupeZ(u8, link);
712 }
713 _ = try io.stat(path, &entry.statx, .{
714 .cb = onCompletion,
715 .ptr = cmd,
716 .msg = @intFromEnum(Msg.stat),
717 });
718 }
719 },
720
721 .localtime => {
722 const fd = try result.open;
723
724 // Largest TZ file on my system is Asia/Hebron at 4791 bytes. We allocate an amount
725 // sufficiently more than that to make sure we do this in a single pass
726 const buffer = try cmd.arena.alloc(u8, 8192);
727 _ = try io.read(fd, buffer, .{
728 .cb = onCompletion,
729 .ptr = cmd,
730 .msg = @intFromEnum(Msg.read_localtime),
731 });
732 },
733
734 .read_localtime => {
735 const n = try result.read;
736 _ = try io.close(task.req.read.fd, .{});
737 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());
740 cmd.tz = .{ .tzinfo = tz };
741 },
742
743 .passwd => {
744 const fd = try result.open;
745
746 // TODO: stat this or do multiple reads. We'll never know a good bound unless we go
747 // really big
748 const buffer = try cmd.arena.alloc(u8, 8192 * 2);
749 _ = try io.read(fd, buffer, .{
750 .cb = onCompletion,
751 .ptr = cmd,
752 .msg = @intFromEnum(Msg.read_passwd),
753 });
754 },
755
756 .read_passwd => {
757 const n = try result.read;
758 _ = try io.close(task.req.read.fd, .{});
759 const bytes = task.req.read.buffer[0..n];
760
761 var lines = std.mem.splitScalar(u8, bytes, '\n');
762
763 var line_count: usize = 0;
764 while (lines.next()) |_| {
765 line_count += 1;
766 }
767 try cmd.users.ensureUnusedCapacity(cmd.arena, line_count);
768 lines.reset();
769 // <name>:<throwaway>:<uid><...garbage>
770 while (lines.next()) |line| {
771 if (line.len == 0) continue;
772 var iter = std.mem.splitScalar(u8, line, ':');
773 const name = iter.first();
774 _ = iter.next();
775 const uid = iter.next().?;
776
777 const user: User = .{
778 .name = name,
779 .uid = try std.fmt.parseInt(u32, uid, 10),
780 };
781
782 cmd.users.appendAssumeCapacity(user);
783 }
784 std.sort.pdq(User, cmd.users.items, {}, User.lessThan);
785 },
786
787 .group => {
788 const fd = try result.open;
789
790 const buffer = try cmd.arena.alloc(u8, 8192);
791 _ = try io.read(fd, buffer, .{
792 .cb = onCompletion,
793 .ptr = cmd,
794 .msg = @intFromEnum(Msg.read_group),
795 });
796 },
797
798 .read_group => {
799 const n = try result.read;
800 _ = try io.close(task.req.read.fd, .{});
801 const bytes = task.req.read.buffer[0..n];
802
803 var lines = std.mem.splitScalar(u8, bytes, '\n');
804
805 var line_count: usize = 0;
806 while (lines.next()) |_| {
807 line_count += 1;
808 }
809 try cmd.groups.ensureUnusedCapacity(cmd.arena, line_count);
810 lines.reset();
811 // <name>:<throwaway>:<uid><...garbage>
812 while (lines.next()) |line| {
813 if (line.len == 0) continue;
814 var iter = std.mem.splitScalar(u8, line, ':');
815 const name = iter.first();
816 _ = iter.next();
817 const gid = iter.next().?;
818
819 const group: Group = .{
820 .name = name,
821 .gid = try std.fmt.parseInt(u32, gid, 10),
822 };
823
824 cmd.groups.appendAssumeCapacity(group);
825 }
826 std.sort.pdq(Group, cmd.groups.items, {}, Group.lessThan);
827 },
828
829 .stat => {
830 _ = result.statx catch {
831 const entry: *Entry = @fieldParentPtr("statx", task.req.statx.result);
832 if (entry.symlink_missing) {
833 // we already got here. Just zero out the statx;
834 entry.statx = std.mem.zeroInit(ourio.Statx, entry.statx);
835 return;
836 }
837
838 entry.symlink_missing = true;
839 _ = try io.lstat(task.req.statx.path, task.req.statx.result, .{
840 .cb = onCompletion,
841 .ptr = cmd,
842 .msg = @intFromEnum(Msg.stat),
843 });
844 return;
845 };
846
847 if (cmd.entry_idx >= cmd.entries.len) return;
848
849 const entry = &cmd.entries[cmd.entry_idx];
850 cmd.entry_idx += 1;
851 const path = try std.fs.path.joinZ(
852 cmd.arena,
853 &.{ cmd.opts.directory, entry.name },
854 );
855
856 if (entry.kind == .sym_link) {
857 var buf: [std.fs.max_path_bytes]u8 = undefined;
858
859 // NOTE: Sadly, we can't do readlink via io_uring
860 const link = try posix.readlink(path, &buf);
861 entry.link_name = try cmd.arena.dupeZ(u8, link);
862 }
863 _ = try io.stat(path, &entry.statx, .{
864 .cb = onCompletion,
865 .ptr = cmd,
866 .msg = @intFromEnum(Msg.stat),
867 });
868 },
869 }
870}
871
872const Icon = struct {
873 icon: []const u8,
874 color: []const u8,
875
876 // Entry types
877 const directory: Icon = .{ .icon = "", .color = Options.Colors.blue };
878 const drive: Icon = .{ .icon = "", .color = Options.Colors.blue };
879 const file: Icon = .{ .icon = "", .color = Options.Colors.fg };
880 const file_hidden: Icon = .{ .icon = "", .color = Options.Colors.fg };
881 const pipe: Icon = .{ .icon = "", .color = Options.Colors.fg };
882 const socket: Icon = .{ .icon = "", .color = Options.Colors.fg };
883 const symlink: Icon = .{ .icon = "", .color = Options.Colors.fg };
884 const symlink_dir: Icon = .{ .icon = "", .color = Options.Colors.blue };
885
886 // Broad file types
887 const executable: Icon = .{ .icon = "", .color = Options.Colors.green };
888 const image: Icon = .{ .icon = "", .color = Options.Colors.yellow };
889 const video: Icon = .{ .icon = "", .color = Options.Colors.yellow };
890
891 // Filetypes
892 const css: Icon = .{ .icon = "", .color = "\x1b[38:2:50:167:220m" };
893 const go: Icon = .{ .icon = "", .color = Options.Colors.blue };
894 const html: Icon = .{ .icon = "", .color = "\x1b[38:2:229:76:33m" };
895 const javascript: Icon = .{ .icon = "", .color = "\x1b[38:2:233:212:77m" };
896 const json: Icon = .{ .icon = "", .color = Options.Colors.blue };
897 const lua: Icon = .{ .icon = "", .color = Options.Colors.blue };
898 const markdown: Icon = .{ .icon = "", .color = "" };
899 const python: Icon = .{ .icon = "", .color = Options.Colors.yellow };
900 const typescript: Icon = .{ .icon = "", .color = Options.Colors.blue };
901 const zig: Icon = .{ .icon = "", .color = "\x1b[38:2:247:164:29m" };
902
903 const by_name: std.StaticStringMap(Icon) = .initComptime(.{});
904
905 const by_extension: std.StaticStringMap(Icon) = .initComptime(.{
906 .{ "css", Icon.css },
907 .{ "gif", Icon.image },
908 .{ "go", Icon.go },
909 .{ "html", Icon.html },
910 .{ "jpeg", Icon.image },
911 .{ "jpg", Icon.image },
912 .{ "js", Icon.javascript },
913 .{ "json", Icon.json },
914 .{ "lua", Icon.lua },
915 .{ "md", Icon.markdown },
916 .{ "mkv", Icon.video },
917 .{ "mp4", Icon.video },
918 .{ "png", Icon.image },
919 .{ "py", Icon.python },
920 .{ "ts", Icon.typescript },
921 .{ "webp", Icon.image },
922 .{ "zig", Icon.zig },
923 .{ "zon", Icon.zig },
924 });
925
926 fn get(entry: Entry, opts: Options) Icon {
927 // 1. By name
928 // 2. By extension
929 // 3. By type
930 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
938 switch (entry.kind) {
939 .block_device => return drive,
940 .character_device => return drive,
941 .directory => return directory,
942 .file => {
943 if (entry.isExecutable()) {
944 return executable;
945 }
946 return file;
947 },
948 .named_pipe => return pipe,
949 .sym_link => {
950 if (opts.long and posix.S.ISDIR(entry.statx.mode)) {
951 return symlink_dir;
952 }
953 return symlink;
954 },
955 .unix_domain_socket => return pipe,
956 else => return file,
957 }
958 }
959};
960
961fn eql(a: []const u8, b: []const u8) bool {
962 return std.mem.eql(u8, a, b);
963}
964
965fn parseArgBool(arg: []const u8) ?bool {
966 if (arg.len == 0) return true;
967
968 if (std.ascii.eqlIgnoreCase(arg, "true")) return true;
969 if (std.ascii.eqlIgnoreCase(arg, "false")) return false;
970 if (std.ascii.eqlIgnoreCase(arg, "1")) return true;
971 if (std.ascii.eqlIgnoreCase(arg, "0")) return false;
972
973 return null;
974}
975
976/// getWinsize gets the window size of the output. Returns null if output is not a terminal
977fn getWinsize(fd: posix.fd_t) ?posix.winsize {
978 var winsize: posix.winsize = .{
979 .row = 0,
980 .col = 0,
981 .xpixel = 0,
982 .ypixel = 0,
983 };
984
985 const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize));
986 switch (posix.errno(err)) {
987 .SUCCESS => return winsize,
988 else => return null,
989 }
990}
991
992fn optKind(a: []const u8) enum { short, long, positional } {
993 if (std.mem.startsWith(u8, a, "--")) return .long;
994 if (std.mem.startsWith(u8, a, "-")) return .short;
995 return .positional;
996}