this repo has no description

Compare changes

Choose any two refs to compare.

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