this repo has no description

Compare changes

Choose any two refs to compare.

Changed files
+315 -106
nix
src
+1
build.zig
··· 45 45 .name = "lsr", 46 46 .root_module = exe_mod, 47 47 }); 48 + exe.linkLibC(); 48 49 49 50 b.installArtifact(exe); 50 51
+7 -7
build.zig.zon
··· 3 3 .version = "1.0.0", 4 4 .fingerprint = 0x495d173f6002e86, // Changing this has security and trust implications. 5 5 6 - .minimum_zig_version = "0.14.0", 6 + .minimum_zig_version = "0.15.1", 7 7 8 8 .dependencies = .{ 9 9 .ourio = .{ 10 - .url = "git+https://github.com/rockorager/ourio#ed8a67650e5dbb0a6dca811c9d769187e306ad94", 11 - .hash = "ourio-0.0.0-_s-z0dAWAgD3XNod2pTh0H8X-a3CjtpAwduh7jcgBz0G", 10 + .url = "git+https://github.com/rockorager/ourio#07bf94db87a9aea70d6e1a1dd99cac6fb9d38b35", 11 + .hash = "ourio-0.0.0-_s-z0Z0XAgBU_BFjdY8QjGhJ8vcdIONPSErlYRwLoxfg", 12 12 }, 13 13 .zeit = .{ 14 - .url = "git+https://github.com/rockorager/zeit#4496d1c40b2223c22a1341e175fc2ecd94cc0de9", 15 - .hash = "zeit-0.6.0-5I6bk1J1AgA13rteb6E0steXiOUKBYTzJZMMIuK9oEmb", 14 + .url = "git+https://github.com/rockorager/zeit#74be5a2afb346b2a6a6349abbb609e89ec7e65a6", 15 + .hash = "zeit-0.6.0-5I6bk4t8AgCP0UGGHVF_khlmWZkAF5XtfQWEKCyLoptU", 16 16 }, 17 17 .zzdoc = .{ 18 - .url = "git+https://github.com/rockorager/zzdoc#57e86eb4e621bc4a96fbe0dd89ad0986db6d0483", 19 - .hash = "zzdoc-0.0.0-tzT1PuPZAACr1jIJxjTrdOsLbfXS6idWFGfTq0gwxJiv", 18 + .url = "git+https://github.com/rockorager/zzdoc#a54223bdc13a80839ccf9f473edf3a171e777946", 19 + .hash = "zzdoc-0.0.0-tzT1Ph7cAAC5YmXQXiBJHAg41_A5AUAC5VOm7ShnUxlz", 20 20 }, 21 21 }, 22 22 .paths = .{
+1 -1
flake.nix
··· 16 16 17 17 packages.default = pkgs.stdenv.mkDerivation { 18 18 pname = "lsr"; 19 - version = "0.2.0"; 19 + version = "1.0.0"; 20 20 doCheck = false; 21 21 src = ./.; 22 22
+2 -2
nix/cache.nix
··· 2 2 3 3 pkgs.stdenv.mkDerivation { 4 4 pname = "lsr-cache"; 5 - version = "0.2.0"; 5 + version = "1.0.0"; 6 6 doCheck = false; 7 7 src = ../.; 8 8 ··· 17 17 mv $ZIG_GLOBAL_CACHE_DIR/p $out 18 18 ''; 19 19 20 - outputHash = "sha256-lnOow40km0mcj21i2mTQiDGXLhcSxQ2kJoAgUhkQiEg="; 20 + outputHash = "sha256-bfc2dlQa1VGq9S6OBeQawAJuvfxU4kgFtQ13fuKhdZc="; 21 21 outputHashMode = "recursive"; 22 22 outputHashAlgo = "sha256"; 23 23 }
+304 -96
src/main.zig
··· 4 4 const zeit = @import("zeit"); 5 5 const natord = @import("natord.zig"); 6 6 const build_options = @import("build_options"); 7 + const grp = @cImport({ 8 + @cInclude("grp.h"); 9 + }); 7 10 8 11 const posix = std.posix; 9 12 10 13 const usage = 11 - \\Usage: 12 - \\ lsr [options] [path] 14 + \\Usage: 15 + \\ lsr [options] [path...] 13 16 \\ 14 17 \\ --help Print this message and exit 15 18 \\ --version Print the version string ··· 26 29 \\ -l, --long Display extended file metadata 27 30 \\ -r, --reverse Reverse the sort order 28 31 \\ -t, --time Sort the entries by modification time, most recent first 32 + \\ --tree[=DEPTH] Display entries in a tree format (optional limit depth) 29 33 \\ 30 34 ; 31 35 ··· 42 46 long: bool = false, 43 47 sort_by_mod_time: bool = false, 44 48 reverse_sort: bool = false, 49 + tree: bool = false, 50 + tree_depth: ?usize = null, 45 51 46 - directory: [:0]const u8 = ".", 52 + directories: std.ArrayListUnmanaged([:0]const u8) = .empty, 47 53 file: ?[]const u8 = null, 48 54 49 55 winsize: ?posix.winsize = null, ··· 146 152 147 153 var cmd: Command = .{ .arena = allocator }; 148 154 149 - cmd.opts.winsize = getWinsize(std.io.getStdOut().handle); 155 + cmd.opts.winsize = getWinsize(std.fs.File.stdout().handle); 150 156 151 157 cmd.opts.shortview = if (cmd.opts.isatty()) .columns else .oneline; 152 158 153 - const stdout = std.io.getStdOut().writer(); 154 - const stderr = std.io.getStdErr().writer(); 155 - var bw = std.io.bufferedWriter(stdout); 159 + var stdout_buf: [4096]u8 = undefined; 160 + var stderr_buf: [4096]u8 = undefined; 161 + var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); 162 + var stderr_writer = std.fs.File.stderr().writer(&stderr_buf); 163 + var stdout = &stdout_writer.interface; 164 + var stderr = &stderr_writer.interface; 156 165 157 166 var args = std.process.args(); 158 167 // skip binary ··· 172 181 'r' => cmd.opts.reverse_sort = true, 173 182 't' => cmd.opts.sort_by_mod_time = true, 174 183 else => { 175 - try stderr.print("Invalid opt: '{c}'", .{b}); 184 + try stderr.print("Invalid opt: '{c}'\n", .{b}); 176 185 std.process.exit(1); 177 186 }, 178 187 } ··· 184 193 const val = split.rest(); 185 194 if (eql(opt, "all")) { 186 195 cmd.opts.all = parseArgBool(val) orelse { 187 - try stderr.print("Invalid boolean: '{s}'", .{val}); 196 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 188 197 std.process.exit(1); 189 198 }; 190 199 } else if (eql(opt, "long")) { 191 200 cmd.opts.long = parseArgBool(val) orelse { 192 - try stderr.print("Invalid boolean: '{s}'", .{val}); 201 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 193 202 std.process.exit(1); 194 203 }; 195 204 } else if (eql(opt, "almost-all")) { 196 205 cmd.opts.@"almost-all" = parseArgBool(val) orelse { 197 - try stderr.print("Invalid boolean: '{s}'", .{val}); 206 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 198 207 std.process.exit(1); 199 208 }; 200 209 } else if (eql(opt, "group-directories-first")) { 201 210 cmd.opts.@"group-directories-first" = parseArgBool(val) orelse { 202 - try stderr.print("Invalid boolean: '{s}'", .{val}); 211 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 203 212 std.process.exit(1); 204 213 }; 205 214 } else if (eql(opt, "color")) { 206 215 cmd.opts.color = std.meta.stringToEnum(Options.When, val) orelse { 207 - try stderr.print("Invalid color option: '{s}'", .{val}); 216 + try stderr.print("Invalid color option: '{s}'\n", .{val}); 208 217 std.process.exit(1); 209 218 }; 210 219 } else if (eql(opt, "human-readable")) { 211 220 // no-op: present for compatibility 212 221 } else if (eql(opt, "hyperlinks")) { 213 222 cmd.opts.hyperlinks = std.meta.stringToEnum(Options.When, val) orelse { 214 - try stderr.print("Invalid hyperlinks option: '{s}'", .{val}); 223 + try stderr.print("Invalid hyperlinks option: '{s}'\n", .{val}); 215 224 std.process.exit(1); 216 225 }; 217 226 } else if (eql(opt, "icons")) { 218 227 cmd.opts.icons = std.meta.stringToEnum(Options.When, val) orelse { 219 - try stderr.print("Invalid color option: '{s}'", .{val}); 228 + try stderr.print("Invalid color option: '{s}'\n", .{val}); 220 229 std.process.exit(1); 221 230 }; 222 231 } else if (eql(opt, "columns")) { 223 232 const c = parseArgBool(val) orelse { 224 - try stderr.print("Invalid columns option: '{s}'", .{val}); 233 + try stderr.print("Invalid columns option: '{s}'\n", .{val}); 225 234 std.process.exit(1); 226 235 }; 227 236 cmd.opts.shortview = if (c) .columns else .oneline; 228 237 } else if (eql(opt, "oneline")) { 229 238 const o = parseArgBool(val) orelse { 230 - try stderr.print("Invalid oneline option: '{s}'", .{val}); 239 + try stderr.print("Invalid oneline option: '{s}'\n", .{val}); 231 240 std.process.exit(1); 232 241 }; 233 242 cmd.opts.shortview = if (o) .oneline else .columns; 234 243 } else if (eql(opt, "time")) { 235 244 cmd.opts.sort_by_mod_time = parseArgBool(val) orelse { 236 - try stderr.print("Invalid boolean: '{s}'", .{val}); 245 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 237 246 std.process.exit(1); 238 247 }; 239 248 } else if (eql(opt, "reverse")) { 240 249 cmd.opts.reverse_sort = parseArgBool(val) orelse { 241 - try stderr.print("Invalid boolean: '{s}'", .{val}); 250 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 242 251 std.process.exit(1); 243 252 }; 253 + } else if (eql(opt, "tree")) { 254 + if (val.len == 0) { 255 + cmd.opts.tree = true; 256 + cmd.opts.tree_depth = null; // unlimited depth 257 + } else { 258 + cmd.opts.tree = true; 259 + cmd.opts.tree_depth = std.fmt.parseInt(usize, val, 10) catch { 260 + try stderr.print("Invalid tree depth: '{s}'\n", .{val}); 261 + std.process.exit(1); 262 + }; 263 + } 244 264 } else if (eql(opt, "help")) { 245 265 return stderr.writeAll(usage); 246 266 } else if (eql(opt, "version")) { 247 - try bw.writer().print("lsr {s}\r\n", .{build_options.version}); 248 - try bw.flush(); 267 + try stdout.print("lsr {s}\r\n", .{build_options.version}); 268 + try stdout.flush(); 249 269 return; 250 270 } else { 251 - try stderr.print("Invalid opt: '{s}'", .{opt}); 271 + try stderr.print("Invalid opt: '{s}'\n", .{opt}); 252 272 std.process.exit(1); 253 273 } 254 274 }, 255 275 .positional => { 256 - cmd.opts.directory = arg; 276 + try cmd.opts.directories.append(allocator, arg); 257 277 }, 258 278 } 259 279 } ··· 262 282 cmd.opts.colors = .default; 263 283 } 264 284 265 - var ring: ourio.Ring = try .init(allocator, queue_size); 266 - defer ring.deinit(); 285 + if (cmd.opts.directories.items.len == 0) { 286 + try cmd.opts.directories.append(allocator, "."); 287 + } 267 288 268 - _ = try ring.open(cmd.opts.directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{ 269 - .ptr = &cmd, 270 - .cb = onCompletion, 271 - .msg = @intFromEnum(Msg.cwd), 272 - }); 289 + const multiple_dirs = cmd.opts.directories.items.len > 1; 290 + 291 + for (cmd.opts.directories.items, 0..) |directory, dir_idx| { 292 + cmd.entries = &.{}; 293 + cmd.entry_idx = 0; 294 + cmd.symlinks.clearRetainingCapacity(); 295 + cmd.groups.clearRetainingCapacity(); 296 + cmd.users.clearRetainingCapacity(); 297 + cmd.tz = null; 298 + cmd.opts.file = null; 299 + cmd.current_directory = directory; 273 300 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, .{ 301 + var ring: ourio.Ring = try .init(allocator, queue_size); 302 + defer ring.deinit(); 303 + 304 + _ = try ring.open(directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{ 286 305 .ptr = &cmd, 287 306 .cb = onCompletion, 288 - .msg = @intFromEnum(Msg.group), 307 + .msg = @intFromEnum(Msg.cwd), 289 308 }); 290 - } 291 309 292 - try ring.run(.until_done); 310 + if (cmd.opts.long) { 311 + _ = try ring.open("/etc/localtime", .{ .CLOEXEC = true }, 0, .{ 312 + .ptr = &cmd, 313 + .cb = onCompletion, 314 + .msg = @intFromEnum(Msg.localtime), 315 + }); 316 + _ = try ring.open("/etc/passwd", .{ .CLOEXEC = true }, 0, .{ 317 + .ptr = &cmd, 318 + .cb = onCompletion, 319 + .msg = @intFromEnum(Msg.passwd), 320 + }); 321 + _ = try ring.open("/etc/group", .{ .CLOEXEC = true }, 0, .{ 322 + .ptr = &cmd, 323 + .cb = onCompletion, 324 + .msg = @intFromEnum(Msg.group), 325 + }); 326 + } 293 327 294 - if (cmd.entries.len == 0) return; 328 + try ring.run(.until_done); 295 329 296 - std.sort.pdq(Entry, cmd.entries, cmd.opts, Entry.lessThan); 330 + if (cmd.entries.len == 0) { 331 + if (multiple_dirs and dir_idx < cmd.opts.directories.items.len - 1) { 332 + try stdout.writeAll("\r\n"); 333 + } 334 + continue; 335 + } 336 + 337 + std.sort.pdq(Entry, cmd.entries, cmd.opts, Entry.lessThan); 297 338 298 - if (cmd.opts.reverse_sort) { 299 - std.mem.reverse(Entry, cmd.entries); 300 - } 339 + if (cmd.opts.reverse_sort) { 340 + std.mem.reverse(Entry, cmd.entries); 341 + } 301 342 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()), 343 + if (multiple_dirs and !cmd.opts.tree) { 344 + if (dir_idx > 0) try stdout.writeAll("\r\n"); 345 + try stdout.print("{s}:\r\n", .{directory}); 346 + } 347 + 348 + if (cmd.opts.tree) { 349 + if (multiple_dirs and dir_idx > 0) try stdout.writeAll("\r\n"); 350 + try printTree(cmd, stdout); 351 + } else if (cmd.opts.long) { 352 + try printLong(&cmd, stdout); 353 + } else switch (cmd.opts.shortview) { 354 + .columns => try printShortColumns(cmd, stdout), 355 + .oneline => try printShortOnePerLine(cmd, stdout), 356 + } 307 357 } 308 - try bw.flush(); 358 + try stdout.flush(); 309 359 } 310 360 311 361 fn printShortColumns(cmd: Command, writer: anytype) !void { ··· 382 432 383 433 if (i < columns.items.len - 1) { 384 434 const spaces = column.width - (icon_width + entry.name.len); 385 - try writer.writeByteNTimes(' ', spaces); 435 + var space_buf = [_][]const u8{" "}; 436 + try writer.writeSplatAll(&space_buf, spaces); 386 437 } 387 438 } 388 439 try writer.writeAll("\r\n"); ··· 397 448 const opts = cmd.opts; 398 449 const colors = opts.colors; 399 450 if (opts.useIcons()) { 400 - const icon = Icon.get(entry, opts); 451 + const icon = Icon.get(entry); 401 452 402 453 if (opts.useColor()) { 403 454 try writer.writeAll(icon.color); ··· 420 471 } 421 472 422 473 if (opts.useHyperlinks()) { 423 - const path = try std.fs.path.join(cmd.arena, &.{ opts.directory, entry.name }); 474 + const path = try std.fs.path.join(cmd.arena, &.{ cmd.current_directory, entry.name }); 424 475 try writer.print("\x1b]8;;file://{s}\x1b\\", .{path}); 425 476 try writer.writeAll(entry.name); 426 477 try writer.writeAll("\x1b]8;;\x1b\\"); ··· 446 497 } 447 498 } 448 499 449 - fn printLong(cmd: Command, writer: anytype) !void { 500 + fn 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 + 516 + fn printTree(cmd: Command, writer: anytype) !void { 517 + const dir_name = if (std.mem.eql(u8, cmd.current_directory, ".")) blk: { 518 + var buf: [std.fs.max_path_bytes]u8 = undefined; 519 + const cwd = try std.process.getCwd(&buf); 520 + break :blk std.fs.path.basename(cwd); 521 + } else std.fs.path.basename(cmd.current_directory); 522 + 523 + try writer.print("{s}\n", .{dir_name}); 524 + 525 + const max_depth = cmd.opts.tree_depth orelse std.math.maxInt(usize); 526 + var prefix_list: std.ArrayList(bool) = .{}; 527 + 528 + for (cmd.entries, 0..) |entry, i| { 529 + const is_last = i == cmd.entries.len - 1; 530 + 531 + try drawTreePrefix(writer, prefix_list.items, is_last); 532 + try printShortEntry(entry, cmd, writer); 533 + try writer.writeAll("\r\n"); 534 + 535 + if (entry.kind == .directory and max_depth > 0) { 536 + const full_path = try std.fs.path.joinZ(cmd.arena, &.{ cmd.current_directory, entry.name }); 537 + 538 + try prefix_list.append(cmd.arena, is_last); 539 + try recurseTree(cmd, writer, full_path, &prefix_list, 1, max_depth); 540 + 541 + _ = prefix_list.pop(); 542 + } 543 + } 544 + } 545 + 546 + fn recurseTree(cmd: Command, writer: anytype, dir_path: [:0]const u8, prefix_list: *std.ArrayList(bool), depth: usize, max_depth: usize) !void { 547 + var dir = std.fs.cwd().openDir(dir_path, .{ .iterate = true }) catch { 548 + return; 549 + }; 550 + defer dir.close(); 551 + 552 + var entries: std.ArrayList(Entry) = .{}; 553 + var iter = dir.iterate(); 554 + 555 + while (try iter.next()) |dirent| { 556 + if (!cmd.opts.showDotfiles() and std.mem.startsWith(u8, dirent.name, ".")) continue; 557 + 558 + const nameZ = try cmd.arena.dupeZ(u8, dirent.name); 559 + try entries.append(cmd.arena, .{ 560 + .name = nameZ, 561 + .kind = dirent.kind, 562 + .statx = undefined, 563 + }); 564 + } 565 + 566 + std.sort.pdq(Entry, entries.items, cmd.opts, Entry.lessThan); 567 + 568 + if (cmd.opts.reverse_sort) { 569 + std.mem.reverse(Entry, entries.items); 570 + } 571 + 572 + for (entries.items, 0..) |entry, i| { 573 + const is_last = i == entries.items.len - 1; 574 + 575 + try drawTreePrefix(writer, prefix_list.items, is_last); 576 + try printTreeEntry(entry, cmd, writer, dir_path); 577 + try writer.writeAll("\r\n"); 578 + 579 + if (entry.kind == .directory and depth < max_depth) { 580 + const full_path = try std.fs.path.joinZ(cmd.arena, &.{ dir_path, entry.name }); 581 + 582 + try prefix_list.append(cmd.arena, is_last); 583 + try recurseTree(cmd, writer, full_path, prefix_list, depth + 1, max_depth); 584 + 585 + _ = prefix_list.pop(); 586 + } 587 + } 588 + } 589 + 590 + fn printTreeEntry(entry: Entry, cmd: Command, writer: anytype, dir_path: [:0]const u8) !void { 591 + const opts = cmd.opts; 592 + const colors = opts.colors; 593 + 594 + if (opts.useIcons()) { 595 + const icon = Icon.get(entry); 596 + 597 + if (opts.useColor()) { 598 + try writer.writeAll(icon.color); 599 + try writer.writeAll(icon.icon); 600 + try writer.writeAll(colors.reset); 601 + } else { 602 + try writer.writeAll(icon.icon); 603 + } 604 + 605 + try writer.writeByte(' '); 606 + } 607 + 608 + switch (entry.kind) { 609 + .directory => try writer.writeAll(colors.dir), 610 + .sym_link => try writer.writeAll(colors.symlink), 611 + else => { 612 + const full_path = try std.fs.path.join(cmd.arena, &.{ dir_path, entry.name }); 613 + const stat_result = std.fs.cwd().statFile(full_path) catch null; 614 + if (stat_result) |stat| { 615 + if (stat.mode & (std.posix.S.IXUSR | std.posix.S.IXGRP | std.posix.S.IXOTH) != 0) { 616 + try writer.writeAll(colors.executable); 617 + } 618 + } 619 + }, 620 + } 621 + 622 + if (opts.useHyperlinks()) { 623 + const path = try std.fs.path.join(cmd.arena, &.{ dir_path, entry.name }); 624 + try writer.print("\x1b]8;;file://{s}\x1b\\", .{path}); 625 + try writer.writeAll(entry.name); 626 + try writer.writeAll("\x1b]8;;\x1b\\"); 627 + try writer.writeAll(colors.reset); 628 + } else { 629 + try writer.writeAll(entry.name); 630 + try writer.writeAll(colors.reset); 631 + } 632 + } 633 + 634 + fn printLong(cmd: *Command, writer: anytype) !void { 450 635 const tz = cmd.tz.?; 451 636 const now = zeit.instant(.{}) catch unreachable; 452 637 const one_year_ago = try now.subtract(.{ .days = 365 }); ··· 458 643 var n_size: usize = 0; 459 644 var n_suff: usize = 0; 460 645 for (cmd.entries) |entry| { 461 - const group = cmd.getGroup(entry.statx.gid); 462 - const user = cmd.getUser(entry.statx.uid); 646 + const group = try cmd.getGroup(entry.statx.gid); 647 + const user = try cmd.getUser(entry.statx.uid); 463 648 464 649 var buf: [16]u8 = undefined; 465 650 const size = try entry.humanReadableSize(&buf); ··· 490 675 }; 491 676 492 677 for (cmd.entries) |entry| { 493 - const user: User = cmd.getUser(entry.statx.uid) orelse 678 + const user: User = try cmd.getUser(entry.statx.uid) orelse 494 679 .{ 495 680 .uid = entry.statx.uid, 496 681 .name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.uid}), 497 682 }; 498 - const group: Group = cmd.getGroup(entry.statx.gid) orelse 683 + const group: Group = try cmd.getGroup(entry.statx.gid) orelse 499 684 .{ 500 685 .gid = entry.statx.gid, 501 686 .name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.gid}), ··· 509 694 try writer.writeAll(&mode); 510 695 try writer.writeByte(' '); 511 696 try writer.writeAll(user.name); 512 - try writer.writeByteNTimes(' ', longest_user - user.name.len); 697 + var space_buf1 = [_][]const u8{" "}; 698 + try writer.writeSplatAll(&space_buf1, longest_user - user.name.len); 513 699 try writer.writeByte(' '); 514 700 try writer.writeAll(group.name); 515 - try writer.writeByteNTimes(' ', longest_group - group.name.len); 701 + var space_buf2 = [_][]const u8{" "}; 702 + try writer.writeSplatAll(&space_buf2, longest_group - group.name.len); 516 703 try writer.writeByte(' '); 517 704 518 705 var size_buf: [16]u8 = undefined; 519 706 const size = try entry.humanReadableSize(&size_buf); 520 707 const suffix = entry.humanReadableSuffix(); 521 708 522 - try writer.writeByteNTimes(' ', longest_size - size.len); 709 + var space_buf3 = [_][]const u8{" "}; 710 + try writer.writeSplatAll(&space_buf3, longest_size - size.len); 523 711 try writer.writeAll(size); 524 712 try writer.writeByte(' '); 525 713 try writer.writeAll(suffix); 526 - try writer.writeByteNTimes(' ', longest_suffix - suffix.len); 714 + var space_buf4 = [_][]const u8{" "}; 715 + try writer.writeSplatAll(&space_buf4, longest_suffix - suffix.len); 527 716 try writer.writeByte(' '); 528 717 529 718 try writer.print("{d: >2} {s} ", .{ ··· 538 727 } 539 728 540 729 if (cmd.opts.useIcons()) { 541 - const icon = Icon.get(entry, cmd.opts); 730 + const icon = Icon.get(entry); 542 731 543 732 if (cmd.opts.useColor()) { 544 733 try writer.writeAll(icon.color); ··· 562 751 } 563 752 564 753 if (cmd.opts.useHyperlinks()) { 565 - const path = try std.fs.path.join(cmd.arena, &.{ cmd.opts.directory, entry.name }); 754 + const path = try std.fs.path.join(cmd.arena, &.{ cmd.current_directory, entry.name }); 566 755 try writer.print("\x1b]8;;file://{s}\x1b\\", .{path}); 567 756 try writer.writeAll(entry.name); 568 757 try writer.writeAll("\x1b]8;;\x1b\\"); ··· 606 795 entries: []Entry = &.{}, 607 796 entry_idx: usize = 0, 608 797 symlinks: std.StringHashMapUnmanaged(Symlink) = .empty, 798 + current_directory: [:0]const u8 = ".", 609 799 610 800 tz: ?zeit.TimeZone = null, 611 801 groups: std.ArrayListUnmanaged(Group) = .empty, 612 802 users: std.ArrayListUnmanaged(User) = .empty, 613 803 614 - fn getUser(self: Command, uid: posix.uid_t) ?User { 804 + fn getUser(self: *Command, uid: posix.uid_t) !?User { 615 805 for (self.users.items) |user| { 616 806 if (user.uid == uid) return user; 617 807 } 808 + if (std.c.getpwuid(uid)) |user| { 809 + if (user.name) |name| { 810 + const new_user = User{ 811 + .uid = uid, 812 + .name = std.mem.span(name), 813 + }; 814 + try self.users.append(self.arena, new_user); 815 + return new_user; 816 + } 817 + } 618 818 return null; 619 819 } 620 820 621 - fn getGroup(self: Command, gid: posix.gid_t) ?Group { 821 + fn getGroup(self: *Command, gid: posix.gid_t) !?Group { 622 822 for (self.groups.items) |group| { 623 823 if (group.gid == gid) return group; 624 824 } 825 + if (grp.getgrgid(gid)) |group| { 826 + const new_group = Group{ 827 + .gid = gid, 828 + .name = std.mem.span(group.*.gr_name), 829 + }; 830 + try self.groups.append(self.arena, new_group); 831 + return new_group; 832 + } 625 833 return null; 626 834 } 627 835 }; ··· 683 891 statx: ourio.Statx, 684 892 685 893 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; 894 + if (opts.@"group-directories-first") { 895 + const lhs_is_dir = posix.S.ISDIR(lhs.statx.mode); 896 + const rhs_is_dir = posix.S.ISDIR(rhs.statx.mode); 897 + 898 + if (lhs_is_dir != rhs_is_dir) return lhs_is_dir; 691 899 } 692 900 693 901 if (opts.sort_by_mod_time) { ··· 786 994 787 995 // if the user specified a file (or something that couldn't be opened as a 788 996 // 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); 997 + const dirname = std.fs.path.dirname(cmd.current_directory) orelse "."; 998 + cmd.opts.file = std.fs.path.basename(cmd.current_directory); 999 + cmd.current_directory = try cmd.arena.dupeZ(u8, dirname); 792 1000 _ = try io.open( 793 - cmd.opts.directory, 1001 + cmd.current_directory, 794 1002 .{ .DIRECTORY = true, .CLOEXEC = true }, 795 1003 0, 796 1004 .{ ··· 811 1019 if (cmd.opts.useHyperlinks()) { 812 1020 var buf: [std.fs.max_path_bytes]u8 = undefined; 813 1021 const cwd = try std.os.getFdPath(fd, &buf); 814 - cmd.opts.directory = try cmd.arena.dupeZ(u8, cwd); 1022 + cmd.current_directory = try cmd.arena.dupeZ(u8, cwd); 815 1023 } 816 1024 817 1025 var temp_results: std.ArrayListUnmanaged(MinimalEntry) = .empty; ··· 873 1081 } 874 1082 const path = try std.fs.path.joinZ( 875 1083 cmd.arena, 876 - &.{ cmd.opts.directory, entry.name }, 1084 + &.{ cmd.current_directory, entry.name }, 877 1085 ); 878 1086 879 1087 if (entry.kind == .sym_link) { ··· 909 1117 const n = try result.read; 910 1118 _ = try io.close(task.req.read.fd, .{}); 911 1119 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()); 1120 + var reader = std.Io.Reader.fixed(bytes); 1121 + const tz = try zeit.timezone.TZInfo.parse(cmd.arena, &reader); 914 1122 cmd.tz = .{ .tzinfo = tz }; 915 1123 }, 916 1124 ··· 1040 1248 cmd.entry_idx += 1; 1041 1249 const path = try std.fs.path.joinZ( 1042 1250 cmd.arena, 1043 - &.{ cmd.opts.directory, entry.name }, 1251 + &.{ cmd.current_directory, entry.name }, 1044 1252 ); 1045 1253 1046 1254 if (entry.kind == .sym_link) { ··· 1128 1336 .{ "zon", Icon.zig }, 1129 1337 }); 1130 1338 1131 - fn get(entry: Entry, opts: Options) Icon { 1339 + fn get(entry: Entry) Icon { 1132 1340 // 1. By name 1133 - // 2. By extension 1134 - // 3. By type 1341 + // 2. By type 1342 + // 3. By extension 1135 1343 if (by_name.get(entry.name)) |icon| return icon; 1136 1344 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 1345 switch (entry.kind) { 1144 1346 .block_device => return drive, 1145 1347 .character_device => return drive, 1146 1348 .directory => return directory, 1147 1349 .file => { 1350 + const ext = std.fs.path.extension(entry.name); 1351 + if (ext.len > 0) { 1352 + const ft = ext[1..]; 1353 + if (by_extension.get(ft)) |icon| return icon; 1354 + } 1355 + 1148 1356 if (entry.isExecutable()) { 1149 1357 return executable; 1150 1358 } ··· 1152 1360 }, 1153 1361 .named_pipe => return pipe, 1154 1362 .sym_link => { 1155 - if (opts.long and posix.S.ISDIR(entry.statx.mode)) { 1363 + if (posix.S.ISDIR(entry.statx.mode)) { 1156 1364 return symlink_dir; 1157 1365 } 1158 1366 return symlink;