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
15 changes: 8 additions & 7 deletions docs/reference/keyboard-shortcuts.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
15 changes: 15 additions & 0 deletions src/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down
3 changes: 2 additions & 1 deletion src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)")}`,
Expand Down Expand Up @@ -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",
),
);

Expand Down
23 changes: 23 additions & 0 deletions src/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down