-
Notifications
You must be signed in to change notification settings - Fork 0
Click on fold arrow (▸/▾) or checkbox (✓) to toggle in TUI #173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
shouze
merged 2 commits into
feat/mouse-tracking-foundation
from
feat/mouse-click-toggle
Aug 23, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import { describe, it, expect } from "bun:test"; | ||
| import { hitTestClick } from "./mouse-hit.ts"; | ||
| import type { RepoGroup, Row } from "../types.ts"; | ||
|
|
||
| function createTestGroup(name: string, repoSelected = true): RepoGroup { | ||
| return { | ||
| repoFullName: name, | ||
| repoSelected, | ||
| folded: false, | ||
| matches: [ | ||
| { | ||
| filePath: "src/test.ts", | ||
| textMatches: [], | ||
| extractSelected: false, | ||
| }, | ||
| ], | ||
| extractSelected: [false], | ||
| pickedFrom: undefined, | ||
| sectionLabel: undefined, | ||
| }; | ||
| } | ||
|
|
||
| describe("hitTestClick", () => { | ||
| it("returns null for click out of bounds (y below rows)", () => { | ||
| const groups = [createTestGroup("org/repo")]; | ||
| const rows: Row[] = [{ type: "repo", repoIndex: 0 }]; | ||
| const result = hitTestClick(groups, rows, 0, 0, 100); | ||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| it("returns null for click out of bounds (y at 0)", () => { | ||
| const groups = [createTestGroup("org/repo")]; | ||
| const rows: Row[] = [{ type: "repo", repoIndex: 0 }]; | ||
| const result = hitTestClick(groups, rows, 0, 0, 0); | ||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| it("detects fold action on repo row arrow column", () => { | ||
| const groups = [createTestGroup("org/repo")]; | ||
| const rows: Row[] = [{ type: "repo", repoIndex: 0 }]; | ||
| const result = hitTestClick(groups, rows, 0, 0, 1); // column 0, row 1 | ||
| expect(result).toEqual({ | ||
| row: rows[0], | ||
| column: 0, | ||
| action: "fold", | ||
| }); | ||
| }); | ||
|
|
||
| it("detects select action on repo row checkbox column", () => { | ||
| const groups = [createTestGroup("org/repo")]; | ||
| const rows: Row[] = [{ type: "repo", repoIndex: 0 }]; | ||
| const result = hitTestClick(groups, rows, 0, 2, 1); // column 2, row 1 | ||
| expect(result).toEqual({ | ||
| row: rows[0], | ||
| column: 2, | ||
| action: "select", | ||
| }); | ||
| }); | ||
|
|
||
| it("detects navigate action on repo row elsewhere", () => { | ||
| const groups = [createTestGroup("org/repo")]; | ||
| const rows: Row[] = [{ type: "repo", repoIndex: 0 }]; | ||
| const result = hitTestClick(groups, rows, 0, 10, 1); // column 10, row 1 | ||
| expect(result).toEqual({ | ||
| row: rows[0], | ||
| column: 10, | ||
| action: "navigate", | ||
| }); | ||
| }); | ||
|
|
||
| it("detects select action on extract row checkbox", () => { | ||
| const groups = [createTestGroup("org/repo")]; | ||
| const rows: Row[] = [{ type: "extract", repoIndex: 0, extractIndex: 0 }]; | ||
| const result = hitTestClick(groups, rows, 0, 2, 1); // column 2, row 1 | ||
| expect(result).toEqual({ | ||
| row: rows[0], | ||
| column: 2, | ||
| action: "select", | ||
| }); | ||
| }); | ||
|
|
||
| it("detects navigate action on extract row elsewhere", () => { | ||
| const groups = [createTestGroup("org/repo")]; | ||
| const rows: Row[] = [{ type: "extract", repoIndex: 0, extractIndex: 0 }]; | ||
| const result = hitTestClick(groups, rows, 0, 10, 1); // column 10, row 1 | ||
| expect(result).toEqual({ | ||
| row: rows[0], | ||
| column: 10, | ||
| action: "navigate", | ||
| }); | ||
| }); | ||
|
|
||
| it("handles multiple rows and identifies the correct row", () => { | ||
| const groups = [createTestGroup("org/repoA"), createTestGroup("org/repoB")]; | ||
| const rows: Row[] = [ | ||
| { type: "repo", repoIndex: 0 }, | ||
| { type: "repo", repoIndex: 1 }, | ||
| ]; | ||
| // Row 1 is repoA, row 2 is repoB | ||
| const result = hitTestClick(groups, rows, 0, 2, 2); // column 2, row 2 | ||
| expect(result?.row.repoIndex).toBe(1); | ||
| expect(result?.action).toBe("select"); | ||
| }); | ||
|
|
||
| it("handles section rows (no special action)", () => { | ||
| const groups = [createTestGroup("org/repo")]; | ||
| const rows: Row[] = [ | ||
| { type: "section", repoIndex: -1, sectionLabel: "section-a" }, | ||
| { type: "repo", repoIndex: 0 }, | ||
| ]; | ||
| const result = hitTestClick(groups, rows, 0, 0, 1); // section row | ||
| expect(result?.row.type).toBe("section"); | ||
| expect(result?.action).toBe("navigate"); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| // Hit-testing for mouse clicks in the TUI viewport. | ||
| // Maps clicked (x, y) coordinates to logical Row and action type. | ||
|
|
||
| import type { RepoGroup, Row } from "../types.ts"; | ||
| import { rowTerminalLines } from "./rows.ts"; | ||
|
|
||
| export interface ClickTarget { | ||
| row: Row; | ||
| column: number; // Column index within the row (0-based terminal position) | ||
| action: "fold" | "select" | "navigate"; | ||
| } | ||
|
|
||
| /** | ||
| * Hit-test a mouse click against the rendered rows. | ||
| * | ||
| * Returns a ClickTarget if the click lands on a valid row; null if out of bounds. | ||
| * Coordinates (x, y) are 1-indexed (terminal convention). | ||
| * | ||
| * Actions: | ||
| * - "fold" on repo rows: click lands on the ▸/▾ column (column 0) | ||
| * - "select" on any row: click lands on the ✓ checkbox column (column ~2-6 depending on line type) | ||
| * - "navigate" on any row: click elsewhere (just move cursor to that row) | ||
| */ | ||
| export function hitTestClick( | ||
| groups: RepoGroup[], | ||
| rows: Row[], | ||
| scrollOffset: number, | ||
| x: number, | ||
| y: number, | ||
| ): ClickTarget | null { | ||
| // Convert 1-indexed terminal coordinates to 0-indexed row list | ||
| const clickedRowIndex = y - 1; | ||
| if (clickedRowIndex < 0 || clickedRowIndex >= rows.length) return null; | ||
|
shouze marked this conversation as resolved.
|
||
|
|
||
| // Calculate cumulative line heights to find which row was clicked | ||
| let lineOffset = 0; | ||
| for (let i = 0; i < rows.length; i++) { | ||
| const row = rows[i]; | ||
| const group = groups[row.repoIndex] ?? undefined; | ||
| const h = rowTerminalLines(group, row); | ||
|
|
||
| if (lineOffset <= clickedRowIndex && clickedRowIndex < lineOffset + h) { | ||
| // This row was clicked | ||
| // Column 0 is the fold arrow (for repo rows) | ||
| // Column ~2 is the checkbox (after "▸ " or " ") | ||
| // For extract rows, the checkbox offset is slightly different based on line type | ||
|
|
||
| let action: "fold" | "select" | "navigate" = "navigate"; | ||
|
|
||
| if (row.type === "repo") { | ||
| // Repo row layout: "▸ ✓ repo-name" | ||
| // Arrow at column 0, checkbox at column 2 (after "▸ ") | ||
| if (x === 0) { | ||
| action = "fold"; | ||
| } else if (x === 2) { | ||
| action = "select"; | ||
| } | ||
| } else if (row.type === "extract") { | ||
| // Extract row layout: " ✓ path:line:col" | ||
| // Checkbox at column 2 (after " ") | ||
| if (x === 2) { | ||
| action = "select"; | ||
| } | ||
| } | ||
| // Section rows don't support any action (just navigate) | ||
|
|
||
| return { row, column: x, action }; | ||
| } | ||
|
|
||
| lineOffset += h; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.