2b98515f8dae

sync stale ~/.nina.conf error strings and man/README search-key + log-path docs to current XDG config path, real search keybindings, and real log path (NINA-017/019)

time
2026-07-02 14:37 UTC · 3 months ago
machine
koh2
agent
claude-sonnet-5
session
fable audit fix — docs drift
parent
a4d2b79b37cb
5 files README.md+4 −4build.zig+5 −0src/config.zig+1 −1src/errors.zig+3 −3src/man.zig+71 −3
README.md+4 −4
@@ -132,7 +132,7 @@132132 133133 ## multi-machine134134 135-Any command runs on a remote machine with `--on <name>`. Configure machines in `~/.nina.conf`:135+Any command runs on a remote machine with `--on <name>`. Configure machines in `~/.config/nina/config`:136136 137137 ```ini138138 [machine]@@ -165,7 +165,7 @@165165 166166 ## search that stays at the prompt167167 168-`nina search` opens an inline widget — no fullscreen takeover. Browse results, see versions and licenses, install to your profile or system config, copy the attr name, or try the package in a temporary shell.168+`nina search` opens an inline widget — no fullscreen takeover. Browse results, see versions and licenses, then press enter to install the highlighted package (choosing profile, system, or try afterward).169169 170170 ```171171 :: search nixpkgs  ripgrep                          12 results@@ -178,7 +178,7 @@178178    recursively search directories for a regex pattern179179    attr   pkgs.ripgrep180180 181-   [i] profile  [s] system  [t] try  [c] copy  [esc] exit181+   enter install  tab info  [up/down] nav  esc cancel182182 ```183183 184184 ---@@ -196,7 +196,7 @@196196 197197 ## config198198 199-`~/.nina.conf` — plain text, no surprises:199+`~/.config/nina/config` — plain text, no surprises:200200 201201 ```ini202202 editor      = hx

view file

build.zig+5 −0
@@ -16,6 +16,11 @@1616     const version = std.mem.trim(u8, version_raw, " \r\n\t");1717     const options = b.addOptions();1818     options.addOption([]const u8, "version", version);19+    // Absolute path to README.md, baked in at build time so tests can read it20+    // regardless of the cwd `zig build test` happens to run from. README.md21+    // lives outside src/, so it can't be @embedFile'd from within the src/22+    // module's package boundary.23+    options.addOption([]const u8, "readme_path", b.pathJoin(&.{ b.build_root.path.?, "README.md" }));1924 2025     const exe = b.addExecutable(.{2126         .name = "nina",

view file

