github-search-cli 9.1.1 → 9.3.0
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 +258 -78
- package/package.json +2 -4
- package/scripts/postinstall.js +10 -7
- package/src/cli.ts +28 -1
- package/src/compare.ts +4 -3
- package/src/help.ts +142 -142
- package/src/markdown-render.ts +216 -0
- 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 +27 -47
- 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
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
import { saveSession, restoreSession } from "./session";
|
|
80
80
|
import { fetchDeepDive, buildDeepDiveText } from "./deepdive";
|
|
81
81
|
import { buildComparisonTable } from "./compare";
|
|
82
|
+
import { renderMarkdown } from "./markdown-render";
|
|
82
83
|
import { fetchTopics, type TopicItem } from "./explore";
|
|
83
84
|
import { exportToFile, type ExportFormat } from "./output";
|
|
84
85
|
import { listThemes, loadTheme } from "./themes";
|
|
@@ -457,28 +458,28 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
457
458
|
for (let i = 0; i < landingOptions.length; i++) {
|
|
458
459
|
const opt = landingOptions[i];
|
|
459
460
|
const card = new BoxRenderable(renderer, {
|
|
460
|
-
flexDirection: "
|
|
461
|
-
height:
|
|
461
|
+
flexDirection: "column",
|
|
462
|
+
height: 5,
|
|
462
463
|
width: "60%",
|
|
463
464
|
marginTop: 1,
|
|
464
|
-
|
|
465
|
+
border: true,
|
|
465
466
|
borderColor: i === 0 ? colors.accent : colors.border,
|
|
466
|
-
borderWidth: 1,
|
|
467
467
|
paddingX: 1,
|
|
468
|
-
alignItems: "center",
|
|
469
468
|
focusable: false,
|
|
470
469
|
});
|
|
471
470
|
landingCards.push(card);
|
|
472
471
|
landingBox.add(card);
|
|
473
472
|
|
|
473
|
+
const spacerTop = new TextRenderable(renderer, { content: "", height: 1 });
|
|
474
474
|
const titleLine = new TextRenderable(renderer, {
|
|
475
475
|
content: opt.title,
|
|
476
476
|
color: i === 0 ? colors.accent : colors.text,
|
|
477
|
-
backgroundColor: undefined,
|
|
478
477
|
height: 1,
|
|
479
|
-
flexGrow: 1,
|
|
480
478
|
});
|
|
479
|
+
const spacerBot = new TextRenderable(renderer, { content: "", height: 1 });
|
|
480
|
+
card.add(spacerTop);
|
|
481
481
|
card.add(titleLine);
|
|
482
|
+
card.add(spacerBot);
|
|
482
483
|
}
|
|
483
484
|
|
|
484
485
|
const landingHint = new TextRenderable(renderer, {
|
|
@@ -503,38 +504,12 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
503
504
|
|
|
504
505
|
function updateLandingCards() {
|
|
505
506
|
for (let i = 0; i < landingCards.length; i++) {
|
|
506
|
-
const opt = landingOptions[i];
|
|
507
507
|
const isSel = i === landingSelected;
|
|
508
|
-
landingCards[i].backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
509
508
|
landingCards[i].borderColor = isSel ? colors.accent : colors.border;
|
|
510
|
-
landingCards[i].borderWidth = isSel ? 1 : 0;
|
|
511
509
|
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;
|
|
510
|
+
const titleChild = children[1] as TextRenderable;
|
|
511
|
+
if (titleChild) {
|
|
512
|
+
titleChild.color = isSel ? colors.accent : colors.text;
|
|
538
513
|
}
|
|
539
514
|
}
|
|
540
515
|
}
|
|
@@ -1735,17 +1710,8 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1735
1710
|
if (!text) {
|
|
1736
1711
|
readmeText.content = ` (no README found for ${repo.fullName})`;
|
|
1737
1712
|
} else {
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
.replace(/```[\s\S]*?```/g, "[code block]")
|
|
1741
|
-
.replace(/#{1,6}\s/g, "")
|
|
1742
|
-
.replace(/\*\*(.+?)\*\*/g, "$1")
|
|
1743
|
-
.replace(/\[(.+?)\]\(.+?\)/g, "$1")
|
|
1744
|
-
.replace(/`([^`]+)`/g, "$1")
|
|
1745
|
-
.split("\n")
|
|
1746
|
-
.map((l) => l.trimEnd())
|
|
1747
|
-
.join("\n");
|
|
1748
|
-
readmeText.content = stripped;
|
|
1713
|
+
const termW = Math.max(40, (process.stdout.columns || 80) - 4);
|
|
1714
|
+
readmeText.content = renderMarkdown(text, { width: termW });
|
|
1749
1715
|
}
|
|
1750
1716
|
} catch {
|
|
1751
1717
|
readmeText.content = " Failed to load README";
|
|
@@ -2041,6 +2007,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
2041
2007
|
searchBox.visible = false;
|
|
2042
2008
|
toolbarText.visible = false;
|
|
2043
2009
|
trendingTabBox.visible = true;
|
|
2010
|
+
body.visible = true;
|
|
2044
2011
|
renderTrendingTabs();
|
|
2045
2012
|
resultsSelect.options = [
|
|
2046
2013
|
{ name: " Loading trending...", description: "", value: null },
|
|
@@ -2671,6 +2638,8 @@ ${pack.description ?? ""}`;
|
|
|
2671
2638
|
showLeaderMenu();
|
|
2672
2639
|
}
|
|
2673
2640
|
return;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2674
2643
|
// 't' toggles theme and persists to config
|
|
2675
2644
|
if (key.name === "t" && !searchInput.focused) {
|
|
2676
2645
|
const current = config.theme;
|
|
@@ -2684,6 +2653,17 @@ ${pack.description ?? ""}`;
|
|
|
2684
2653
|
renderer.requestRender();
|
|
2685
2654
|
return;
|
|
2686
2655
|
}
|
|
2656
|
+
|
|
2657
|
+
// 'c' toggles compare on selected repo (when not typing in search)
|
|
2658
|
+
if (key.name === "c" && !searchInput.focused) {
|
|
2659
|
+
if (currentOverlay === "compare") {
|
|
2660
|
+
showOverlay("none");
|
|
2661
|
+
} else {
|
|
2662
|
+
toggleCompareOnSelected();
|
|
2663
|
+
refreshCompare();
|
|
2664
|
+
showOverlay("compare");
|
|
2665
|
+
}
|
|
2666
|
+
return;
|
|
2687
2667
|
}
|
|
2688
2668
|
|
|
2689
2669
|
// '?' / Ctrl+H toggle help overlay (fast path)
|
package/vendor/bun.exe
DELETED
|
Binary file
|