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
1 change: 1 addition & 0 deletions docs/reference/keyboard-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Section header rows (shown when `--group-by-team-prefix` is active) are skipped
| `Space` | Toggle selection on the current repo or extract. On a repo row: cascades to all its extracts. |
| `a` | Select **all**. On a repo row: selects all repos and their extracts. On an extract row: selects all extracts in the current repo. Respects active filters. |
| `n` | Select **none**. Same context rules as `a`. Respects active filters. |
| `o` | **Open in browser** β€” opens the focused item in the default browser. On a repo row: opens the repository page. On an extract row: opens the file directly. |

## Filtering

Expand Down
9 changes: 8 additions & 1 deletion src/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,12 @@ describe("renderHelpOverlay", () => {
expect(stripped).toContain("fold / unfold all repos");
});

it("documents the o open-in-browser shortcut", () => {
const out = renderHelpOverlay();
const stripped = out.replace(/\x1b\[[0-9;]*m/g, "");
expect(stripped).toContain("open in browser");
});

it("is returned by renderGroups when showHelp=true", () => {
const groups = [makeGroup("org/repo", ["a.ts"])];
const rows = buildRows(groups);
Expand Down Expand Up @@ -975,12 +981,13 @@ describe("renderGroups filter opts", () => {
expect(stripped).not.toContain("Filter:");
});

it("status bar hint line includes Z fold-all shortcut", () => {
it("status bar hint line includes Z fold-all and o open shortcuts", () => {
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");
expect(stripped).toContain("o open");
});

it("shows mode badge [content] when filterTarget=content", () => {
Expand Down
3 changes: 2 additions & 1 deletion src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function renderHelpOverlay(): string {
` ${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)")}`,
` ${pc.yellow("o")} open in browser ${pc.dim("(repo row β†’ repo page, extract row β†’ file)")}`,
` ${pc.yellow("f")} enter filter mode ${pc.yellow("r")} reset filter`,
` ${pc.yellow("t")} cycle filter target ${pc.dim("(path β†’ content β†’ repo)")}`,
` ${pc.yellow("h")} / ${pc.yellow("?")} toggle this help ${pc.yellow("q")} / Ctrl+C quit`,
Expand Down Expand Up @@ -298,7 +299,7 @@ export function renderGroups(

lines.push(
pc.dim(
"← / β†’ fold/unfold Z fold-all ↑ / ↓ 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 o open f filter t target h help ↡ confirm q quit\n",
),
);

Expand Down
42 changes: 42 additions & 0 deletions src/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,35 @@ function nextWordBoundary(s: string, pos: number): number {
return i;
}

// ─── Browser helper ──────────────────────────────────────────────────────────

/**
* Open a URL in the system default browser.
* macOS: `open`, Linux: `xdg-open`, Windows: `cmd /c start "" <url>`.
* Fire-and-forget with all stdio set to null so the TUI remains fully responsive.
*/
function openInBrowser(url: string): void {
let command: string;
let args: string[];

if (process.platform === "darwin") {
command = "open";
args = [url];
} else if (process.platform === "win32") {
// `start` is a cmd.exe built-in, not a standalone executable.
// The empty string is the mandatory window-title argument; without it,
// `start` mis-parses the URL as the title and may fail to open it.
command = "cmd";
args = ["/c", "start", "", url];
} else {
command = "xdg-open";
args = [url];
}

// Fire-and-forget: do not await, and set all stdio to null so the TUI stays responsive.
Bun.spawn([command, ...args], { stdout: null, stderr: null, stdin: null });
}

// ─── Interactive TUI ─────────────────────────────────────────────────────────

export async function runInteractive(
Expand Down Expand Up @@ -435,6 +464,19 @@ export async function runInteractive(
applySelectNone(groups, row, filterPath, filterTarget, filterRegex);
}

// `o` β€” open focused result (or repo) in the default browser
if (key === "o" && row && row.type !== "section") {
let url: string;
if (row.type === "repo") {
// Open the repository page on GitHub
url = `https://github.com/${groups[row.repoIndex].repoFullName}`;
} else {
// Open the specific file at the matching line
url = groups[row.repoIndex].matches[row.extractIndex!].htmlUrl;
}
openInBrowser(url);
}

redraw();
}
}