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