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