diff --git a/docs/reference/keyboard-shortcuts.md b/docs/reference/keyboard-shortcuts.md index 49377c5..8b74257 100644 --- a/docs/reference/keyboard-shortcuts.md +++ b/docs/reference/keyboard-shortcuts.md @@ -1,15 +1,16 @@ # Keyboard shortcuts -All shortcuts are active in the interactive TUI. Keys are **case-sensitive** and must be typed in lowercase. +All shortcuts are active in the interactive TUI. Keys are **case-sensitive** — most use lowercase letters, but a few bindings (such as `Z` and `G`) require an uppercase letter. ## Navigation -| Key | Action | -| --------- | ------------------------------------- | -| `↑` / `k` | Move cursor up (repos and extracts) | -| `↓` / `j` | Move cursor down (repos and extracts) | -| `←` | Fold the repo under the cursor | -| `→` | Unfold the repo under the cursor | +| Key | Action | +| --------- | ------------------------------------------------------------------------------------------ | +| `↑` / `k` | Move cursor up (repos and extracts) | +| `↓` / `j` | Move cursor down (repos and extracts) | +| `←` | Fold the repo under the cursor | +| `→` | Unfold the repo under the cursor | +| `Z` | **Global fold / unfold** — fold all repos if any is unfolded; unfold all if all are folded | Section header rows (shown when `--group-by-team-prefix` is active) are skipped automatically during navigation. diff --git a/src/render.test.ts b/src/render.test.ts index 3e33c48..9f29bcc 100644 --- a/src/render.test.ts +++ b/src/render.test.ts @@ -902,6 +902,13 @@ describe("renderHelpOverlay", () => { expect(stripped).toContain("Filter mode:"); }); + it("documents the Z global fold/unfold shortcut", () => { + const out = renderHelpOverlay(); + const stripped = out.replace(/\x1b\[[0-9;]*m/g, ""); + expect(stripped).toContain("Z"); + expect(stripped).toContain("fold / unfold all repos"); + }); + it("is returned by renderGroups when showHelp=true", () => { const groups = [makeGroup("org/repo", ["a.ts"])]; const rows = buildRows(groups); @@ -968,6 +975,14 @@ describe("renderGroups filter opts", () => { expect(stripped).not.toContain("Filter:"); }); + it("status bar hint line includes Z fold-all shortcut", () => { + const groups = [makeGroup("org/repo", ["a.ts"])]; + const rows = buildRows(groups); + const out = renderGroups(groups, 0, rows, 40, 0, "q", "org", {}); + const stripped = out.replace(/\x1b\[[0-9;]*m/g, ""); + expect(stripped).toContain("Z fold-all"); + }); + it("shows mode badge [content] when filterTarget=content", () => { const groups = [makeGroup("org/repo", ["a.ts"], false, true)]; const rows = buildRows(groups, "code", "content"); diff --git a/src/render.ts b/src/render.ts index 0e96728..91a3032 100644 --- a/src/render.ts +++ b/src/render.ts @@ -33,6 +33,7 @@ export function renderHelpOverlay(): string { bar, ` ${pc.yellow("↑")} / ${pc.yellow("k")} navigate up ${pc.yellow("↓")} / ${pc.yellow("j")} navigate down`, ` ${pc.yellow("←")} fold repo ${pc.yellow("→")} unfold repo`, + ` ${pc.yellow("Z")} fold / unfold all repos`, ` ${pc.yellow("Space")} toggle selection ${pc.yellow("Enter")} confirm & output`, ` ${pc.yellow("a")} select all ${pc.yellow("n")} select none`, ` ${pc.dim("(respects active filter)")}`, @@ -297,7 +298,7 @@ export function renderGroups( lines.push( pc.dim( - "← / → fold/unfold ↑ / ↓ navigate spc select a all n none f filter t target h help ↵ confirm q quit\n", + "← / → fold/unfold Z fold-all ↑ / ↓ navigate spc select a all n none f filter t target h help ↵ confirm q quit\n", ), ); diff --git a/src/tui.ts b/src/tui.ts index 8f269a7..990c0a0 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -389,6 +389,29 @@ export async function runInteractive( } } + // `Z` — global fold / unfold: fold all if any repo is unfolded, else unfold all + if (key === "Z") { + const anyUnfolded = groups.some((g) => !g.folded); + for (const g of groups) { + g.folded = anyUnfolded; + } + // Adjust scroll so cursor stays aligned with the same repo after bulk fold. + // When folding, extract rows disappear: map the current row's repoIndex to + // its repo header row so the cursor does not jump to a different repository. + if (anyUnfolded) { + const newRows = buildRows(groups, filterPath, filterTarget, filterRegex); + if (row && (row.type === "repo" || row.type === "extract")) { + const headerIdx = newRows.findIndex( + (r) => r.type === "repo" && r.repoIndex === row.repoIndex, + ); + cursor = headerIdx !== -1 ? headerIdx : Math.min(cursor, Math.max(0, newRows.length - 1)); + } else { + cursor = Math.min(cursor, Math.max(0, newRows.length - 1)); + } + scrollOffset = Math.min(scrollOffset, cursor); + } + } + if (key === " " && row && row.type !== "section") { if (row.type === "repo") { const group = groups[row.repoIndex];