From 448399f7d4a7f73ea8a20a90f765dc2e736ea069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Sun, 1 Mar 2026 23:55:47 +0100 Subject: [PATCH 1/2] Add Z shortcut for global fold/unfold of all repositories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #67 - Z folds all repos if any is unfolded; unfolds all when all are folded - Clamps cursor and scrollOffset after bulk fold to keep view consistent - renderHelpOverlay: adds Z line next to ← / → fold bindings - Status bar hint line: adds 'Z fold-all' entry - Tests: help overlay contains 'Z' + 'fold / unfold all repos', renderGroups status bar contains 'Z fold-all' - docs/reference/keyboard-shortcuts.md: documents the new shortcut --- docs/reference/keyboard-shortcuts.md | 13 +++++++------ src/render.test.ts | 15 +++++++++++++++ src/render.ts | 3 ++- src/tui.ts | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/docs/reference/keyboard-shortcuts.md b/docs/reference/keyboard-shortcuts.md index 49377c5..deb0f2b 100644 --- a/docs/reference/keyboard-shortcuts.md +++ b/docs/reference/keyboard-shortcuts.md @@ -4,12 +4,13 @@ All shortcuts are active in the interactive TUI. Keys are **case-sensitive** and ## 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..a5985b7 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -389,6 +389,20 @@ 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 visible after bulk fold + if (anyUnfolded) { + const newRows = buildRows(groups, filterPath, filterTarget, filterRegex); + 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]; From 9cf3ba24007744a76c5c775a99d9f3ab313ecc8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Mon, 2 Mar 2026 00:09:43 +0100 Subject: [PATCH 2/2] Address review feedback on Z global fold/unfold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tui.ts: when Z folds all repos from an extract row, preserve cursor context by finding the repo header row in newRows rather than doing a blind numeric clamp — avoids landing on a different repository - keyboard-shortcuts.md: remove incorrect 'must be typed in lowercase' claim; note that some bindings (Z, G) require uppercase --- docs/reference/keyboard-shortcuts.md | 2 +- src/tui.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/reference/keyboard-shortcuts.md b/docs/reference/keyboard-shortcuts.md index deb0f2b..8b74257 100644 --- a/docs/reference/keyboard-shortcuts.md +++ b/docs/reference/keyboard-shortcuts.md @@ -1,6 +1,6 @@ # 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 diff --git a/src/tui.ts b/src/tui.ts index a5985b7..990c0a0 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -395,10 +395,19 @@ export async function runInteractive( for (const g of groups) { g.folded = anyUnfolded; } - // Adjust scroll so cursor stays visible after bulk fold + // 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); - cursor = Math.min(cursor, Math.max(0, newRows.length - 1)); + 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); } }