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
↓ nina-2b98515f.tar.gzatlas at this save
files changed5 files
README.md+4 −4build.zig+5 −0src/config.zig+1 −1src/errors.zig+3 −3src/man.zig+71 −3README.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 = hxbuild.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",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| {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" {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