this repo has no description

Compare changes

Choose any two refs to compare.

+29 -23
README.md
··· 19 19 ## Usage 20 20 21 21 ``` 22 - lsr [options] [directory] 22 + lsr [options] [path] 23 23 24 24 --help Print this message and exit 25 25 --version Print the version string ··· 30 30 -A, --almost-all Like --all, but skips implicit "." and ".." directories 31 31 -C, --columns Print the output in columns 32 32 --color=WHEN When to use colors (always, auto, never) 33 - --group-directories-first When to use colors (always, auto, never) 33 + --group-directories-first Print all directories before printing regular files 34 + --hyperlinks=WHEN When to use OSC 8 hyperlinks (always, auto, never) 34 35 --icons=WHEN When to display icons (always, auto, never) 35 36 -l, --long Display extended file metadata 37 + -r, --reverse Reverse the sort order 38 + -t, --time Sort the entries by modification time, most recent first 36 39 37 40 ``` 38 41 ··· 43 46 (because io_uring). `lsr` does work on macOS/BSD as well, but will not see the 44 47 syscall batching benefits that are available with io_uring. 45 48 46 - | Program | Version | 47 - |:-------:|:-------:| 48 - | lsr | 0.1.0 | 49 - | ls | 9.7 | 50 - | eza | 0.21.3 | 51 - | lsd | 1.1.5 | 52 - | uutils | 0.0.30 | 49 + | Program | Version | 50 + |:--------:|:-------:| 51 + | lsr | 0.1.0 | 52 + | ls | 9.7 | 53 + | eza | 0.21.3 | 54 + | lsd | 1.1.5 | 55 + | uutils | 0.0.30 | 56 + | busybox | 1.36.1 | 53 57 54 58 ### Time 55 59 56 60 Data gathered with `hyperfine` on a directory of `n` plain files. 57 61 58 - | Program | n=10 | n=100 | n=1,000 | n=10,000 | 59 - |:-------------:|:--------:|:--------:|:-------:|:--------:| 60 - | lsr -al | 372.6 ยตs | 634.3 ยตs | 2.7 ms | 22.1 ms | 61 - | ls -al | 1.4 ms | 1.7 ms | 4.7 ms | 38.0 ms | 62 - | eza -al | 2.9 ms | 3.3 ms | 6.6 ms | 40.2 ms | 63 - | lsd -al | 2.1 ms | 3.5 ms | 17.0 ms | 153.4 ms | 64 - | uutils ls -al | 2.9 ms | 3.6 ms | 11.3 ms | 89.6 ms | 62 + | Program | n=10 | n=100 | n=1,000 | n=10,000 | 63 + |:--------------:|:--------:|:--------:|:-------:|:--------:| 64 + | lsr -al | 372.6 ยตs | 634.3 ยตs | 2.7 ms | 22.1 ms | 65 + | busybox ls -al | 403.8 ยตs | 1.1 ms | 3.5 ms | 32.5 ms | 66 + | ls -al | 1.4 ms | 1.7 ms | 4.7 ms | 38.0 ms | 67 + | eza -al | 2.9 ms | 3.3 ms | 6.6 ms | 40.2 ms | 68 + | lsd -al | 2.1 ms | 3.5 ms | 17.0 ms | 153.4 ms | 69 + | uutils ls -al | 2.9 ms | 3.6 ms | 11.3 ms | 89.6 ms | 65 70 66 71 ### Syscalls 67 72 68 73 Data gathered with `strace -c` on a directory of `n` plain files. (Lower is better) 69 74 70 - | Program | n=10 | n=100 | n=1,000 | n=10,000 | 71 - |:-------------:|:----:|:-----:|:-------:|:--------:| 72 - | lsr -al | 20 | 28 | 105 | 848 | 73 - | ls -al | 405 | 675 | 3,377 | 30,396 | 74 - | eza -al | 319 | 411 | 1,320 | 10,364 | 75 - | lsd -al | 508 | 1,408 | 10,423 | 100,512 | 76 - | uutils ls -al | 445 | 986 | 6,397 | 10,005 | 75 + | Program | n=10 | n=100 | n=1,000 | n=10,000 | 76 + |:--------------:|:----:|:-----:|:-------:|:--------:| 77 + | lsr -al | 20 | 28 | 105 | 848 | 78 + | busybox ls -al | 84 | 410 | 2,128 | 20,383 | 79 + | ls -al | 405 | 675 | 3,377 | 30,396 | 80 + | eza -al | 319 | 411 | 1,320 | 10,364 | 81 + | lsd -al | 508 | 1,408 | 10,423 | 100,512 | 82 + | uutils ls -al | 445 | 986 | 6,397 | 10,005 |
+4 -1
build.zig
··· 2 2 const zzdoc = @import("zzdoc"); 3 3 4 4 /// Must be kept in sync with git tags 5 - const version: std.SemanticVersion = .{ .major = 0, .minor = 2, .patch = 0 }; 5 + const version: std.SemanticVersion = .{ .major = 1, .minor = 0, .patch = 0 }; 6 6 7 7 pub fn build(b: *std.Build) void { 8 8 const target = b.standardTargetOptions(.{}); ··· 45 45 .name = "lsr", 46 46 .root_module = exe_mod, 47 47 }); 48 + exe.linkLibC(); 48 49 49 50 b.installArtifact(exe); 50 51 ··· 82 83 "-C", 83 84 b.build_root.path orelse ".", 84 85 "describe", 86 + "--match", 87 + "*.*.*", 85 88 "--tags", 86 89 "--abbrev=9", 87 90 }, &code, .Ignore) catch {
+8 -8
build.zig.zon
··· 1 1 .{ 2 2 .name = .lsr, 3 - .version = "0.2.0", 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#54c1a1ed8d0994636770e5185ecdb59fe6d8535e", 11 - .hash = "ourio-0.0.0-_s-z0asOAgAhpi7gSpLLvWGj_4XURez4W9TWN6SGs5BP", 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 = .{
+11 -2
docs/lsr.1.scd
··· 6 6 7 7 # SYNOPSIS 8 8 9 - *lsr* [options...] [directory] 9 + *lsr* [options...] [path] 10 10 11 11 # DESCRIPTION 12 12 ··· 31 31 When to use colors (always, auto, never) 32 32 33 33 *--group-directories-first* 34 - When to use colors (always, auto, never) 34 + Print all directories before printing regular files 35 35 36 36 *--help* 37 37 Print the help menu and exit 38 38 39 + *--hyperlinks=WHEN* 40 + When to use OSC 8 hyperlinks (always, auto, never) 41 + 39 42 *--icons=WHEN* 40 43 When to display icons (always, auto, never) 41 44 42 45 *-l*, *--long* 43 46 Display extended file metadata 47 + 48 + *-r*, *--reverse* 49 + Reverse the sort order 50 + 51 + *-t*, *--time* 52 + Sort the entries by modification time, most recent first 44 53 45 54 *--version* 46 55 Print the version and exit
+1 -1
flake.nix
··· 16 16 17 17 packages.default = pkgs.stdenv.mkDerivation { 18 18 pname = "lsr"; 19 - version = "0.1.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.1.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-hAq1/uE9eu/82+e079y+v9EnN0ViXX7k3GwkgQkxOyo="; 20 + outputHash = "sha256-bfc2dlQa1VGq9S6OBeQawAJuvfxU4kgFtQ13fuKhdZc="; 21 21 outputHashMode = "recursive"; 22 22 outputHashAlgo = "sha256"; 23 23 }
+490 -111
src/main.zig
··· 2 2 const builtin = @import("builtin"); 3 3 const ourio = @import("ourio"); 4 4 const zeit = @import("zeit"); 5 + const natord = @import("natord.zig"); 5 6 const build_options = @import("build_options"); 7 + const grp = @cImport({ 8 + @cInclude("grp.h"); 9 + }); 6 10 7 11 const posix = std.posix; 8 12 9 13 const usage = 10 - \\Usage: 11 - \\ lsr [options] [directory] 14 + \\Usage: 15 + \\ lsr [options] [path...] 12 16 \\ 13 17 \\ --help Print this message and exit 14 18 \\ --version Print the version string ··· 19 23 \\ -A, --almost-all Like --all, but skips implicit "." and ".." directories 20 24 \\ -C, --columns Print the output in columns 21 25 \\ --color=WHEN When to use colors (always, auto, never) 22 - \\ --group-directories-first When to use colors (always, auto, never) 26 + \\ --group-directories-first Print all directories before printing regular files 27 + \\ --hyperlinks=WHEN When to use OSC 8 hyperlinks (always, auto, never) 23 28 \\ --icons=WHEN When to display icons (always, auto, never) 24 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) 25 33 \\ 26 34 ; 27 35 ··· 33 41 color: When = .auto, 34 42 shortview: enum { columns, oneline } = .oneline, 35 43 @"group-directories-first": bool = true, 44 + hyperlinks: When = .auto, 36 45 icons: When = .auto, 37 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, 38 51 39 - directory: [:0]const u8 = ".", 52 + directories: std.ArrayListUnmanaged([:0]const u8) = .empty, 53 + file: ?[]const u8 = null, 40 54 41 55 winsize: ?posix.winsize = null, 42 56 colors: Colors = .none, ··· 101 115 } 102 116 } 103 117 118 + fn useHyperlinks(self: Options) bool { 119 + switch (self.hyperlinks) { 120 + .never => return false, 121 + .always => return true, 122 + .auto => return self.isatty(), 123 + } 124 + } 125 + 126 + fn showDotfiles(self: Options) bool { 127 + return self.@"almost-all" or self.all; 128 + } 129 + 104 130 fn isatty(self: Options) bool { 105 131 return self.winsize != null; 106 132 } ··· 126 152 127 153 var cmd: Command = .{ .arena = allocator }; 128 154 129 - cmd.opts.winsize = getWinsize(std.io.getStdOut().handle); 155 + cmd.opts.winsize = getWinsize(std.fs.File.stdout().handle); 130 156 131 157 cmd.opts.shortview = if (cmd.opts.isatty()) .columns else .oneline; 132 158 133 - const stdout = std.io.getStdOut().writer(); 134 - const stderr = std.io.getStdErr().writer(); 135 - 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; 136 165 137 166 var args = std.process.args(); 138 167 // skip binary ··· 147 176 'A' => cmd.opts.@"almost-all" = true, 148 177 'C' => cmd.opts.shortview = .columns, 149 178 'a' => cmd.opts.all = true, 179 + 'h' => {}, // human-readable: present for compatibility 150 180 'l' => cmd.opts.long = true, 181 + 'r' => cmd.opts.reverse_sort = true, 182 + 't' => cmd.opts.sort_by_mod_time = true, 151 183 else => { 152 - try stderr.print("Invalid opt: '{c}'", .{b}); 184 + try stderr.print("Invalid opt: '{c}'\n", .{b}); 153 185 std.process.exit(1); 154 186 }, 155 187 } ··· 161 193 const val = split.rest(); 162 194 if (eql(opt, "all")) { 163 195 cmd.opts.all = parseArgBool(val) orelse { 164 - try stderr.print("Invalid boolean: '{s}'", .{val}); 196 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 165 197 std.process.exit(1); 166 198 }; 167 199 } else if (eql(opt, "long")) { 168 200 cmd.opts.long = parseArgBool(val) orelse { 169 - try stderr.print("Invalid boolean: '{s}'", .{val}); 201 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 170 202 std.process.exit(1); 171 203 }; 172 204 } else if (eql(opt, "almost-all")) { 173 205 cmd.opts.@"almost-all" = parseArgBool(val) orelse { 174 - try stderr.print("Invalid boolean: '{s}'", .{val}); 206 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 175 207 std.process.exit(1); 176 208 }; 177 209 } else if (eql(opt, "group-directories-first")) { 178 210 cmd.opts.@"group-directories-first" = parseArgBool(val) orelse { 179 - try stderr.print("Invalid boolean: '{s}'", .{val}); 211 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 180 212 std.process.exit(1); 181 213 }; 182 214 } else if (eql(opt, "color")) { 183 215 cmd.opts.color = std.meta.stringToEnum(Options.When, val) orelse { 184 - try stderr.print("Invalid color option: '{s}'", .{val}); 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}); 185 224 std.process.exit(1); 186 225 }; 187 226 } else if (eql(opt, "icons")) { 188 227 cmd.opts.icons = std.meta.stringToEnum(Options.When, val) orelse { 189 - try stderr.print("Invalid color option: '{s}'", .{val}); 228 + try stderr.print("Invalid color option: '{s}'\n", .{val}); 190 229 std.process.exit(1); 191 230 }; 192 231 } else if (eql(opt, "columns")) { 193 232 const c = parseArgBool(val) orelse { 194 - try stderr.print("Invalid columns option: '{s}'", .{val}); 233 + try stderr.print("Invalid columns option: '{s}'\n", .{val}); 195 234 std.process.exit(1); 196 235 }; 197 236 cmd.opts.shortview = if (c) .columns else .oneline; 198 237 } else if (eql(opt, "oneline")) { 199 238 const o = parseArgBool(val) orelse { 200 - try stderr.print("Invalid oneline option: '{s}'", .{val}); 239 + try stderr.print("Invalid oneline option: '{s}'\n", .{val}); 201 240 std.process.exit(1); 202 241 }; 203 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 + } 204 264 } else if (eql(opt, "help")) { 205 265 return stderr.writeAll(usage); 206 266 } else if (eql(opt, "version")) { 207 - try bw.writer().print("lsr {s}\r\n", .{build_options.version}); 208 - try bw.flush(); 267 + try stdout.print("lsr {s}\r\n", .{build_options.version}); 268 + try stdout.flush(); 209 269 return; 210 270 } else { 211 - try stderr.print("Invalid opt: '{s}'", .{opt}); 271 + try stderr.print("Invalid opt: '{s}'\n", .{opt}); 212 272 std.process.exit(1); 213 273 } 214 274 }, 215 275 .positional => { 216 - cmd.opts.directory = arg; 276 + try cmd.opts.directories.append(allocator, arg); 217 277 }, 218 278 } 219 279 } ··· 222 282 cmd.opts.colors = .default; 223 283 } 224 284 225 - var ring: ourio.Ring = try .init(allocator, queue_size); 226 - defer ring.deinit(); 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; 227 300 228 - _ = try ring.open(cmd.opts.directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{ 229 - .ptr = &cmd, 230 - .cb = onCompletion, 231 - .msg = @intFromEnum(Msg.cwd), 232 - }); 301 + var ring: ourio.Ring = try .init(allocator, queue_size); 302 + defer ring.deinit(); 233 303 234 - if (cmd.opts.long) { 235 - _ = try ring.open("/etc/localtime", .{ .CLOEXEC = true }, 0, .{ 304 + _ = try ring.open(directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{ 236 305 .ptr = &cmd, 237 306 .cb = onCompletion, 238 - .msg = @intFromEnum(Msg.localtime), 307 + .msg = @intFromEnum(Msg.cwd), 239 308 }); 240 - _ = try ring.open("/etc/passwd", .{ .CLOEXEC = true }, 0, .{ 241 - .ptr = &cmd, 242 - .cb = onCompletion, 243 - .msg = @intFromEnum(Msg.passwd), 244 - }); 245 - _ = try ring.open("/etc/group", .{ .CLOEXEC = true }, 0, .{ 246 - .ptr = &cmd, 247 - .cb = onCompletion, 248 - .msg = @intFromEnum(Msg.group), 249 - }); 250 - } 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); 251 329 252 - try ring.run(.until_done); 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 + } 253 336 254 - if (cmd.entries.len == 0) return; 337 + std.sort.pdq(Entry, cmd.entries, cmd.opts, Entry.lessThan); 255 338 256 - if (cmd.opts.long) { 257 - try printLong(cmd, bw.writer()); 258 - } else switch (cmd.opts.shortview) { 259 - .columns => try printShortColumns(cmd, bw.writer()), 260 - .oneline => try printShortOnePerLine(cmd, bw.writer()), 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 + } 261 357 } 262 - try bw.flush(); 358 + try stdout.flush(); 263 359 } 264 360 265 361 fn printShortColumns(cmd: Command, writer: anytype) !void { ··· 332 428 for (columns.items, 0..) |column, i| { 333 429 if (row >= column.entries.len) continue; 334 430 const entry = column.entries[row]; 335 - try printShortEntry(column.entries[row], cmd.opts, writer); 431 + try printShortEntry(column.entries[row], cmd, writer); 336 432 337 433 if (i < columns.items.len - 1) { 338 434 const spaces = column.width - (icon_width + entry.name.len); 339 - try writer.writeByteNTimes(' ', spaces); 435 + var space_buf = [_][]const u8{" "}; 436 + try writer.writeSplatAll(&space_buf, spaces); 340 437 } 341 438 } 342 439 try writer.writeAll("\r\n"); ··· 347 444 return idx + n_short_cols >= n_cols; 348 445 } 349 446 350 - fn printShortEntry(entry: Entry, opts: Options, writer: anytype) !void { 447 + fn printShortEntry(entry: Entry, cmd: Command, writer: anytype) !void { 448 + const opts = cmd.opts; 351 449 const colors = opts.colors; 352 450 if (opts.useIcons()) { 353 - const icon = Icon.get(entry, opts); 451 + const icon = Icon.get(entry); 354 452 355 453 if (opts.useColor()) { 356 454 try writer.writeAll(icon.color); ··· 371 469 } 372 470 }, 373 471 } 374 - try writer.writeAll(entry.name); 375 - try writer.writeAll(colors.reset); 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 + } 376 483 } 377 484 378 485 fn printShortOneRow(cmd: Command, writer: anytype) !void { ··· 385 492 386 493 fn printShortOnePerLine(cmd: Command, writer: anytype) !void { 387 494 for (cmd.entries) |entry| { 388 - try printShortEntry(entry, cmd.opts, writer); 495 + try printShortEntry(entry, cmd, writer); 496 + try writer.writeAll("\r\n"); 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); 389 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 + } 390 587 } 391 588 } 392 589 393 - fn printLong(cmd: Command, writer: anytype) !void { 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 { 394 635 const tz = cmd.tz.?; 395 636 const now = zeit.instant(.{}) catch unreachable; 396 637 const one_year_ago = try now.subtract(.{ .days = 365 }); ··· 402 643 var n_size: usize = 0; 403 644 var n_suff: usize = 0; 404 645 for (cmd.entries) |entry| { 405 - const group = cmd.getGroup(entry.statx.gid); 406 - 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); 407 648 408 649 var buf: [16]u8 = undefined; 409 650 const size = try entry.humanReadableSize(&buf); ··· 434 675 }; 435 676 436 677 for (cmd.entries) |entry| { 437 - const user: User = cmd.getUser(entry.statx.uid) orelse 678 + const user: User = try cmd.getUser(entry.statx.uid) orelse 438 679 .{ 439 680 .uid = entry.statx.uid, 440 681 .name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.uid}), 441 682 }; 442 - const group: Group = cmd.getGroup(entry.statx.gid) orelse 683 + const group: Group = try cmd.getGroup(entry.statx.gid) orelse 443 684 .{ 444 685 .gid = entry.statx.gid, 445 686 .name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.gid}), ··· 453 694 try writer.writeAll(&mode); 454 695 try writer.writeByte(' '); 455 696 try writer.writeAll(user.name); 456 - 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); 457 699 try writer.writeByte(' '); 458 700 try writer.writeAll(group.name); 459 - 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); 460 703 try writer.writeByte(' '); 461 704 462 705 var size_buf: [16]u8 = undefined; 463 706 const size = try entry.humanReadableSize(&size_buf); 464 707 const suffix = entry.humanReadableSuffix(); 465 708 466 - try writer.writeByteNTimes(' ', longest_size - size.len); 709 + var space_buf3 = [_][]const u8{" "}; 710 + try writer.writeSplatAll(&space_buf3, longest_size - size.len); 467 711 try writer.writeAll(size); 468 712 try writer.writeByte(' '); 469 713 try writer.writeAll(suffix); 470 - try writer.writeByteNTimes(' ', longest_suffix - suffix.len); 714 + var space_buf4 = [_][]const u8{" "}; 715 + try writer.writeSplatAll(&space_buf4, longest_suffix - suffix.len); 471 716 try writer.writeByte(' '); 472 717 473 718 try writer.print("{d: >2} {s} ", .{ ··· 482 727 } 483 728 484 729 if (cmd.opts.useIcons()) { 485 - const icon = Icon.get(entry, cmd.opts); 730 + const icon = Icon.get(entry); 486 731 487 732 if (cmd.opts.useColor()) { 488 733 try writer.writeAll(icon.color); ··· 504 749 } 505 750 }, 506 751 } 507 - try writer.writeAll(entry.name); 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\\"); 758 + } else { 759 + try writer.writeAll(entry.name); 760 + } 508 761 try writer.writeAll(colors.reset); 509 762 510 - if (entry.kind == .sym_link) { 511 - try writer.writeAll(" -> "); 512 - const color = if (entry.symlink_missing) 513 - colors.symlink_missing 514 - else 515 - colors.symlink_target; 516 - try writer.writeAll(color); 517 - try writer.writeAll(entry.link_name); 518 - try writer.writeAll(colors.reset); 763 + switch (entry.kind) { 764 + .sym_link => { 765 + try writer.writeAll(" -> "); 766 + 767 + const symlink: Symlink = cmd.symlinks.get(entry.name) orelse .{ 768 + .name = "[missing]", 769 + .exists = false, 770 + }; 771 + 772 + const color = if (symlink.exists) colors.symlink_target else colors.symlink_missing; 773 + 774 + try writer.writeAll(color); 775 + if (cmd.opts.useHyperlinks() and symlink.exists) { 776 + try writer.print("\x1b]8;;file://{s}\x1b\\", .{symlink.name}); 777 + try writer.writeAll(symlink.name); 778 + try writer.writeAll("\x1b]8;;\x1b\\"); 779 + } else { 780 + try writer.writeAll(symlink.name); 781 + } 782 + try writer.writeAll(colors.reset); 783 + }, 784 + 785 + else => {}, 519 786 } 520 787 521 788 try writer.writeAll("\r\n"); ··· 527 794 opts: Options = .{}, 528 795 entries: []Entry = &.{}, 529 796 entry_idx: usize = 0, 797 + symlinks: std.StringHashMapUnmanaged(Symlink) = .empty, 798 + current_directory: [:0]const u8 = ".", 530 799 531 800 tz: ?zeit.TimeZone = null, 532 801 groups: std.ArrayListUnmanaged(Group) = .empty, 533 802 users: std.ArrayListUnmanaged(User) = .empty, 534 803 535 - fn getUser(self: Command, uid: posix.uid_t) ?User { 804 + fn getUser(self: *Command, uid: posix.uid_t) !?User { 536 805 for (self.users.items) |user| { 537 806 if (user.uid == uid) return user; 538 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 + } 539 818 return null; 540 819 } 541 820 542 - fn getGroup(self: Command, gid: posix.gid_t) ?Group { 821 + fn getGroup(self: *Command, gid: posix.gid_t) !?Group { 543 822 for (self.groups.items) |group| { 544 823 if (group.gid == gid) return group; 545 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 + } 546 833 return null; 547 834 } 548 835 }; ··· 560 847 }; 561 848 562 849 const User = struct { 563 - uid: posix.uid_t, 850 + uid: if (builtin.os.tag == .macos) i33 else posix.uid_t, 564 851 name: []const u8, 565 852 566 853 fn lessThan(_: void, lhs: User, rhs: User) bool { ··· 569 856 }; 570 857 571 858 const Group = struct { 572 - gid: posix.gid_t, 859 + gid: if (builtin.os.tag == .macos) i33 else posix.gid_t, 573 860 name: []const u8, 574 861 575 862 fn lessThan(_: void, lhs: Group, rhs: Group) bool { ··· 593 880 } 594 881 }; 595 882 883 + const Symlink = struct { 884 + name: [:0]const u8, 885 + exists: bool = true, 886 + }; 887 + 596 888 const Entry = struct { 597 889 name: [:0]const u8, 598 890 kind: std.fs.File.Kind, 599 891 statx: ourio.Statx, 600 - link_name: [:0]const u8 = "", 601 - symlink_missing: bool = false, 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) { 902 + if (lhs.statx.mtime.sec == rhs.statx.mtime.sec) { 903 + return lhs.statx.mtime.nsec > rhs.statx.mtime.nsec; 904 + } 905 + return lhs.statx.mtime.sec > rhs.statx.mtime.sec; 906 + } 907 + 908 + return natord.orderIgnoreCase(lhs.name, rhs.name) == .lt; 909 + } 602 910 603 911 fn modeStr(self: Entry) [10]u8 { 604 912 var mode = [_]u8{'-'} ** 10; ··· 678 986 679 987 switch (msg) { 680 988 .cwd => { 681 - const fd = try result.open; 989 + const fd = result.open catch |err| { 990 + switch (err) { 991 + error.NotDir => { 992 + // Guard against infinite recursion 993 + if (cmd.opts.file != null) return err; 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 + .{ 1005 + .ptr = cmd, 1006 + .cb = onCompletion, 1007 + .msg = @intFromEnum(Msg.cwd), 1008 + }, 1009 + ); 1010 + return; 1011 + }, 1012 + else => return err, 1013 + } 1014 + }; 682 1015 // we are async, no need to defer! 683 1016 _ = try io.close(fd, .{}); 684 1017 const dir: std.fs.Dir = .{ .fd = fd }; 1018 + 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 + } 685 1024 686 1025 var temp_results: std.ArrayListUnmanaged(MinimalEntry) = .empty; 687 1026 ··· 702 1041 703 1042 var iter = dir.iterate(); 704 1043 while (try iter.next()) |dirent| { 705 - if (!cmd.opts.@"almost-all" and std.mem.startsWith(u8, dirent.name, ".")) continue; 1044 + if (!cmd.opts.showDotfiles() and std.mem.startsWith(u8, dirent.name, ".")) continue; 1045 + if (cmd.opts.file) |file| { 1046 + if (eql(file, dirent.name)) { 1047 + const nameZ = try cmd.arena.dupeZ(u8, dirent.name); 1048 + try temp_results.append(cmd.arena, .{ 1049 + .name = nameZ, 1050 + .kind = dirent.kind, 1051 + }); 1052 + } 1053 + continue; 1054 + } 706 1055 const nameZ = try cmd.arena.dupeZ(u8, dirent.name); 707 1056 try temp_results.append(cmd.arena, .{ 708 1057 .name = nameZ, ··· 732 1081 } 733 1082 const path = try std.fs.path.joinZ( 734 1083 cmd.arena, 735 - &.{ cmd.opts.directory, entry.name }, 1084 + &.{ cmd.current_directory, entry.name }, 736 1085 ); 737 1086 738 1087 if (entry.kind == .sym_link) { ··· 740 1089 741 1090 // NOTE: Sadly, we can't do readlink via io_uring 742 1091 const link = try posix.readlink(path, &buf); 743 - entry.link_name = try cmd.arena.dupeZ(u8, link); 1092 + const symlink: Symlink = .{ .name = try cmd.arena.dupeZ(u8, link) }; 1093 + try cmd.symlinks.put(cmd.arena, entry.name, symlink); 744 1094 } 745 1095 _ = try io.stat(path, &entry.statx, .{ 746 1096 .cb = onCompletion, ··· 756 1106 // Largest TZ file on my system is Asia/Hebron at 4791 bytes. We allocate an amount 757 1107 // sufficiently more than that to make sure we do this in a single pass 758 1108 const buffer = try cmd.arena.alloc(u8, 8192); 759 - _ = try io.read(fd, buffer, .{ 1109 + _ = try io.read(fd, buffer, .file, .{ 760 1110 .cb = onCompletion, 761 1111 .ptr = cmd, 762 1112 .msg = @intFromEnum(Msg.read_localtime), ··· 767 1117 const n = try result.read; 768 1118 _ = try io.close(task.req.read.fd, .{}); 769 1119 const bytes = task.req.read.buffer[0..n]; 770 - var fbs = std.io.fixedBufferStream(bytes); 771 - 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); 772 1122 cmd.tz = .{ .tzinfo = tz }; 773 1123 }, 774 1124 ··· 778 1128 // TODO: stat this or do multiple reads. We'll never know a good bound unless we go 779 1129 // really big 780 1130 const buffer = try cmd.arena.alloc(u8, 8192 * 2); 781 - _ = try io.read(fd, buffer, .{ 1131 + _ = try io.read(fd, buffer, .file, .{ 782 1132 .cb = onCompletion, 783 1133 .ptr = cmd, 784 1134 .msg = @intFromEnum(Msg.read_passwd), ··· 801 1151 // <name>:<throwaway>:<uid><...garbage> 802 1152 while (lines.next()) |line| { 803 1153 if (line.len == 0) continue; 1154 + if (std.mem.startsWith(u8, line, "#")) continue; 1155 + 804 1156 var iter = std.mem.splitScalar(u8, line, ':'); 805 1157 const name = iter.first(); 806 1158 _ = iter.next(); ··· 808 1160 809 1161 const user: User = .{ 810 1162 .name = name, 811 - .uid = try std.fmt.parseInt(u32, uid, 10), 1163 + .uid = try std.fmt.parseInt( 1164 + if (builtin.os.tag == .macos) i33 else u32, 1165 + uid, 1166 + 10, 1167 + ), 812 1168 }; 813 1169 814 1170 cmd.users.appendAssumeCapacity(user); ··· 820 1176 const fd = try result.open; 821 1177 822 1178 const buffer = try cmd.arena.alloc(u8, 8192); 823 - _ = try io.read(fd, buffer, .{ 1179 + _ = try io.read(fd, buffer, .file, .{ 824 1180 .cb = onCompletion, 825 1181 .ptr = cmd, 826 1182 .msg = @intFromEnum(Msg.read_group), ··· 843 1199 // <name>:<throwaway>:<uid><...garbage> 844 1200 while (lines.next()) |line| { 845 1201 if (line.len == 0) continue; 1202 + if (std.mem.startsWith(u8, line, "#")) continue; 1203 + 846 1204 var iter = std.mem.splitScalar(u8, line, ':'); 847 1205 const name = iter.first(); 848 1206 _ = iter.next(); ··· 850 1208 851 1209 const group: Group = .{ 852 1210 .name = name, 853 - .gid = try std.fmt.parseInt(u32, gid, 10), 1211 + .gid = try std.fmt.parseInt( 1212 + if (builtin.os.tag == .macos) i33 else u32, 1213 + gid, 1214 + 10, 1215 + ), 854 1216 }; 855 1217 856 1218 cmd.groups.appendAssumeCapacity(group); ··· 859 1221 }, 860 1222 861 1223 .stat => { 862 - _ = result.statx catch { 1224 + _ = result.statx catch |err| { 863 1225 const entry: *Entry = @fieldParentPtr("statx", task.req.statx.result); 864 - if (entry.symlink_missing) { 865 - // we already got here. Just zero out the statx; 1226 + const symlink = cmd.symlinks.getPtr(entry.name) orelse return err; 1227 + 1228 + if (!symlink.exists) { 1229 + // We already lstated this and found an error. Just zero out statx and move 1230 + // along 866 1231 entry.statx = std.mem.zeroInit(ourio.Statx, entry.statx); 867 1232 return; 868 1233 } 869 1234 870 - entry.symlink_missing = true; 1235 + symlink.exists = false; 1236 + 871 1237 _ = try io.lstat(task.req.statx.path, task.req.statx.result, .{ 872 1238 .cb = onCompletion, 873 1239 .ptr = cmd, ··· 882 1248 cmd.entry_idx += 1; 883 1249 const path = try std.fs.path.joinZ( 884 1250 cmd.arena, 885 - &.{ cmd.opts.directory, entry.name }, 1251 + &.{ cmd.current_directory, entry.name }, 886 1252 ); 887 1253 888 1254 if (entry.kind == .sym_link) { ··· 890 1256 891 1257 // NOTE: Sadly, we can't do readlink via io_uring 892 1258 const link = try posix.readlink(path, &buf); 893 - entry.link_name = try cmd.arena.dupeZ(u8, link); 1259 + const symlink: Symlink = .{ .name = try cmd.arena.dupeZ(u8, link) }; 1260 + try cmd.symlinks.put(cmd.arena, entry.name, symlink); 894 1261 } 895 1262 _ = try io.stat(path, &entry.statx, .{ 896 1263 .cb = onCompletion, ··· 928 1295 const json: Icon = .{ .icon = "๎˜‹", .color = Options.Colors.blue }; 929 1296 const lua: Icon = .{ .icon = "๓ฐขฑ", .color = Options.Colors.blue }; 930 1297 const markdown: Icon = .{ .icon = "๎˜‰", .color = "" }; 1298 + const nix: Icon = .{ .icon = "๓ฑ„…", .color = "\x1b[38:2:127:185:228m" }; 931 1299 const python: Icon = .{ .icon = "๎œผ", .color = Options.Colors.yellow }; 932 1300 const rust: Icon = .{ .icon = "๎žจ", .color = "" }; 933 1301 const typescript: Icon = .{ .icon = "๎ฃŠ", .color = Options.Colors.blue }; 934 1302 const zig: Icon = .{ .icon = "๎šฉ", .color = "\x1b[38:2:247:164:29m" }; 935 1303 936 - const by_name: std.StaticStringMap(Icon) = .initComptime(.{}); 1304 + const by_name: std.StaticStringMap(Icon) = .initComptime(.{ 1305 + .{ "flake.lock", Icon.nix }, 1306 + .{ "go.mod", Icon.go }, 1307 + .{ "go.sum", Icon.go }, 1308 + }); 937 1309 938 1310 const by_extension: std.StaticStringMap(Icon) = .initComptime(.{ 939 1311 .{ "cjs", Icon.javascript }, 940 1312 .{ "css", Icon.css }, 1313 + .{ "drv", Icon.nix }, 941 1314 .{ "gif", Icon.image }, 942 1315 .{ "go", Icon.go }, 943 1316 .{ "html", Icon.html }, ··· 951 1324 .{ "mjs", Icon.javascript }, 952 1325 .{ "mkv", Icon.video }, 953 1326 .{ "mp4", Icon.video }, 1327 + .{ "nar", Icon.nix }, 1328 + .{ "nix", Icon.nix }, 954 1329 .{ "png", Icon.image }, 955 1330 .{ "py", Icon.python }, 956 1331 .{ "rs", Icon.rust }, ··· 961 1336 .{ "zon", Icon.zig }, 962 1337 }); 963 1338 964 - fn get(entry: Entry, opts: Options) Icon { 1339 + fn get(entry: Entry) Icon { 965 1340 // 1. By name 966 - // 2. By extension 967 - // 3. By type 1341 + // 2. By type 1342 + // 3. By extension 968 1343 if (by_name.get(entry.name)) |icon| return icon; 969 1344 970 - const ext = std.fs.path.extension(entry.name); 971 - if (ext.len > 0) { 972 - const ft = ext[1..]; 973 - if (by_extension.get(ft)) |icon| return icon; 974 - } 975 - 976 1345 switch (entry.kind) { 977 1346 .block_device => return drive, 978 1347 .character_device => return drive, 979 1348 .directory => return directory, 980 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 + 981 1356 if (entry.isExecutable()) { 982 1357 return executable; 983 1358 } ··· 985 1360 }, 986 1361 .named_pipe => return pipe, 987 1362 .sym_link => { 988 - if (opts.long and posix.S.ISDIR(entry.statx.mode)) { 1363 + if (posix.S.ISDIR(entry.statx.mode)) { 989 1364 return symlink_dir; 990 1365 } 991 1366 return symlink; ··· 1032 1407 if (std.mem.startsWith(u8, a, "-")) return .short; 1033 1408 return .positional; 1034 1409 } 1410 + 1411 + test "ref" { 1412 + _ = natord; 1413 + }
+347
src/natord.zig
··· 1 + //! This file is a port of C implementaion that can be found here 2 + //! https://github.com/sourcefrog/natsort. 3 + const std = @import("std"); 4 + const isSpace = std.ascii.isWhitespace; 5 + const isDigit = std.ascii.isDigit; 6 + const Order = std.math.Order; 7 + const testing = std.testing; 8 + 9 + pub fn order(a: []const u8, b: []const u8) Order { 10 + return natOrder(a, b, false); 11 + } 12 + 13 + pub fn orderIgnoreCase(a: []const u8, b: []const u8) Order { 14 + return natOrder(a, b, true); 15 + } 16 + 17 + fn natOrder(a: []const u8, b: []const u8, comptime fold_case: bool) Order { 18 + var ai: usize = 0; 19 + var bi: usize = 0; 20 + 21 + while (true) : ({ 22 + ai += 1; 23 + bi += 1; 24 + }) { 25 + var ca = if (ai == a.len) 0 else a[ai]; 26 + var cb = if (bi == b.len) 0 else b[bi]; 27 + 28 + while (isSpace(ca)) { 29 + ai += 1; 30 + ca = if (ai == a.len) 0 else a[ai]; 31 + } 32 + 33 + while (isSpace(cb)) { 34 + bi += 1; 35 + cb = if (bi == b.len) 0 else b[bi]; 36 + } 37 + 38 + if (isDigit(ca) and isDigit(cb)) { 39 + const fractional = ca == '0' or cb == '0'; 40 + 41 + if (fractional) { 42 + const result = compareLeft(a[ai..], b[bi..]); 43 + if (result != .eq) return result; 44 + } else { 45 + const result = compareRight(a[ai..], b[bi..]); 46 + if (result != .eq) return result; 47 + } 48 + } 49 + 50 + if (ca == 0 and cb == 0) { 51 + return .eq; 52 + } 53 + 54 + if (fold_case) { 55 + ca = std.ascii.toUpper(ca); 56 + cb = std.ascii.toUpper(cb); 57 + } 58 + 59 + if (ca < cb) { 60 + return .lt; 61 + } 62 + 63 + if (ca > cb) { 64 + return .gt; 65 + } 66 + } 67 + } 68 + 69 + fn compareLeft(a: []const u8, b: []const u8) Order { 70 + var i: usize = 0; 71 + while (true) : (i += 1) { 72 + const ca = if (i == a.len) 0 else a[i]; 73 + const cb = if (i == b.len) 0 else b[i]; 74 + 75 + if (!isDigit(ca) and !isDigit(cb)) { 76 + return .eq; 77 + } 78 + if (!isDigit(ca)) { 79 + return .lt; 80 + } 81 + if (!isDigit(cb)) { 82 + return .gt; 83 + } 84 + if (ca < cb) { 85 + return .lt; 86 + } 87 + if (ca > cb) { 88 + return .gt; 89 + } 90 + } 91 + 92 + return .eq; 93 + } 94 + 95 + fn compareRight(a: []const u8, b: []const u8) Order { 96 + var bias = Order.eq; 97 + 98 + var i: usize = 0; 99 + while (true) : (i += 1) { 100 + const ca = if (i == a.len) 0 else a[i]; 101 + const cb = if (i == b.len) 0 else b[i]; 102 + 103 + if (!isDigit(ca) and !isDigit(cb)) { 104 + return bias; 105 + } 106 + if (!isDigit(ca)) { 107 + return .lt; 108 + } 109 + if (!isDigit(cb)) { 110 + return .gt; 111 + } 112 + 113 + if (ca < cb) { 114 + if (bias != .eq) { 115 + bias = .lt; 116 + } 117 + } else if (ca > cb) { 118 + if (bias != .eq) { 119 + bias = .gt; 120 + } 121 + } else if (ca == 0 and cb == 0) { 122 + return bias; 123 + } 124 + } 125 + 126 + return .eq; 127 + } 128 + 129 + const SortContext = struct { 130 + ignore_case: bool = false, 131 + reverse: bool = false, 132 + 133 + fn compare(self: @This(), a: []const u8, b: []const u8) bool { 134 + const ord: std.math.Order = if (self.reverse) .gt else .lt; 135 + if (self.ignore_case) { 136 + return orderIgnoreCase(a, b) == ord; 137 + } else { 138 + return order(a, b) == ord; 139 + } 140 + } 141 + }; 142 + 143 + test "lt" { 144 + try testing.expectEqual(Order.lt, order("a_1", "a_10")); 145 + } 146 + 147 + test "eq" { 148 + try testing.expectEqual(Order.eq, order("a_1", "a_1")); 149 + } 150 + 151 + test "gt" { 152 + try testing.expectEqual(Order.gt, order("a_10", "a_1")); 153 + } 154 + 155 + fn sortAndAssert(context: SortContext, input: [][]const u8, want: []const []const u8) !void { 156 + std.sort.pdq([]const u8, input, context, SortContext.compare); 157 + 158 + for (input, want) |actual, expected| { 159 + try testing.expectEqualStrings(expected, actual); 160 + } 161 + } 162 + 163 + test "sorting" { 164 + const context = SortContext{}; 165 + var items = [_][]const u8{ 166 + "item100", 167 + "item10", 168 + "item1", 169 + }; 170 + const want = [_][]const u8{ 171 + "item1", 172 + "item10", 173 + "item100", 174 + }; 175 + 176 + try sortAndAssert(context, &items, &want); 177 + } 178 + 179 + test "sorting 2" { 180 + const context = SortContext{}; 181 + var items = [_][]const u8{ 182 + "item_30", 183 + "item_15", 184 + "item_3", 185 + "item_2", 186 + "item_10", 187 + }; 188 + const want = [_][]const u8{ 189 + "item_2", 190 + "item_3", 191 + "item_10", 192 + "item_15", 193 + "item_30", 194 + }; 195 + 196 + try sortAndAssert(context, &items, &want); 197 + } 198 + 199 + test "leading zeros" { 200 + const context = SortContext{}; 201 + var items = [_][]const u8{ 202 + "item100", 203 + "item999", 204 + "item001", 205 + "item010", 206 + "item000", 207 + }; 208 + const want = [_][]const u8{ 209 + "item000", 210 + "item001", 211 + "item010", 212 + "item100", 213 + "item999", 214 + }; 215 + 216 + try sortAndAssert(context, &items, &want); 217 + } 218 + 219 + test "dates" { 220 + const context = SortContext{}; 221 + var items = [_][]const u8{ 222 + "2000-1-10", 223 + "2000-1-2", 224 + "1999-12-25", 225 + "2000-3-23", 226 + "1999-3-3", 227 + }; 228 + const want = [_][]const u8{ 229 + "1999-3-3", 230 + "1999-12-25", 231 + "2000-1-2", 232 + "2000-1-10", 233 + "2000-3-23", 234 + }; 235 + 236 + try sortAndAssert(context, &items, &want); 237 + } 238 + 239 + test "fractions" { 240 + const context = SortContext{}; 241 + var items = [_][]const u8{ 242 + "Fractional release numbers", 243 + "1.011.02", 244 + "1.010.12", 245 + "1.009.02", 246 + "1.009.20", 247 + "1.009.10", 248 + "1.002.08", 249 + "1.002.03", 250 + "1.002.01", 251 + }; 252 + const want = [_][]const u8{ 253 + "1.002.01", 254 + "1.002.03", 255 + "1.002.08", 256 + "1.009.02", 257 + "1.009.10", 258 + "1.009.20", 259 + "1.010.12", 260 + "1.011.02", 261 + "Fractional release numbers", 262 + }; 263 + 264 + try sortAndAssert(context, &items, &want); 265 + } 266 + 267 + test "words" { 268 + const context = SortContext{}; 269 + var items = [_][]const u8{ 270 + "fred", 271 + "pic2", 272 + "pic100a", 273 + "pic120", 274 + "pic121", 275 + "jane", 276 + "tom", 277 + "pic02a", 278 + "pic3", 279 + "pic4", 280 + "1-20", 281 + "pic100", 282 + "pic02000", 283 + "10-20", 284 + "1-02", 285 + "1-2", 286 + "x2-y7", 287 + "x8-y8", 288 + "x2-y08", 289 + "x2-g8", 290 + "pic01", 291 + "pic02", 292 + "pic 6", 293 + "pic 7", 294 + "pic 5", 295 + "pic05", 296 + "pic 5 ", 297 + "pic 5 something", 298 + "pic 4 else", 299 + }; 300 + const want = [_][]const u8{ 301 + "1-02", 302 + "1-2", 303 + "1-20", 304 + "10-20", 305 + "fred", 306 + "jane", 307 + "pic01", 308 + "pic02", 309 + "pic02a", 310 + "pic02000", 311 + "pic05", 312 + "pic2", 313 + "pic3", 314 + "pic4", 315 + "pic 4 else", 316 + "pic 5", 317 + "pic 5 ", 318 + "pic 5 something", 319 + "pic 6", 320 + "pic 7", 321 + "pic100", 322 + "pic100a", 323 + "pic120", 324 + "pic121", 325 + "tom", 326 + "x2-g8", 327 + "x2-y08", 328 + "x2-y7", 329 + "x8-y8", 330 + }; 331 + 332 + try sortAndAssert(context, &items, &want); 333 + } 334 + 335 + test "fuzz" { 336 + const Context = struct { 337 + fn testOne(context: @This(), input: []const u8) anyerror!void { 338 + _ = context; 339 + 340 + const a = input[0..(input.len / 2)]; 341 + const b = input[(input.len / 2)..]; 342 + _ = order(a, b); 343 + } 344 + }; 345 + 346 + try std.testing.fuzz(Context{}, Context.testOne, .{}); 347 + }