a4d2b79b37cb
man pager help screen's i==0 row now emits exactly one line instead of two, so the help frame is exactly PAGE_ROWS tall and the redraw's cursorUp(PAGE_ROWS + FOOTER_ROWS) no longer drifts the frame up a row on every dismiss/redraw; dismissing help with any key now forces an immediate body+footer redraw instead of leaving stale help content until the next navigation key; extends the v3.4.5 ghosting-drift regression coverage to drawHelpBody and drawBody with newline-count tests against captured output (NINA-015)
- time
- 2026-07-02 14:31 UTC · 3 months ago
- machine
- koh2
- agent
- claude-sonnet-5
- session
- fable audit fix — TUI raw mode and pager
- parent
- 48628db291a4
↓ nina-a4d2b79b.tar.gzatlas at this save
files changed1 file
src/man.zig+62 −2src/man.zig+62 −2
@@ -632,7 +632,11 @@632632 'h' => top = 0,633633 '?' => {634634 showHelp();635- redraw = false;635+ // showHelp leaves the help screen on the terminal until the636+ // next redraw; force one now instead of waiting for the next637+ // navigation key, so dismissing help doesn't leave stale638+ // help content on screen (NINA-015).639+ redraw = true;636640 },637641 else => redraw = false,638642 }@@ -907,11 +911,18 @@907911 .{ .key = "?", .desc = "this help" },908912 .{ .key = "q / esc", .desc = "leave the manpage" },909913 };914+ // Every iteration must emit exactly one row (one trailing "\n") — the915+ // i == 0 arm used to print a title line plus a blank spacer line916+ // ("\n\n") in a single iteration, so the help frame came out PAGE_ROWS+1917+ // rows tall while every redraw's `cursorUp(PAGE_ROWS + FOOTER_ROWS)`918+ // assumed exactly PAGE_ROWS, drifting the frame up a row on every help919+ // dismiss/redraw (same ghosting-drift bug class as the v3.4.5 body fix,920+ // NINA-015).910921 var i: usize = 0;911922 while (i < PAGE_ROWS) : (i += 1) {912923 output.raw("\x1b[K");913924 if (i == 0) {914- output.p(" {s}{s}nina man · keys{s}\n\n", .{ c(BOLD), c(PINK), c(RESET) });925+ output.p(" {s}{s}nina man · keys{s}\n", .{ c(BOLD), c(PINK), c(RESET) });915926 } else if (i - 1 < pairs.len) {916927 const p = pairs[i - 1];917928 output.p(" {s}{s:<14}{s} {s}{s}{s}\n", .{ c(GOLD), p.key, c(RESET), c(TEXT), p.desc, c(RESET) });@@ -919,4 +930,53 @@919930 output.raw("\n");920931 }921932 }933+}934+935+// --- Ghosting-drift regression tests (v3.4.5 fixed the body; this extends936+// coverage to the help screen, NINA-015) ---937+//938+// The redraw arithmetic in run()'s key loop (`cursorUp(PAGE_ROWS +939+// FOOTER_ROWS)`) only stays correct if every screen-drawing function emits940+// exactly the row count its name promises. These tests capture real output941+// through output.zig (the same module the TUI writes through) and count942+// newlines, so a regression that re-introduces an extra or missing row in943+// either function fails loudly instead of drifting the frame on-screen.944+945+fn newlineCount(bytes: []const u8) usize {946+ return std.mem.count(u8, bytes, "\n");947+}948+949+test "drawHelpBody emits exactly PAGE_ROWS rows regardless of PAGE_ROWS' value" {950+ const saved_page_rows = PAGE_ROWS;951+ defer PAGE_ROWS = saved_page_rows;952+953+ var writer: std.Io.Writer.Allocating = .init(std.testing.allocator);954+ defer writer.deinit();955+ var env: std.process.Environ.Map = .init(std.testing.allocator);956+ defer env.deinit();957+ output.init(&writer.writer, std.testing.io, &env, false);958+959+ for ([_]u16{ 5, 12, MIN_PAGE_ROWS }) |rows| {960+ PAGE_ROWS = rows;961+ writer.clearRetainingCapacity();962+ drawHelpBody();963+ try std.testing.expectEqual(@as(usize, rows), newlineCount(writer.written()));964+ }965+}966+967+test "drawBody emits exactly PAGE_ROWS rows" {968+ const saved_page_rows = PAGE_ROWS;969+ defer PAGE_ROWS = saved_page_rows;970+ PAGE_ROWS = 6;971+972+ var writer: std.Io.Writer.Allocating = .init(std.testing.allocator);973+ defer writer.deinit();974+ var env: std.process.Environ.Map = .init(std.testing.allocator);975+ defer env.deinit();976+ output.init(&writer.writer, std.testing.io, &env, false);977+978+ const lines = [_][]const u8{ "one\n", "two\n", "three\n" };979+ drawBody(&lines, 0, lines.len, &.{}, 0);980+981+ try std.testing.expectEqual(@as(usize, PAGE_ROWS), newlineCount(writer.written()));922982 }\ No newline at end of file