this repo has no description

Compare changes

Choose any two refs to compare.

+1
.gitignore
··· 1 1 .zig-cache/ 2 2 zig-out/ 3 + result/
+63 -23
README.md
··· 4 4 5 5 ![screenshot](screenshot.png) 6 6 7 + ## Installation 8 + 9 + `lsr` uses the zig build system. To install, you will need zig 0.14.0. To 10 + install for the local user (assuming `$HOME/.local/bin` is in `$PATH`), run: 11 + 12 + ```sh 13 + zig build -Doptimize=ReleaseSmall --prefix $HOME/.local 14 + ``` 15 + 16 + which will install `lsr` and the associated manpage appropriately. Replace 17 + `$HOME/.local` with your preferred installation directory. 18 + 19 + ## Usage 20 + 21 + ``` 22 + lsr [options] [path] 23 + 24 + --help Print this message and exit 25 + --version Print the version string 26 + 27 + DISPLAY OPTIONS 28 + -1, --oneline Print entries one per line 29 + -a, --all Show files that start with a dot (ASCII 0x2E) 30 + -A, --almost-all Like --all, but skips implicit "." and ".." directories 31 + -C, --columns Print the output in columns 32 + --color=WHEN 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) 35 + --icons=WHEN When to display icons (always, auto, never) 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 39 + 40 + ``` 41 + 7 42 ## Benchmarks 8 43 9 44 Benchmarks were all gathered on the same set of directories, using the latest 10 - releases of each program (versions are shown below). 45 + releases of each program (versions are shown below). All benchmarks run on Linux 46 + (because io_uring). `lsr` does work on macOS/BSD as well, but will not see the 47 + syscall batching benefits that are available with io_uring. 11 48 12 - | Program | Version | 13 - |:-------:|:-------:| 14 - | lsr | 0.1.0 | 15 - | ls | 9.7 | 16 - | eza | 0.21.3 | 17 - | lsd | 1.1.5 | 18 - | 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 | 19 57 20 58 ### Time 21 59 22 60 Data gathered with `hyperfine` on a directory of `n` plain files. 23 61 24 - | Program | n=10 | n=100 | n=1,000 | n=10,000 | 25 - |:-------------:|:--------:|:--------:|:-------:|:--------:| 26 - | lsr -al | 372.6 ยตs | 634.3 ยตs | 2.7 ms | 22.1 ms | 27 - | ls -al | 1.4 ms | 1.7 ms | 4.7 ms | 38.0 ms | 28 - | eza -al | 2.9 ms | 3.3 ms | 6.6 ms | 40.2 ms | 29 - | lsd -al | 2.1 ms | 3.5 ms | 17.0 ms | 153.4 ms | 30 - | 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 | 31 70 32 71 ### Syscalls 33 72 34 - Data gathered with `strace -c` on a directory of `n` plain files. 73 + Data gathered with `strace -c` on a directory of `n` plain files. (Lower is better) 35 74 36 - | Program | n=10 | n=100 | n=1,000 | n=10,000 | 37 - |:-------------:|:----:|:-----:|:-------:|:--------:| 38 - | lsr -al | 20 | 28 | 105 | 848 | 39 - | ls -al | 405 | 675 | 3,377 | 30,396 | 40 - | eza -al | 319 | 411 | 1,320 | 10,364 | 41 - | lsd -al | 508 | 1,408 | 10,423 | 100,512 | 42 - | 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 |
+15 -1
build.zig
··· 1 1 const std = @import("std"); 2 + const zzdoc = @import("zzdoc"); 2 3 3 4 /// Must be kept in sync with git tags 4 - const version: std.SemanticVersion = .{ .major = 0, .minor = 1, .patch = 0 }; 5 + const version: std.SemanticVersion = .{ .major = 1, .minor = 0, .patch = 0 }; 5 6 6 7 pub fn build(b: *std.Build) void { 7 8 const target = b.standardTargetOptions(.{}); 8 9 const optimize = b.standardOptimizeOption(.{}); 9 10 11 + // manpages 12 + { 13 + var man_step = zzdoc.addManpageStep(b, .{ 14 + .root_doc_dir = b.path("docs/"), 15 + }); 16 + 17 + const install_step = man_step.addInstallStep(.{}); 18 + b.default_step.dependOn(&install_step.step); 19 + } 20 + 10 21 const exe_mod = b.createModule(.{ 11 22 .root_source_file = b.path("src/main.zig"), 12 23 .target = target, ··· 34 45 .name = "lsr", 35 46 .root_module = exe_mod, 36 47 }); 48 + exe.linkLibC(); 37 49 38 50 b.installArtifact(exe); 39 51 ··· 71 83 "-C", 72 84 b.build_root.path orelse ".", 73 85 "describe", 86 + "--match", 87 + "*.*.*", 74 88 "--tags", 75 89 "--abbrev=9", 76 90 }, &code, .Ignore) catch {
+10 -6
build.zig.zon
··· 1 1 .{ 2 2 .name = .lsr, 3 - .version = "0.1.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#1afffffe2424f9c5923271ac764e950efbdde696", 11 - .hash = "ourio-0.0.0-_s-z0cALAgCdk4d-uZk0B07uX47Lf64fJDJ9L4Ejj3Rs", 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 + }, 17 + .zzdoc = .{ 18 + .url = "git+https://github.com/rockorager/zzdoc#a54223bdc13a80839ccf9f473edf3a171e777946", 19 + .hash = "zzdoc-0.0.0-tzT1Ph7cAAC5YmXQXiBJHAg41_A5AUAC5VOm7ShnUxlz", 16 20 }, 17 21 }, 18 22 .paths = .{
+60
docs/lsr.1.scd
··· 1 + lsr(1) 2 + 3 + # NAME 4 + 5 + lsr - list directory contents, but with io_uring 6 + 7 + # SYNOPSIS 8 + 9 + *lsr* [options...] [path] 10 + 11 + # DESCRIPTION 12 + 13 + lsr is an implementation of ls(1) which utilizes io_uring to perform syscall 14 + batching. 15 + 16 + # OPTIONS 17 + 18 + *-1*, *--oneline* 19 + Print entries one per line 20 + 21 + *-a*, *--all* 22 + Show files that start with a dot (ASCII 0x2E) 23 + 24 + *-A*, *--almost-all* 25 + Like --all, but skips implicit "." and ".." directories 26 + 27 + *-C*, *--columns* 28 + Print the output in columns 29 + 30 + *--color=WHEN* 31 + When to use colors (always, auto, never) 32 + 33 + *--group-directories-first* 34 + Print all directories before printing regular files 35 + 36 + *--help* 37 + Print the help menu and exit 38 + 39 + *--hyperlinks=WHEN* 40 + When to use OSC 8 hyperlinks (always, auto, never) 41 + 42 + *--icons=WHEN* 43 + When to display icons (always, auto, never) 44 + 45 + *-l*, *--long* 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 53 + 54 + *--version* 55 + Print the version and exit 56 + 57 + # AUTHORS 58 + 59 + Written and maintained by Tim Culverhouse <tim@timculverhouse.com>, assisted by 60 + open source contributors.
+62
flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixpkgs": { 4 + "locked": { 5 + "lastModified": 1746397377, 6 + "narHash": "sha256-5oLdRa3vWSRbuqPIFFmQBGGUqaYZBxX+GGtN9f/n4lU=", 7 + "owner": "nixos", 8 + "repo": "nixpkgs", 9 + "rev": "ed30f8aba41605e3ab46421e3dcb4510ec560ff8", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "owner": "nixos", 14 + "ref": "nixpkgs-unstable", 15 + "repo": "nixpkgs", 16 + "type": "github" 17 + } 18 + }, 19 + "root": { 20 + "inputs": { 21 + "nixpkgs": "nixpkgs", 22 + "utils": "utils" 23 + } 24 + }, 25 + "systems": { 26 + "locked": { 27 + "lastModified": 1681028828, 28 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 29 + "owner": "nix-systems", 30 + "repo": "default", 31 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 32 + "type": "github" 33 + }, 34 + "original": { 35 + "owner": "nix-systems", 36 + "repo": "default", 37 + "type": "github" 38 + } 39 + }, 40 + "utils": { 41 + "inputs": { 42 + "systems": "systems" 43 + }, 44 + "locked": { 45 + "lastModified": 1731533236, 46 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 47 + "owner": "numtide", 48 + "repo": "flake-utils", 49 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 50 + "type": "github" 51 + }, 52 + "original": { 53 + "owner": "numtide", 54 + "ref": "main", 55 + "repo": "flake-utils", 56 + "type": "github" 57 + } 58 + } 59 + }, 60 + "root": "root", 61 + "version": 7 62 + }
+44
flake.nix
··· 1 + { 2 + inputs = { 3 + utils.url = "github:numtide/flake-utils/main"; 4 + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 5 + }; 6 + 7 + outputs = { self, nixpkgs, utils }: 8 + utils.lib.eachDefaultSystem(system: 9 + let 10 + pkgs = import nixpkgs { inherit system; }; 11 + cache = import ./nix/cache.nix { inherit pkgs; }; 12 + in { 13 + devshells.default = pkgs.mkShell { 14 + nativeBuildInputs = with pkgs; [ zig zls ]; 15 + }; 16 + 17 + packages.default = pkgs.stdenv.mkDerivation { 18 + pname = "lsr"; 19 + version = "1.0.0"; 20 + doCheck = false; 21 + src = ./.; 22 + 23 + nativeBuildInputs = with pkgs; [ zig ]; 24 + 25 + buildPhase = '' 26 + export ZIG_GLOBAL_CACHE_DIR=$(mktemp -d) 27 + ln -sf ${cache} $ZIG_GLOBAL_CACHE_DIR/p 28 + zig build -Doptimize=ReleaseFast --summary all 29 + ''; 30 + 31 + installPhase = '' 32 + install -Ds -m755 zig-out/bin/lsr $out/bin/lsr 33 + ''; 34 + 35 + meta = with pkgs.lib; { 36 + description = "ls(1) but with io_uring"; 37 + homepage = "https://tangled.sh/@rockorager.dev/lsr"; 38 + maintainers = with maintainers; [ rockorager ]; 39 + platforms = platforms.linux; 40 + license = licenses.mit; 41 + }; 42 + }; 43 + }); 44 + }
+23
nix/cache.nix
··· 1 + { pkgs, ... }: 2 + 3 + pkgs.stdenv.mkDerivation { 4 + pname = "lsr-cache"; 5 + version = "1.0.0"; 6 + doCheck = false; 7 + src = ../.; 8 + 9 + nativeBuildInputs = with pkgs; [ zig ]; 10 + 11 + buildPhase = '' 12 + export ZIG_GLOBAL_CACHE_DIR=$(mktemp -d) 13 + zig build --fetch --summary none 14 + ''; 15 + 16 + installPhase = '' 17 + mv $ZIG_GLOBAL_CACHE_DIR/p $out 18 + ''; 19 + 20 + outputHash = "sha256-bfc2dlQa1VGq9S6OBeQawAJuvfxU4kgFtQ13fuKhdZc="; 21 + outputHashMode = "recursive"; 22 + outputHashAlgo = "sha256"; 23 + }
+530 -113
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 18 + \\ --version Print the version string 14 19 \\ 15 20 \\DISPLAY OPTIONS 16 21 \\ -1, --oneline Print entries one per line ··· 18 23 \\ -A, --almost-all Like --all, but skips implicit "." and ".." directories 19 24 \\ -C, --columns Print the output in columns 20 25 \\ --color=WHEN When to use colors (always, auto, never) 21 - \\ --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) 22 28 \\ --icons=WHEN When to display icons (always, auto, never) 23 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 + \\ 24 34 ; 25 35 26 36 const queue_size = 256; ··· 31 41 color: When = .auto, 32 42 shortview: enum { columns, oneline } = .oneline, 33 43 @"group-directories-first": bool = true, 44 + hyperlinks: When = .auto, 34 45 icons: When = .auto, 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, 36 51 37 - directory: [:0]const u8 = ".", 52 + directories: std.ArrayListUnmanaged([:0]const u8) = .empty, 53 + file: ?[]const u8 = null, 38 54 39 55 winsize: ?posix.winsize = null, 40 56 colors: Colors = .none, ··· 99 115 } 100 116 } 101 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 + 102 130 fn isatty(self: Options) bool { 103 131 return self.winsize != null; 104 132 } ··· 124 152 125 153 var cmd: Command = .{ .arena = allocator }; 126 154 127 - cmd.opts.winsize = getWinsize(std.io.getStdOut().handle); 155 + cmd.opts.winsize = getWinsize(std.fs.File.stdout().handle); 128 156 129 157 cmd.opts.shortview = if (cmd.opts.isatty()) .columns else .oneline; 130 158 131 - const stdout = std.io.getStdOut().writer(); 132 - const stderr = std.io.getStdErr().writer(); 133 - 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; 134 165 135 166 var args = std.process.args(); 136 167 // skip binary ··· 145 176 'A' => cmd.opts.@"almost-all" = true, 146 177 'C' => cmd.opts.shortview = .columns, 147 178 'a' => cmd.opts.all = true, 179 + 'h' => {}, // human-readable: present for compatibility 148 180 'l' => cmd.opts.long = true, 181 + 'r' => cmd.opts.reverse_sort = true, 182 + 't' => cmd.opts.sort_by_mod_time = true, 149 183 else => { 150 - try stderr.print("Invalid opt: '{c}'", .{b}); 184 + try stderr.print("Invalid opt: '{c}'\n", .{b}); 151 185 std.process.exit(1); 152 186 }, 153 187 } ··· 159 193 const val = split.rest(); 160 194 if (eql(opt, "all")) { 161 195 cmd.opts.all = parseArgBool(val) orelse { 162 - try stderr.print("Invalid boolean: '{s}'", .{val}); 196 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 163 197 std.process.exit(1); 164 198 }; 165 199 } else if (eql(opt, "long")) { 166 200 cmd.opts.long = parseArgBool(val) orelse { 167 - try stderr.print("Invalid boolean: '{s}'", .{val}); 201 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 168 202 std.process.exit(1); 169 203 }; 170 204 } else if (eql(opt, "almost-all")) { 171 205 cmd.opts.@"almost-all" = parseArgBool(val) orelse { 172 - try stderr.print("Invalid boolean: '{s}'", .{val}); 206 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 173 207 std.process.exit(1); 174 208 }; 175 209 } else if (eql(opt, "group-directories-first")) { 176 210 cmd.opts.@"group-directories-first" = parseArgBool(val) orelse { 177 - try stderr.print("Invalid boolean: '{s}'", .{val}); 211 + try stderr.print("Invalid boolean: '{s}'\n", .{val}); 178 212 std.process.exit(1); 179 213 }; 180 214 } else if (eql(opt, "color")) { 181 215 cmd.opts.color = std.meta.stringToEnum(Options.When, val) orelse { 182 - 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}); 183 224 std.process.exit(1); 184 225 }; 185 226 } else if (eql(opt, "icons")) { 186 227 cmd.opts.icons = std.meta.stringToEnum(Options.When, val) orelse { 187 - try stderr.print("Invalid color option: '{s}'", .{val}); 228 + try stderr.print("Invalid color option: '{s}'\n", .{val}); 188 229 std.process.exit(1); 189 230 }; 190 231 } else if (eql(opt, "columns")) { 191 232 const c = parseArgBool(val) orelse { 192 - try stderr.print("Invalid columns option: '{s}'", .{val}); 233 + try stderr.print("Invalid columns option: '{s}'\n", .{val}); 193 234 std.process.exit(1); 194 235 }; 195 236 cmd.opts.shortview = if (c) .columns else .oneline; 196 237 } else if (eql(opt, "oneline")) { 197 238 const o = parseArgBool(val) orelse { 198 - try stderr.print("Invalid oneline option: '{s}'", .{val}); 239 + try stderr.print("Invalid oneline option: '{s}'\n", .{val}); 199 240 std.process.exit(1); 200 241 }; 201 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 + } 202 264 } else if (eql(opt, "help")) { 203 265 return stderr.writeAll(usage); 204 266 } else if (eql(opt, "version")) { 205 - try bw.writer().print("lsr {s}\r\n", .{build_options.version}); 206 - try bw.flush(); 267 + try stdout.print("lsr {s}\r\n", .{build_options.version}); 268 + try stdout.flush(); 207 269 return; 208 270 } else { 209 - try stderr.print("Invalid opt: '{s}'", .{opt}); 271 + try stderr.print("Invalid opt: '{s}'\n", .{opt}); 210 272 std.process.exit(1); 211 273 } 212 274 }, 213 275 .positional => { 214 - cmd.opts.directory = arg; 276 + try cmd.opts.directories.append(allocator, arg); 215 277 }, 216 278 } 217 279 } ··· 220 282 cmd.opts.colors = .default; 221 283 } 222 284 223 - var ring: ourio.Ring = try .init(allocator, queue_size); 224 - defer ring.deinit(); 285 + if (cmd.opts.directories.items.len == 0) { 286 + try cmd.opts.directories.append(allocator, "."); 287 + } 225 288 226 - _ = try ring.open(cmd.opts.directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{ 227 - .ptr = &cmd, 228 - .cb = onCompletion, 229 - .msg = @intFromEnum(Msg.cwd), 230 - }); 289 + const multiple_dirs = cmd.opts.directories.items.len > 1; 231 290 232 - if (cmd.opts.long) { 233 - _ = try ring.open("/etc/localtime", .{ .CLOEXEC = true }, 0, .{ 234 - .ptr = &cmd, 235 - .cb = onCompletion, 236 - .msg = @intFromEnum(Msg.localtime), 237 - }); 238 - _ = try ring.open("/etc/passwd", .{ .CLOEXEC = true }, 0, .{ 239 - .ptr = &cmd, 240 - .cb = onCompletion, 241 - .msg = @intFromEnum(Msg.passwd), 242 - }); 243 - _ = try ring.open("/etc/group", .{ .CLOEXEC = true }, 0, .{ 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, .{ 244 305 .ptr = &cmd, 245 306 .cb = onCompletion, 246 - .msg = @intFromEnum(Msg.group), 307 + .msg = @intFromEnum(Msg.cwd), 247 308 }); 248 - } 249 309 250 - 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 + } 251 327 252 - if (cmd.entries.len == 0) return; 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 + } 253 347 254 - if (cmd.opts.long) { 255 - try printLong(cmd, bw.writer()); 256 - } else switch (cmd.opts.shortview) { 257 - .columns => try printShortColumns(cmd, bw.writer()), 258 - .oneline => try printShortOnePerLine(cmd, bw.writer()), 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 + } 259 357 } 260 - try bw.flush(); 358 + try stdout.flush(); 261 359 } 262 360 263 361 fn printShortColumns(cmd: Command, writer: anytype) !void { ··· 330 428 for (columns.items, 0..) |column, i| { 331 429 if (row >= column.entries.len) continue; 332 430 const entry = column.entries[row]; 333 - try printShortEntry(column.entries[row], cmd.opts, writer); 431 + try printShortEntry(column.entries[row], cmd, writer); 334 432 335 433 if (i < columns.items.len - 1) { 336 434 const spaces = column.width - (icon_width + entry.name.len); 337 - try writer.writeByteNTimes(' ', spaces); 435 + var space_buf = [_][]const u8{" "}; 436 + try writer.writeSplatAll(&space_buf, spaces); 338 437 } 339 438 } 340 439 try writer.writeAll("\r\n"); ··· 345 444 return idx + n_short_cols >= n_cols; 346 445 } 347 446 348 - fn printShortEntry(entry: Entry, opts: Options, writer: anytype) !void { 447 + fn printShortEntry(entry: Entry, cmd: Command, writer: anytype) !void { 448 + const opts = cmd.opts; 349 449 const colors = opts.colors; 350 450 if (opts.useIcons()) { 351 - const icon = Icon.get(entry, opts); 451 + const icon = Icon.get(entry); 352 452 353 453 if (opts.useColor()) { 354 454 try writer.writeAll(icon.color); ··· 369 469 } 370 470 }, 371 471 } 372 - try writer.writeAll(entry.name); 373 - 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 + } 374 483 } 375 484 376 485 fn printShortOneRow(cmd: Command, writer: anytype) !void { ··· 383 492 384 493 fn printShortOnePerLine(cmd: Command, writer: anytype) !void { 385 494 for (cmd.entries) |entry| { 386 - 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); 387 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 + } 388 543 } 389 544 } 390 545 391 - fn printLong(cmd: Command, writer: anytype) !void { 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 { 392 635 const tz = cmd.tz.?; 393 636 const now = zeit.instant(.{}) catch unreachable; 394 637 const one_year_ago = try now.subtract(.{ .days = 365 }); ··· 400 643 var n_size: usize = 0; 401 644 var n_suff: usize = 0; 402 645 for (cmd.entries) |entry| { 403 - const group = cmd.getGroup(entry.statx.gid).?; 404 - const user = cmd.getGroup(entry.statx.uid).?; 646 + const group = try cmd.getGroup(entry.statx.gid); 647 + const user = try cmd.getUser(entry.statx.uid); 648 + 405 649 var buf: [16]u8 = undefined; 406 650 const size = try entry.humanReadableSize(&buf); 407 - n_group = @max(n_group, group.name.len); 408 - n_user = @max(n_user, user.name.len); 651 + const group_len: usize = if (group) |g| g.name.len else switch (entry.statx.gid) { 652 + 0...9 => 1, 653 + 10...99 => 2, 654 + 100...999 => 3, 655 + 1000...9999 => 4, 656 + 10000...99999 => 5, 657 + else => 6, 658 + }; 659 + 660 + const user_len: usize = if (user) |u| u.name.len else switch (entry.statx.uid) { 661 + 0...9 => 1, 662 + 10...99 => 2, 663 + 100...999 => 3, 664 + 1000...9999 => 4, 665 + 10000...99999 => 5, 666 + else => 6, 667 + }; 668 + 669 + n_group = @max(n_group, group_len); 670 + n_user = @max(n_user, user_len); 409 671 n_size = @max(n_size, size.len); 410 672 n_suff = @max(n_suff, entry.humanReadableSuffix().len); 411 673 } ··· 413 675 }; 414 676 415 677 for (cmd.entries) |entry| { 416 - const user = cmd.getUser(entry.statx.uid).?; 417 - const group = cmd.getGroup(entry.statx.gid).?; 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}), 687 + }; 418 688 const ts = @as(i128, entry.statx.mtime.sec) * std.time.ns_per_s; 419 689 const inst: zeit.Instant = .{ .timestamp = ts, .timezone = &tz }; 420 690 const time = inst.time(); ··· 424 694 try writer.writeAll(&mode); 425 695 try writer.writeByte(' '); 426 696 try writer.writeAll(user.name); 427 - 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); 428 699 try writer.writeByte(' '); 429 700 try writer.writeAll(group.name); 430 - 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); 431 703 try writer.writeByte(' '); 432 704 433 705 var size_buf: [16]u8 = undefined; 434 706 const size = try entry.humanReadableSize(&size_buf); 435 707 const suffix = entry.humanReadableSuffix(); 436 708 437 - try writer.writeByteNTimes(' ', longest_size - size.len); 709 + var space_buf3 = [_][]const u8{" "}; 710 + try writer.writeSplatAll(&space_buf3, longest_size - size.len); 438 711 try writer.writeAll(size); 439 712 try writer.writeByte(' '); 440 713 try writer.writeAll(suffix); 441 - try writer.writeByteNTimes(' ', longest_suffix - suffix.len); 714 + var space_buf4 = [_][]const u8{" "}; 715 + try writer.writeSplatAll(&space_buf4, longest_suffix - suffix.len); 442 716 try writer.writeByte(' '); 443 717 444 718 try writer.print("{d: >2} {s} ", .{ ··· 453 727 } 454 728 455 729 if (cmd.opts.useIcons()) { 456 - const icon = Icon.get(entry, cmd.opts); 730 + const icon = Icon.get(entry); 457 731 458 732 if (cmd.opts.useColor()) { 459 733 try writer.writeAll(icon.color); ··· 475 749 } 476 750 }, 477 751 } 478 - 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 + } 479 761 try writer.writeAll(colors.reset); 480 762 481 - if (entry.kind == .sym_link) { 482 - try writer.writeAll(" -> "); 483 - const color = if (entry.symlink_missing) 484 - colors.symlink_missing 485 - else 486 - colors.symlink_target; 487 - try writer.writeAll(color); 488 - try writer.writeAll(entry.link_name); 489 - try writer.writeAll(colors.reset); 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 => {}, 490 786 } 491 787 492 788 try writer.writeAll("\r\n"); ··· 498 794 opts: Options = .{}, 499 795 entries: []Entry = &.{}, 500 796 entry_idx: usize = 0, 797 + symlinks: std.StringHashMapUnmanaged(Symlink) = .empty, 798 + current_directory: [:0]const u8 = ".", 501 799 502 800 tz: ?zeit.TimeZone = null, 503 801 groups: std.ArrayListUnmanaged(Group) = .empty, 504 802 users: std.ArrayListUnmanaged(User) = .empty, 505 803 506 - fn getUser(self: Command, uid: posix.uid_t) ?User { 804 + fn getUser(self: *Command, uid: posix.uid_t) !?User { 507 805 for (self.users.items) |user| { 508 806 if (user.uid == uid) return user; 509 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 + } 510 818 return null; 511 819 } 512 820 513 - fn getGroup(self: Command, gid: posix.gid_t) ?Group { 821 + fn getGroup(self: *Command, gid: posix.gid_t) !?Group { 514 822 for (self.groups.items) |group| { 515 823 if (group.gid == gid) return group; 516 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 + } 517 833 return null; 518 834 } 519 835 }; ··· 531 847 }; 532 848 533 849 const User = struct { 534 - uid: posix.uid_t, 850 + uid: if (builtin.os.tag == .macos) i33 else posix.uid_t, 535 851 name: []const u8, 536 852 537 853 fn lessThan(_: void, lhs: User, rhs: User) bool { ··· 540 856 }; 541 857 542 858 const Group = struct { 543 - gid: posix.gid_t, 859 + gid: if (builtin.os.tag == .macos) i33 else posix.gid_t, 544 860 name: []const u8, 545 861 546 862 fn lessThan(_: void, lhs: Group, rhs: Group) bool { ··· 564 880 } 565 881 }; 566 882 883 + const Symlink = struct { 884 + name: [:0]const u8, 885 + exists: bool = true, 886 + }; 887 + 567 888 const Entry = struct { 568 889 name: [:0]const u8, 569 890 kind: std.fs.File.Kind, 570 891 statx: ourio.Statx, 571 - link_name: [:0]const u8 = "", 572 - 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 + } 573 910 574 911 fn modeStr(self: Entry) [10]u8 { 575 912 var mode = [_]u8{'-'} ** 10; 576 913 switch (self.kind) { 914 + .block_device => mode[0] = 'b', 915 + .character_device => mode[0] = 'c', 577 916 .directory => mode[0] = 'd', 917 + .named_pipe => mode[0] = 'p', 578 918 .sym_link => mode[0] = 'l', 579 919 else => {}, 580 920 } ··· 646 986 647 987 switch (msg) { 648 988 .cwd => { 649 - 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 + }; 650 1015 // we are async, no need to defer! 651 1016 _ = try io.close(fd, .{}); 652 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 + } 653 1024 654 1025 var temp_results: std.ArrayListUnmanaged(MinimalEntry) = .empty; 655 1026 ··· 670 1041 671 1042 var iter = dir.iterate(); 672 1043 while (try iter.next()) |dirent| { 673 - 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 + } 674 1055 const nameZ = try cmd.arena.dupeZ(u8, dirent.name); 675 1056 try temp_results.append(cmd.arena, .{ 676 1057 .name = nameZ, ··· 700 1081 } 701 1082 const path = try std.fs.path.joinZ( 702 1083 cmd.arena, 703 - &.{ cmd.opts.directory, entry.name }, 1084 + &.{ cmd.current_directory, entry.name }, 704 1085 ); 705 1086 706 1087 if (entry.kind == .sym_link) { ··· 708 1089 709 1090 // NOTE: Sadly, we can't do readlink via io_uring 710 1091 const link = try posix.readlink(path, &buf); 711 - 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); 712 1094 } 713 1095 _ = try io.stat(path, &entry.statx, .{ 714 1096 .cb = onCompletion, ··· 724 1106 // Largest TZ file on my system is Asia/Hebron at 4791 bytes. We allocate an amount 725 1107 // sufficiently more than that to make sure we do this in a single pass 726 1108 const buffer = try cmd.arena.alloc(u8, 8192); 727 - _ = try io.read(fd, buffer, .{ 1109 + _ = try io.read(fd, buffer, .file, .{ 728 1110 .cb = onCompletion, 729 1111 .ptr = cmd, 730 1112 .msg = @intFromEnum(Msg.read_localtime), ··· 735 1117 const n = try result.read; 736 1118 _ = try io.close(task.req.read.fd, .{}); 737 1119 const bytes = task.req.read.buffer[0..n]; 738 - var fbs = std.io.fixedBufferStream(bytes); 739 - const tz = try zeit.timezone.TZInfo.parse(cmd.arena, fbs.reader()); 1120 + var reader = std.Io.Reader.fixed(bytes); 1121 + const tz = try zeit.timezone.TZInfo.parse(cmd.arena, &reader); 740 1122 cmd.tz = .{ .tzinfo = tz }; 741 1123 }, 742 1124 ··· 746 1128 // TODO: stat this or do multiple reads. We'll never know a good bound unless we go 747 1129 // really big 748 1130 const buffer = try cmd.arena.alloc(u8, 8192 * 2); 749 - _ = try io.read(fd, buffer, .{ 1131 + _ = try io.read(fd, buffer, .file, .{ 750 1132 .cb = onCompletion, 751 1133 .ptr = cmd, 752 1134 .msg = @intFromEnum(Msg.read_passwd), ··· 769 1151 // <name>:<throwaway>:<uid><...garbage> 770 1152 while (lines.next()) |line| { 771 1153 if (line.len == 0) continue; 1154 + if (std.mem.startsWith(u8, line, "#")) continue; 1155 + 772 1156 var iter = std.mem.splitScalar(u8, line, ':'); 773 1157 const name = iter.first(); 774 1158 _ = iter.next(); ··· 776 1160 777 1161 const user: User = .{ 778 1162 .name = name, 779 - .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 + ), 780 1168 }; 781 1169 782 1170 cmd.users.appendAssumeCapacity(user); ··· 788 1176 const fd = try result.open; 789 1177 790 1178 const buffer = try cmd.arena.alloc(u8, 8192); 791 - _ = try io.read(fd, buffer, .{ 1179 + _ = try io.read(fd, buffer, .file, .{ 792 1180 .cb = onCompletion, 793 1181 .ptr = cmd, 794 1182 .msg = @intFromEnum(Msg.read_group), ··· 811 1199 // <name>:<throwaway>:<uid><...garbage> 812 1200 while (lines.next()) |line| { 813 1201 if (line.len == 0) continue; 1202 + if (std.mem.startsWith(u8, line, "#")) continue; 1203 + 814 1204 var iter = std.mem.splitScalar(u8, line, ':'); 815 1205 const name = iter.first(); 816 1206 _ = iter.next(); ··· 818 1208 819 1209 const group: Group = .{ 820 1210 .name = name, 821 - .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 + ), 822 1216 }; 823 1217 824 1218 cmd.groups.appendAssumeCapacity(group); ··· 827 1221 }, 828 1222 829 1223 .stat => { 830 - _ = result.statx catch { 1224 + _ = result.statx catch |err| { 831 1225 const entry: *Entry = @fieldParentPtr("statx", task.req.statx.result); 832 - if (entry.symlink_missing) { 833 - // 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 834 1231 entry.statx = std.mem.zeroInit(ourio.Statx, entry.statx); 835 1232 return; 836 1233 } 837 1234 838 - entry.symlink_missing = true; 1235 + symlink.exists = false; 1236 + 839 1237 _ = try io.lstat(task.req.statx.path, task.req.statx.result, .{ 840 1238 .cb = onCompletion, 841 1239 .ptr = cmd, ··· 850 1248 cmd.entry_idx += 1; 851 1249 const path = try std.fs.path.joinZ( 852 1250 cmd.arena, 853 - &.{ cmd.opts.directory, entry.name }, 1251 + &.{ cmd.current_directory, entry.name }, 854 1252 ); 855 1253 856 1254 if (entry.kind == .sym_link) { ··· 858 1256 859 1257 // NOTE: Sadly, we can't do readlink via io_uring 860 1258 const link = try posix.readlink(path, &buf); 861 - 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); 862 1261 } 863 1262 _ = try io.stat(path, &entry.statx, .{ 864 1263 .cb = onCompletion, ··· 896 1295 const json: Icon = .{ .icon = "๎˜‹", .color = Options.Colors.blue }; 897 1296 const lua: Icon = .{ .icon = "๓ฐขฑ", .color = Options.Colors.blue }; 898 1297 const markdown: Icon = .{ .icon = "๎˜‰", .color = "" }; 1298 + const nix: Icon = .{ .icon = "๓ฑ„…", .color = "\x1b[38:2:127:185:228m" }; 899 1299 const python: Icon = .{ .icon = "๎œผ", .color = Options.Colors.yellow }; 1300 + const rust: Icon = .{ .icon = "๎žจ", .color = "" }; 900 1301 const typescript: Icon = .{ .icon = "๎ฃŠ", .color = Options.Colors.blue }; 901 1302 const zig: Icon = .{ .icon = "๎šฉ", .color = "\x1b[38:2:247:164:29m" }; 902 1303 903 - 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 + }); 904 1309 905 1310 const by_extension: std.StaticStringMap(Icon) = .initComptime(.{ 1311 + .{ "cjs", Icon.javascript }, 906 1312 .{ "css", Icon.css }, 1313 + .{ "drv", Icon.nix }, 907 1314 .{ "gif", Icon.image }, 908 1315 .{ "go", Icon.go }, 909 1316 .{ "html", Icon.html }, 910 1317 .{ "jpeg", Icon.image }, 911 1318 .{ "jpg", Icon.image }, 912 1319 .{ "js", Icon.javascript }, 1320 + .{ "jsx", Icon.javascript }, 913 1321 .{ "json", Icon.json }, 914 1322 .{ "lua", Icon.lua }, 915 1323 .{ "md", Icon.markdown }, 1324 + .{ "mjs", Icon.javascript }, 916 1325 .{ "mkv", Icon.video }, 917 1326 .{ "mp4", Icon.video }, 1327 + .{ "nar", Icon.nix }, 1328 + .{ "nix", Icon.nix }, 918 1329 .{ "png", Icon.image }, 919 1330 .{ "py", Icon.python }, 1331 + .{ "rs", Icon.rust }, 920 1332 .{ "ts", Icon.typescript }, 1333 + .{ "tsx", Icon.typescript }, 921 1334 .{ "webp", Icon.image }, 922 1335 .{ "zig", Icon.zig }, 923 1336 .{ "zon", Icon.zig }, 924 1337 }); 925 1338 926 - fn get(entry: Entry, opts: Options) Icon { 1339 + fn get(entry: Entry) Icon { 927 1340 // 1. By name 928 - // 2. By extension 929 - // 3. By type 1341 + // 2. By type 1342 + // 3. By extension 930 1343 if (by_name.get(entry.name)) |icon| return icon; 931 - 932 - const ext = std.fs.path.extension(entry.name); 933 - if (ext.len > 0) { 934 - const ft = ext[1..]; 935 - if (by_extension.get(ft)) |icon| return icon; 936 - } 937 1344 938 1345 switch (entry.kind) { 939 1346 .block_device => return drive, 940 1347 .character_device => return drive, 941 1348 .directory => return directory, 942 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 + 943 1356 if (entry.isExecutable()) { 944 1357 return executable; 945 1358 } ··· 947 1360 }, 948 1361 .named_pipe => return pipe, 949 1362 .sym_link => { 950 - if (opts.long and posix.S.ISDIR(entry.statx.mode)) { 1363 + if (posix.S.ISDIR(entry.statx.mode)) { 951 1364 return symlink_dir; 952 1365 } 953 1366 return symlink; ··· 994 1407 if (std.mem.startsWith(u8, a, "-")) return .short; 995 1408 return .positional; 996 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 + }