Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/menu-bar-toggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": minor
---

Add a configurable menu bar toggle so keyboard-driven reviews can reclaim one row of terminal space.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ watch = false
exclude_untracked = false
line_numbers = true
wrap_lines = false
menu_bar = true
agent_notes = false
transparent_background = false
```
Expand Down
4 changes: 4 additions & 0 deletions src/core/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ describe("config resolution", () => {
[
'theme = "github-light-default"',
"wrap_lines = true",
"menu_bar = false",
"",
"[pager]",
"hunk_headers = false",
Expand All @@ -93,6 +94,7 @@ describe("config resolution", () => {
theme: "github-light-default",
lineNumbers: false,
wrapLines: true,
menuBar: false,
hunkHeaders: false,
agentNotes: true,
transparentBackground: true,
Expand Down Expand Up @@ -485,6 +487,7 @@ describe("config resolution", () => {
'theme = "github-light-default"',
"line_numbers = false",
"wrap_lines = true",
"menu_bar = false",
"hunk_headers = false",
"agent_notes = true",
"copy_decorations = false",
Expand All @@ -511,6 +514,7 @@ describe("config resolution", () => {
expect(bootstrap.initialTheme).toBe("github-light-default");
expect(bootstrap.initialShowLineNumbers).toBe(false);
expect(bootstrap.initialWrapLines).toBe(true);
expect(bootstrap.initialShowMenuBar).toBe(false);
expect(bootstrap.initialShowHunkHeaders).toBe(false);
expect(bootstrap.initialShowAgentNotes).toBe(true);
expect(bootstrap.initialCopyDecorations).toBe(false);
Expand Down
5 changes: 5 additions & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const DEFAULT_VIEW_PREFERENCES: PersistedViewPreferences = {
showLineNumbers: true,
wrapLines: false,
showHunkHeaders: true,
showMenuBar: true,
showAgentNotes: false,
copyDecorations: false,
};
Expand Down Expand Up @@ -236,6 +237,7 @@ function readConfigPreferences(source: Record<string, unknown>): CommonOptions {
lineNumbers: normalizeBoolean(source.line_numbers),
wrapLines: normalizeBoolean(source.wrap_lines),
hunkHeaders: normalizeBoolean(source.hunk_headers),
menuBar: normalizeBoolean(source.menu_bar),
agentNotes: normalizeBoolean(source.agent_notes),
copyDecorations: normalizeBoolean(source.copy_decorations),
transparentBackground:
Expand All @@ -259,6 +261,7 @@ function mergeOptions(base: CommonOptions, overrides: CommonOptions): CommonOpti
lineNumbers: overrides.lineNumbers ?? base.lineNumbers,
wrapLines: overrides.wrapLines ?? base.wrapLines,
hunkHeaders: overrides.hunkHeaders ?? base.hunkHeaders,
menuBar: overrides.menuBar ?? base.menuBar,
agentNotes: overrides.agentNotes ?? base.agentNotes,
copyDecorations: overrides.copyDecorations ?? base.copyDecorations,
transparentBackground: overrides.transparentBackground ?? base.transparentBackground,
Expand Down Expand Up @@ -325,6 +328,7 @@ export function resolveConfiguredCliInput(
lineNumbers: DEFAULT_VIEW_PREFERENCES.showLineNumbers,
wrapLines: DEFAULT_VIEW_PREFERENCES.wrapLines,
hunkHeaders: DEFAULT_VIEW_PREFERENCES.showHunkHeaders,
menuBar: DEFAULT_VIEW_PREFERENCES.showMenuBar,
agentNotes: DEFAULT_VIEW_PREFERENCES.showAgentNotes,
copyDecorations: DEFAULT_VIEW_PREFERENCES.copyDecorations,
transparentBackground: false,
Expand Down Expand Up @@ -355,6 +359,7 @@ export function resolveConfiguredCliInput(
lineNumbers: resolvedOptions.lineNumbers ?? DEFAULT_VIEW_PREFERENCES.showLineNumbers,
wrapLines: resolvedOptions.wrapLines ?? DEFAULT_VIEW_PREFERENCES.wrapLines,
hunkHeaders: resolvedOptions.hunkHeaders ?? DEFAULT_VIEW_PREFERENCES.showHunkHeaders,
menuBar: resolvedOptions.menuBar ?? DEFAULT_VIEW_PREFERENCES.showMenuBar,
agentNotes: resolvedOptions.agentNotes ?? DEFAULT_VIEW_PREFERENCES.showAgentNotes,
copyDecorations: resolvedOptions.copyDecorations ?? DEFAULT_VIEW_PREFERENCES.copyDecorations,
transparentBackground: resolvedOptions.transparentBackground ?? false,
Expand Down
1 change: 1 addition & 0 deletions src/core/loaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ export async function loadAppBootstrap(
initialShowLineNumbers: input.options.lineNumbers ?? true,
initialWrapLines: input.options.wrapLines ?? false,
initialShowHunkHeaders: input.options.hunkHeaders ?? true,
initialShowMenuBar: input.options.menuBar ?? true,
initialShowAgentNotes: input.options.agentNotes ?? false,
initialCopyDecorations: input.options.copyDecorations ?? false,
};
Expand Down
3 changes: 3 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export interface CommonOptions {
lineNumbers?: boolean;
wrapLines?: boolean;
hunkHeaders?: boolean;
menuBar?: boolean;
agentNotes?: boolean;
copyDecorations?: boolean;
transparentBackground?: boolean;
Expand Down Expand Up @@ -155,6 +156,7 @@ export interface PersistedViewPreferences {
showLineNumbers: boolean;
wrapLines: boolean;
showHunkHeaders: boolean;
showMenuBar: boolean;
showAgentNotes: boolean;
copyDecorations: boolean;
}
Expand Down Expand Up @@ -364,6 +366,7 @@ export interface AppBootstrap {
initialShowLineNumbers?: boolean;
initialWrapLines?: boolean;
initialShowHunkHeaders?: boolean;
initialShowMenuBar?: boolean;
initialShowAgentNotes?: boolean;
initialCopyDecorations?: boolean;
}
22 changes: 20 additions & 2 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ function withCurrentViewOptions(
showAgentNotes: boolean;
showHunkHeaders: boolean;
showLineNumbers: boolean;
showMenuBar: boolean;
wrapLines: boolean;
},
): CliInput {
Expand All @@ -79,6 +80,7 @@ function withCurrentViewOptions(
agentNotes: view.showAgentNotes,
hunkHeaders: view.showHunkHeaders,
lineNumbers: view.showLineNumbers,
menuBar: view.showMenuBar,
wrapLines: view.wrapLines,
},
};
Expand Down Expand Up @@ -134,6 +136,7 @@ export function App({
const [copyDecorations, setCopyDecorations] = useState(bootstrap.initialCopyDecorations ?? false);
const [codeHorizontalOffset, setCodeHorizontalOffset] = useState(0);
const [showHunkHeaders, setShowHunkHeaders] = useState(bootstrap.initialShowHunkHeaders ?? true);
const [showMenuBar, setShowMenuBar] = useState(bootstrap.initialShowMenuBar ?? true);
const [themeSelectorState, setThemeSelectorState] = useState<ThemeSelectorState>({
open: false,
selectedIndex: 0,
Expand Down Expand Up @@ -504,6 +507,11 @@ export function App({
setShowHunkHeaders((current) => !current);
};

/** Toggle the top menu bar while keeping F10 menu navigation available. */
const toggleMenuBar = () => {
setShowMenuBar((current) => !current);
};

const canRefreshCurrentInput = canReloadInput(bootstrap.input);
const watchEnabled = Boolean(bootstrap.input.options.watch && canRefreshCurrentInput);

Expand All @@ -519,6 +527,7 @@ export function App({
showAgentNotes,
showHunkHeaders,
showLineNumbers,
showMenuBar,
wrapLines,
});

Expand All @@ -540,6 +549,7 @@ export function App({
showAgentNotes,
showHunkHeaders,
showLineNumbers,
showMenuBar,
themeId,
wrapLines,
]);
Expand Down Expand Up @@ -743,6 +753,7 @@ export function App({
showHelp,
showHunkHeaders,
showLineNumbers,
showMenuBar,
renderSidebar,
toggleCopyDecorations,
toggleAgentNotes,
Expand All @@ -751,6 +762,7 @@ export function App({
toggleHelp,
toggleHunkHeaders,
toggleLineNumbers,
toggleMenuBar,
toggleLineWrap,
toggleSidebar,
triggerEditSelectedFile,
Expand All @@ -774,12 +786,14 @@ export function App({
showHelp,
showHunkHeaders,
showLineNumbers,
showMenuBar,
renderSidebar,
toggleAgentNotes,
toggleFocusArea,
toggleHelp,
toggleHunkHeaders,
toggleLineNumbers,
toggleMenuBar,
toggleLineWrap,
toggleSidebar,
triggerEditSelectedFile,
Expand Down Expand Up @@ -839,6 +853,7 @@ export function App({
toggleHelp,
toggleHunkHeaders,
toggleLineNumbers,
toggleMenuBar,
toggleLineWrap,
toggleSidebar,
triggerEditSelectedFile,
Expand Down Expand Up @@ -906,7 +921,7 @@ export function App({
// this in lockstep with the body container's paddingLeft and the sidebar render branch below.
const diffPaneScreenLeft =
bodyPadding / 2 + (renderSidebar ? clampedSidebarWidth + DIVIDER_WIDTH : 0);
const diffPaneScreenTop = pagerMode ? 0 : 1;
const diffPaneScreenTop = pagerMode || !showMenuBar ? 0 : 1;

return (
<box
Expand All @@ -917,7 +932,7 @@ export function App({
backgroundColor: activeTheme.background,
}}
>
{!pagerMode ? (
{!pagerMode && showMenuBar ? (
<MenuBar
activeMenuId={activeMenuId}
menuSpecs={menuSpecs}
Expand Down Expand Up @@ -961,6 +976,7 @@ export function App({
entries={review.sidebarEntries}
scrollRef={sidebarScrollRef}
selectedFileId={selectedFile?.id}
showTopChrome={showMenuBar}
textWidth={sidebarTextWidth}
theme={activeTheme}
width={clampedSidebarWidth}
Expand Down Expand Up @@ -994,6 +1010,7 @@ export function App({
pagerMode={pagerMode}
screenLeft={diffPaneScreenLeft}
screenTop={diffPaneScreenTop}
showTopChrome={showMenuBar && !pagerMode}
headerLabelWidth={diffHeaderLabelWidth}
headerStatsWidth={diffHeaderStatsWidth}
layout={resolvedLayout}
Expand Down Expand Up @@ -1060,6 +1077,7 @@ export function App({
activeMenuItemIndex={activeMenuItemIndex}
activeMenuSpec={activeMenuSpec}
activeMenuWidth={activeMenuWidth}
top={showMenuBar ? 1 : 0}
terminalWidth={terminal.width}
theme={baseTheme}
onHoverItem={setActiveMenuItemIndex}
Expand Down
71 changes: 71 additions & 0 deletions src/ui/AppHost.interactions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ function createNumberedAssignmentLines(start: number, count: number, valueOffset
});
}

function firstNonEmptyLine(text: string) {
return text.split("\n").find((line) => line.trim().length > 0) ?? "";
}

function createMockHostClient({
cwd = process.cwd(),
repoRoot = process.cwd(),
Expand Down Expand Up @@ -722,6 +726,73 @@ describe("App interactions", () => {
}
});

test("Shift-M hides the menu bar without disabling F10 menus", async () => {
const setup = await testRender(<AppHost bootstrap={createSingleFileBootstrap()} />, {
width: 240,
height: 24,
});

try {
await flush(setup);

let frame = setup.captureCharFrame();
expect(frame).toContain("File View Navigate Agent Help");

await act(async () => {
await setup.mockInput.pressKey("m", { shift: true });
});
await flush(setup);

frame = setup.captureCharFrame();
expect(frame).not.toContain("File View Navigate Agent Help");
expect(firstNonEmptyLine(frame)).not.toContain("─");

await act(async () => {
await setup.mockInput.pressKey("F10");
});
frame = await waitForFrame(setup, (nextFrame) =>
nextFrame.includes("Toggle files/filter focus"),
);
expect(frame).toContain("Toggle files/filter focus");
expect(frame).not.toContain("File View Navigate Agent Help");
} finally {
await act(async () => {
setup.renderer.destroy();
});
}
});

test("configured hidden menu bar starts hidden while menus remain keyboard-accessible", async () => {
const setup = await testRender(
<AppHost bootstrap={{ ...createSingleFileBootstrap(), initialShowMenuBar: false }} />,
{
width: 240,
height: 24,
},
);

try {
await flush(setup);

let frame = setup.captureCharFrame();
expect(frame).not.toContain("File View Navigate Agent Help");
expect(firstNonEmptyLine(frame)).not.toContain("─");

await act(async () => {
await setup.mockInput.pressKey("F10");
});
frame = await waitForFrame(setup, (nextFrame) =>
nextFrame.includes("Toggle files/filter focus"),
);
expect(frame).toContain("Toggle files/filter focus");
expect(frame).not.toContain("File View Navigate Agent Help");
} finally {
await act(async () => {
setup.renderer.destroy();
});
}
});

test("theme shortcut opens a selector and Enter applies the highlighted theme", async () => {
const setup = await testRender(<AppHost bootstrap={createSingleFileBootstrap()} />, {
width: 240,
Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/chrome/HelpDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function HelpDialog({
["s / t", "sidebar / theme selector"],
["a", "toggle AI notes"],
["z", "toggle unchanged context"],
["l / w / m", "lines / wrap / metadata"],
["l / w / m / M", "lines / wrap / metadata / menu"],
["e", "open file in $EDITOR"],
],
},
Expand Down
4 changes: 3 additions & 1 deletion src/ui/components/chrome/MenuDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function MenuDropdown({
activeMenuItemIndex,
activeMenuSpec,
activeMenuWidth,
top = 1,
terminalWidth,
theme,
onHoverItem,
Expand All @@ -49,6 +50,7 @@ export function MenuDropdown({
activeMenuItemIndex: number;
activeMenuSpec: MenuSpec;
activeMenuWidth: number;
top?: number;
terminalWidth: number;
theme: AppTheme;
onHoverItem: (index: number) => void;
Expand All @@ -61,7 +63,7 @@ export function MenuDropdown({
<box
style={{
position: "absolute",
top: 1,
top,
left: clampedLeft,
width: clampedWidth,
height: activeMenuEntries.length + 2,
Expand Down
Loading