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