fix: leave a row of slack below the results panel (#30)

Move the results panel height into a tested resultsPanelOuter helper so the view stays one row short of the overflow:hidden body, matching Downloads and Seeding. Fixes #21.
This commit is contained in:
Anand Hegde
2026-07-01 02:36:28 -04:00
committed by GitHub
parent 17a06fa37a
commit 1b53ef3094
3 changed files with 45 additions and 3 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ import { Panel } from "./Panel";
import { Rule } from "./Rule";
import { useConcurrentSearch } from "../hooks/useConcurrentSearch";
import { getSource, SOURCES } from "../../sources/registry";
import { wrapStep, windowStart } from "../move";
import { wrapStep, windowStart, resultsPanelOuter } from "../move";
import { sortResults, nextSort, sortLabel, sortArrow, type Sort, type SortField } from "../sort";
import { COLOR, GUTTER, ICON, SOURCE_STYLE } from "../theme";
import { cleanText, formatBytes, formatRelative, truncate } from "../../util/format";
@@ -155,7 +155,7 @@ export function Results() {
const clamped = Math.min(cursor, Math.max(0, results.length - 1));
const searchH = 3;
const panelOuter = Math.max(5, listRows - searchH - 1);
const panelOuter = resultsPanelOuter(listRows, searchH);
const listHeight = Math.max(3, panelOuter - 4);
const pageJump = Math.max(1, listHeight - 1);
+28 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { wrapStep, windowStart } from "./move";
import { wrapStep, windowStart, resultsPanelOuter } from "./move";
describe("wrapStep", () => {
it("wraps around both ends", () => {
@@ -18,3 +18,30 @@ describe("windowStart", () => {
expect(windowStart(2, 4, 10)).toBe(0);
});
});
describe("resultsPanelOuter", () => {
// The results view is: search bar (searchH rows) + a 1-row gap + the panel.
const searchH = 3;
const resultsHeight = (listRows: number): number =>
searchH + 1 + resultsPanelOuter(listRows, searchH);
it("leaves a row of slack so results never exactly fill the body box (issue #21)", () => {
// An exact fit inside the parent overflow:hidden body desyncs Ink's
// incremental renderer and swallows a row while scrolling. The view must
// stay strictly shorter than the row budget it is given.
for (let listRows = 12; listRows <= 80; listRows++) {
expect(resultsHeight(listRows)).toBeLessThan(listRows);
}
});
it("uses exactly one row of slack, matching Downloads/Seeding (listRows - 1)", () => {
for (let listRows = 12; listRows <= 80; listRows++) {
expect(resultsHeight(listRows)).toBe(listRows - 1);
}
});
it("clamps to a minimum usable panel height on tiny terminals", () => {
expect(resultsPanelOuter(4, searchH)).toBe(5);
expect(resultsPanelOuter(0, searchH)).toBe(5);
});
});
+15
View File
@@ -8,3 +8,18 @@ export function windowStart(cursor: number, total: number, height: number): numb
const half = Math.floor(height / 2);
return Math.max(0, Math.min(cursor - half, total - height));
}
/**
* Outer height of the results panel given the body's row budget.
*
* The results view stacks a search bar (`searchH` rows) + a one-row gap on top
* of the panel. We intentionally subtract one extra row so the view never
* *exactly* fills the parent `overflow: "hidden"` body box. An exact fit
* desyncs Ink's incremental terminal renderer and makes it swallow a row while
* scrolling — the "highlighted numbering is wrong" bug (issue #21). Downloads
* and Seeding already leave this slack via `listRows - 1`; this keeps Results
* consistent with them.
*/
export function resultsPanelOuter(listRows: number, searchH: number): number {
return Math.max(5, listRows - searchH - 2);
}