github-search-cli 9.7.0 → 9.8.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/CHANGELOG.md +63 -0
- package/README.md +17 -16
- package/bin/ghfind.js +5 -5
- package/dist/cli.js +1068 -342
- package/package.json +9 -3
- package/release-notes/9.7.1.md +62 -0
- package/release-notes/9.8.0.md +55 -0
- package/scripts/build.ts +1 -1
- package/scripts/postinstall.js +8 -8
- package/src/cli.ts +21 -13
- package/src/compare.ts +5 -5
- package/src/config.ts +3 -3
- package/src/deepdive.ts +0 -1
- package/src/doctor.ts +144 -8
- package/src/error-report.ts +14 -8
- package/src/export.ts +1 -1
- package/src/help.ts +2 -1
- package/src/history.ts +5 -5
- package/src/image-loader.ts +248 -0
- package/src/image-render.ts +209 -0
- package/src/init.ts +2 -2
- package/src/login.ts +4 -4
- package/src/markdown-images.ts +123 -0
- package/src/markdown-view.ts +300 -0
- package/src/mcp-server.ts +4 -3
- package/src/open-url.ts +16 -5
- package/src/output.ts +1 -1
- package/src/package-browser.ts +1 -1
- package/src/png.ts +260 -0
- package/src/releases.ts +1 -14
- package/src/search-state.ts +1 -4
- package/src/search.ts +3 -4
- package/src/share.ts +2 -2
- package/src/storage.ts +4 -4
- package/src/themes.ts +1 -0
- package/src/trending-parser.ts +3 -4
- package/src/trending.ts +7 -7
- package/src/tui.ts +115 -83
- package/src/update-check.ts +96 -34
- package/src/version.ts +3 -3
- package/src/watch.ts +1 -1
- package/src/markdown-render.ts +0 -211
package/src/search.ts
CHANGED
|
@@ -181,7 +181,7 @@ export function suggestFor(input: string): string[] {
|
|
|
181
181
|
return QUALIFIER_SUGGESTIONS[trimmed];
|
|
182
182
|
}
|
|
183
183
|
const matches = KNOWN_QUALIFIERS.filter((k) => k.startsWith(trimmed));
|
|
184
|
-
return matches.map((k) => k
|
|
184
|
+
return matches.map((k) => `${k}:`);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
export function tokenize(input: string): string[] {
|
|
@@ -329,7 +329,6 @@ export function githubSortParam(sort: SortStrategy): {
|
|
|
329
329
|
return { sort: "forks", order: "desc" };
|
|
330
330
|
case "updated":
|
|
331
331
|
return { sort: "updated", order: "desc" };
|
|
332
|
-
case "best-match":
|
|
333
332
|
default:
|
|
334
333
|
return {};
|
|
335
334
|
}
|
|
@@ -359,7 +358,6 @@ export function rankRepos(repos: Repo[], strategy: SortStrategy): Repo[] {
|
|
|
359
358
|
case "updated":
|
|
360
359
|
copy.sort(byKey((r) => Date.parse(r.updatedAt)));
|
|
361
360
|
break;
|
|
362
|
-
case "best-match":
|
|
363
361
|
default:
|
|
364
362
|
break;
|
|
365
363
|
}
|
|
@@ -473,7 +471,7 @@ export class GitHubSearchAdapter implements SearchAdapter {
|
|
|
473
471
|
if (
|
|
474
472
|
!parsed ||
|
|
475
473
|
typeof parsed !== "object" ||
|
|
476
|
-
!Array.isArray((parsed as
|
|
474
|
+
!Array.isArray((parsed as { items?: unknown }).items)
|
|
477
475
|
) {
|
|
478
476
|
this.logger.error(
|
|
479
477
|
`[github] unexpected response shape: ${JSON.stringify(parsed).slice(0, 200)}`,
|
|
@@ -630,6 +628,7 @@ function trendingRepoToRepo(r: RawTrendingRepo): Repo {
|
|
|
630
628
|
*/
|
|
631
629
|
export class TrendingAdapter implements SearchAdapter {
|
|
632
630
|
readonly name = "trending";
|
|
631
|
+
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: part of the adapter constructor contract — callers pass a shared logger (search.ts:757) even though this adapter currently logs nothing itself.
|
|
633
632
|
private readonly logger: Logger;
|
|
634
633
|
|
|
635
634
|
constructor(logger: Logger = noopLogger) {
|
package/src/share.ts
CHANGED
|
@@ -19,7 +19,7 @@ export function formatShare(repo: Repo, format: ShareFormat): string {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/** Copy text to clipboard via platform command. */
|
|
22
|
-
import { spawn } from "child_process";
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
23
|
|
|
24
24
|
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
25
25
|
try {
|
|
@@ -46,7 +46,7 @@ export async function copyToClipboard(text: string): Promise<boolean> {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
async function findLinuxClipboard(): Promise<string[] | null> {
|
|
49
|
-
const { execFileSync } = await import("child_process");
|
|
49
|
+
const { execFileSync } = await import("node:child_process");
|
|
50
50
|
try {
|
|
51
51
|
execFileSync("xclip", ["-version"], { stdio: "ignore" });
|
|
52
52
|
return ["xclip", "-selection", "clipboard"];
|
package/src/storage.ts
CHANGED
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
writeFileSync,
|
|
13
13
|
appendFileSync,
|
|
14
14
|
unlinkSync,
|
|
15
|
-
} from "fs";
|
|
16
|
-
import { join } from "path";
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
17
|
import { stateDir } from "./config";
|
|
18
18
|
|
|
19
19
|
/** Ensure the state directory exists. */
|
|
@@ -43,7 +43,7 @@ export function appendJSONL(filename: string, obj: unknown): void {
|
|
|
43
43
|
try {
|
|
44
44
|
appendFileSync(
|
|
45
45
|
join(stateDir(), filename),
|
|
46
|
-
JSON.stringify(obj)
|
|
46
|
+
`${JSON.stringify(obj)}\n`,
|
|
47
47
|
"utf-8",
|
|
48
48
|
);
|
|
49
49
|
} catch {
|
|
@@ -119,7 +119,7 @@ export const memoryStorage = {
|
|
|
119
119
|
},
|
|
120
120
|
appendJSONL(filename: string, obj: unknown): void {
|
|
121
121
|
const existing = memStore.get(filename) ?? "";
|
|
122
|
-
memStore.set(filename, existing + JSON.stringify(obj)
|
|
122
|
+
memStore.set(filename, `${existing + JSON.stringify(obj)}\n`);
|
|
123
123
|
},
|
|
124
124
|
readJSONL<T>(filename: string): T[] {
|
|
125
125
|
const raw = memStore.get(filename);
|
package/src/themes.ts
CHANGED
|
@@ -242,6 +242,7 @@ export async function detectTerminalBackground(
|
|
|
242
242
|
// Reply: ESC ] 11 ; <spec> (BEL | ESC \)
|
|
243
243
|
const onData = (d: Buffer) => {
|
|
244
244
|
buffer += d.toString("latin1");
|
|
245
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: matches raw ANSI OSC-11 escape bytes, which are control characters by definition
|
|
245
246
|
const m = buffer.match(/\x1b\]11;([^\x07\x1b]*)(?:\x07|\x1b\\)/);
|
|
246
247
|
if (m) finish(parseOscColor(m[1]));
|
|
247
248
|
};
|
package/src/trending-parser.ts
CHANGED
|
@@ -35,7 +35,7 @@ function parseRepoLink(block: string): RepoLink | undefined {
|
|
|
35
35
|
// `/login?...&return_to=...` href fails the bare-path requirement and the
|
|
36
36
|
// stargazers/forks links don't carry the heading's class="Link".
|
|
37
37
|
const anchor = block.match(
|
|
38
|
-
/<a[^>]*?href="\/([A-Za-z0-9._-]+)\/([
|
|
38
|
+
/<a[^>]*?href="\/([A-Za-z0-9._-]+)\/([^"?#]+)"[^>]*class="Link"/,
|
|
39
39
|
);
|
|
40
40
|
if (!anchor) return undefined;
|
|
41
41
|
return { owner: anchor[1], name: anchor[2] };
|
|
@@ -43,7 +43,7 @@ function parseRepoLink(block: string): RepoLink | undefined {
|
|
|
43
43
|
|
|
44
44
|
function parseStars(block: string): number {
|
|
45
45
|
const starsMatch = block.match(
|
|
46
|
-
/href="\/[
|
|
46
|
+
/href="\/[^/"]+\/[^/"]+\/stargazers"[^>]*>.*?<\/svg>\s*(\d[\d,]*)/,
|
|
47
47
|
);
|
|
48
48
|
return starsMatch ? parseInt(starsMatch[1].replace(/,/g, ""), 10) : 0;
|
|
49
49
|
}
|
|
@@ -69,10 +69,9 @@ export function parseTrendingHtml(html: string): RawTrendingRepo[] {
|
|
|
69
69
|
const flat = flattenHtml(html);
|
|
70
70
|
const repos: RawTrendingRepo[] = [];
|
|
71
71
|
const articleRe = /<article class="Box-row">(.*?)<\/article>/gi;
|
|
72
|
-
let match: RegExpExecArray | null;
|
|
73
72
|
let rank = 0;
|
|
74
73
|
|
|
75
|
-
|
|
74
|
+
for (const match of flat.matchAll(articleRe)) {
|
|
76
75
|
rank++;
|
|
77
76
|
const block = match[1];
|
|
78
77
|
const link = parseRepoLink(block);
|
package/src/trending.ts
CHANGED
|
@@ -118,13 +118,13 @@ export function formatRepoLine(repo: Repo, rank: number): StyledText {
|
|
|
118
118
|
const rankStr = String(rank).padStart(2, "0");
|
|
119
119
|
const full = `${repo.owner}/${repo.name}`;
|
|
120
120
|
const lc = langColor(repo.language ?? "");
|
|
121
|
-
const
|
|
121
|
+
const _rc = rankColor(rank);
|
|
122
122
|
const star = "★";
|
|
123
123
|
const arrow = repo.score > 0 ? "▲" : repo.score < 0 ? "▼" : "—";
|
|
124
124
|
const growth = `${arrow} ${fmtSigned(repo.score)} today`;
|
|
125
125
|
const desc =
|
|
126
126
|
(repo.description ?? "").length > 60
|
|
127
|
-
? (repo.description ?? "").slice(0, 57)
|
|
127
|
+
? `${(repo.description ?? "").slice(0, 57)}...`
|
|
128
128
|
: (repo.description ?? "");
|
|
129
129
|
const line1 = t`${dim(fg(C.rankBg)(`[${rankStr}]`))} ${bold(fg("#c0caf5")(full))} ${fg(lc)(`● ${repo.language}`)} ${fg(C.gold)(`${star} ${fmtStars(repo.stars)}`)} ${bold(fg(C.green)(growth))}`;
|
|
130
130
|
const line2 = t` ${dim(fg(C.muted)(desc))}`;
|
|
@@ -182,7 +182,7 @@ export async function launchTrending(): Promise<void> {
|
|
|
182
182
|
let currentPeriod = "this week";
|
|
183
183
|
let repos: Repo[] = [];
|
|
184
184
|
let selectedRepoIdx = 0;
|
|
185
|
-
let
|
|
185
|
+
let _isLoading = false;
|
|
186
186
|
// ── Fetch from github.com/trending ──
|
|
187
187
|
async function loadTab(index: number) {
|
|
188
188
|
if (index < 0 || index >= TAB_NAMES.length) return;
|
|
@@ -194,7 +194,7 @@ export async function launchTrending(): Promise<void> {
|
|
|
194
194
|
: since === "weekly"
|
|
195
195
|
? "this week"
|
|
196
196
|
: "this month";
|
|
197
|
-
|
|
197
|
+
_isLoading = true;
|
|
198
198
|
for (const rb of rowBoxes) scrollBox.remove(rb);
|
|
199
199
|
rowBoxes.length = 0;
|
|
200
200
|
rowTexts.length = 0;
|
|
@@ -233,7 +233,7 @@ export async function launchTrending(): Promise<void> {
|
|
|
233
233
|
});
|
|
234
234
|
scrollBox.add(errText);
|
|
235
235
|
}
|
|
236
|
-
|
|
236
|
+
_isLoading = false;
|
|
237
237
|
renderTabBar();
|
|
238
238
|
renderer.requestRender();
|
|
239
239
|
}
|
|
@@ -359,7 +359,7 @@ export async function launchTrending(): Promise<void> {
|
|
|
359
359
|
const lc = langColor(r.language ?? "");
|
|
360
360
|
const desc =
|
|
361
361
|
(r.description ?? "").length > 55
|
|
362
|
-
? (r.description ?? "").slice(0, 52)
|
|
362
|
+
? `${(r.description ?? "").slice(0, 52)}...`
|
|
363
363
|
: (r.description ?? "");
|
|
364
364
|
const arrow = r.score > 0 ? "▲" : r.score < 0 ? "▼" : "—";
|
|
365
365
|
const growthStr = `${arrow} ${fmtStars(Math.abs(r.score))} ${currentPeriod}`;
|
|
@@ -373,7 +373,7 @@ export async function launchTrending(): Promise<void> {
|
|
|
373
373
|
fg: rc,
|
|
374
374
|
height: 1,
|
|
375
375
|
});
|
|
376
|
-
const descStr = desc.length > 60 ? desc.slice(0, 57)
|
|
376
|
+
const descStr = desc.length > 60 ? `${desc.slice(0, 57)}...` : desc;
|
|
377
377
|
const pad = " ";
|
|
378
378
|
const line1 = t`${bold(fg("#c0caf5")(nameStr))}${fg(lc)(langStr)}${fg(C.muted)(starsStr)}${bold(fg(C.green)(growthStr))}`;
|
|
379
379
|
const line2 = t`${fg(C.descText)(`${pad}${descStr}`)}`;
|
package/src/tui.ts
CHANGED
|
@@ -38,6 +38,8 @@ import type {
|
|
|
38
38
|
SortStrategy,
|
|
39
39
|
Package,
|
|
40
40
|
SessionState,
|
|
41
|
+
HistoryEntry,
|
|
42
|
+
SavedSearch,
|
|
41
43
|
} from "./types";
|
|
42
44
|
import { fetchOrgProfile, type OrgProfile } from "./org";
|
|
43
45
|
import {
|
|
@@ -54,13 +56,7 @@ import {
|
|
|
54
56
|
import { tabSince, TAB_NAMES, fmtStars } from "./trending";
|
|
55
57
|
import { loadConfig, saveConfig } from "./config";
|
|
56
58
|
import { buildHelpSections, HELP_KEYS_COLUMN } from "./help";
|
|
57
|
-
import {
|
|
58
|
-
SearchCliError,
|
|
59
|
-
NetworkError,
|
|
60
|
-
RateLimitError,
|
|
61
|
-
BadQueryError,
|
|
62
|
-
NoResultsError,
|
|
63
|
-
} from "./errors";
|
|
59
|
+
import { SearchCliError, NoResultsError } from "./errors";
|
|
64
60
|
import {
|
|
65
61
|
appendHistory,
|
|
66
62
|
readHistory,
|
|
@@ -68,12 +64,7 @@ import {
|
|
|
68
64
|
clearHistory,
|
|
69
65
|
rotateHistory,
|
|
70
66
|
} from "./history";
|
|
71
|
-
import {
|
|
72
|
-
getBookmarks,
|
|
73
|
-
isBookmarked,
|
|
74
|
-
toggleBookmark,
|
|
75
|
-
removeBookmark,
|
|
76
|
-
} from "./bookmarks";
|
|
67
|
+
import { getBookmarks, toggleBookmark, removeBookmark } from "./bookmarks";
|
|
77
68
|
import {
|
|
78
69
|
getSavedSearches,
|
|
79
70
|
saveSearch,
|
|
@@ -83,8 +74,9 @@ import {
|
|
|
83
74
|
import { saveSession, restoreSession } from "./session";
|
|
84
75
|
import { fetchDeepDive, buildDeepDiveText } from "./deepdive";
|
|
85
76
|
import { buildComparisonTable } from "./compare";
|
|
86
|
-
import {
|
|
87
|
-
import {
|
|
77
|
+
import { createMarkdownView } from "./markdown-view";
|
|
78
|
+
import { fetchImageBytes, readmeBaseUrl } from "./image-loader";
|
|
79
|
+
import { fetchTopics } from "./explore";
|
|
88
80
|
import { exportToFile, type ExportFormat } from "./output";
|
|
89
81
|
import {
|
|
90
82
|
listThemes,
|
|
@@ -269,8 +261,8 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
269
261
|
let currentPage = 1;
|
|
270
262
|
let totalCount = 0;
|
|
271
263
|
let deepDiveActive = false;
|
|
272
|
-
|
|
273
|
-
|
|
264
|
+
const compareList: Repo[] = [];
|
|
265
|
+
const _quitArmed = false;
|
|
274
266
|
|
|
275
267
|
const header = new TextRenderable(renderer, {
|
|
276
268
|
content:
|
|
@@ -1088,12 +1080,22 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1088
1080
|
paddingLeft: 2,
|
|
1089
1081
|
paddingRight: 2,
|
|
1090
1082
|
});
|
|
1091
|
-
|
|
1083
|
+
// Version header only — the release notes below it are rendered markdown.
|
|
1084
|
+
const updateHeader = new TextRenderable(renderer, {
|
|
1092
1085
|
content: "",
|
|
1093
1086
|
fg: colors.accent,
|
|
1094
1087
|
bg: colors.surfaceDim,
|
|
1095
1088
|
});
|
|
1096
|
-
|
|
1089
|
+
const updateNotes = createMarkdownView({
|
|
1090
|
+
renderer,
|
|
1091
|
+
colors,
|
|
1092
|
+
// Notes ship with the release and are mostly prose/lists; skip the
|
|
1093
|
+
// image pipeline in this narrow panel.
|
|
1094
|
+
images: false,
|
|
1095
|
+
background: colors.surfaceDim,
|
|
1096
|
+
});
|
|
1097
|
+
// ponytail: the notes are markdown now; keep the content a column so
|
|
1098
|
+
// blocks stack and the scroll box can measure them.
|
|
1097
1099
|
const updateScroll = new ScrollBoxRenderable(renderer, {
|
|
1098
1100
|
flexGrow: 1,
|
|
1099
1101
|
backgroundColor: colors.surfaceDim,
|
|
@@ -1196,7 +1198,8 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1196
1198
|
paddingX: 1,
|
|
1197
1199
|
});
|
|
1198
1200
|
updateBox.add(updateScroll);
|
|
1199
|
-
updateScroll.add(
|
|
1201
|
+
updateScroll.add(updateHeader);
|
|
1202
|
+
updateScroll.add(updateNotes.renderable);
|
|
1200
1203
|
updateBox.add(updateOptionsRow);
|
|
1201
1204
|
updateBox.add(updateFooter);
|
|
1202
1205
|
root.add(updateDim);
|
|
@@ -1259,7 +1262,8 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1259
1262
|
function popMenuLevel(): boolean {
|
|
1260
1263
|
if (menuStack.length <= 1) return false;
|
|
1261
1264
|
const prev = menuStack[menuStack.length - 2];
|
|
1262
|
-
const current = menuStack.pop()
|
|
1265
|
+
const current = menuStack.pop();
|
|
1266
|
+
if (!current) return false;
|
|
1263
1267
|
leaderSelect.options = prev.entries.map((e) => ({
|
|
1264
1268
|
name:
|
|
1265
1269
|
e.type === "category" ? ` ${e.icon} ${e.name} \u2192` : ` ${e.name}`,
|
|
@@ -1283,7 +1287,10 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1283
1287
|
function buildMenuHierarchy(): MenuCategory[] {
|
|
1284
1288
|
const hasRepo = (() => {
|
|
1285
1289
|
const opt = resultsSelect.getSelectedOption();
|
|
1286
|
-
return
|
|
1290
|
+
return (
|
|
1291
|
+
opt?.value &&
|
|
1292
|
+
typeof (opt.value as { fullName?: unknown })?.fullName === "string"
|
|
1293
|
+
);
|
|
1287
1294
|
})();
|
|
1288
1295
|
|
|
1289
1296
|
const groups: MenuCategory[] = [];
|
|
@@ -1602,17 +1609,11 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1602
1609
|
if (state.lastInstalledVersion === currentVersion) return;
|
|
1603
1610
|
// We just upgraded — show notes for the new version
|
|
1604
1611
|
const notes = fetchReleaseNotes(currentVersion);
|
|
1605
|
-
// Box is 55% of the terminal; subtract border + padding so wrapped
|
|
1606
|
-
// lines never clip horizontally.
|
|
1607
|
-
const notesWidth = Math.max(
|
|
1608
|
-
30,
|
|
1609
|
-
Math.floor((process.stdout.columns || 80) * 0.55) - 8,
|
|
1610
|
-
);
|
|
1611
|
-
const notesBlock = notes
|
|
1612
|
-
? renderMarkdown(notes, { width: notesWidth })
|
|
1613
|
-
: " No release notes found for this version.";
|
|
1614
1612
|
updateSelectedOption = 0;
|
|
1615
|
-
|
|
1613
|
+
updateHeader.content = ` Updated ghfind ${state.lastInstalledVersion} → ${currentVersion}\n`;
|
|
1614
|
+
updateNotes.setContent(
|
|
1615
|
+
notes ?? "_No release notes found for this version._",
|
|
1616
|
+
);
|
|
1616
1617
|
renderUpdateOptions();
|
|
1617
1618
|
showOverlay("update");
|
|
1618
1619
|
}
|
|
@@ -1630,14 +1631,8 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1630
1631
|
if (latest) {
|
|
1631
1632
|
updateSelectedOption = 0;
|
|
1632
1633
|
const notes = fetchReleaseNotes(latest);
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
Math.floor((process.stdout.columns || 80) * 0.55) - 8,
|
|
1636
|
-
);
|
|
1637
|
-
const notesBlock = notes
|
|
1638
|
-
? renderMarkdown(notes, { width: notesWidth })
|
|
1639
|
-
: ` A new version is available\n\n Current: ${currentVersion}\n Latest: ${latest}\n\n Run "npm install -g ghfind"`;
|
|
1640
|
-
updateText.content = ` A new version is available\n\n Current: ${currentVersion}\n Latest: ${latest}\n\n${notesBlock}`;
|
|
1634
|
+
updateHeader.content = ` A new version is available\n\n Current: ${currentVersion}\n Latest: ${latest}\n`;
|
|
1635
|
+
updateNotes.setContent(notes ?? "Run `npm install -g ghfind` to update.");
|
|
1641
1636
|
renderUpdateOptions();
|
|
1642
1637
|
showOverlay("update");
|
|
1643
1638
|
} else {
|
|
@@ -1694,7 +1689,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1694
1689
|
if (currentQueryInput) {
|
|
1695
1690
|
const name =
|
|
1696
1691
|
currentQueryInput.length > 40
|
|
1697
|
-
? currentQueryInput.slice(0, 37)
|
|
1692
|
+
? `${currentQueryInput.slice(0, 37)}...`
|
|
1698
1693
|
: currentQueryInput;
|
|
1699
1694
|
saveSearch(
|
|
1700
1695
|
name,
|
|
@@ -1857,15 +1852,22 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1857
1852
|
},
|
|
1858
1853
|
},
|
|
1859
1854
|
});
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1855
|
+
// Full markdown rendering (headings, tables, code, inline images).
|
|
1856
|
+
const readmeView = createMarkdownView({
|
|
1857
|
+
renderer,
|
|
1858
|
+
colors,
|
|
1859
|
+
loadImage: (url) => fetchImageBytes(url, { token: githubToken }),
|
|
1860
|
+
getImageWidth: () => Math.max(24, (process.stdout.columns || 80) - 12),
|
|
1864
1861
|
});
|
|
1865
|
-
readmeScroll.add(
|
|
1862
|
+
readmeScroll.add(readmeView.renderable);
|
|
1866
1863
|
readmeBox.add(readmeScroll);
|
|
1864
|
+
|
|
1865
|
+
const readmeFooterText = () =>
|
|
1866
|
+
` ↑↓/jk scroll PgUp/PgDn page i images: ${
|
|
1867
|
+
readmeView.imagesEnabled ? "on" : "off"
|
|
1868
|
+
} Esc/q close`;
|
|
1867
1869
|
const readmeFooter = new TextRenderable(renderer, {
|
|
1868
|
-
content:
|
|
1870
|
+
content: readmeFooterText(),
|
|
1869
1871
|
fg: colors.muted,
|
|
1870
1872
|
bg: colors.bg,
|
|
1871
1873
|
height: 1,
|
|
@@ -1881,33 +1883,36 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1881
1883
|
setStatus("No repo selected");
|
|
1882
1884
|
return;
|
|
1883
1885
|
}
|
|
1884
|
-
|
|
1886
|
+
readmeView.setContent(`Loading README for ${repo.fullName}…`);
|
|
1887
|
+
readmeFooter.content = readmeFooterText();
|
|
1888
|
+
readmeScroll.scrollTop = 0;
|
|
1885
1889
|
showOverlay("readme");
|
|
1886
1890
|
try {
|
|
1887
1891
|
const headers: Record<string, string> = { "User-Agent": "ghfind/1.0" };
|
|
1888
1892
|
if (githubToken) headers.Authorization = `Bearer ${githubToken}`;
|
|
1889
1893
|
let text = "";
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1894
|
+
let sourceUrl = "";
|
|
1895
|
+
for (const [branch, file] of [
|
|
1896
|
+
["main", "README.md"],
|
|
1897
|
+
["master", "README.md"],
|
|
1898
|
+
["main", "README.rst"],
|
|
1899
|
+
] as const) {
|
|
1900
|
+
// The README's own URL doubles as the base for relative image paths.
|
|
1901
|
+
const url = readmeBaseUrl(repo.owner, repo.name, branch, file);
|
|
1902
|
+
const r = await fetch(url, { headers });
|
|
1898
1903
|
if (r.ok) {
|
|
1899
1904
|
text = await r.text();
|
|
1905
|
+
sourceUrl = url;
|
|
1900
1906
|
break;
|
|
1901
1907
|
}
|
|
1902
1908
|
}
|
|
1903
1909
|
if (!text) {
|
|
1904
|
-
|
|
1910
|
+
readmeView.setContent(`_(no README found for ${repo.fullName})_`);
|
|
1905
1911
|
} else {
|
|
1906
|
-
|
|
1907
|
-
readmeText.content = renderMarkdown(text, { width: termW });
|
|
1912
|
+
readmeView.setContent(text, sourceUrl);
|
|
1908
1913
|
}
|
|
1909
1914
|
} catch {
|
|
1910
|
-
|
|
1915
|
+
readmeView.setContent("_Failed to load README._");
|
|
1911
1916
|
}
|
|
1912
1917
|
renderer.requestRender();
|
|
1913
1918
|
}
|
|
@@ -1925,16 +1930,11 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1925
1930
|
statusBar.content = ` ${text}`;
|
|
1926
1931
|
renderer.requestRender();
|
|
1927
1932
|
});
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
if (currentOverlay === "none") {
|
|
1934
|
-
setStatus(nextTip());
|
|
1935
|
-
}
|
|
1936
|
-
}, 2000);
|
|
1937
|
-
}
|
|
1933
|
+
setTimeout(() => {
|
|
1934
|
+
if (currentOverlay === "none") {
|
|
1935
|
+
setStatus(nextTip());
|
|
1936
|
+
}
|
|
1937
|
+
}, 2000);
|
|
1938
1938
|
|
|
1939
1939
|
// ── Helper functions ────────────────────────────────────────────────
|
|
1940
1940
|
|
|
@@ -2097,7 +2097,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
2097
2097
|
val >= 1000
|
|
2098
2098
|
? `${(val / 1000).toFixed(1).replace(/\.0$/, "")}k`
|
|
2099
2099
|
: String(val);
|
|
2100
|
-
label = label.padStart(5)
|
|
2100
|
+
label = `${label.padStart(5)} `;
|
|
2101
2101
|
}
|
|
2102
2102
|
lines.push(`${label}┆${chars.join("")}`);
|
|
2103
2103
|
}
|
|
@@ -2515,6 +2515,24 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
2515
2515
|
}
|
|
2516
2516
|
});
|
|
2517
2517
|
|
|
2518
|
+
// Pasted content flows through "input"; purge JSON-shaped blobs that
|
|
2519
|
+
// slipped into the query (a search line is never raw JSON: objects/arrays
|
|
2520
|
+
// always arrive as paste payloads from API responses / deep-dive output).
|
|
2521
|
+
searchInput.on("input", () => {
|
|
2522
|
+
const v = searchInput.value;
|
|
2523
|
+
if (v.length > 0) {
|
|
2524
|
+
const t = v.trim();
|
|
2525
|
+
if (
|
|
2526
|
+
(t.startsWith("{") && t.endsWith("}")) ||
|
|
2527
|
+
(t.startsWith("[") && t.endsWith("]"))
|
|
2528
|
+
) {
|
|
2529
|
+
searchInput.value = "";
|
|
2530
|
+
setStatus("Ignored JSON paste — search queries accept plain text");
|
|
2531
|
+
renderer.requestRender();
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2518
2536
|
// Navigate results → update detail pane
|
|
2519
2537
|
resultsSelect.on("selectionChanged", () => {
|
|
2520
2538
|
const opt = resultsSelect.getSelectedOption();
|
|
@@ -2616,6 +2634,14 @@ ${pack.description ?? ""}`;
|
|
|
2616
2634
|
|
|
2617
2635
|
// README viewer: scrollable, Esc/q already handled above
|
|
2618
2636
|
if (currentOverlay === "readme") {
|
|
2637
|
+
if (key.name === "i") {
|
|
2638
|
+
readmeView.setImagesEnabled(!readmeView.imagesEnabled);
|
|
2639
|
+
readmeFooter.content = readmeFooterText();
|
|
2640
|
+
setStatus(
|
|
2641
|
+
readmeView.imagesEnabled ? "README images on" : "README images off",
|
|
2642
|
+
);
|
|
2643
|
+
renderer.requestRender();
|
|
2644
|
+
}
|
|
2619
2645
|
return;
|
|
2620
2646
|
}
|
|
2621
2647
|
|
|
@@ -2624,7 +2650,8 @@ ${pack.description ?? ""}`;
|
|
|
2624
2650
|
if (key.name === "d") {
|
|
2625
2651
|
const sel = historySelect.getSelectedOption();
|
|
2626
2652
|
if (sel?.value) {
|
|
2627
|
-
|
|
2653
|
+
const idx = (sel.value as { index?: number }).index;
|
|
2654
|
+
if (idx !== undefined) deleteHistoryEntry(idx);
|
|
2628
2655
|
refreshHistory();
|
|
2629
2656
|
setStatus("Deleted history entry");
|
|
2630
2657
|
}
|
|
@@ -2641,8 +2668,9 @@ ${pack.description ?? ""}`;
|
|
|
2641
2668
|
if (key.name === "enter" || key.name === "return") {
|
|
2642
2669
|
const sel = historySelect.getSelectedOption();
|
|
2643
2670
|
if (sel?.value) {
|
|
2644
|
-
const entry = (sel.value as
|
|
2671
|
+
const entry = (sel.value as { entry?: HistoryEntry }).entry;
|
|
2645
2672
|
showOverlay("none");
|
|
2673
|
+
if (!entry) return;
|
|
2646
2674
|
if (entry.mode === "trending") {
|
|
2647
2675
|
if (entry.tab)
|
|
2648
2676
|
trendingTab = entry.tab as (typeof TAB_NAMES)[number];
|
|
@@ -2663,7 +2691,10 @@ ${pack.description ?? ""}`;
|
|
|
2663
2691
|
if (key.name === "d") {
|
|
2664
2692
|
const sel = bookmarksSelect.getSelectedOption();
|
|
2665
2693
|
if (sel?.value) {
|
|
2666
|
-
removeBookmark(
|
|
2694
|
+
removeBookmark(
|
|
2695
|
+
(sel.value as { repo?: { fullName: string } }).repo?.fullName ??
|
|
2696
|
+
"",
|
|
2697
|
+
);
|
|
2667
2698
|
refreshBookmarks();
|
|
2668
2699
|
setStatus("Bookmark removed");
|
|
2669
2700
|
}
|
|
@@ -2672,7 +2703,8 @@ ${pack.description ?? ""}`;
|
|
|
2672
2703
|
}
|
|
2673
2704
|
if (key.name === "enter" || key.name === "return") {
|
|
2674
2705
|
const sel = bookmarksSelect.getSelectedOption();
|
|
2675
|
-
if (sel?.value)
|
|
2706
|
+
if (sel?.value)
|
|
2707
|
+
openUrl((sel.value as { repo?: { url?: string } }).repo?.url ?? "");
|
|
2676
2708
|
return;
|
|
2677
2709
|
}
|
|
2678
2710
|
return;
|
|
@@ -2683,7 +2715,7 @@ ${pack.description ?? ""}`;
|
|
|
2683
2715
|
if (key.name === "d") {
|
|
2684
2716
|
const sel = savedSelect.getSelectedOption();
|
|
2685
2717
|
if (sel?.value) {
|
|
2686
|
-
deleteSavedSearch((sel.value as
|
|
2718
|
+
deleteSavedSearch((sel.value as { name?: string }).name ?? "");
|
|
2687
2719
|
refreshSavedSearches();
|
|
2688
2720
|
setStatus("Saved search deleted");
|
|
2689
2721
|
}
|
|
@@ -2693,7 +2725,7 @@ ${pack.description ?? ""}`;
|
|
|
2693
2725
|
if (key.name === "enter" || key.name === "return") {
|
|
2694
2726
|
const sel = savedSelect.getSelectedOption();
|
|
2695
2727
|
if (sel?.value) {
|
|
2696
|
-
const s = sel.value as
|
|
2728
|
+
const s = sel.value as SavedSearch;
|
|
2697
2729
|
showOverlay("none");
|
|
2698
2730
|
touchSavedSearch(s.name);
|
|
2699
2731
|
if (s.mode === "trending") {
|
|
@@ -2718,9 +2750,9 @@ ${pack.description ?? ""}`;
|
|
|
2718
2750
|
if (key.name === "enter" || key.name === "return") {
|
|
2719
2751
|
const sel = topicsSelect.getSelectedOption();
|
|
2720
2752
|
if (sel?.value) {
|
|
2721
|
-
const topic = sel.value as
|
|
2753
|
+
const topic = sel.value as { name?: string };
|
|
2722
2754
|
showOverlay("none");
|
|
2723
|
-
searchInput.value = `topic:${topic.name}`;
|
|
2755
|
+
searchInput.value = `topic:${topic.name ?? ""}`;
|
|
2724
2756
|
showSearchMode();
|
|
2725
2757
|
doSearch(`topic:${topic.name}`);
|
|
2726
2758
|
}
|
|
@@ -2973,7 +3005,7 @@ ${pack.description ?? ""}`;
|
|
|
2973
3005
|
// Number keys 1-5 switch trending tabs (only in trending mode)
|
|
2974
3006
|
if (/^[1-5]$/.test(key.name)) {
|
|
2975
3007
|
if (currentMode === "trending") {
|
|
2976
|
-
const idx = parseInt(key.name) - 1;
|
|
3008
|
+
const idx = parseInt(key.name, 10) - 1;
|
|
2977
3009
|
if (idx >= 0 && idx < TAB_NAMES.length) {
|
|
2978
3010
|
trendingTab = TAB_NAMES[idx];
|
|
2979
3011
|
loadTrending();
|
|
@@ -3025,16 +3057,16 @@ ${pack.description ?? ""}`;
|
|
|
3025
3057
|
});
|
|
3026
3058
|
|
|
3027
3059
|
// ── Update check (before start) ──────────────────────────────────────
|
|
3028
|
-
const
|
|
3060
|
+
const _currentVersion = getVersion();
|
|
3029
3061
|
// ── Start ──────────────────────────────────────────────────────────
|
|
3030
3062
|
// Check for updates (non-blocking — doesn't delay TUI startup)
|
|
3031
|
-
if (!session
|
|
3063
|
+
if (!session?.mode) {
|
|
3032
3064
|
showLanding(true);
|
|
3033
3065
|
}
|
|
3034
3066
|
checkForUpdateAndShow();
|
|
3035
3067
|
checkPostUpgradePanel();
|
|
3036
3068
|
renderer.start();
|
|
3037
|
-
if (session
|
|
3069
|
+
if (session?.mode) {
|
|
3038
3070
|
if (currentMode === "trending") {
|
|
3039
3071
|
loadTrending();
|
|
3040
3072
|
} else {
|