src/config.zig+1 −1
@@ -182,7 +182,7 @@182182             if (std.mem.eql(u8, m.name, n)) return m;183183         }184184         errors.error_info.setMessage("'{s}' is not in your config", .{n});185-        errors.error_info.setSuggestion("check ~/.nina.conf", .{});185+        errors.error_info.setSuggestion("check ~/.config/nina/config", .{});186186         return error.MachineNotFound;187187     }188188     for (config.machines) |m| {

view file

src/errors.zig+3 −3
@@ -73,8 +73,8 @@7373 7474 pub fn staticMessage(err: anyerror) []const u8 {7575     return switch (err) {76-        error.ConfigNotFound => "~/.nina.conf not found",77-        error.ConfigParseError => "could not parse ~/.nina.conf",76+        error.ConfigNotFound => "~/.config/nina/config not found",77+        error.ConfigParseError => "could not parse ~/.config/nina/config",7878         error.SearchFailed => "nix search failed",7979         error.NetworkError => "network unavailable",8080         error.OutOfMemory => "out of memory",@@ -703,7 +703,7 @@703703 }704704 705705 test "staticMessage" {706-    try std.testing.expectEqualStrings("~/.nina.conf not found", staticMessage(error.ConfigNotFound));706+    try std.testing.expectEqualStrings("~/.config/nina/config not found", staticMessage(error.ConfigNotFound));707707 }708708 709709 test "extractWarnings: translates known warnings" {

view file

src/man.zig+71 −3
@@ -16,6 +16,7 @@1616 const std = @import("std");1717 const output = @import("output.zig");1818 const rawterm = @import("rawterm.zig");19+const build_options = @import("build_options");1920 2021 const PINK = "\x1b[38;2;170;106;82m"; // --pink (#AA6A52)2122 const GOLD = "\x1b[38;2;173;138;80m"; // --gold (#AD8A50)@@ -162,14 +163,14 @@162163         .blocks = &[_]Block{163164             .{ .para = "inline search that never takes over your screen. results appear right below the prompt, and you navigate them with the keyboard. press esc any time to go back to what you were doing." },164165             .{ .para = "search nixpkgs for packages in an 18-line inline widget — no fullscreen takeover, no context switch." },165-            .{ .cmd_block = "$ nina search ripgrep\n:: search nixpkgs ripgrep  3 results\n\n   ripgrep       14.1.0   recursively search directories\n   ripgrep-all   0.10.0   ripgrep, but also search in PDFs\n   ripgrep-fixed  0.1.0    ripgrep with fixed-string matching\n\n   ↓↑ navigate  ·  i install  ·  s system  ·  t try  ·  esc" },166+            .{ .cmd_block = "$ nina search ripgrep\n:: search nixpkgs ripgrep  3 results\n\n   ripgrep       14.1.0   recursively search directories\n   ripgrep-all   0.10.0   ripgrep, but also search in PDFs\n   ripgrep-fixed  0.1.0    ripgrep with fixed-string matching\n\n   enter install  ·  tab info  ·  [up/down] nav  ·  esc cancel" },166167             .{ .para = "search nixpkgs options the same way:" },167168             .{ .cmd_block = "$ nina option logind\n:: search options logind  N results" },168169             .{169170                 .callout = .{170171                     .face = "( •̀ω•́ )✧",171172                     .color = .mint,172-                    .body = "keys: ↓↑ or j k move through results · i or Enter profile install · s system install · t try · c copy attr · Esc or q exit",173+                    .body = "keys: [up/down] move through results · enter install the highlighted package · tab toggle package info · esc cancel",173174                 },174175             },175176         },@@ -257,7 +258,7 @@257258             .{ .para = "your system's whole story is saved in generations. browse them, compare them, travel back to any of them. nothing is gone until you clean it up." },258259             .{ .cmd_block = "$ nina history\n:: generations  kyoshi\n\n   349   2026-05-30   09:32:01   current\n   348   2026-05-29   14:22:44\n   347   2026-05-25   11:05:33" },259260             .{ .para = "switch to any specific generation by number using `nix-env --switch-generation`. use this when `nina back` is not far enough." },260-            .{ .para = "show nina's operation log from ~/.local/share/nina/nina.log. displays the last 50 entries by default. use `--last N` to show more or fewer." },261+            .{ .para = "show nina's operation log from ~/.nina.log. displays the last 50 entries by default. use `--last N` to show more or fewer." },261262             .{262263                 .callout = .{263264                     .face = "(´▽`)",@@ -979,4 +980,71 @@979980     drawBody(&lines, 0, lines.len, &.{}, 0);980981 981982     try std.testing.expectEqual(@as(usize, PAGE_ROWS), newlineCount(writer.written()));983+}984+985+// NINA-017/019 drift guard: the man page and README are hand-maintained prose986+// that mirror the actual config path, log path, and search widget keybindings.987+// Past drift (fable audit 2026-07-02) left these pointing at a retired988+// ~/.nina.conf path, a retired ~/.local/share/nina/nina.log log path, and a989+// pre-key-collision-fix i/s/t/c keymap that no longer exists in search.zig.990+// This walks MAN_PAGE's own text plus README.md looking for those exact991+// retired strings so the drift can't silently reappear. The config section's992+// migration note is the one legitimate historical mention of ~/.nina.conf993+// left in MAN_PAGE (it explains the auto-migration), so that string is994+// allowed at most once there, not zero.995+fn collectManPageText(gpa: std.mem.Allocator) ![]u8 {996+    var buf: std.ArrayList(u8) = .empty;997+    errdefer buf.deinit(gpa);998+    for (MAN_PAGE) |section| {999+        try buf.appendSlice(gpa, section.title);1000+        try buf.append(gpa, '\n');1001+        for (section.blocks) |block| {1002+            switch (block) {1003+                .chapter, .title, .badge, .para, .cmd_sig, .cmd_block => |s| {1004+                    try buf.appendSlice(gpa, s);1005+                    try buf.append(gpa, '\n');1006+                },1007+                .flags_row => |r| {1008+                    try buf.appendSlice(gpa, r.flag);1009+                    try buf.append(gpa, '\n');1010+                    try buf.appendSlice(gpa, r.desc);1011+                    try buf.append(gpa, '\n');1012+                },1013+                .callout => |co| {1014+                    try buf.appendSlice(gpa, co.body);1015+                    try buf.append(gpa, '\n');1016+                },1017+                .kaomoji_card => |k| {1018+                    try buf.appendSlice(gpa, k.name);1019+                    try buf.append(gpa, '\n');1020+                    try buf.appendSlice(gpa, k.desc);1021+                    try buf.append(gpa, '\n');1022+                    try buf.appendSlice(gpa, k.when);1023+                    try buf.append(gpa, '\n');1024+                },1025+                .accent, .blank => {},1026+            }1027+        }1028+    }1029+    return buf.toOwnedSlice(gpa);1030+}1031+1032+test "MAN_PAGE and README stay free of retired config/search-key/log-path strings" {1033+    const gpa = std.testing.allocator;1034+    const text = try collectManPageText(gpa);1035+    defer gpa.free(text);1036+1037+    try std.testing.expect(std.mem.count(u8, text, "~/.nina.conf") <= 1);1038+    try std.testing.expect(std.mem.count(u8, text, "c copy attr") == 0);1039+    try std.testing.expect(std.mem.count(u8, text, "~/.local/share/nina/nina.log") == 0);1040+1041+    // README.md lives outside src/'s package boundary, so it's read at its1042+    // build-time-baked absolute path (build_options.readme_path) rather than1043+    // @embedFile'd, and it's read fresh here rather than cached, so editing1044+    // README.md is picked up without touching this file.1045+    const readme = try std.Io.Dir.readFileAlloc(.cwd(), std.testing.io, build_options.readme_path, gpa, .unlimited);1046+    defer gpa.free(readme);1047+    try std.testing.expect(std.mem.count(u8, readme, "~/.nina.conf") == 0);1048+    try std.testing.expect(std.mem.count(u8, readme, "c copy attr") == 0);1049+    try std.testing.expect(std.mem.count(u8, readme, "~/.local/share/nina/nina.log") == 0);9821050 }\ No newline at end of file

view file