Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
7 changes: 5 additions & 2 deletions src/ui/components/panes/DiffPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export function DiffPane({
copyDecorations = false,
screenLeft = 0,
screenTop = 0,
showTopChrome = !pagerMode,
Comment thread
eduwass marked this conversation as resolved.
Outdated
showAgentNotes,
showLineNumbers,
showHunkHeaders,
Expand Down Expand Up @@ -237,6 +238,7 @@ export function DiffPane({
copyDecorations?: boolean;
screenLeft?: number;
screenTop?: number;
showTopChrome?: boolean;
showAgentNotes: boolean;
showLineNumbers: boolean;
showHunkHeaders: boolean;
Expand Down Expand Up @@ -1728,10 +1730,11 @@ export function DiffPane({
<box
style={{
width,
border: pagerMode ? [] : ["top"],
border: showTopChrome ? ["top"] : [],
borderColor: theme.border,
backgroundColor: theme.panel,
paddingY: pagerMode ? 0 : 1,
paddingTop: showTopChrome ? 1 : 0,
paddingBottom: pagerMode ? 0 : 1,
paddingX: 0,
flexDirection: "column",
}}
Expand Down
Loading