ls but with io_uring
1const std = @import("std");
2const builtin = @import("builtin");
3const ourio = @import("ourio");
4const zeit = @import("zeit");
5const natord = @import("natord.zig");
6const build_options = @import("build_options");
7const grp = @cImport({
8 @cInclude("grp.h");
9});
10
11const posix = std.posix;
12
13const usage =
14 \\Usage:
15 \\ lsr [options] [path...]
16 \\
17 \\ --help Print this message and exit
18 \\ --version Print the version string
19 \\
20 \\DISPLAY OPTIONS
21 \\ -1, --oneline Print entries one per line
22 \\ -a, --all Show files that start with a dot (ASCII 0x2E)
23 \\ -A, --almost-all Like --all, but skips implicit "." and ".." directories
24 \\ -C, --columns Print the output in columns
25 \\ --color=WHEN 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)
28 \\ --icons=WHEN When to display icons (always, auto, never)
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
36const queue_size = 256;
37
38const Options = struct {
39 all: bool = false,
40 @"almost-all": bool = false,
41 color: When = .auto,
42 shortview: enum { columns, oneline } = .oneline,
43 @"group-directories-first": bool = true,
44 hyperlinks: When = .auto,
45 icons: When = .auto,
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,
56 colors: Colors = .none,
57
58 const When = enum {
59 never,
60 auto,
61 always,
62 };
63
64 const Colors = struct {
65 reset: []const u8,
66 dir: []const u8,
67 executable: []const u8,
68 symlink: []const u8,
69 symlink_target: []const u8,
70 symlink_missing: []const u8,
71
72 const none: Colors = .{
73 .reset = "",
74 .dir = "",
75 .executable = "",
76 .symlink = "",
77 .symlink_target = "",
78 .symlink_missing = "",
79 };
80
81 const default: Colors = .{
82 .reset = _reset,
83 .dir = bold ++ blue,
84 .executable = bold ++ green,
85 .symlink = bold ++ purple,
86 .symlink_target = bold ++ cyan,
87 .symlink_missing = bold ++ red,
88 };
89
90 const _reset = "\x1b[m";
91 const red = "\x1b[31m";
92 const green = "\x1b[32m";
93 const yellow = "\x1b[33m";
94 const blue = "\x1b[34m";
95 const purple = "\x1b[35m";
96 const cyan = "\x1b[36m";
97 const fg = "\x1b[37m";
98
99 const bold = "\x1b[1m";
100 };
101
102 fn useColor(self: Options) bool {
103 switch (self.color) {
104 .never => return false,
105 .always => return true,
106 .auto => return self.isatty(),
107 }
108 }
109
110 fn useIcons(self: Options) bool {
111 switch (self.icons) {
112 .never => return false,
113 .always => return true,
114 .auto => return self.isatty(),
115 }
116 }
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
130 fn isatty(self: Options) bool {
131 return self.winsize != null;
132 }
133};
134
135pub fn main() !void {
136 var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
137 const gpa, const is_debug = gpa: {
138 break :gpa switch (builtin.mode) {
139 .Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true },
140 .ReleaseFast, .ReleaseSmall => .{ std.heap.smp_allocator, false },
141 };
142 };
143 defer if (is_debug) {
144 _ = debug_allocator.deinit();
145 };
146
147 var arena = std.heap.ArenaAllocator.init(gpa);
148 defer arena.deinit();
149
150 var sfb = std.heap.stackFallback(1 << 20, arena.allocator());
151 const allocator = sfb.get();
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
168 _ = args.next();
169 while (args.next()) |arg| {
170 switch (optKind(arg)) {
171 .short => {
172 const str = arg[1..];
173 for (str) |b| {
174 switch (b) {
175 '1' => cmd.opts.shortview = .oneline,
176 'A' => cmd.opts.@"almost-all" = true,
177 'C' => cmd.opts.shortview = .columns,
178 'a' => cmd.opts.all = true,
179 'h' => {}, // human-readable: present for compatibility
180 'l' => cmd.opts.long = true,
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 }
188 }
189 },
190 .long => {
191 var split = std.mem.splitScalar(u8, arg[2..], '=');
192 const opt = split.first();
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});
268try 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 }
280
281 if (cmd.opts.useColor()) {
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
361fn printShortColumns(cmd: Command, writer: anytype) !void {
362 const win_width = blk: {
363 const ws = cmd.opts.winsize orelse break :blk 80;
364 break :blk ws.col;
365 };
366 if (win_width == 0) return printShortOnePerLine(cmd, writer);
367
368 const icon_width: u2 = if (cmd.opts.useIcons()) 2 else 0;
369
370 var n_cols = @min(win_width, cmd.entries.len);
371
372 const Column = struct {
373 width: usize = 0,
374 entries: []const Entry = &.{},
375 };
376
377 var columns: std.ArrayListUnmanaged(Column) = try .initCapacity(cmd.arena, n_cols);
378
379 outer: while (n_cols > 0) {
380 columns.clearRetainingCapacity();
381 const n_rows = std.math.divCeil(usize, cmd.entries.len, n_cols) catch unreachable;
382 const padding = (n_cols - 1) * 2;
383
384 // The number of columns that are short by one entry
385 const short_cols = n_cols * n_rows - cmd.entries.len;
386
387 var idx: usize = 0;
388 var line_width: usize = padding + icon_width * n_cols;
389
390 if (line_width > win_width) {
391 n_cols -= 1;
392 continue :outer;
393 }
394
395 for (0..n_cols) |i| {
396 const col_entries = if (isShortColumn(i, n_cols, short_cols)) n_rows - 1 else n_rows;
397 const entries = cmd.entries[idx .. idx + col_entries];
398 idx += col_entries;
399
400 var max_width: usize = 0;
401 for (entries) |entry| {
402 max_width = @max(max_width, entry.name.len);
403 }
404
405 // line_width already includes all icons and padding
406 line_width += max_width;
407
408 const col_width = max_width + icon_width + 2;
409
410 columns.appendAssumeCapacity(.{
411 .entries = entries,
412 .width = col_width,
413 });
414
415 if (line_width > win_width) {
416 n_cols -= 1;
417 continue :outer;
418 }
419 }
420
421 break :outer;
422 }
423
424 if (n_cols <= 1) return printShortOnePerLine(cmd, writer);
425
426 const n_rows = std.math.divCeil(usize, cmd.entries.len, columns.items.len) catch unreachable;
427 for (0..n_rows) |row| {
428 for (columns.items, 0..) |column, i| {
429 if (row >= column.entries.len) continue;
430 const entry = column.entries[row];
431 try printShortEntry(column.entries[row], cmd, writer);
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");
440 }
441}
442
443fn isShortColumn(idx: usize, n_cols: usize, n_short_cols: usize) bool {
444 return idx + n_short_cols >= n_cols;
445}
446
447fn printShortEntry(entry: Entry, cmd: Command, writer: anytype) !void {
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);
455 try writer.writeAll(icon.icon);
456 try writer.writeAll(colors.reset);
457 } else {
458 try writer.writeAll(icon.icon);
459 }
460
461 try writer.writeByte(' ');
462 }
463 switch (entry.kind) {
464 .directory => try writer.writeAll(colors.dir),
465 .sym_link => try writer.writeAll(colors.symlink),
466 else => {
467 if (entry.isExecutable()) {
468 try writer.writeAll(colors.executable);
469 }
470 },
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\\");
478 try writer.writeAll(colors.reset);
479 } else {
480 try writer.writeAll(entry.name);
481 try writer.writeAll(colors.reset);
482 }
483}
484
485fn printShortOneRow(cmd: Command, writer: anytype) !void {
486 for (cmd.entries) |entry| {
487 try printShortEntry(entry, cmd.opts, writer);
488 try writer.writeAll(" ");
489 }
490 try writer.writeAll("\r\n");
491}
492
493fn printShortOnePerLine(cmd: Command, writer: anytype) !void {
494 for (cmd.entries) |entry| {
495 try printShortEntry(entry, cmd, writer);
496 try writer.writeAll("\r\n");
497 }
498}
499
500fn 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
516fn 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
546fn 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
590fn 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
634fn 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 });
638 const colors = cmd.opts.colors;
639
640 const longest_group, const longest_user, const longest_size, const longest_suffix = blk: {
641 var n_group: usize = 0;
642 var n_user: usize = 0;
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);
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);
671 n_size = @max(n_size, size.len);
672 n_suff = @max(n_suff, entry.humanReadableSuffix().len);
673 }
674 break :blk .{ n_group, n_user, n_size, n_suff };
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}),
687 };
688 const ts = @as(i128, entry.statx.mtime.sec) * std.time.ns_per_s;
689 const inst: zeit.Instant = .{ .timestamp = ts, .timezone = &tz };
690 const time = inst.time();
691
692 const mode = entry.modeStr();
693
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} ", .{
719 time.day,
720 time.month.shortName(),
721 });
722
723 if (ts > one_year_ago.timestamp) {
724 try writer.print("{d: >2}:{d:0>2} ", .{ time.hour, time.minute });
725 } else {
726 try writer.print("{d: >5} ", .{@as(u32, @intCast(time.year))});
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);
734 try writer.writeAll(icon.icon);
735 try writer.writeAll(colors.reset);
736 } else {
737 try writer.writeAll(icon.icon);
738 }
739
740 try writer.writeByte(' ');
741 }
742
743 switch (entry.kind) {
744 .directory => try writer.writeAll(colors.dir),
745 .sym_link => try writer.writeAll(colors.symlink),
746 else => {
747 if (entry.isExecutable()) {
748 try writer.writeAll(colors.executable);
749 }
750 },
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\\");
758 } else {
759 try writer.writeAll(entry.name);
760 }
761 try writer.writeAll(colors.reset);
762
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 => {},
786 }
787
788 try writer.writeAll("\r\n");
789 }
790}
791
792const Command = struct {
793 arena: std.mem.Allocator,
794 opts: Options = .{},
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};
836
837const Msg = enum(u16) {
838 cwd,
839 localtime,
840 passwd,
841 group,
842 stat,
843
844 read_localtime,
845 read_passwd,
846 read_group,
847};
848
849const User = struct {
850 uid: if (builtin.os.tag == .macos) i33 else posix.uid_t,
851 name: []const u8,
852
853 fn lessThan(_: void, lhs: User, rhs: User) bool {
854 return lhs.uid < rhs.uid;
855 }
856};
857
858const Group = struct {
859 gid: if (builtin.os.tag == .macos) i33 else posix.gid_t,
860 name: []const u8,
861
862 fn lessThan(_: void, lhs: Group, rhs: Group) bool {
863 return lhs.gid < rhs.gid;
864 }
865};
866
867const MinimalEntry = struct {
868 name: [:0]const u8,
869 kind: std.fs.File.Kind,
870
871 fn lessThan(opts: Options, lhs: MinimalEntry, rhs: MinimalEntry) bool {
872 if (opts.@"group-directories-first" and
873 lhs.kind != rhs.kind and
874 (lhs.kind == .directory or rhs.kind == .directory))
875 {
876 return lhs.kind == .directory;
877 }
878
879 return std.ascii.lessThanIgnoreCase(lhs.name, rhs.name);
880 }
881};
882
883const Symlink = struct {
884 name: [:0]const u8,
885 exists: bool = true,
886};
887
888const Entry = struct {
889 name: [:0]const u8,
890 kind: std.fs.File.Kind,
891 statx: ourio.Statx,
892
893 fn lessThan(opts: Options, lhs: Entry, rhs: Entry) bool {
894 if (opts.@"group-directories-first" and
895 lhs.kind != rhs.kind and
896 (lhs.kind == .directory or rhs.kind == .directory))
897 {
898 return lhs.kind == .directory;
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 }
910
911 fn modeStr(self: Entry) [10]u8 {
912 var mode = [_]u8{'-'} ** 10;
913 switch (self.kind) {
914 .block_device => mode[0] = 'b',
915 .character_device => mode[0] = 'c',
916 .directory => mode[0] = 'd',
917 .named_pipe => mode[0] = 'p',
918 .sym_link => mode[0] = 'l',
919 else => {},
920 }
921
922 if (self.statx.mode & posix.S.IRUSR != 0) mode[1] = 'r';
923 if (self.statx.mode & posix.S.IWUSR != 0) mode[2] = 'w';
924 if (self.statx.mode & posix.S.IXUSR != 0) mode[3] = 'x';
925
926 if (self.statx.mode & posix.S.IRGRP != 0) mode[4] = 'r';
927 if (self.statx.mode & posix.S.IWGRP != 0) mode[5] = 'w';
928 if (self.statx.mode & posix.S.IXGRP != 0) mode[6] = 'x';
929
930 if (self.statx.mode & posix.S.IROTH != 0) mode[7] = 'r';
931 if (self.statx.mode & posix.S.IWOTH != 0) mode[8] = 'w';
932 if (self.statx.mode & posix.S.IXOTH != 0) mode[9] = 'x';
933 return mode;
934 }
935
936 fn humanReadableSuffix(self: Entry) []const u8 {
937 if (self.kind == .directory) return "-";
938
939 const buckets = [_]u64{
940 1 << 40, // TB
941 1 << 30, // GB
942 1 << 20, // MB
943 1 << 10, // KB
944 };
945
946 const suffixes = [_][]const u8{ "TB", "GB", "MB", "KB" };
947
948 for (buckets, suffixes) |bucket, suffix| {
949 if (self.statx.size >= bucket) {
950 return suffix;
951 }
952 }
953 return "B";
954 }
955
956 fn humanReadableSize(self: Entry, out: []u8) ![]u8 {
957 if (self.kind == .directory) return &.{};
958
959 const buckets = [_]u64{
960 1 << 40, // TB
961 1 << 30, // GB
962 1 << 20, // MB
963 1 << 10, // KB
964 };
965
966 for (buckets) |bucket| {
967 if (self.statx.size >= bucket) {
968 const size_f: f64 = @floatFromInt(self.statx.size);
969 const bucket_f: f64 = @floatFromInt(bucket);
970 const val = size_f / bucket_f;
971 return std.fmt.bufPrint(out, "{d:0.1}", .{val});
972 }
973 }
974 return std.fmt.bufPrint(out, "{d}", .{self.statx.size});
975 }
976
977 fn isExecutable(self: Entry) bool {
978 return self.statx.mode & (posix.S.IXUSR | posix.S.IXGRP | posix.S.IXOTH) != 0;
979 }
980};
981
982fn onCompletion(io: *ourio.Ring, task: ourio.Task) anyerror!void {
983 const cmd = task.userdataCast(Command);
984 const msg = task.msgToEnum(Msg);
985 const result = task.result.?;
986
987 switch (msg) {
988 .cwd => {
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 };
1015 // we are async, no need to defer!
1016 _ = try io.close(fd, .{});
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 }
1024
1025 var temp_results: std.ArrayListUnmanaged(MinimalEntry) = .empty;
1026
1027 // Preallocate some memory
1028 try temp_results.ensureUnusedCapacity(cmd.arena, queue_size);
1029
1030 // zig skips "." and "..", so we manually add them if needed
1031 if (cmd.opts.all) {
1032 temp_results.appendAssumeCapacity(.{
1033 .name = ".",
1034 .kind = .directory,
1035 });
1036 temp_results.appendAssumeCapacity(.{
1037 .name = "..",
1038 .kind = .directory,
1039 });
1040 }
1041
1042 var iter = dir.iterate();
1043 while (try iter.next()) |dirent| {
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 }
1055 const nameZ = try cmd.arena.dupeZ(u8, dirent.name);
1056 try temp_results.append(cmd.arena, .{
1057 .name = nameZ,
1058 .kind = dirent.kind,
1059 });
1060 }
1061
1062 // sort the entries on the minimal struct. This has better memory locality since it is
1063 // much smaller than bringing in the ourio.Statx struct
1064 std.sort.pdq(MinimalEntry, temp_results.items, cmd.opts, MinimalEntry.lessThan);
1065
1066 var results: std.ArrayListUnmanaged(Entry) = .empty;
1067 try results.ensureUnusedCapacity(cmd.arena, temp_results.items.len);
1068 for (temp_results.items) |tmp| {
1069 results.appendAssumeCapacity(.{
1070 .name = tmp.name,
1071 .kind = tmp.kind,
1072 .statx = undefined,
1073 });
1074 }
1075 cmd.entries = results.items;
1076
1077 for (cmd.entries, 0..) |*entry, i| {
1078 if (i >= queue_size) {
1079 cmd.entry_idx = i;
1080 break;
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) {
1088 var buf: [std.fs.max_path_bytes]u8 = undefined;
1089
1090 // NOTE: Sadly, we can't do readlink via io_uring
1091 const link = try posix.readlink(path, &buf);
1092 const symlink: Symlink = .{ .name = try cmd.arena.dupeZ(u8, link) };
1093 try cmd.symlinks.put(cmd.arena, entry.name, symlink);
1094 }
1095 _ = try io.stat(path, &entry.statx, .{
1096 .cb = onCompletion,
1097 .ptr = cmd,
1098 .msg = @intFromEnum(Msg.stat),
1099 });
1100 }
1101 },
1102
1103 .localtime => {
1104 const fd = try result.open;
1105
1106 // Largest TZ file on my system is Asia/Hebron at 4791 bytes. We allocate an amount
1107 // sufficiently more than that to make sure we do this in a single pass
1108 const buffer = try cmd.arena.alloc(u8, 8192);
1109 _ = try io.read(fd, buffer, .file, .{
1110 .cb = onCompletion,
1111 .ptr = cmd,
1112 .msg = @intFromEnum(Msg.read_localtime),
1113 });
1114 },
1115
1116 .read_localtime => {
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
1125 .passwd => {
1126 const fd = try result.open;
1127
1128 // TODO: stat this or do multiple reads. We'll never know a good bound unless we go
1129 // really big
1130 const buffer = try cmd.arena.alloc(u8, 8192 * 2);
1131 _ = try io.read(fd, buffer, .file, .{
1132 .cb = onCompletion,
1133 .ptr = cmd,
1134 .msg = @intFromEnum(Msg.read_passwd),
1135 });
1136 },
1137
1138 .read_passwd => {
1139 const n = try result.read;
1140 _ = try io.close(task.req.read.fd, .{});
1141 const bytes = task.req.read.buffer[0..n];
1142
1143 var lines = std.mem.splitScalar(u8, bytes, '\n');
1144
1145 var line_count: usize = 0;
1146 while (lines.next()) |_| {
1147 line_count += 1;
1148 }
1149 try cmd.users.ensureUnusedCapacity(cmd.arena, line_count);
1150 lines.reset();
1151 // <name>:<throwaway>:<uid><...garbage>
1152 while (lines.next()) |line| {
1153 if (line.len == 0) continue;
1154 if (std.mem.startsWith(u8, line, "#")) continue;
1155
1156 var iter = std.mem.splitScalar(u8, line, ':');
1157 const name = iter.first();
1158 _ = iter.next();
1159 const uid = iter.next().?;
1160
1161 const user: User = .{
1162 .name = name,
1163 .uid = try std.fmt.parseInt(
1164 if (builtin.os.tag == .macos) i33 else u32,
1165 uid,
1166 10,
1167 ),
1168 };
1169
1170 cmd.users.appendAssumeCapacity(user);
1171 }
1172 std.sort.pdq(User, cmd.users.items, {}, User.lessThan);
1173 },
1174
1175 .group => {
1176 const fd = try result.open;
1177
1178 const buffer = try cmd.arena.alloc(u8, 8192);
1179 _ = try io.read(fd, buffer, .file, .{
1180 .cb = onCompletion,
1181 .ptr = cmd,
1182 .msg = @intFromEnum(Msg.read_group),
1183 });
1184 },
1185
1186 .read_group => {
1187 const n = try result.read;
1188 _ = try io.close(task.req.read.fd, .{});
1189 const bytes = task.req.read.buffer[0..n];
1190
1191 var lines = std.mem.splitScalar(u8, bytes, '\n');
1192
1193 var line_count: usize = 0;
1194 while (lines.next()) |_| {
1195 line_count += 1;
1196 }
1197 try cmd.groups.ensureUnusedCapacity(cmd.arena, line_count);
1198 lines.reset();
1199 // <name>:<throwaway>:<uid><...garbage>
1200 while (lines.next()) |line| {
1201 if (line.len == 0) continue;
1202 if (std.mem.startsWith(u8, line, "#")) continue;
1203
1204 var iter = std.mem.splitScalar(u8, line, ':');
1205 const name = iter.first();
1206 _ = iter.next();
1207 const gid = iter.next().?;
1208
1209 const group: Group = .{
1210 .name = name,
1211 .gid = try std.fmt.parseInt(
1212 if (builtin.os.tag == .macos) i33 else u32,
1213 gid,
1214 10,
1215 ),
1216 };
1217
1218 cmd.groups.appendAssumeCapacity(group);
1219 }
1220 std.sort.pdq(Group, cmd.groups.items, {}, Group.lessThan);
1221 },
1222
1223 .stat => {
1224 _ = result.statx catch |err| {
1225 const entry: *Entry = @fieldParentPtr("statx", task.req.statx.result);
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
1231 entry.statx = std.mem.zeroInit(ourio.Statx, entry.statx);
1232 return;
1233 }
1234
1235 symlink.exists = false;
1236
1237 _ = try io.lstat(task.req.statx.path, task.req.statx.result, .{
1238 .cb = onCompletion,
1239 .ptr = cmd,
1240 .msg = @intFromEnum(Msg.stat),
1241 });
1242 return;
1243 };
1244
1245 if (cmd.entry_idx >= cmd.entries.len) return;
1246
1247 const entry = &cmd.entries[cmd.entry_idx];
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) {
1255 var buf: [std.fs.max_path_bytes]u8 = undefined;
1256
1257 // NOTE: Sadly, we can't do readlink via io_uring
1258 const link = try posix.readlink(path, &buf);
1259 const symlink: Symlink = .{ .name = try cmd.arena.dupeZ(u8, link) };
1260 try cmd.symlinks.put(cmd.arena, entry.name, symlink);
1261 }
1262 _ = try io.stat(path, &entry.statx, .{
1263 .cb = onCompletion,
1264 .ptr = cmd,
1265 .msg = @intFromEnum(Msg.stat),
1266 });
1267 },
1268 }
1269}
1270
1271const Icon = struct {
1272 icon: []const u8,
1273 color: []const u8,
1274
1275 // Entry types
1276 const directory: Icon = .{ .icon = "", .color = Options.Colors.blue };
1277 const drive: Icon = .{ .icon = "", .color = Options.Colors.blue };
1278 const file: Icon = .{ .icon = "", .color = Options.Colors.fg };
1279 const file_hidden: Icon = .{ .icon = "", .color = Options.Colors.fg };
1280 const pipe: Icon = .{ .icon = "", .color = Options.Colors.fg };
1281 const socket: Icon = .{ .icon = "", .color = Options.Colors.fg };
1282 const symlink: Icon = .{ .icon = "", .color = Options.Colors.fg };
1283 const symlink_dir: Icon = .{ .icon = "", .color = Options.Colors.blue };
1284
1285 // Broad file types
1286 const executable: Icon = .{ .icon = "", .color = Options.Colors.green };
1287 const image: Icon = .{ .icon = "", .color = Options.Colors.yellow };
1288 const video: Icon = .{ .icon = "", .color = Options.Colors.yellow };
1289
1290 // Filetypes
1291 const css: Icon = .{ .icon = "", .color = "\x1b[38:2:50:167:220m" };
1292 const go: Icon = .{ .icon = "", .color = Options.Colors.blue };
1293 const html: Icon = .{ .icon = "", .color = "\x1b[38:2:229:76:33m" };
1294 const javascript: Icon = .{ .icon = "", .color = "\x1b[38:2:233:212:77m" };
1295 const json: Icon = .{ .icon = "", .color = Options.Colors.blue };
1296 const lua: Icon = .{ .icon = "", .color = Options.Colors.blue };
1297 const markdown: Icon = .{ .icon = "", .color = "" };
1298 const nix: Icon = .{ .icon = "", .color = "\x1b[38:2:127:185:228m" };
1299 const python: Icon = .{ .icon = "", .color = Options.Colors.yellow };
1300 const rust: Icon = .{ .icon = "", .color = "" };
1301 const typescript: Icon = .{ .icon = "", .color = Options.Colors.blue };
1302 const zig: Icon = .{ .icon = "", .color = "\x1b[38:2:247:164:29m" };
1303
1304 const by_name: std.StaticStringMap(Icon) = .initComptime(.{
1305 .{ "flake.lock", Icon.nix },
1306 .{ "go.mod", Icon.go },
1307 .{ "go.sum", Icon.go },
1308 });
1309
1310 const by_extension: std.StaticStringMap(Icon) = .initComptime(.{
1311 .{ "cjs", Icon.javascript },
1312 .{ "css", Icon.css },
1313 .{ "drv", Icon.nix },
1314 .{ "gif", Icon.image },
1315 .{ "go", Icon.go },
1316 .{ "html", Icon.html },
1317 .{ "jpeg", Icon.image },
1318 .{ "jpg", Icon.image },
1319 .{ "js", Icon.javascript },
1320 .{ "jsx", Icon.javascript },
1321 .{ "json", Icon.json },
1322 .{ "lua", Icon.lua },
1323 .{ "md", Icon.markdown },
1324 .{ "mjs", Icon.javascript },
1325 .{ "mkv", Icon.video },
1326 .{ "mp4", Icon.video },
1327 .{ "nar", Icon.nix },
1328 .{ "nix", Icon.nix },
1329 .{ "png", Icon.image },
1330 .{ "py", Icon.python },
1331 .{ "rs", Icon.rust },
1332 .{ "ts", Icon.typescript },
1333 .{ "tsx", Icon.typescript },
1334 .{ "webp", Icon.image },
1335 .{ "zig", Icon.zig },
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 }
1359 return file;
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;
1367 },
1368 .unix_domain_socket => return pipe,
1369 else => return file,
1370 }
1371 }
1372};
1373
1374fn eql(a: []const u8, b: []const u8) bool {
1375 return std.mem.eql(u8, a, b);
1376}
1377
1378fn parseArgBool(arg: []const u8) ?bool {
1379 if (arg.len == 0) return true;
1380
1381 if (std.ascii.eqlIgnoreCase(arg, "true")) return true;
1382 if (std.ascii.eqlIgnoreCase(arg, "false")) return false;
1383 if (std.ascii.eqlIgnoreCase(arg, "1")) return true;
1384 if (std.ascii.eqlIgnoreCase(arg, "0")) return false;
1385
1386 return null;
1387}
1388
1389/// getWinsize gets the window size of the output. Returns null if output is not a terminal
1390fn getWinsize(fd: posix.fd_t) ?posix.winsize {
1391 var winsize: posix.winsize = .{
1392 .row = 0,
1393 .col = 0,
1394 .xpixel = 0,
1395 .ypixel = 0,
1396 };
1397
1398 const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize));
1399 switch (posix.errno(err)) {
1400 .SUCCESS => return winsize,
1401 else => return null,
1402 }
1403}
1404
1405fn optKind(a: []const u8) enum { short, long, positional } {
1406 if (std.mem.startsWith(u8, a, "--")) return .long;
1407 if (std.mem.startsWith(u8, a, "-")) return .short;
1408 return .positional;
1409}
1410
1411test "ref" {
1412 _ = natord;
1413}