ls but with io_uring
at main 49 kB view raw
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}