github-search-cli 9.1.1 → 9.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -10
- package/bin/ghfind.js +3 -3
- package/dist/cli.js +76 -68
- package/package.json +2 -4
- package/scripts/postinstall.js +10 -7
- package/src/cli.ts +28 -1
- package/src/help.ts +142 -142
- package/src/package-browser.ts +305 -305
- package/src/package.ts +137 -137
- package/src/search.ts +1 -0
- package/src/trending.ts +31 -5
- package/src/tui.ts +23 -36
- package/vendor/bun.exe +0 -0
package/src/package.ts
CHANGED
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* package.ts — package-registry search provider (prototype for ticket 002-008).
|
|
3
|
-
*
|
|
4
|
-
* npm-only for the MVP (ticket 002-005). Query is passed through verbatim to npm's
|
|
5
|
-
* native `/-/v1/search` (ticket 002-006) and the response normalized to the parallel
|
|
6
|
-
* `Package` shape (ticket 002-007). Rough on purpose — this is the artifact to react to.
|
|
7
|
-
*/
|
|
8
|
-
import type { Package } from "./types";
|
|
9
|
-
|
|
10
|
-
export interface PackageSearchResult {
|
|
11
|
-
totalCount: number;
|
|
12
|
-
packages: Package[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** The npm `/-/v1/search` object shape we actually consume (research 002-001). */
|
|
16
|
-
interface NpmSearchObject {
|
|
17
|
-
package: {
|
|
18
|
-
name?: string;
|
|
19
|
-
scope?: string;
|
|
20
|
-
version?: string;
|
|
21
|
-
description?: string | null;
|
|
22
|
-
keywords?: string[];
|
|
23
|
-
date?: string;
|
|
24
|
-
links?: { npm?: string; homepage?: string; repository?: string };
|
|
25
|
-
publisher?: { username?: string };
|
|
26
|
-
author?: { name?: string };
|
|
27
|
-
};
|
|
28
|
-
score?: { final?: number; detail?: { quality?: number; popularity?: number; maintenance?: number } };
|
|
29
|
-
searchScore?: number;
|
|
30
|
-
downloads?: { npm?: number; total?: number; monthly?: number };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function normalizeNpmObject(obj: NpmSearchObject): Package {
|
|
34
|
-
const p = obj.package;
|
|
35
|
-
const name = p.name ?? "";
|
|
36
|
-
return {
|
|
37
|
-
name,
|
|
38
|
-
version: p.version ?? "",
|
|
39
|
-
description: p.description ?? null,
|
|
40
|
-
url: p.links?.npm ?? `https://www.npmjs.com/package/${name}`,
|
|
41
|
-
downloads: obj.downloads?.monthly ?? 0,
|
|
42
|
-
score: obj.score?.final ?? obj.searchScore ?? 0,
|
|
43
|
-
publisher: p.publisher?.username ?? p.author?.name ?? null,
|
|
44
|
-
keywords: p.keywords ?? [],
|
|
45
|
-
registry: "npm",
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function createPackageSearch() {
|
|
50
|
-
return {
|
|
51
|
-
async searchPackage(rawQuery: string, limit: number): Promise<PackageSearchResult> {
|
|
52
|
-
const size = Math.min(Math.max(Math.trunc(limit) || 50, 1), 250); // npm caps size at 250
|
|
53
|
-
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(rawQuery)}&size=${size}`;
|
|
54
|
-
const res = await fetch(url, {
|
|
55
|
-
headers: { "User-Agent": "ghfind", Accept: "application/json" },
|
|
56
|
-
});
|
|
57
|
-
if (!res.ok) {
|
|
58
|
-
throw new Error(`npm search failed: HTTP ${res.status}`);
|
|
59
|
-
}
|
|
60
|
-
const data = (await res.json()) as { objects?: NpmSearchObject[]; total?: number };
|
|
61
|
-
const packages = (data.objects ?? []).map(normalizeNpmObject);
|
|
62
|
-
return { totalCount: data.total ?? packages.length, packages };
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Sort strategies for package results (npm registry order IS best-match). */
|
|
68
|
-
export type PackageSortMode = "best-match" | "score" | "downloads" | "name";
|
|
69
|
-
|
|
70
|
-
/** Sort modes in cycle order for the TUI Sort menu. */
|
|
71
|
-
export const PACKAGE_SORT_MODES: { key: PackageSortMode; label: string }[] = [
|
|
72
|
-
{ key: "best-match", label: "best-match" },
|
|
73
|
-
{ key: "downloads", label: "downloads" },
|
|
74
|
-
{ key: "score", label: "score" },
|
|
75
|
-
{ key: "name", label: "name" },
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Sort fetched packages. "best-match" is a no-op: it preserves the registry's
|
|
80
|
-
* relevance order, which is the canonical npm ranking.
|
|
81
|
-
*/
|
|
82
|
-
export function sortPackages(pkgs: Package[], sort: PackageSortMode): Package[] {
|
|
83
|
-
if (sort === "best-match" || pkgs.length < 2) return pkgs;
|
|
84
|
-
const arr = [...pkgs];
|
|
85
|
-
if (sort === "score") {
|
|
86
|
-
arr.sort((a, b) => b.score - a.score);
|
|
87
|
-
} else if (sort === "downloads") {
|
|
88
|
-
arr.sort((a, b) => b.downloads - a.downloads);
|
|
89
|
-
} else {
|
|
90
|
-
arr.sort((a, b) => a.name.localeCompare(b.name));
|
|
91
|
-
}
|
|
92
|
-
return arr;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ── Package output formatters (parallel to output.ts for repo) ─────────
|
|
96
|
-
// Entity model (ticket 002-007): json/text/count/limit are shared generics
|
|
97
|
-
// (json uses JSON.stringify on Package[]); csv/markdown/urls/names get package
|
|
98
|
-
// variants here. git-only formats (ssh-urls, clone-commands, ids) stay repo-only.
|
|
99
|
-
|
|
100
|
-
function csvEscape(val: unknown): string {
|
|
101
|
-
const str = String(val ?? "");
|
|
102
|
-
if (str.includes('"') || str.includes(",") || str.includes("\n")) {
|
|
103
|
-
return `"${str.replace(/"/g, '""')}"`;
|
|
104
|
-
}
|
|
105
|
-
return str;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function formatPackagesJson(pkgs: Package[]): string {
|
|
109
|
-
return JSON.stringify(pkgs, null, 2);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function formatPackagesCsv(pkgs: Package[]): string {
|
|
113
|
-
const header = "rank,name,version,downloads,score,url";
|
|
114
|
-
const rows = pkgs.map(
|
|
115
|
-
(p, i) =>
|
|
116
|
-
`${i + 1},${csvEscape(p.name)},${p.version},${p.downloads},${p.score.toFixed(2)},${csvEscape(p.url)}`,
|
|
117
|
-
);
|
|
118
|
-
return [header, ...rows].join("\n");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function formatPackagesMarkdown(pkgs: Package[]): string {
|
|
122
|
-
const header = "| # | Package | Version | Downloads | Score |";
|
|
123
|
-
const sep = "|---|---------|---------|-----------|-------|";
|
|
124
|
-
const rows = pkgs.map(
|
|
125
|
-
(p, i) =>
|
|
126
|
-
`| ${i + 1} | [${p.name}@${p.version}](${p.url}) | ${p.version} | ${p.downloads.toLocaleString()} | ${p.score.toFixed(2)} |`,
|
|
127
|
-
);
|
|
128
|
-
return [header, sep, ...rows].join("\n");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function formatPackagesUrls(pkgs: Package[]): string {
|
|
132
|
-
return pkgs.map((p) => p.url).join("\n");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function formatPackagesNames(pkgs: Package[]): string {
|
|
136
|
-
return pkgs.map((p) => p.name).join("\n");
|
|
137
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* package.ts — package-registry search provider (prototype for ticket 002-008).
|
|
3
|
+
*
|
|
4
|
+
* npm-only for the MVP (ticket 002-005). Query is passed through verbatim to npm's
|
|
5
|
+
* native `/-/v1/search` (ticket 002-006) and the response normalized to the parallel
|
|
6
|
+
* `Package` shape (ticket 002-007). Rough on purpose — this is the artifact to react to.
|
|
7
|
+
*/
|
|
8
|
+
import type { Package } from "./types";
|
|
9
|
+
|
|
10
|
+
export interface PackageSearchResult {
|
|
11
|
+
totalCount: number;
|
|
12
|
+
packages: Package[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The npm `/-/v1/search` object shape we actually consume (research 002-001). */
|
|
16
|
+
interface NpmSearchObject {
|
|
17
|
+
package: {
|
|
18
|
+
name?: string;
|
|
19
|
+
scope?: string;
|
|
20
|
+
version?: string;
|
|
21
|
+
description?: string | null;
|
|
22
|
+
keywords?: string[];
|
|
23
|
+
date?: string;
|
|
24
|
+
links?: { npm?: string; homepage?: string; repository?: string };
|
|
25
|
+
publisher?: { username?: string };
|
|
26
|
+
author?: { name?: string };
|
|
27
|
+
};
|
|
28
|
+
score?: { final?: number; detail?: { quality?: number; popularity?: number; maintenance?: number } };
|
|
29
|
+
searchScore?: number;
|
|
30
|
+
downloads?: { npm?: number; total?: number; monthly?: number };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function normalizeNpmObject(obj: NpmSearchObject): Package {
|
|
34
|
+
const p = obj.package;
|
|
35
|
+
const name = p.name ?? "";
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
version: p.version ?? "",
|
|
39
|
+
description: p.description ?? null,
|
|
40
|
+
url: p.links?.npm ?? `https://www.npmjs.com/package/${name}`,
|
|
41
|
+
downloads: obj.downloads?.monthly ?? 0,
|
|
42
|
+
score: obj.score?.final ?? obj.searchScore ?? 0,
|
|
43
|
+
publisher: p.publisher?.username ?? p.author?.name ?? null,
|
|
44
|
+
keywords: p.keywords ?? [],
|
|
45
|
+
registry: "npm",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createPackageSearch() {
|
|
50
|
+
return {
|
|
51
|
+
async searchPackage(rawQuery: string, limit: number): Promise<PackageSearchResult> {
|
|
52
|
+
const size = Math.min(Math.max(Math.trunc(limit) || 50, 1), 250); // npm caps size at 250
|
|
53
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(rawQuery)}&size=${size}`;
|
|
54
|
+
const res = await fetch(url, {
|
|
55
|
+
headers: { "User-Agent": "ghfind", Accept: "application/json" },
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
throw new Error(`npm search failed: HTTP ${res.status}`);
|
|
59
|
+
}
|
|
60
|
+
const data = (await res.json()) as { objects?: NpmSearchObject[]; total?: number };
|
|
61
|
+
const packages = (data.objects ?? []).map(normalizeNpmObject);
|
|
62
|
+
return { totalCount: data.total ?? packages.length, packages };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Sort strategies for package results (npm registry order IS best-match). */
|
|
68
|
+
export type PackageSortMode = "best-match" | "score" | "downloads" | "name";
|
|
69
|
+
|
|
70
|
+
/** Sort modes in cycle order for the TUI Sort menu. */
|
|
71
|
+
export const PACKAGE_SORT_MODES: { key: PackageSortMode; label: string }[] = [
|
|
72
|
+
{ key: "best-match", label: "best-match" },
|
|
73
|
+
{ key: "downloads", label: "downloads" },
|
|
74
|
+
{ key: "score", label: "score" },
|
|
75
|
+
{ key: "name", label: "name" },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sort fetched packages. "best-match" is a no-op: it preserves the registry's
|
|
80
|
+
* relevance order, which is the canonical npm ranking.
|
|
81
|
+
*/
|
|
82
|
+
export function sortPackages(pkgs: Package[], sort: PackageSortMode): Package[] {
|
|
83
|
+
if (sort === "best-match" || pkgs.length < 2) return pkgs;
|
|
84
|
+
const arr = [...pkgs];
|
|
85
|
+
if (sort === "score") {
|
|
86
|
+
arr.sort((a, b) => b.score - a.score);
|
|
87
|
+
} else if (sort === "downloads") {
|
|
88
|
+
arr.sort((a, b) => b.downloads - a.downloads);
|
|
89
|
+
} else {
|
|
90
|
+
arr.sort((a, b) => a.name.localeCompare(b.name));
|
|
91
|
+
}
|
|
92
|
+
return arr;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Package output formatters (parallel to output.ts for repo) ─────────
|
|
96
|
+
// Entity model (ticket 002-007): json/text/count/limit are shared generics
|
|
97
|
+
// (json uses JSON.stringify on Package[]); csv/markdown/urls/names get package
|
|
98
|
+
// variants here. git-only formats (ssh-urls, clone-commands, ids) stay repo-only.
|
|
99
|
+
|
|
100
|
+
function csvEscape(val: unknown): string {
|
|
101
|
+
const str = String(val ?? "");
|
|
102
|
+
if (str.includes('"') || str.includes(",") || str.includes("\n")) {
|
|
103
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
104
|
+
}
|
|
105
|
+
return str;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function formatPackagesJson(pkgs: Package[]): string {
|
|
109
|
+
return JSON.stringify(pkgs, null, 2);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function formatPackagesCsv(pkgs: Package[]): string {
|
|
113
|
+
const header = "rank,name,version,downloads,score,url";
|
|
114
|
+
const rows = pkgs.map(
|
|
115
|
+
(p, i) =>
|
|
116
|
+
`${i + 1},${csvEscape(p.name)},${p.version},${p.downloads},${p.score.toFixed(2)},${csvEscape(p.url)}`,
|
|
117
|
+
);
|
|
118
|
+
return [header, ...rows].join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function formatPackagesMarkdown(pkgs: Package[]): string {
|
|
122
|
+
const header = "| # | Package | Version | Downloads | Score |";
|
|
123
|
+
const sep = "|---|---------|---------|-----------|-------|";
|
|
124
|
+
const rows = pkgs.map(
|
|
125
|
+
(p, i) =>
|
|
126
|
+
`| ${i + 1} | [${p.name}@${p.version}](${p.url}) | ${p.version} | ${p.downloads.toLocaleString()} | ${p.score.toFixed(2)} |`,
|
|
127
|
+
);
|
|
128
|
+
return [header, sep, ...rows].join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function formatPackagesUrls(pkgs: Package[]): string {
|
|
132
|
+
return pkgs.map((p) => p.url).join("\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function formatPackagesNames(pkgs: Package[]): string {
|
|
136
|
+
return pkgs.map((p) => p.name).join("\n");
|
|
137
|
+
}
|
package/src/search.ts
CHANGED
package/src/trending.ts
CHANGED
|
@@ -253,6 +253,14 @@ export async function launchTrending(): Promise<void> {
|
|
|
253
253
|
paddingX: 1,
|
|
254
254
|
});
|
|
255
255
|
const tabTexts: TextRenderable[] = [];
|
|
256
|
+
const underlineBox = new BoxRenderable(renderer, {
|
|
257
|
+
height: 1,
|
|
258
|
+
backgroundColor: C.bg,
|
|
259
|
+
paddingX: 1,
|
|
260
|
+
});
|
|
261
|
+
function tabWidth(name: string): number {
|
|
262
|
+
return name.length + 2; // " name " padding
|
|
263
|
+
}
|
|
256
264
|
function renderTabBar() {
|
|
257
265
|
for (const t of tabTexts) tabBox.remove(t);
|
|
258
266
|
tabTexts.length = 0;
|
|
@@ -261,8 +269,8 @@ export async function launchTrending(): Promise<void> {
|
|
|
261
269
|
const label = ` ${name} `;
|
|
262
270
|
const tt = new TextRenderable(renderer, {
|
|
263
271
|
content: isActive ? t`${bold(fg(C.bg)(label))}` : t`${fg(C.muted)(label)}`,
|
|
264
|
-
|
|
265
|
-
|
|
272
|
+
fg: isActive ? C.bg : C.muted,
|
|
273
|
+
bg: isActive ? C.cyan : C.bg,
|
|
266
274
|
height: 1,
|
|
267
275
|
});
|
|
268
276
|
tabTexts.push(tt);
|
|
@@ -270,17 +278,35 @@ export async function launchTrending(): Promise<void> {
|
|
|
270
278
|
if (i < TAB_NAMES.length - 1) {
|
|
271
279
|
const sp = new TextRenderable(renderer, {
|
|
272
280
|
content: " ",
|
|
273
|
-
|
|
274
|
-
|
|
281
|
+
fg: C.muted,
|
|
282
|
+
bg: C.bg,
|
|
275
283
|
height: 1,
|
|
276
284
|
});
|
|
277
285
|
tabTexts.push(sp);
|
|
278
286
|
tabBox.add(sp);
|
|
279
287
|
}
|
|
280
288
|
});
|
|
289
|
+
// ── cyan underline bar under active tab ──
|
|
290
|
+
let offset = 1; // paddingX
|
|
291
|
+
for (let i = 0; i < selectedTab; i++) {
|
|
292
|
+
offset += tabWidth(TAB_NAMES[i]) + 2; // tab + separator
|
|
293
|
+
}
|
|
294
|
+
const w = tabWidth(TAB_NAMES[selectedTab]);
|
|
295
|
+
const dots = "┉".repeat(w);
|
|
296
|
+
underlineBox.remove(underlineText);
|
|
297
|
+
underlineText.content = t`${fg(C.cyan)(dots)}`;
|
|
298
|
+
underlineText.left = offset;
|
|
281
299
|
}
|
|
300
|
+
const underlineText = new TextRenderable(renderer, {
|
|
301
|
+
content: "",
|
|
302
|
+
fg: C.cyan,
|
|
303
|
+
bg: C.bg,
|
|
304
|
+
height: 1,
|
|
305
|
+
});
|
|
306
|
+
underlineBox.add(underlineText);
|
|
282
307
|
renderTabBar();
|
|
283
308
|
outerBox.add(tabBox);
|
|
309
|
+
outerBox.add(underlineBox);
|
|
284
310
|
const divider = new TextRenderable(renderer, {
|
|
285
311
|
content: "─".repeat(80),
|
|
286
312
|
color: C.border,
|
|
@@ -357,7 +383,7 @@ export async function launchTrending(): Promise<void> {
|
|
|
357
383
|
paddingX: 1,
|
|
358
384
|
});
|
|
359
385
|
const footerText = new TextRenderable(renderer, {
|
|
360
|
-
content: " ↑↓ navigate ↵ open / search 1-
|
|
386
|
+
content: " ↑↓ navigate ↵ open / search 1-3 tab q quit r refresh",
|
|
361
387
|
color: C.muted,
|
|
362
388
|
height: 1,
|
|
363
389
|
});
|
package/src/tui.ts
CHANGED
|
@@ -457,28 +457,28 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
457
457
|
for (let i = 0; i < landingOptions.length; i++) {
|
|
458
458
|
const opt = landingOptions[i];
|
|
459
459
|
const card = new BoxRenderable(renderer, {
|
|
460
|
-
flexDirection: "
|
|
461
|
-
height:
|
|
460
|
+
flexDirection: "column",
|
|
461
|
+
height: 5,
|
|
462
462
|
width: "60%",
|
|
463
463
|
marginTop: 1,
|
|
464
|
-
|
|
464
|
+
border: true,
|
|
465
465
|
borderColor: i === 0 ? colors.accent : colors.border,
|
|
466
|
-
borderWidth: 1,
|
|
467
466
|
paddingX: 1,
|
|
468
|
-
alignItems: "center",
|
|
469
467
|
focusable: false,
|
|
470
468
|
});
|
|
471
469
|
landingCards.push(card);
|
|
472
470
|
landingBox.add(card);
|
|
473
471
|
|
|
472
|
+
const spacerTop = new TextRenderable(renderer, { content: "", height: 1 });
|
|
474
473
|
const titleLine = new TextRenderable(renderer, {
|
|
475
474
|
content: opt.title,
|
|
476
475
|
color: i === 0 ? colors.accent : colors.text,
|
|
477
|
-
backgroundColor: undefined,
|
|
478
476
|
height: 1,
|
|
479
|
-
flexGrow: 1,
|
|
480
477
|
});
|
|
478
|
+
const spacerBot = new TextRenderable(renderer, { content: "", height: 1 });
|
|
479
|
+
card.add(spacerTop);
|
|
481
480
|
card.add(titleLine);
|
|
481
|
+
card.add(spacerBot);
|
|
482
482
|
}
|
|
483
483
|
|
|
484
484
|
const landingHint = new TextRenderable(renderer, {
|
|
@@ -503,38 +503,12 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
503
503
|
|
|
504
504
|
function updateLandingCards() {
|
|
505
505
|
for (let i = 0; i < landingCards.length; i++) {
|
|
506
|
-
const opt = landingOptions[i];
|
|
507
506
|
const isSel = i === landingSelected;
|
|
508
|
-
landingCards[i].backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
509
507
|
landingCards[i].borderColor = isSel ? colors.accent : colors.border;
|
|
510
|
-
landingCards[i].borderWidth = isSel ? 1 : 0;
|
|
511
508
|
const children = landingCards[i].children ?? [];
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const iconTextChild = iconChildren[0] as TextRenderable;
|
|
516
|
-
if (iconTextChild) {
|
|
517
|
-
iconTextChild.color = isSel ? colors.accent : colors.text;
|
|
518
|
-
iconTextChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
519
|
-
}
|
|
520
|
-
const iconSpacerChild = iconChildren[1] as TextRenderable;
|
|
521
|
-
if (iconSpacerChild) {
|
|
522
|
-
iconSpacerChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
if (children[1]) {
|
|
526
|
-
const contentCol = children[1] as BoxRenderable;
|
|
527
|
-
const contentChildren = contentCol.children ?? [];
|
|
528
|
-
const titleChild = contentChildren[0] as TextRenderable;
|
|
529
|
-
if (titleChild) {
|
|
530
|
-
titleChild.color = isSel ? colors.accent : colors.text;
|
|
531
|
-
titleChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
532
|
-
}
|
|
533
|
-
const descChild = contentChildren[1] as TextRenderable;
|
|
534
|
-
if (descChild) {
|
|
535
|
-
descChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
536
|
-
}
|
|
537
|
-
const hintChild = contentChildren[2] as TextRenderable;
|
|
509
|
+
const titleChild = children[1] as TextRenderable;
|
|
510
|
+
if (titleChild) {
|
|
511
|
+
titleChild.color = isSel ? colors.accent : colors.text;
|
|
538
512
|
}
|
|
539
513
|
}
|
|
540
514
|
}
|
|
@@ -2041,6 +2015,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
2041
2015
|
searchBox.visible = false;
|
|
2042
2016
|
toolbarText.visible = false;
|
|
2043
2017
|
trendingTabBox.visible = true;
|
|
2018
|
+
body.visible = true;
|
|
2044
2019
|
renderTrendingTabs();
|
|
2045
2020
|
resultsSelect.options = [
|
|
2046
2021
|
{ name: " Loading trending...", description: "", value: null },
|
|
@@ -2686,6 +2661,18 @@ ${pack.description ?? ""}`;
|
|
|
2686
2661
|
}
|
|
2687
2662
|
}
|
|
2688
2663
|
|
|
2664
|
+
// 'c' toggles compare on selected repo (when not typing in search)
|
|
2665
|
+
if (key.name === "c" && !searchInput.focused) {
|
|
2666
|
+
if (currentOverlay === "compare") {
|
|
2667
|
+
showOverlay("none");
|
|
2668
|
+
} else {
|
|
2669
|
+
toggleCompareOnSelected();
|
|
2670
|
+
refreshCompare();
|
|
2671
|
+
showOverlay("compare");
|
|
2672
|
+
}
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2689
2676
|
// '?' / Ctrl+H toggle help overlay (fast path)
|
|
2690
2677
|
if (key.name === "?" || (key.name === "h" && key.ctrl)) {
|
|
2691
2678
|
showOverlay("help");
|
package/vendor/bun.exe
DELETED
|
Binary file
|