Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable user-visible changes to Hunk are documented in this file.
### Added

- Added `vcs = "jj"` support, enabling `hunk diff [revset]` and `hunk show [revset]`.
- Added a pager-mode sidebar file tree that can be revealed with the existing `s` shortcut while keeping pager chrome hidden by default.

### Changed

Expand Down
9 changes: 4 additions & 5 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export function App({
const DIVIDER_WIDTH = 1;
const DIVIDER_HIT_WIDTH = 5;

const pagerMode = Boolean(bootstrap.input.options.pager);
const renderer = useRenderer();
const terminal = useTerminalDimensions();
const sidebarScrollRef = useRef<ScrollBoxRenderable | null>(null);
Expand All @@ -109,15 +110,14 @@ export function App({
const [wrapLines, setWrapLines] = useState(bootstrap.initialWrapLines ?? false);
const [codeHorizontalOffset, setCodeHorizontalOffset] = useState(0);
const [showHunkHeaders, setShowHunkHeaders] = useState(bootstrap.initialShowHunkHeaders ?? true);
const [sidebarVisible, setSidebarVisible] = useState(true);
const [sidebarVisible, setSidebarVisible] = useState(() => !pagerMode);
const [forceSidebarOpen, setForceSidebarOpen] = useState(false);
const [showHelp, setShowHelp] = useState(false);
const [focusArea, setFocusArea] = useState<FocusArea>("files");
const [sidebarWidth, setSidebarWidth] = useState(34);
const [resizeDragOriginX, setResizeDragOriginX] = useState<number | null>(null);
const [resizeStartWidth, setResizeStartWidth] = useState<number | null>(null);

const pagerMode = Boolean(bootstrap.input.options.pager);
const activeTheme = resolveTheme(themeId, renderer.themeMode);
const review = useReviewController({ files: bootstrap.changeset.files });
const filteredFiles = review.visibleFiles;
Expand Down Expand Up @@ -160,9 +160,8 @@ export function App({
const bodyWidth = Math.max(0, terminal.width - bodyPadding);
const responsiveLayout = resolveResponsiveLayout(layoutMode, terminal.width);
const canForceShowSidebar = bodyWidth >= SIDEBAR_MIN_WIDTH + DIVIDER_WIDTH + DIFF_MIN_WIDTH;
const renderSidebar = pagerMode
? false
: sidebarVisible && (responsiveLayout.showSidebar || (forceSidebarOpen && canForceShowSidebar));
const renderSidebar =
sidebarVisible && (responsiveLayout.showSidebar || (forceSidebarOpen && canForceShowSidebar));
const centerWidth = bodyWidth;
const resolvedLayout = responsiveLayout.layout;
const availableCenterWidth = renderSidebar
Expand Down
36 changes: 36 additions & 0 deletions src/ui/AppHost.interactions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1831,6 +1831,42 @@ describe("App interactions", () => {
}
});

test("pager mode can toggle the sidebar file tree", async () => {
const setup = await testRender(<AppHost bootstrap={createBootstrap("auto", true)} />, {
width: 220,
height: 24,
});

try {
await flush(setup);

let frame = setup.captureCharFrame();
expect(frame).not.toContain("File View Navigate Theme Agent Help");
expect((frame.match(/alpha\.ts/g) ?? []).length).toBe(1);

await act(async () => {
await setup.mockInput.typeText("s");
});
await flush(setup);

frame = setup.captureCharFrame();
expect(frame).not.toContain("File View Navigate Theme Agent Help");
expect((frame.match(/alpha\.ts/g) ?? []).length).toBe(2);

await act(async () => {
await setup.mockInput.typeText("s");
});
await flush(setup);

frame = setup.captureCharFrame();
expect((frame.match(/alpha\.ts/g) ?? []).length).toBe(1);
} finally {
await act(async () => {
setup.renderer.destroy();
});
}
});

test("sidebar shortcut can force the sidebar open when responsive layout hides it", async () => {
const setup = await testRender(<AppHost bootstrap={createBootstrap("auto")} />, {
width: 160,
Expand Down
5 changes: 5 additions & 0 deletions src/ui/hooks/useAppKeyboardShortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ export function useAppKeyboardShortcuts({

if (key.name === "w" || key.sequence === "w") {
toggleLineWrap();
return;
}

if (key.name === "s" || key.sequence === "s") {
toggleSidebar();
}
};

Expand Down
30 changes: 30 additions & 0 deletions test/pty/ui-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,36 @@ describe("live UI integration", () => {
}
});

test("general pager mode can display the sidebar file tree", async () => {
const fixture = harness.createPagerPatchFixture();
const session = await harness.launchHunkWithFileBackedStdin({
stdinFile: fixture.patchFile,
args: ["pager"],
cols: 120,
rows: 14,
});

try {
const initial = await session.waitForText(/scroll\.ts/, { timeout: 15_000 });

expect(initial).not.toContain("View Navigate Theme Agent Help");
expect(harness.countMatches(initial, /scroll\.ts/g)).toBe(1);

await session.press("s");
const sidebarRow = /\bM scroll\.ts\s+\+40 -40/;
const withSidebar = await harness.waitForSnapshot(
session,
(text) => sidebarRow.test(text),
5_000,
);

expect(withSidebar).not.toContain("View Navigate Theme Agent Help");
expect(withSidebar).toMatch(sidebarRow);
} finally {
session.close();
}
});

test("explicit pager mode still supports mouse wheel scrolling on a TTY", async () => {
const fixture = harness.createPagerPatchFixture(60);
const session = await harness.launchHunk({
Expand Down
Loading