From 9af38f3a8151d71df52ae8e0b00f78600d850637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Thu, 19 Feb 2026 03:37:54 +0100 Subject: [PATCH 1/3] feat(filter): as you type --- src/render.test.ts | 104 ++++++++++++++++++++++++++++++++++++--------- src/tui.ts | 50 ++++++++++++++++------ 2 files changed, 123 insertions(+), 31 deletions(-) diff --git a/src/render.test.ts b/src/render.test.ts index eeaf40a..670bf90 100644 --- a/src/render.test.ts +++ b/src/render.test.ts @@ -16,7 +16,12 @@ import { } from "./render.ts"; import type { RepoGroup, Row } from "./types.ts"; -function makeGroup(repo: string, paths: string[], folded = true, withFragments = false): RepoGroup { +function makeGroup( + repo: string, + paths: string[], + folded = true, + withFragments = false, +): RepoGroup { return { repoFullName: repo, matches: paths.map((p) => ({ @@ -24,7 +29,9 @@ function makeGroup(repo: string, paths: string[], folded = true, withFragments = repoFullName: repo, htmlUrl: `https://github.com/${repo}/blob/main/${p}`, archived: false, - textMatches: withFragments ? [{ fragment: `some code with ${p}`, matches: [] }] : [], + textMatches: withFragments + ? [{ fragment: `some code with ${p}`, matches: [] }] + : [], })), folded, repoSelected: true, @@ -87,7 +94,9 @@ describe("highlightFragment", () => { it("adds 'more lines' indicator when fragment exceeds 6 lines", () => { // 8 lines → 6 shown + 1 indicator line - const longFragment = Array.from({ length: 8 }, (_, i) => `line${i}`).join("\n"); + const longFragment = Array.from({ length: 8 }, (_, i) => `line${i}`).join( + "\n", + ); const result = highlightFragment(longFragment, [], "file.ts"); // 6 code lines + the indicator expect(result).toHaveLength(7); @@ -217,14 +226,20 @@ describe("buildSummary", () => { }); it("returns plural for multiple repos / unique paths", () => { - const groups = [makeGroup("org/repoA", ["a.ts", "b.ts"]), makeGroup("org/repoB", ["c.ts"])]; + const groups = [ + makeGroup("org/repoA", ["a.ts", "b.ts"]), + makeGroup("org/repoB", ["c.ts"]), + ]; const summary = buildSummary(groups); const stripped = summary.replace(/\x1b\[[0-9;]*m/g, ""); expect(stripped).toBe("2 repos · 3 files"); }); it("shows files · matches when same path appears in multiple repos", () => { - const groups = [makeGroup("org/repoA", ["a.ts"]), makeGroup("org/repoB", ["a.ts"])]; + const groups = [ + makeGroup("org/repoA", ["a.ts"]), + makeGroup("org/repoB", ["a.ts"]), + ]; const summary = buildSummary(groups); const stripped = summary.replace(/\x1b\[[0-9;]*m/g, ""); expect(stripped).toBe("2 repos · 1 file · 2 matches"); @@ -385,7 +400,9 @@ describe("isCursorVisible", () => { }); it("returns false when cursor is above scrollOffset", () => { - const groups = [makeGroup("org/repo", ["a.ts", "b.ts", "c.ts"], false, false)]; + const groups = [ + makeGroup("org/repo", ["a.ts", "b.ts", "c.ts"], false, false), + ]; const rows = buildRows(groups); // cursor=0, scrollOffset=1 → row 0 is above the viewport expect(isCursorVisible(rows, groups, 0, 1, 5)).toBe(false); @@ -393,7 +410,9 @@ describe("isCursorVisible", () => { it("returns false when cursor would exceed viewportHeight (2-line rows)", () => { // 3 extract rows with fragments = 2 lines each → 6 terminal lines - const groups = [makeGroup("org/repo", ["a.ts", "b.ts", "c.ts"], false, true)]; + const groups = [ + makeGroup("org/repo", ["a.ts", "b.ts", "c.ts"], false, true), + ]; const rows = buildRows(groups); // rows: [repo(0), extract(0,0), extract(0,1), extract(0,2)] // viewportHeight=4 lines, scrollOffset=0 @@ -413,19 +432,28 @@ describe("isCursorVisible", () => { describe("buildSummaryFull", () => { it("shows plain file counts when everything is selected (unique paths)", () => { - const groups = [makeGroup("org/repoA", ["a.ts", "b.ts"]), makeGroup("org/repoB", ["c.ts"])]; + const groups = [ + makeGroup("org/repoA", ["a.ts", "b.ts"]), + makeGroup("org/repoB", ["c.ts"]), + ]; const stripped = buildSummaryFull(groups).replace(/\x1b\[[0-9;]*m/g, ""); expect(stripped).toBe("2 repos · 3 files"); }); it("shows files · matches when same path appears in multiple repos", () => { - const groups = [makeGroup("org/repoA", ["a.ts"]), makeGroup("org/repoB", ["a.ts"])]; + const groups = [ + makeGroup("org/repoA", ["a.ts"]), + makeGroup("org/repoB", ["a.ts"]), + ]; const stripped = buildSummaryFull(groups).replace(/\x1b\[[0-9;]*m/g, ""); expect(stripped).toBe("2 repos · 1 file · 2 matches"); }); it("annotates repos with selected count when some are deselected", () => { - const groups = [makeGroup("org/repoA", ["a.ts"]), makeGroup("org/repoB", ["b.ts"], true)]; + const groups = [ + makeGroup("org/repoA", ["a.ts"]), + makeGroup("org/repoB", ["b.ts"], true), + ]; groups[1].repoSelected = false; const stripped = buildSummaryFull(groups).replace(/\x1b\[[0-9;]*m/g, ""); expect(stripped).toContain("2 repos (1 selected)"); @@ -442,7 +470,10 @@ describe("buildSummaryFull", () => { // a.ts appears in both repos; repoA keeps it selected, repoB deselects it. // Unique file a.ts is still selected (via repoA) → no (selected) annotation on files. // But match count is 2 total, 1 selected → matches gets a (1 selected) annotation. - const groups = [makeGroup("org/repoA", ["a.ts"]), makeGroup("org/repoB", ["a.ts"])]; + const groups = [ + makeGroup("org/repoA", ["a.ts"]), + makeGroup("org/repoB", ["a.ts"]), + ]; groups[1].extractSelected[0] = false; const stripped = buildSummaryFull(groups).replace(/\x1b\[[0-9;]*m/g, ""); expect(stripped).toContain("1 file"); @@ -460,8 +491,13 @@ describe("buildSelectionSummary", () => { }); it("shows files · matches when same path appears in multiple repos", () => { - const groups = [makeGroup("org/repoA", ["a.ts"]), makeGroup("org/repoB", ["a.ts"])]; - expect(buildSelectionSummary(groups)).toBe("2 repos · 1 file · 2 matches selected"); + const groups = [ + makeGroup("org/repoA", ["a.ts"]), + makeGroup("org/repoB", ["a.ts"]), + ]; + expect(buildSelectionSummary(groups)).toBe( + "2 repos · 1 file · 2 matches selected", + ); }); it("respects deselected extracts", () => { @@ -478,7 +514,10 @@ describe("buildSelectionSummary", () => { // ─── applySelectAll ─────────────────────────────────────────────────────────────────── describe("applySelectAll", () => { it("selects all repos+extracts when context is a repo row", () => { - const groups = [makeGroup("org/repoA", ["a.ts", "b.ts"]), makeGroup("org/repoB", ["c.ts"])]; + const groups = [ + makeGroup("org/repoA", ["a.ts", "b.ts"]), + makeGroup("org/repoB", ["c.ts"]), + ]; groups[0].repoSelected = false; groups[0].extractSelected = [false, false]; const repoRow: import("./types.ts").Row = { type: "repo", repoIndex: 0 }; @@ -489,7 +528,10 @@ describe("applySelectAll", () => { }); it("selects only current-repo extracts when context is an extract row", () => { - const groups = [makeGroup("org/repoA", ["a.ts", "b.ts"]), makeGroup("org/repoB", ["c.ts"])]; + const groups = [ + makeGroup("org/repoA", ["a.ts", "b.ts"]), + makeGroup("org/repoB", ["c.ts"]), + ]; groups[0].extractSelected = [false, false]; groups[1].repoSelected = false; const extractRow: import("./types.ts").Row = { @@ -507,7 +549,10 @@ describe("applySelectAll", () => { // ─── applySelectNone ──────────────────────────────────────────────────────────────── describe("applySelectNone", () => { it("deselects all repos+extracts when context is a repo row", () => { - const groups = [makeGroup("org/repoA", ["a.ts", "b.ts"]), makeGroup("org/repoB", ["c.ts"])]; + const groups = [ + makeGroup("org/repoA", ["a.ts", "b.ts"]), + makeGroup("org/repoB", ["c.ts"]), + ]; const repoRow: import("./types.ts").Row = { type: "repo", repoIndex: 0 }; applySelectNone(groups, repoRow); expect(groups[0].repoSelected).toBe(false); @@ -516,7 +561,10 @@ describe("applySelectNone", () => { }); it("deselects only current-repo extracts when context is an extract row", () => { - const groups = [makeGroup("org/repoA", ["a.ts", "b.ts"]), makeGroup("org/repoB", ["c.ts"])]; + const groups = [ + makeGroup("org/repoA", ["a.ts", "b.ts"]), + makeGroup("org/repoB", ["c.ts"]), + ]; const extractRow: import("./types.ts").Row = { type: "extract", repoIndex: 0, @@ -626,7 +674,9 @@ describe("buildRows with filterPath", () => { }); it("does not show extracts of folded repos even with filter", () => { - const groups = [makeGroup("org/repo", ["src/a.ts", "src/b.ts"], true /* folded */)]; + const groups = [ + makeGroup("org/repo", ["src/a.ts", "src/b.ts"], true /* folded */), + ]; const rows = buildRows(groups, "src"); expect(rows.length).toBe(1); // only repo row, folded expect(rows[0].type).toBe("repo"); @@ -666,7 +716,9 @@ describe("buildFilterStats", () => { describe("applySelectAll with filterPath", () => { it("selects only matching extracts when filter is active", () => { - const groups = [makeGroup("org/repo", ["src/a.ts", "lib/b.ts"], false, false)]; + const groups = [ + makeGroup("org/repo", ["src/a.ts", "lib/b.ts"], false, false), + ]; groups[0].repoSelected = false; groups[0].extractSelected = [false, false]; const row: Row = { type: "repo", repoIndex: 0 }; @@ -781,6 +833,20 @@ describe("renderGroups filter opts", () => { expect(stripped).toContain("Enter confirm"); }); + it("live-filters rows by filterInput when filterMode=true", () => { + // The caller (tui.ts) builds rows with filterInput as the active filter + // while in filterMode so the view updates as the user types. + const groups = [makeGroup("org/repo", ["src/a.ts", "lib/b.ts"], false)]; + const rows = buildRows(groups, "src"); // rows already filtered by filterInput + const out = renderGroups(groups, 0, rows, 40, 0, "q", "org", { + filterMode: true, + filterInput: "src", + }); + const stripped = out.replace(/\x1b\[[0-9;]*m/g, ""); + expect(stripped).toContain("src/a.ts"); + expect(stripped).not.toContain("lib/b.ts"); + }); + it("shows confirmed filter path with stats when filterPath is set", () => { const groups = [makeGroup("org/repo", ["src/a.ts", "lib/b.ts"], false)]; const rows = buildRows(groups, "src"); diff --git a/src/tui.ts b/src/tui.ts index ca03fb8..ac63681 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -52,13 +52,23 @@ export async function runInteractive( let showHelp = false; const redraw = () => { - const rows = buildRows(groups, filterPath); - const rendered = renderGroups(groups, cursor, rows, termHeight, scrollOffset, query, org, { - filterPath, - filterMode, - filterInput, - showHelp, - }); + const activeFilter = filterMode ? filterInput : filterPath; + const rows = buildRows(groups, activeFilter); + const rendered = renderGroups( + groups, + cursor, + rows, + termHeight, + scrollOffset, + query, + org, + { + filterPath, + filterMode, + filterInput, + showHelp, + }, + ); process.stdout.write(ANSI_CLEAR); process.stdout.write(rendered); }; @@ -88,11 +98,15 @@ export async function runInteractive( cursor = Math.min(cursor, Math.max(0, newRows.length - 1)); scrollOffset = Math.min(scrollOffset, cursor); } else if (key === "\x7f" || key === "\b") { - // Backspace + // Backspace — trim and clamp cursor to new live-filtered row list filterInput = filterInput.slice(0, -1); + const newRows = buildRows(groups, filterInput); + cursor = Math.min(cursor, Math.max(0, newRows.length - 1)); } else if (key.length === 1 && key >= " ") { - // Printable character + // Printable character — clamp cursor to new live-filtered row list filterInput += key; + const newRows = buildRows(groups, filterInput); + cursor = Math.min(cursor, Math.max(0, newRows.length - 1)); } redraw(); continue; @@ -118,7 +132,15 @@ export async function runInteractive( process.stdout.write(ANSI_CLEAR); process.stdin.setRawMode(false); console.log( - buildOutput(groups, query, org, excludedRepos, excludedExtractRefs, format, outputType), + buildOutput( + groups, + query, + org, + excludedRepos, + excludedExtractRefs, + format, + outputType, + ), ); process.exit(0); } @@ -182,7 +204,9 @@ export async function runInteractive( if (row?.type === "repo") { groups[row.repoIndex].folded = true; } else if (row?.type === "extract") { - const parentIdx = rows.findIndex((r) => r.type === "repo" && r.repoIndex === row.repoIndex); + const parentIdx = rows.findIndex( + (r) => r.type === "repo" && r.repoIndex === row.repoIndex, + ); groups[row.repoIndex].folded = true; cursor = parentIdx; if (cursor < scrollOffset) scrollOffset = cursor; @@ -200,7 +224,9 @@ export async function runInteractive( if (row.type === "repo") { const group = groups[row.repoIndex]; group.repoSelected = !group.repoSelected; - group.extractSelected = group.extractSelected.map(() => group.repoSelected); + group.extractSelected = group.extractSelected.map( + () => group.repoSelected, + ); } else { const group = groups[row.repoIndex]; const ei = row.extractIndex!; From 81b6e0173672ad825fabc966c7dc6da9cc61773d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Thu, 19 Feb 2026 03:38:31 +0100 Subject: [PATCH 2/3] doc(readme): update --- README.md | 26 ++++++++++---- src/render.test.ts | 90 ++++++++++------------------------------------ src/tui.ts | 39 +++++--------------- 3 files changed, 47 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index 99ee2b9..022d899 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ The script automatically detects your OS (Linux, macOS) and architecture (x64, a To install a specific version or to a custom directory: ```bash -INSTALL_DIR=~/.local/bin VERSION=v1.2.0 \ +INSTALL_DIR=~/.local/bin VERSION=v1.0.6 \ curl -fsSL https://raw.githubusercontent.com/fulll/github-code-search/main/install.sh | bash ``` @@ -150,7 +150,6 @@ GitHub Code Search: useFeatureFlag in fulll After pressing **Enter**: -``` 3 repos · 6 files · 7 matches selected - **fulll/auth-service** (2 matches) @@ -164,11 +163,16 @@ After pressing **Enter**: - [src/hooks/useFeatureFlag.ts:1:1](https://github.com/fulll/frontend-app/blob/main/src/hooks/useFeatureFlag.ts#L1) - [src/components/Dashboard.tsx:4:3](https://github.com/fulll/frontend-app/blob/main/src/components/Dashboard.tsx#L4) -# Replay: +
+replay command + +```bash github-code-search "useFeatureFlag" --org fulll --no-interactive \ --exclude-repositories legacy-monolith ``` +
+ ## Non-interactive mode (CI) ### Why use it? @@ -202,7 +206,6 @@ github-code-search "useFeatureFlag" --org fulll --no-interactive $ CI=true github-code-search "useFeatureFlag" --org fulll ``` -``` 3 repos · 5 matches selected - **fulll/auth-service** @@ -214,10 +217,15 @@ $ CI=true github-code-search "useFeatureFlag" --org fulll - **fulll/frontend-app** - [src/hooks/useFeatureFlag.ts:1:1](https://github.com/fulll/frontend-app/blob/main/src/hooks/useFeatureFlag.ts#L1) -# Replay: +
+replay command + +```bash github-code-search "useFeatureFlag" --org fulll --no-interactive ``` +
+ ## Exclusion options ### `--exclude-repositories` @@ -265,7 +273,6 @@ github-code-search "useFeatureFlag" --org fulll **2. Output + replay command:** -``` 2 repos · 3 matches selected - **fulll/auth-service** @@ -276,12 +283,17 @@ github-code-search "useFeatureFlag" --org fulll - **fulll/frontend-app** - [src/hooks/useFeatureFlag.ts:1:1](https://github.com/fulll/frontend-app/blob/main/src/hooks/useFeatureFlag.ts#L1) -# Replay: +
+replay command + +```bash github-code-search "useFeatureFlag" --org fulll --no-interactive \ --exclude-repositories legacy-monolith \ --exclude-extracts auth-service:tests/unit/featureFlags.test.ts:0 ``` +
+ **3. Replay without UI (CI, scripting, documentation):** ```bash diff --git a/src/render.test.ts b/src/render.test.ts index 670bf90..cbbc957 100644 --- a/src/render.test.ts +++ b/src/render.test.ts @@ -16,12 +16,7 @@ import { } from "./render.ts"; import type { RepoGroup, Row } from "./types.ts"; -function makeGroup( - repo: string, - paths: string[], - folded = true, - withFragments = false, -): RepoGroup { +function makeGroup(repo: string, paths: string[], folded = true, withFragments = false): RepoGroup { return { repoFullName: repo, matches: paths.map((p) => ({ @@ -29,9 +24,7 @@ function makeGroup( repoFullName: repo, htmlUrl: `https://github.com/${repo}/blob/main/${p}`, archived: false, - textMatches: withFragments - ? [{ fragment: `some code with ${p}`, matches: [] }] - : [], + textMatches: withFragments ? [{ fragment: `some code with ${p}`, matches: [] }] : [], })), folded, repoSelected: true, @@ -94,9 +87,7 @@ describe("highlightFragment", () => { it("adds 'more lines' indicator when fragment exceeds 6 lines", () => { // 8 lines → 6 shown + 1 indicator line - const longFragment = Array.from({ length: 8 }, (_, i) => `line${i}`).join( - "\n", - ); + const longFragment = Array.from({ length: 8 }, (_, i) => `line${i}`).join("\n"); const result = highlightFragment(longFragment, [], "file.ts"); // 6 code lines + the indicator expect(result).toHaveLength(7); @@ -226,20 +217,14 @@ describe("buildSummary", () => { }); it("returns plural for multiple repos / unique paths", () => { - const groups = [ - makeGroup("org/repoA", ["a.ts", "b.ts"]), - makeGroup("org/repoB", ["c.ts"]), - ]; + const groups = [makeGroup("org/repoA", ["a.ts", "b.ts"]), makeGroup("org/repoB", ["c.ts"])]; const summary = buildSummary(groups); const stripped = summary.replace(/\x1b\[[0-9;]*m/g, ""); expect(stripped).toBe("2 repos · 3 files"); }); it("shows files · matches when same path appears in multiple repos", () => { - const groups = [ - makeGroup("org/repoA", ["a.ts"]), - makeGroup("org/repoB", ["a.ts"]), - ]; + const groups = [makeGroup("org/repoA", ["a.ts"]), makeGroup("org/repoB", ["a.ts"])]; const summary = buildSummary(groups); const stripped = summary.replace(/\x1b\[[0-9;]*m/g, ""); expect(stripped).toBe("2 repos · 1 file · 2 matches"); @@ -400,9 +385,7 @@ describe("isCursorVisible", () => { }); it("returns false when cursor is above scrollOffset", () => { - const groups = [ - makeGroup("org/repo", ["a.ts", "b.ts", "c.ts"], false, false), - ]; + const groups = [makeGroup("org/repo", ["a.ts", "b.ts", "c.ts"], false, false)]; const rows = buildRows(groups); // cursor=0, scrollOffset=1 → row 0 is above the viewport expect(isCursorVisible(rows, groups, 0, 1, 5)).toBe(false); @@ -410,9 +393,7 @@ describe("isCursorVisible", () => { it("returns false when cursor would exceed viewportHeight (2-line rows)", () => { // 3 extract rows with fragments = 2 lines each → 6 terminal lines - const groups = [ - makeGroup("org/repo", ["a.ts", "b.ts", "c.ts"], false, true), - ]; + const groups = [makeGroup("org/repo", ["a.ts", "b.ts", "c.ts"], false, true)]; const rows = buildRows(groups); // rows: [repo(0), extract(0,0), extract(0,1), extract(0,2)] // viewportHeight=4 lines, scrollOffset=0 @@ -432,28 +413,19 @@ describe("isCursorVisible", () => { describe("buildSummaryFull", () => { it("shows plain file counts when everything is selected (unique paths)", () => { - const groups = [ - makeGroup("org/repoA", ["a.ts", "b.ts"]), - makeGroup("org/repoB", ["c.ts"]), - ]; + const groups = [makeGroup("org/repoA", ["a.ts", "b.ts"]), makeGroup("org/repoB", ["c.ts"])]; const stripped = buildSummaryFull(groups).replace(/\x1b\[[0-9;]*m/g, ""); expect(stripped).toBe("2 repos · 3 files"); }); it("shows files · matches when same path appears in multiple repos", () => { - const groups = [ - makeGroup("org/repoA", ["a.ts"]), - makeGroup("org/repoB", ["a.ts"]), - ]; + const groups = [makeGroup("org/repoA", ["a.ts"]), makeGroup("org/repoB", ["a.ts"])]; const stripped = buildSummaryFull(groups).replace(/\x1b\[[0-9;]*m/g, ""); expect(stripped).toBe("2 repos · 1 file · 2 matches"); }); it("annotates repos with selected count when some are deselected", () => { - const groups = [ - makeGroup("org/repoA", ["a.ts"]), - makeGroup("org/repoB", ["b.ts"], true), - ]; + const groups = [makeGroup("org/repoA", ["a.ts"]), makeGroup("org/repoB", ["b.ts"], true)]; groups[1].repoSelected = false; const stripped = buildSummaryFull(groups).replace(/\x1b\[[0-9;]*m/g, ""); expect(stripped).toContain("2 repos (1 selected)"); @@ -470,10 +442,7 @@ describe("buildSummaryFull", () => { // a.ts appears in both repos; repoA keeps it selected, repoB deselects it. // Unique file a.ts is still selected (via repoA) → no (selected) annotation on files. // But match count is 2 total, 1 selected → matches gets a (1 selected) annotation. - const groups = [ - makeGroup("org/repoA", ["a.ts"]), - makeGroup("org/repoB", ["a.ts"]), - ]; + const groups = [makeGroup("org/repoA", ["a.ts"]), makeGroup("org/repoB", ["a.ts"])]; groups[1].extractSelected[0] = false; const stripped = buildSummaryFull(groups).replace(/\x1b\[[0-9;]*m/g, ""); expect(stripped).toContain("1 file"); @@ -491,13 +460,8 @@ describe("buildSelectionSummary", () => { }); it("shows files · matches when same path appears in multiple repos", () => { - const groups = [ - makeGroup("org/repoA", ["a.ts"]), - makeGroup("org/repoB", ["a.ts"]), - ]; - expect(buildSelectionSummary(groups)).toBe( - "2 repos · 1 file · 2 matches selected", - ); + const groups = [makeGroup("org/repoA", ["a.ts"]), makeGroup("org/repoB", ["a.ts"])]; + expect(buildSelectionSummary(groups)).toBe("2 repos · 1 file · 2 matches selected"); }); it("respects deselected extracts", () => { @@ -514,10 +478,7 @@ describe("buildSelectionSummary", () => { // ─── applySelectAll ─────────────────────────────────────────────────────────────────── describe("applySelectAll", () => { it("selects all repos+extracts when context is a repo row", () => { - const groups = [ - makeGroup("org/repoA", ["a.ts", "b.ts"]), - makeGroup("org/repoB", ["c.ts"]), - ]; + const groups = [makeGroup("org/repoA", ["a.ts", "b.ts"]), makeGroup("org/repoB", ["c.ts"])]; groups[0].repoSelected = false; groups[0].extractSelected = [false, false]; const repoRow: import("./types.ts").Row = { type: "repo", repoIndex: 0 }; @@ -528,10 +489,7 @@ describe("applySelectAll", () => { }); it("selects only current-repo extracts when context is an extract row", () => { - const groups = [ - makeGroup("org/repoA", ["a.ts", "b.ts"]), - makeGroup("org/repoB", ["c.ts"]), - ]; + const groups = [makeGroup("org/repoA", ["a.ts", "b.ts"]), makeGroup("org/repoB", ["c.ts"])]; groups[0].extractSelected = [false, false]; groups[1].repoSelected = false; const extractRow: import("./types.ts").Row = { @@ -549,10 +507,7 @@ describe("applySelectAll", () => { // ─── applySelectNone ──────────────────────────────────────────────────────────────── describe("applySelectNone", () => { it("deselects all repos+extracts when context is a repo row", () => { - const groups = [ - makeGroup("org/repoA", ["a.ts", "b.ts"]), - makeGroup("org/repoB", ["c.ts"]), - ]; + const groups = [makeGroup("org/repoA", ["a.ts", "b.ts"]), makeGroup("org/repoB", ["c.ts"])]; const repoRow: import("./types.ts").Row = { type: "repo", repoIndex: 0 }; applySelectNone(groups, repoRow); expect(groups[0].repoSelected).toBe(false); @@ -561,10 +516,7 @@ describe("applySelectNone", () => { }); it("deselects only current-repo extracts when context is an extract row", () => { - const groups = [ - makeGroup("org/repoA", ["a.ts", "b.ts"]), - makeGroup("org/repoB", ["c.ts"]), - ]; + const groups = [makeGroup("org/repoA", ["a.ts", "b.ts"]), makeGroup("org/repoB", ["c.ts"])]; const extractRow: import("./types.ts").Row = { type: "extract", repoIndex: 0, @@ -674,9 +626,7 @@ describe("buildRows with filterPath", () => { }); it("does not show extracts of folded repos even with filter", () => { - const groups = [ - makeGroup("org/repo", ["src/a.ts", "src/b.ts"], true /* folded */), - ]; + const groups = [makeGroup("org/repo", ["src/a.ts", "src/b.ts"], true /* folded */)]; const rows = buildRows(groups, "src"); expect(rows.length).toBe(1); // only repo row, folded expect(rows[0].type).toBe("repo"); @@ -716,9 +666,7 @@ describe("buildFilterStats", () => { describe("applySelectAll with filterPath", () => { it("selects only matching extracts when filter is active", () => { - const groups = [ - makeGroup("org/repo", ["src/a.ts", "lib/b.ts"], false, false), - ]; + const groups = [makeGroup("org/repo", ["src/a.ts", "lib/b.ts"], false, false)]; groups[0].repoSelected = false; groups[0].extractSelected = [false, false]; const row: Row = { type: "repo", repoIndex: 0 }; diff --git a/src/tui.ts b/src/tui.ts index ac63681..19779f5 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -54,21 +54,12 @@ export async function runInteractive( const redraw = () => { const activeFilter = filterMode ? filterInput : filterPath; const rows = buildRows(groups, activeFilter); - const rendered = renderGroups( - groups, - cursor, - rows, - termHeight, - scrollOffset, - query, - org, - { - filterPath, - filterMode, - filterInput, - showHelp, - }, - ); + const rendered = renderGroups(groups, cursor, rows, termHeight, scrollOffset, query, org, { + filterPath, + filterMode, + filterInput, + showHelp, + }); process.stdout.write(ANSI_CLEAR); process.stdout.write(rendered); }; @@ -132,15 +123,7 @@ export async function runInteractive( process.stdout.write(ANSI_CLEAR); process.stdin.setRawMode(false); console.log( - buildOutput( - groups, - query, - org, - excludedRepos, - excludedExtractRefs, - format, - outputType, - ), + buildOutput(groups, query, org, excludedRepos, excludedExtractRefs, format, outputType), ); process.exit(0); } @@ -204,9 +187,7 @@ export async function runInteractive( if (row?.type === "repo") { groups[row.repoIndex].folded = true; } else if (row?.type === "extract") { - const parentIdx = rows.findIndex( - (r) => r.type === "repo" && r.repoIndex === row.repoIndex, - ); + const parentIdx = rows.findIndex((r) => r.type === "repo" && r.repoIndex === row.repoIndex); groups[row.repoIndex].folded = true; cursor = parentIdx; if (cursor < scrollOffset) scrollOffset = cursor; @@ -224,9 +205,7 @@ export async function runInteractive( if (row.type === "repo") { const group = groups[row.repoIndex]; group.repoSelected = !group.repoSelected; - group.extractSelected = group.extractSelected.map( - () => group.repoSelected, - ); + group.extractSelected = group.extractSelected.map(() => group.repoSelected); } else { const group = groups[row.repoIndex]; const ei = row.extractIndex!; From 0e440d17a9fdc2ffe67e65ee17856e5a47c75cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Thu, 19 Feb 2026 03:40:45 +0100 Subject: [PATCH 3/3] v1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5194d92..c0f2951 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "github-code-search", - "version": "1.0.5", + "version": "1.1.0", "description": "Interactive GitHub code search with per-repo aggregation", "license": "MIT", "author": "fulll",