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