this repo has no description
at v0.2.0 35 kB view raw
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}