github-search-cli 0.8.1 → 0.9.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 +111 -39
- package/package.json +16 -11
- package/scripts/brew.rb +32 -0
- package/scripts/build.ts +19 -0
- package/scripts/generate-svg-demo.js +168 -0
- package/scripts/postinstall.js +106 -0
- package/src/bookmarks.ts +4 -17
- package/src/cli.ts +90 -61
- package/src/config.ts +2 -2
- package/src/history.ts +16 -52
- package/src/notifications.ts +5 -12
- package/src/output.ts +124 -55
- package/src/saved-searches.ts +4 -12
- package/src/search-state.ts +229 -0
- package/src/search.ts +99 -2
- package/src/session.ts +6 -18
- package/src/storage.ts +106 -0
- package/src/themes.ts +47 -7
- package/src/trending.ts +27 -150
- package/src/tui.ts +1110 -278
- package/src/types.ts +2 -0
- package/src/update-check.ts +162 -0
- package/src/watch.ts +23 -11
- package/vendor/bun.exe +0 -0
package/src/tui.ts
CHANGED
|
@@ -32,40 +32,105 @@ import {
|
|
|
32
32
|
ScrollBoxRenderable,
|
|
33
33
|
} from "@opentui/core";
|
|
34
34
|
import type { Repo, SearchOptions, SortStrategy } from "./types";
|
|
35
|
-
import {
|
|
36
|
-
|
|
35
|
+
import {
|
|
36
|
+
parseQuery,
|
|
37
|
+
applyFlagFilters,
|
|
38
|
+
validateQuery,
|
|
39
|
+
suggestFor,
|
|
40
|
+
rankRepos,
|
|
41
|
+
createGitHubSearch,
|
|
42
|
+
SearchModule,
|
|
43
|
+
TrendingAdapter,
|
|
44
|
+
type Logger,
|
|
45
|
+
} from "./search";
|
|
46
|
+
import {
|
|
47
|
+
tabSince,
|
|
48
|
+
TAB_NAMES,
|
|
49
|
+
fmtStars,
|
|
50
|
+
} from "./trending";
|
|
37
51
|
import { loadConfig } from "./config";
|
|
38
52
|
import { buildHelpSections, HELP_KEYS_COLUMN } from "./help";
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
import {
|
|
54
|
+
SearchCliError,
|
|
55
|
+
NetworkError,
|
|
56
|
+
RateLimitError,
|
|
57
|
+
BadQueryError,
|
|
58
|
+
NoResultsError,
|
|
59
|
+
} from "./errors";
|
|
60
|
+
import {
|
|
61
|
+
appendHistory,
|
|
62
|
+
readHistory,
|
|
63
|
+
deleteHistoryEntry,
|
|
64
|
+
clearHistory,
|
|
65
|
+
rotateHistory,
|
|
66
|
+
} from "./history";
|
|
67
|
+
import {
|
|
68
|
+
getBookmarks,
|
|
69
|
+
isBookmarked,
|
|
70
|
+
toggleBookmark,
|
|
71
|
+
removeBookmark,
|
|
72
|
+
} from "./bookmarks";
|
|
73
|
+
import {
|
|
74
|
+
getSavedSearches,
|
|
75
|
+
saveSearch,
|
|
76
|
+
deleteSavedSearch,
|
|
77
|
+
touchSavedSearch,
|
|
78
|
+
} from "./saved-searches";
|
|
43
79
|
import { saveSession, restoreSession } from "./session";
|
|
44
80
|
import { fetchDeepDive, buildDeepDiveText } from "./deepdive";
|
|
45
81
|
import { buildComparisonTable } from "./compare";
|
|
46
82
|
import { fetchTopics, type TopicItem } from "./explore";
|
|
47
|
-
import { exportToFile, type ExportFormat } from "./
|
|
83
|
+
import { exportToFile, type ExportFormat } from "./output";
|
|
48
84
|
import { loadTheme } from "./themes";
|
|
49
85
|
import { StatusManager } from "./status";
|
|
50
|
-
import {
|
|
86
|
+
import {
|
|
87
|
+
addNotification,
|
|
88
|
+
getNotifications,
|
|
89
|
+
dismissNotification,
|
|
90
|
+
dismissAll,
|
|
91
|
+
} from "./notifications";
|
|
51
92
|
import { formatShare, copyToClipboard, type ShareFormat } from "./share";
|
|
52
93
|
import { nextTip } from "./tips";
|
|
53
94
|
import { openUrl } from "./open-url";
|
|
95
|
+
import { readFileSync } from "fs";
|
|
96
|
+
import { join, dirname } from "path";
|
|
97
|
+
import { fileURLToPath } from "url";
|
|
98
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
99
|
+
const PKG_DIR = join(__dirname, "..");
|
|
54
100
|
|
|
55
|
-
|
|
101
|
+
let cachedVersion: string | null = null;
|
|
102
|
+
import {
|
|
103
|
+
checkForUpdate,
|
|
104
|
+
shouldCheckUpdate,
|
|
105
|
+
markUpdateChecked,
|
|
106
|
+
snoozeUpdateNotices,
|
|
107
|
+
suppressUpdateNotices,
|
|
108
|
+
performUpdate,
|
|
109
|
+
} from "./update-check";
|
|
110
|
+
import { debugLog } from "./storage";
|
|
56
111
|
const colors = {
|
|
57
|
-
bg: "#
|
|
58
|
-
surface: "#
|
|
59
|
-
|
|
60
|
-
|
|
112
|
+
bg: "#3D3B3B",
|
|
113
|
+
surface: "#4a4848",
|
|
114
|
+
surfaceAlt: "#525050",
|
|
115
|
+
text: "#e0e4f0",
|
|
116
|
+
muted: "#a8a6a6",
|
|
61
117
|
blue: "#89b4fa",
|
|
62
118
|
green: "#a6e3a1",
|
|
63
119
|
yellow: "#f9e2af",
|
|
64
120
|
red: "#f38ba8",
|
|
65
121
|
teal: "#94e2d5",
|
|
66
|
-
|
|
67
|
-
|
|
122
|
+
purple: "#cba6f7",
|
|
123
|
+
orange: "#fab387",
|
|
124
|
+
border: "#1C1C1C",
|
|
125
|
+
borderAlt: "#2a2a2a",
|
|
126
|
+
separator: "#1C1C1C", // Dark thin colorblock - thicker than border, visible separation
|
|
127
|
+
selectionBg: "#2A2A9C",
|
|
68
128
|
selectionText: "#ffffff",
|
|
129
|
+
// Premium accents for the command menu
|
|
130
|
+
accent: "#89b4fa",
|
|
131
|
+
accentDim: "#587cf5",
|
|
132
|
+
surfaceDim: "#1a1919",
|
|
133
|
+
borderAccent: "#1a1919",
|
|
69
134
|
};
|
|
70
135
|
|
|
71
136
|
// ─── Logger ───────────────────────────────────────────────────────────
|
|
@@ -90,11 +155,21 @@ export async function launchBrowser(): Promise<void> {
|
|
|
90
155
|
try {
|
|
91
156
|
renderer = await createCliRenderer();
|
|
92
157
|
} catch (err) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"Install
|
|
97
|
-
);
|
|
158
|
+
const isNode = process.versions.bun === undefined;
|
|
159
|
+
if (isNode) {
|
|
160
|
+
console.error("\n❌ The interactive TUI requires Bun runtime.\n");
|
|
161
|
+
console.error(" Install Bun: curl -fsSL https://bun.sh/install | bash");
|
|
162
|
+
console.error(" Then run: bunx ghfind");
|
|
163
|
+
console.error("\n Or use non-interactive mode (works on Node.js):");
|
|
164
|
+
console.error(" ghfind \"language:Rust\" --json | jq '.[].fullName'");
|
|
165
|
+
console.error(" ghfind --trending --count");
|
|
166
|
+
} else {
|
|
167
|
+
console.error(
|
|
168
|
+
"Failed to start the interactive browser. OpenTUI native backend unavailable.\n",
|
|
169
|
+
`Reason: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
170
|
+
"Install Zig or use a platform with native OpenTUI support.",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
98
173
|
process.exit(1);
|
|
99
174
|
return;
|
|
100
175
|
}
|
|
@@ -121,33 +196,48 @@ export async function launchBrowser(): Promise<void> {
|
|
|
121
196
|
let currentQueryInput = session?.query ?? "";
|
|
122
197
|
let isLoading = false;
|
|
123
198
|
let currentMode: "search" | "trending" = session?.mode ?? "search";
|
|
124
|
-
let trendingTab: typeof TAB_NAMES[number] =
|
|
199
|
+
let trendingTab: (typeof TAB_NAMES)[number] =
|
|
200
|
+
(session?.trendingTab as (typeof TAB_NAMES)[number]) ?? "This Week";
|
|
125
201
|
let trendingPeriod = "this week";
|
|
126
202
|
let graphFullscreen = false;
|
|
127
203
|
let chartCommitData: number[] = [];
|
|
128
|
-
let currentOverlay:
|
|
204
|
+
let currentOverlay:
|
|
205
|
+
| "none"
|
|
206
|
+
| "history"
|
|
207
|
+
| "bookmarks"
|
|
208
|
+
| "saved"
|
|
209
|
+
| "help"
|
|
210
|
+
| "topics"
|
|
211
|
+
| "export"
|
|
212
|
+
| "compare"
|
|
213
|
+
| "notifications"
|
|
214
|
+
| "share"
|
|
215
|
+
| "leader"
|
|
216
|
+
| "readme"
|
|
217
|
+
| "update" = "none";
|
|
129
218
|
let currentPage = 1;
|
|
130
219
|
let totalCount = 0;
|
|
131
220
|
let deepDiveActive = false;
|
|
132
221
|
let compareList: Repo[] = [];
|
|
133
222
|
let quitArmed = false;
|
|
134
223
|
|
|
135
|
-
|
|
136
224
|
// ── Header bar ──────────────────────────────────────────────────────
|
|
137
225
|
const header = new TextRenderable(renderer, {
|
|
138
226
|
content:
|
|
139
|
-
" ghfind — GitHub repo browser [/]search [Space]menu [?]help [q]uit",
|
|
227
|
+
" ghfind — GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
|
|
140
228
|
backgroundColor: colors.bg,
|
|
141
229
|
color: colors.muted,
|
|
142
230
|
height: 1,
|
|
231
|
+
borderBottom: true,
|
|
232
|
+
borderBottomColor: colors.border,
|
|
143
233
|
});
|
|
144
|
-
root.add(header);
|
|
145
234
|
|
|
146
235
|
// ── Trending tab bar (hidden in search mode) ────────────────────────
|
|
147
236
|
const trendingTabBox = new BoxRenderable(renderer, {
|
|
148
237
|
flexDirection: "row",
|
|
149
238
|
height: 1,
|
|
150
|
-
backgroundColor: colors.
|
|
239
|
+
backgroundColor: colors.surface,
|
|
240
|
+
// No border — pure contract-edge separation via text contrast
|
|
151
241
|
paddingX: 1,
|
|
152
242
|
visible: false,
|
|
153
243
|
});
|
|
@@ -180,30 +270,45 @@ export async function launchBrowser(): Promise<void> {
|
|
|
180
270
|
}
|
|
181
271
|
});
|
|
182
272
|
}
|
|
273
|
+
root.add(header);
|
|
183
274
|
root.add(trendingTabBox);
|
|
184
275
|
|
|
185
276
|
// ── Search input row ────────────────────────────────────────────────
|
|
186
277
|
const searchBox = new BoxRenderable(renderer, {
|
|
187
|
-
visible: true,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
278
|
+
visible: true,
|
|
279
|
+
borderBottom: true,
|
|
280
|
+
borderBottomColor: colors.border,
|
|
281
|
+
title: " SEARCH ",
|
|
282
|
+
titleColor: colors.blue,
|
|
283
|
+
width: "100%",
|
|
284
|
+
flexDirection: "row",
|
|
285
|
+
paddingTop: 0,
|
|
286
|
+
paddingBottom: 0,
|
|
287
|
+
backgroundColor: colors.surface,
|
|
192
288
|
});
|
|
193
289
|
const searchInput = new InputRenderable(renderer, {
|
|
194
|
-
placeholder:
|
|
290
|
+
placeholder:
|
|
291
|
+
"Search GitHub repos (e.g. rust cli, or language:Rust stars:>100)",
|
|
195
292
|
value: "",
|
|
293
|
+
backgroundColor: colors.surface,
|
|
294
|
+
textColor: colors.text,
|
|
295
|
+
borderColor: colors.blue,
|
|
296
|
+
selectedBorderColor: colors.blue,
|
|
297
|
+
paddingX: 0,
|
|
298
|
+
flexGrow: 1,
|
|
196
299
|
});
|
|
197
300
|
searchBox.add(searchInput);
|
|
198
301
|
root.add(searchBox);
|
|
199
302
|
|
|
200
303
|
// ── Toolbar (sort + limit indicator) ────────────────────────────────
|
|
201
304
|
const toolbarText = new TextRenderable(renderer, {
|
|
202
|
-
content: formatToolbar(currentSort, currentLimit),
|
|
305
|
+
content: formatToolbar(currentSort, currentLimit, totalCount),
|
|
203
306
|
visible: true, // hidden in trending mode
|
|
204
|
-
backgroundColor: colors.
|
|
307
|
+
backgroundColor: colors.surface, // Surface panel
|
|
205
308
|
color: colors.muted,
|
|
206
309
|
height: 1,
|
|
310
|
+
borderBottom: true,
|
|
311
|
+
borderBottomColor: colors.border,
|
|
207
312
|
paddingX: 1,
|
|
208
313
|
});
|
|
209
314
|
root.add(toolbarText);
|
|
@@ -219,8 +324,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
219
324
|
|
|
220
325
|
// Results pane
|
|
221
326
|
const resultsBox = new BoxRenderable(renderer, {
|
|
222
|
-
|
|
223
|
-
|
|
327
|
+
borderBottom: true,
|
|
328
|
+
borderBottomColor: colors.border,
|
|
224
329
|
title: " Results ",
|
|
225
330
|
titleColor: colors.blue,
|
|
226
331
|
width: "50%",
|
|
@@ -229,11 +334,16 @@ export async function launchBrowser(): Promise<void> {
|
|
|
229
334
|
focusable: false,
|
|
230
335
|
});
|
|
231
336
|
const resultsSelect = new SelectRenderable(renderer, {
|
|
232
|
-
options: [
|
|
337
|
+
options: [
|
|
338
|
+
{
|
|
339
|
+
name: "",
|
|
340
|
+
description: "Type a query and press Enter to search",
|
|
341
|
+
value: null,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
233
344
|
showDescription: false,
|
|
234
345
|
showSelectionIndicator: false,
|
|
235
346
|
flexGrow: 1,
|
|
236
|
-
backgroundColor: colors.bg,
|
|
237
347
|
textColor: colors.text,
|
|
238
348
|
selectedBackgroundColor: colors.selectionBg,
|
|
239
349
|
selectedTextColor: colors.selectionText,
|
|
@@ -245,8 +355,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
245
355
|
|
|
246
356
|
// Detail / Graph pane
|
|
247
357
|
const detailBox = new BoxRenderable(renderer, {
|
|
248
|
-
|
|
249
|
-
|
|
358
|
+
borderBottom: true,
|
|
359
|
+
borderBottomColor: colors.border,
|
|
250
360
|
title: " Details ",
|
|
251
361
|
titleColor: colors.green,
|
|
252
362
|
width: "50%",
|
|
@@ -254,6 +364,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
254
364
|
paddingX: 1,
|
|
255
365
|
paddingY: 1,
|
|
256
366
|
focusable: false,
|
|
367
|
+
backgroundColor: colors.surface,
|
|
257
368
|
});
|
|
258
369
|
const detailText = new TextRenderable(renderer, {
|
|
259
370
|
content: "",
|
|
@@ -265,8 +376,6 @@ export async function launchBrowser(): Promise<void> {
|
|
|
265
376
|
body.add(detailBox);
|
|
266
377
|
root.add(body);
|
|
267
378
|
|
|
268
|
-
|
|
269
|
-
|
|
270
379
|
// ── Overlay system ──────────────────────────────────────────────────
|
|
271
380
|
|
|
272
381
|
// Hide main content so overlays can take 100% of the content area
|
|
@@ -298,7 +407,10 @@ export async function launchBrowser(): Promise<void> {
|
|
|
298
407
|
notifsBox.visible = false;
|
|
299
408
|
shareBox.visible = false;
|
|
300
409
|
leaderBox.visible = false;
|
|
410
|
+
leaderDim.visible = false;
|
|
301
411
|
readmeBox.visible = false;
|
|
412
|
+
updateBox.visible = false;
|
|
413
|
+
updateDim.visible = false;
|
|
302
414
|
|
|
303
415
|
if (type === "none") {
|
|
304
416
|
currentOverlay = "none";
|
|
@@ -307,18 +419,38 @@ export async function launchBrowser(): Promise<void> {
|
|
|
307
419
|
return;
|
|
308
420
|
}
|
|
309
421
|
currentOverlay = type;
|
|
310
|
-
|
|
311
|
-
if (type
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
else if (type === "
|
|
317
|
-
|
|
318
|
-
else if (type === "
|
|
319
|
-
|
|
320
|
-
else if (type === "
|
|
321
|
-
|
|
422
|
+
// Leader menu is a floating overlay — main content stays visible behind it
|
|
423
|
+
if (type !== "leader") {
|
|
424
|
+
hideMainContent();
|
|
425
|
+
}
|
|
426
|
+
if (type === "help") {
|
|
427
|
+
helpBox.visible = true;
|
|
428
|
+
} else if (type === "history") {
|
|
429
|
+
historyBox.visible = true;
|
|
430
|
+
} else if (type === "bookmarks") {
|
|
431
|
+
bookmarksBox.visible = true;
|
|
432
|
+
} else if (type === "saved") {
|
|
433
|
+
savedBox.visible = true;
|
|
434
|
+
} else if (type === "topics") {
|
|
435
|
+
topicsBox.visible = true;
|
|
436
|
+
} else if (type === "export") {
|
|
437
|
+
exportBox.visible = true;
|
|
438
|
+
} else if (type === "compare") {
|
|
439
|
+
compareBox.visible = true;
|
|
440
|
+
} else if (type === "notifications") {
|
|
441
|
+
notifsBox.visible = true;
|
|
442
|
+
} else if (type === "share") {
|
|
443
|
+
shareBox.visible = true;
|
|
444
|
+
} else if (type === "leader") {
|
|
445
|
+
leaderDim.visible = true;
|
|
446
|
+
leaderBox.visible = true;
|
|
447
|
+
} else if (type === "readme") {
|
|
448
|
+
readmeBox.visible = true;
|
|
449
|
+
} else if (type === "update") {
|
|
450
|
+
updateDim.visible = true;
|
|
451
|
+
updateBox.visible = true;
|
|
452
|
+
updateSelect.focus();
|
|
453
|
+
}
|
|
322
454
|
renderer.requestRender();
|
|
323
455
|
}
|
|
324
456
|
|
|
@@ -328,8 +460,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
328
460
|
visible: false,
|
|
329
461
|
flexGrow: 1,
|
|
330
462
|
backgroundColor: colors.bg,
|
|
331
|
-
|
|
332
|
-
|
|
463
|
+
borderBottom: true,
|
|
464
|
+
borderBottomColor: colors.border,
|
|
333
465
|
});
|
|
334
466
|
const helpScroll = new ScrollBoxRenderable(renderer, {
|
|
335
467
|
flexGrow: 1,
|
|
@@ -347,10 +479,12 @@ export async function launchBrowser(): Promise<void> {
|
|
|
347
479
|
});
|
|
348
480
|
for (const section of helpSections) {
|
|
349
481
|
const sectionBox = new BoxRenderable(renderer, {
|
|
350
|
-
|
|
351
|
-
|
|
482
|
+
borderBottom: true,
|
|
483
|
+
borderBottomColor: colors.border,
|
|
352
484
|
title: section.title,
|
|
353
|
-
titleColor:
|
|
485
|
+
titleColor:
|
|
486
|
+
(colors as Record<string, string>)[section.titleColor] ??
|
|
487
|
+
section.titleColor,
|
|
354
488
|
flexDirection: "column",
|
|
355
489
|
paddingY: 0,
|
|
356
490
|
marginLeft: 1,
|
|
@@ -359,16 +493,20 @@ export async function launchBrowser(): Promise<void> {
|
|
|
359
493
|
});
|
|
360
494
|
for (const row of section.rows) {
|
|
361
495
|
const keys = row.keys.padEnd(HELP_KEYS_COLUMN);
|
|
362
|
-
sectionBox.add(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
496
|
+
sectionBox.add(
|
|
497
|
+
new TextRenderable(renderer, {
|
|
498
|
+
content: ` ${keys} ${row.action}`,
|
|
499
|
+
color: colors.text,
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
366
502
|
}
|
|
367
503
|
if (section.note) {
|
|
368
|
-
sectionBox.add(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
504
|
+
sectionBox.add(
|
|
505
|
+
new TextRenderable(renderer, {
|
|
506
|
+
content: ` ${section.note}`,
|
|
507
|
+
color: colors.muted,
|
|
508
|
+
}),
|
|
509
|
+
);
|
|
372
510
|
}
|
|
373
511
|
helpScroll.add(sectionBox);
|
|
374
512
|
}
|
|
@@ -380,8 +518,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
380
518
|
visible: false,
|
|
381
519
|
flexGrow: 1,
|
|
382
520
|
backgroundColor: colors.bg,
|
|
383
|
-
|
|
384
|
-
|
|
521
|
+
borderBottom: true,
|
|
522
|
+
borderBottomColor: colors.border,
|
|
385
523
|
title: " Search History ",
|
|
386
524
|
titleColor: colors.blue,
|
|
387
525
|
});
|
|
@@ -402,7 +540,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
402
540
|
function refreshHistory() {
|
|
403
541
|
const entries = readHistory();
|
|
404
542
|
if (entries.length === 0) {
|
|
405
|
-
historySelect.options = [
|
|
543
|
+
historySelect.options = [
|
|
544
|
+
{ name: "(no history)", description: "", value: null },
|
|
545
|
+
];
|
|
406
546
|
} else {
|
|
407
547
|
historySelect.options = entries.map((e, i) => ({
|
|
408
548
|
name: `${e.query} — ${e.mode}${e.tab ? ` (${e.tab})` : ""}`,
|
|
@@ -418,8 +558,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
418
558
|
visible: false,
|
|
419
559
|
flexGrow: 1,
|
|
420
560
|
backgroundColor: colors.bg,
|
|
421
|
-
|
|
422
|
-
|
|
561
|
+
borderBottom: true,
|
|
562
|
+
borderBottomColor: colors.border,
|
|
423
563
|
title: " Bookmarks ",
|
|
424
564
|
titleColor: colors.green,
|
|
425
565
|
});
|
|
@@ -440,7 +580,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
440
580
|
function refreshBookmarks() {
|
|
441
581
|
const bookmarks = getBookmarks();
|
|
442
582
|
if (bookmarks.length === 0) {
|
|
443
|
-
bookmarksSelect.options = [
|
|
583
|
+
bookmarksSelect.options = [
|
|
584
|
+
{ name: "(no bookmarks)", description: "", value: null },
|
|
585
|
+
];
|
|
444
586
|
} else {
|
|
445
587
|
bookmarksSelect.options = bookmarks.map((b) => ({
|
|
446
588
|
name: `${b.repo.fullName} ★ ${b.repo.stars.toLocaleString()}`,
|
|
@@ -456,8 +598,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
456
598
|
visible: false,
|
|
457
599
|
flexGrow: 1,
|
|
458
600
|
backgroundColor: colors.bg,
|
|
459
|
-
|
|
460
|
-
|
|
601
|
+
borderBottom: true,
|
|
602
|
+
borderBottomColor: colors.border,
|
|
461
603
|
title: " Saved Searches ",
|
|
462
604
|
titleColor: colors.yellow,
|
|
463
605
|
});
|
|
@@ -480,8 +622,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
480
622
|
visible: false,
|
|
481
623
|
flexGrow: 1,
|
|
482
624
|
backgroundColor: colors.bg,
|
|
483
|
-
|
|
484
|
-
|
|
625
|
+
borderBottom: true,
|
|
626
|
+
borderBottomColor: colors.border,
|
|
485
627
|
title: " Topics ",
|
|
486
628
|
titleColor: colors.purple ?? colors.blue,
|
|
487
629
|
});
|
|
@@ -508,7 +650,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
508
650
|
value: t,
|
|
509
651
|
}));
|
|
510
652
|
} catch {
|
|
511
|
-
topicsSelect.options = [
|
|
653
|
+
topicsSelect.options = [
|
|
654
|
+
{ name: "(failed to load topics)", description: "", value: null },
|
|
655
|
+
];
|
|
512
656
|
}
|
|
513
657
|
topicsSelect.setSelectedIndex(0);
|
|
514
658
|
}
|
|
@@ -518,16 +662,28 @@ export async function launchBrowser(): Promise<void> {
|
|
|
518
662
|
visible: false,
|
|
519
663
|
flexGrow: 1,
|
|
520
664
|
backgroundColor: colors.bg,
|
|
521
|
-
|
|
522
|
-
|
|
665
|
+
borderBottom: true,
|
|
666
|
+
borderBottomColor: colors.border,
|
|
523
667
|
title: " Export ",
|
|
524
668
|
titleColor: colors.green,
|
|
525
669
|
});
|
|
526
670
|
const exportSelect = new SelectRenderable(renderer, {
|
|
527
671
|
options: [
|
|
528
|
-
{
|
|
529
|
-
|
|
530
|
-
|
|
672
|
+
{
|
|
673
|
+
name: " JSON",
|
|
674
|
+
description: "Full repo data as JSON array",
|
|
675
|
+
value: "json",
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
name: " CSV",
|
|
679
|
+
description: "Rank, name, stars, forks, language, URL",
|
|
680
|
+
value: "csv",
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
name: " Markdown",
|
|
684
|
+
description: "Pretty table with links (paste into README)",
|
|
685
|
+
value: "markdown",
|
|
686
|
+
},
|
|
531
687
|
{ name: " Plain text", description: "One repo per line", value: "text" },
|
|
532
688
|
],
|
|
533
689
|
showDescription: true,
|
|
@@ -547,8 +703,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
547
703
|
visible: false,
|
|
548
704
|
flexGrow: 1,
|
|
549
705
|
backgroundColor: colors.bg,
|
|
550
|
-
|
|
551
|
-
|
|
706
|
+
borderBottom: true,
|
|
707
|
+
borderBottomColor: colors.border,
|
|
552
708
|
title: " Comparison ",
|
|
553
709
|
titleColor: colors.yellow,
|
|
554
710
|
});
|
|
@@ -565,8 +721,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
565
721
|
visible: false,
|
|
566
722
|
flexGrow: 1,
|
|
567
723
|
backgroundColor: colors.bg,
|
|
568
|
-
|
|
569
|
-
|
|
724
|
+
borderBottom: true,
|
|
725
|
+
borderBottomColor: colors.border,
|
|
570
726
|
title: " Notifications ",
|
|
571
727
|
titleColor: colors.yellow,
|
|
572
728
|
});
|
|
@@ -594,16 +750,28 @@ export async function launchBrowser(): Promise<void> {
|
|
|
594
750
|
visible: false,
|
|
595
751
|
flexGrow: 1,
|
|
596
752
|
backgroundColor: colors.bg,
|
|
597
|
-
|
|
598
|
-
|
|
753
|
+
borderBottom: true,
|
|
754
|
+
borderBottomColor: colors.border,
|
|
599
755
|
title: " Share Repo ",
|
|
600
756
|
titleColor: colors.blue,
|
|
601
757
|
});
|
|
602
758
|
const shareSelect = new SelectRenderable(renderer, {
|
|
603
759
|
options: [
|
|
604
|
-
{
|
|
605
|
-
|
|
606
|
-
|
|
760
|
+
{
|
|
761
|
+
name: " Markdown",
|
|
762
|
+
description: "[owner/repo](url) — description",
|
|
763
|
+
value: "markdown",
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
name: " Plain",
|
|
767
|
+
description: "owner/repo — description — url",
|
|
768
|
+
value: "plain",
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
name: " GitHub CLI",
|
|
772
|
+
description: "gh repo view owner/repo",
|
|
773
|
+
value: "gh-cli",
|
|
774
|
+
},
|
|
607
775
|
{ name: " Short", description: "owner/repo", value: "short" },
|
|
608
776
|
],
|
|
609
777
|
showDescription: true,
|
|
@@ -620,7 +788,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
620
788
|
|
|
621
789
|
function refreshCompare() {
|
|
622
790
|
if (compareList.length < 2) {
|
|
623
|
-
compareText.content =
|
|
791
|
+
compareText.content =
|
|
792
|
+
"Select at least 2 repos to compare (press c to add).";
|
|
624
793
|
} else {
|
|
625
794
|
compareText.content = buildComparisonTable(compareList);
|
|
626
795
|
}
|
|
@@ -629,7 +798,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
629
798
|
function refreshSavedSearches() {
|
|
630
799
|
const saved = getSavedSearches();
|
|
631
800
|
if (saved.length === 0) {
|
|
632
|
-
savedSelect.options = [
|
|
801
|
+
savedSelect.options = [
|
|
802
|
+
{ name: "(no saved searches)", description: "", value: null },
|
|
803
|
+
];
|
|
633
804
|
} else {
|
|
634
805
|
savedSelect.options = saved.map((s) => ({
|
|
635
806
|
name: `${s.name}`,
|
|
@@ -641,96 +812,547 @@ export async function launchBrowser(): Promise<void> {
|
|
|
641
812
|
}
|
|
642
813
|
|
|
643
814
|
// ── Leader menu overlay (Space key) ─────────────────────────────
|
|
815
|
+
// Dim overlay — sits behind the menu, above main content
|
|
816
|
+
const leaderDim = new BoxRenderable(renderer, {
|
|
817
|
+
visible: false,
|
|
818
|
+
position: "absolute",
|
|
819
|
+
top: 0,
|
|
820
|
+
left: 0,
|
|
821
|
+
width: "100%",
|
|
822
|
+
height: "100%",
|
|
823
|
+
backgroundColor: "#00000099", // 60% opacity black (hex alpha: 0x99 ≈ 153/255)
|
|
824
|
+
});
|
|
825
|
+
root.add(leaderDim);
|
|
826
|
+
|
|
644
827
|
const leaderBox = new BoxRenderable(renderer, {
|
|
645
828
|
visible: false,
|
|
646
|
-
|
|
647
|
-
|
|
829
|
+
position: "absolute",
|
|
830
|
+
width: "80%",
|
|
831
|
+
height: 18,
|
|
832
|
+
left: "10%",
|
|
833
|
+
top: "30%",
|
|
834
|
+
backgroundColor: colors.surfaceDim,
|
|
648
835
|
border: true,
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
836
|
+
borderStyle: "rounded",
|
|
837
|
+
borderColor: colors.borderAccent,
|
|
838
|
+
title: " ⚙ Menu ",
|
|
839
|
+
titleColor: colors.accent,
|
|
840
|
+
titleAlignment: "left",
|
|
652
841
|
flexDirection: "column",
|
|
842
|
+
paddingX: 1,
|
|
653
843
|
});
|
|
654
844
|
const leaderSelect = new SelectRenderable(renderer, {
|
|
655
845
|
options: [{ name: "(empty)", description: "", value: null }],
|
|
656
846
|
showDescription: true,
|
|
657
847
|
showSelectionIndicator: true,
|
|
658
848
|
flexGrow: 1,
|
|
659
|
-
backgroundColor: colors.
|
|
849
|
+
backgroundColor: colors.surfaceDim,
|
|
660
850
|
textColor: colors.text,
|
|
661
|
-
|
|
851
|
+
focusedBackgroundColor: colors.surfaceDim,
|
|
852
|
+
focusedTextColor: colors.text,
|
|
853
|
+
selectedBackgroundColor: colors.accent,
|
|
662
854
|
selectedTextColor: colors.selectionText,
|
|
855
|
+
descriptionColor: colors.muted,
|
|
856
|
+
selectedDescriptionColor: colors.selectionText,
|
|
663
857
|
itemSpacing: 0,
|
|
664
858
|
});
|
|
665
859
|
const leaderFooter = new TextRenderable(renderer, {
|
|
666
|
-
content: "
|
|
860
|
+
content: " ↑↓ select Enter run Esc/q close ",
|
|
667
861
|
color: colors.muted,
|
|
668
|
-
backgroundColor: colors.
|
|
862
|
+
backgroundColor: colors.surfaceDim,
|
|
669
863
|
height: 1,
|
|
670
|
-
|
|
864
|
+
borderTop: true,
|
|
865
|
+
borderTopColor: colors.border,
|
|
866
|
+
paddingX: 2,
|
|
671
867
|
});
|
|
672
868
|
leaderBox.add(leaderSelect);
|
|
673
869
|
leaderBox.add(leaderFooter);
|
|
870
|
+
root.add(leaderDim);
|
|
674
871
|
root.add(leaderBox);
|
|
675
872
|
|
|
676
|
-
|
|
873
|
+
// ── Update panel overlay ────────────────────────────────────────
|
|
874
|
+
const updateDim = new BoxRenderable(renderer, {
|
|
875
|
+
visible: false,
|
|
876
|
+
position: "absolute",
|
|
877
|
+
top: 0,
|
|
878
|
+
left: 0,
|
|
879
|
+
width: "100%",
|
|
880
|
+
height: "100%",
|
|
881
|
+
backgroundColor: "#00000088",
|
|
882
|
+
});
|
|
677
883
|
|
|
678
|
-
|
|
679
|
-
const items: LeaderItem[] = [];
|
|
884
|
+
root.add(updateDim);
|
|
680
885
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
886
|
+
const updateBox = new BoxRenderable(renderer, {
|
|
887
|
+
visible: false,
|
|
888
|
+
position: "absolute",
|
|
889
|
+
width: "55%",
|
|
890
|
+
height: 16,
|
|
891
|
+
left: "22.5%",
|
|
892
|
+
top: "25%",
|
|
893
|
+
backgroundColor: colors.surfaceDim,
|
|
894
|
+
border: true,
|
|
895
|
+
borderStyle: "rounded",
|
|
896
|
+
borderColor: colors.border,
|
|
897
|
+
title: " ghfind ",
|
|
898
|
+
titleColor: colors.accent,
|
|
899
|
+
titleAlignment: "left",
|
|
900
|
+
flexDirection: "column",
|
|
901
|
+
paddingX: 2,
|
|
902
|
+
paddingLeft: 2,
|
|
903
|
+
paddingRight: 2,
|
|
904
|
+
});
|
|
905
|
+
const updateText = new TextRenderable(renderer, {
|
|
906
|
+
content: "",
|
|
907
|
+
color: colors.accent,
|
|
908
|
+
backgroundColor: colors.surfaceDim,
|
|
909
|
+
});
|
|
694
910
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
911
|
+
// Horizontal option boxes
|
|
912
|
+
let updateSelectedOption = 0;
|
|
913
|
+
|
|
914
|
+
const updateOptionsRow = new BoxRenderable(renderer, {
|
|
915
|
+
flexDirection: "row",
|
|
916
|
+
gap: 1,
|
|
917
|
+
backgroundColor: colors.surfaceDim,
|
|
918
|
+
height: 3,
|
|
919
|
+
width: "100%",
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
const updateNowBox = new BoxRenderable(renderer, {
|
|
923
|
+
border: true,
|
|
924
|
+
borderColor: updateSelectedOption === 0 ? colors.yellow : colors.border,
|
|
925
|
+
backgroundColor: colors.surfaceDim,
|
|
926
|
+
height: 3,
|
|
927
|
+
width: "50%",
|
|
928
|
+
paddingLeft: 1,
|
|
929
|
+
paddingRight: 1,
|
|
930
|
+
});
|
|
931
|
+
const updateNowText = new TextRenderable(renderer, {
|
|
932
|
+
content: " Update Now ",
|
|
933
|
+
color: updateSelectedOption === 0 ? colors.yellow : colors.text,
|
|
934
|
+
backgroundColor: colors.surfaceDim,
|
|
935
|
+
});
|
|
936
|
+
updateNowBox.add(updateNowText);
|
|
937
|
+
|
|
938
|
+
const laterBox = new BoxRenderable(renderer, {
|
|
939
|
+
border: true,
|
|
940
|
+
borderColor: updateSelectedOption === 1 ? colors.yellow : colors.border,
|
|
941
|
+
backgroundColor: colors.surfaceDim,
|
|
942
|
+
height: 3,
|
|
943
|
+
width: "50%",
|
|
944
|
+
paddingLeft: 1,
|
|
945
|
+
paddingRight: 1,
|
|
946
|
+
});
|
|
947
|
+
const laterText = new TextRenderable(renderer, {
|
|
948
|
+
content: " Later",
|
|
949
|
+
color: updateSelectedOption === 1 ? colors.yellow : colors.text,
|
|
950
|
+
backgroundColor: colors.surfaceDim,
|
|
951
|
+
});
|
|
952
|
+
laterBox.add(laterText);
|
|
953
|
+
|
|
954
|
+
const neverBox = new BoxRenderable(renderer, {
|
|
955
|
+
border: true,
|
|
956
|
+
borderColor: updateSelectedOption === 2 ? colors.yellow : colors.border,
|
|
957
|
+
backgroundColor: colors.surfaceDim,
|
|
958
|
+
height: 3,
|
|
959
|
+
width: "50%",
|
|
960
|
+
paddingLeft: 1,
|
|
961
|
+
paddingRight: 1,
|
|
962
|
+
});
|
|
963
|
+
const neverText = new TextRenderable(renderer, {
|
|
964
|
+
content: " Don't show again",
|
|
965
|
+
color: updateSelectedOption === 2 ? colors.yellow : colors.text,
|
|
966
|
+
backgroundColor: colors.surfaceDim,
|
|
967
|
+
});
|
|
968
|
+
neverBox.add(neverText);
|
|
969
|
+
|
|
970
|
+
function renderUpdateOptions() {
|
|
971
|
+
updateNowBox.borderColor = updateSelectedOption === 0 ? colors.yellow : colors.border;
|
|
972
|
+
laterBox.borderColor = updateSelectedOption === 1 ? colors.yellow : colors.border;
|
|
973
|
+
neverBox.borderColor = updateSelectedOption === 2 ? colors.yellow : colors.border;
|
|
974
|
+
updateNowText.color = updateSelectedOption === 0 ? colors.yellow : colors.text;
|
|
975
|
+
laterText.color = updateSelectedOption === 1 ? colors.yellow : colors.text;
|
|
976
|
+
neverText.color = updateSelectedOption === 2 ? colors.yellow : colors.text;
|
|
977
|
+
renderer.requestRender();
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
updateOptionsRow.add(updateNowBox);
|
|
981
|
+
updateOptionsRow.add(laterBox);
|
|
982
|
+
|
|
983
|
+
const updateFooter = new TextRenderable(renderer, {
|
|
984
|
+
content: " ←→ select Enter Esc/q close",
|
|
985
|
+
color: colors.muted,
|
|
986
|
+
backgroundColor: colors.surfaceDim,
|
|
987
|
+
height: 1,
|
|
988
|
+
paddingX: 1,
|
|
989
|
+
});
|
|
990
|
+
updateBox.add(updateText);
|
|
991
|
+
updateBox.add(updateOptionsRow);
|
|
992
|
+
updateBox.add(updateFooter);
|
|
993
|
+
root.add(updateDim);
|
|
994
|
+
root.add(updateBox);
|
|
995
|
+
|
|
996
|
+
// ── Hierarchical menu types ──────────────────────────────────────
|
|
997
|
+
interface MenuAction {
|
|
998
|
+
type: "action";
|
|
999
|
+
name: string;
|
|
1000
|
+
description: string;
|
|
1001
|
+
action: () => void;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
interface MenuCategory {
|
|
1005
|
+
type: "category";
|
|
1006
|
+
name: string;
|
|
1007
|
+
description: string;
|
|
1008
|
+
icon: string;
|
|
1009
|
+
children: MenuEntry[];
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
type MenuEntry = MenuAction | MenuCategory;
|
|
1013
|
+
|
|
1014
|
+
// ── Submenu navigation ──────────────────────────────────────────
|
|
1015
|
+
interface MenuLevel {
|
|
1016
|
+
title: string;
|
|
1017
|
+
parentTitle: string;
|
|
1018
|
+
entries: MenuEntry[];
|
|
1019
|
+
selectedIndex: number;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const menuStack: MenuLevel[] = [];
|
|
1023
|
+
|
|
1024
|
+
function pushMenuLevel(
|
|
1025
|
+
title: string,
|
|
1026
|
+
parentTitle: string,
|
|
1027
|
+
entries: MenuEntry[],
|
|
1028
|
+
selectIndex = 0,
|
|
1029
|
+
) {
|
|
1030
|
+
menuStack.push({
|
|
1031
|
+
title,
|
|
1032
|
+
parentTitle,
|
|
1033
|
+
entries,
|
|
1034
|
+
selectedIndex: leaderSelect.getSelectedIndex?.(),
|
|
1035
|
+
});
|
|
1036
|
+
leaderSelect.options = entries.map((e) => ({
|
|
1037
|
+
name:
|
|
1038
|
+
e.type === "category" ? ` ${e.icon} ${e.name} \u2192` : ` ${e.name}`,
|
|
1039
|
+
description: e.description,
|
|
1040
|
+
value: e,
|
|
1041
|
+
}));
|
|
1042
|
+
leaderSelect.setSelectedIndex(selectIndex);
|
|
1043
|
+
leaderBox.title = title;
|
|
1044
|
+
const isTop = menuStack.length === 1;
|
|
1045
|
+
leaderFooter.content = isTop
|
|
1046
|
+
? " \u2191\u2193 select \u2192 open category Enter run Esc/q close"
|
|
1047
|
+
: " Esc back \u2191\u2193 select Enter run";
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function popMenuLevel(): boolean {
|
|
1051
|
+
if (menuStack.length <= 1) return false;
|
|
1052
|
+
const prev = menuStack[menuStack.length - 2];
|
|
1053
|
+
const current = menuStack.pop()!;
|
|
1054
|
+
leaderSelect.options = prev.entries.map((e) => ({
|
|
1055
|
+
name:
|
|
1056
|
+
e.type === "category" ? ` ${e.icon} ${e.name} \u2192` : ` ${e.name}`,
|
|
1057
|
+
description: e.description,
|
|
1058
|
+
value: e,
|
|
1059
|
+
}));
|
|
1060
|
+
leaderSelect.setSelectedIndex(current.selectedIndex);
|
|
1061
|
+
leaderBox.title = prev.title;
|
|
1062
|
+
const isTop = menuStack.length === 1;
|
|
1063
|
+
leaderFooter.content = isTop
|
|
1064
|
+
? " \u2191\u2193 select \u2192 open category Enter run Esc/q close"
|
|
1065
|
+
: " Esc back \u2191\u2193 select Enter run";
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function resetMenu() {
|
|
1070
|
+
menuStack.length = 0;
|
|
1071
|
+
}
|
|
703
1072
|
|
|
704
|
-
|
|
1073
|
+
// ── Build hierarchical menu ─────────────────────────────────────
|
|
1074
|
+
function buildMenuHierarchy(): MenuCategory[] {
|
|
705
1075
|
const hasRepo = (() => {
|
|
706
1076
|
const opt = resultsSelect.getSelectedOption();
|
|
707
1077
|
return opt?.value && typeof (opt.value as any)?.fullName === "string";
|
|
708
1078
|
})();
|
|
709
1079
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1080
|
+
const groups: MenuCategory[] = [];
|
|
1081
|
+
|
|
1082
|
+
// Search group
|
|
1083
|
+
const searchItems: MenuEntry[] = [];
|
|
1084
|
+
if (currentMode === "search") {
|
|
1085
|
+
searchItems.push(
|
|
1086
|
+
{
|
|
1087
|
+
type: "action",
|
|
1088
|
+
name: "Sort",
|
|
1089
|
+
description: `Cycle sort (current: ${currentSort})`,
|
|
1090
|
+
action: () => changeSort(),
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
type: "action",
|
|
1094
|
+
name: "Limit",
|
|
1095
|
+
description: `Cycle result limit (current: ${currentLimit})`,
|
|
1096
|
+
action: () => changeLimit(),
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
type: "action",
|
|
1100
|
+
name: "Refresh",
|
|
1101
|
+
description: "Re-run current search",
|
|
1102
|
+
action: () => {
|
|
1103
|
+
if (currentQueryInput) doSearch(currentQueryInput);
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
{
|
|
1107
|
+
type: "action",
|
|
1108
|
+
name: "Save search",
|
|
1109
|
+
description: "Save current query as a named search",
|
|
1110
|
+
action: () => saveCurrentSearch(),
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
type: "action",
|
|
1114
|
+
name: "Saved searches",
|
|
1115
|
+
description: "Load a saved search",
|
|
1116
|
+
action: () => {
|
|
1117
|
+
refreshSavedSearches();
|
|
1118
|
+
showOverlay("saved");
|
|
1119
|
+
},
|
|
1120
|
+
},
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
if (searchItems.length > 0) {
|
|
1124
|
+
groups.push({
|
|
1125
|
+
type: "category",
|
|
1126
|
+
name: "Search",
|
|
1127
|
+
description: "Sort, limit, refresh, save searches",
|
|
1128
|
+
icon: "\uD83D\uDD0D",
|
|
1129
|
+
children: searchItems,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Repo group
|
|
1134
|
+
const repoItems: MenuEntry[] = [];
|
|
1135
|
+
if (hasRepo) {
|
|
1136
|
+
repoItems.push(
|
|
1137
|
+
{
|
|
1138
|
+
type: "action",
|
|
1139
|
+
name: "Open in browser",
|
|
1140
|
+
description: "Open selected repo in browser",
|
|
1141
|
+
action: () => openUrlIfSelected(),
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
type: "action",
|
|
1145
|
+
name: "Readme",
|
|
1146
|
+
description: "Full README viewer",
|
|
1147
|
+
action: () => {
|
|
1148
|
+
showReadme();
|
|
1149
|
+
},
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
type: "action",
|
|
1153
|
+
name: "Activity graph",
|
|
1154
|
+
description: "Toggle commit chart fullscreen",
|
|
1155
|
+
action: () => toggleGraph(),
|
|
1156
|
+
},
|
|
1157
|
+
);
|
|
1158
|
+
if (currentMode === "search") {
|
|
1159
|
+
repoItems.push({
|
|
1160
|
+
type: "action",
|
|
1161
|
+
name: "Deep-dive",
|
|
1162
|
+
description: "Languages, contributors, README excerpt",
|
|
1163
|
+
action: () => {
|
|
1164
|
+
showOverlay("none");
|
|
1165
|
+
triggerDeepDive();
|
|
1166
|
+
},
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
repoItems.push(
|
|
1170
|
+
{
|
|
1171
|
+
type: "action",
|
|
1172
|
+
name: "Bookmark",
|
|
1173
|
+
description: "Save / unsave selected repo",
|
|
1174
|
+
action: () => toggleBookmarkOnSelected(),
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
type: "action",
|
|
1178
|
+
name: "Compare",
|
|
1179
|
+
description: "Add/remove repo to comparison",
|
|
1180
|
+
action: () => toggleCompareOnSelected(),
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
type: "action",
|
|
1184
|
+
name: "Share",
|
|
1185
|
+
description: "Copy repo link to clipboard",
|
|
1186
|
+
action: () => showShareIfRepo(),
|
|
1187
|
+
},
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
if (repoItems.length > 0) {
|
|
1191
|
+
groups.push({
|
|
1192
|
+
type: "category",
|
|
1193
|
+
name: "Repo",
|
|
1194
|
+
description: "Open, readme, bookmark, share, compare",
|
|
1195
|
+
icon: "\uD83D\uDCCB",
|
|
1196
|
+
children: repoItems,
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Navigate group
|
|
1201
|
+
const navItems: MenuEntry[] = [];
|
|
1202
|
+
if (currentMode === "search") {
|
|
1203
|
+
navItems.push({
|
|
1204
|
+
type: "action",
|
|
1205
|
+
name: "Trending",
|
|
1206
|
+
description: "Browse GitHub trending repos",
|
|
1207
|
+
action: () => loadTrending(),
|
|
1208
|
+
});
|
|
1209
|
+
} else {
|
|
1210
|
+
navItems.push({
|
|
1211
|
+
type: "action",
|
|
1212
|
+
name: "Search mode",
|
|
1213
|
+
description: "Switch to query search",
|
|
1214
|
+
action: () => showSearchMode(),
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
navItems.push(
|
|
1218
|
+
{
|
|
1219
|
+
type: "action",
|
|
1220
|
+
name: "History",
|
|
1221
|
+
description: "Search history",
|
|
1222
|
+
action: () => {
|
|
1223
|
+
refreshHistory();
|
|
1224
|
+
showOverlay("history");
|
|
1225
|
+
},
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
type: "action",
|
|
1229
|
+
name: "Bookmarks",
|
|
1230
|
+
description: "Browse saved repos",
|
|
1231
|
+
action: () => {
|
|
1232
|
+
refreshBookmarks();
|
|
1233
|
+
showOverlay("bookmarks");
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1236
|
+
{
|
|
1237
|
+
type: "action",
|
|
1238
|
+
name: "Topics",
|
|
1239
|
+
description: "Browse popular GitHub topics",
|
|
1240
|
+
action: () => {
|
|
1241
|
+
refreshTopics();
|
|
1242
|
+
showOverlay("topics");
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
type: "action",
|
|
1247
|
+
name: "Export",
|
|
1248
|
+
description: "Export results to JSON/CSV/Markdown",
|
|
1249
|
+
action: () => showOverlay("export"),
|
|
1250
|
+
},
|
|
722
1251
|
);
|
|
1252
|
+
groups.push({
|
|
1253
|
+
type: "category",
|
|
1254
|
+
name: "Navigate",
|
|
1255
|
+
description: "Trending, history, bookmarks, topics, export",
|
|
1256
|
+
icon: "\uD83E\uDDED",
|
|
1257
|
+
children: navItems,
|
|
1258
|
+
});
|
|
723
1259
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1260
|
+
// ⚙️ System group
|
|
1261
|
+
const sysItems: MenuEntry[] = [
|
|
1262
|
+
{
|
|
1263
|
+
type: "action",
|
|
1264
|
+
name: "Check for updates",
|
|
1265
|
+
description: "Check npm for a newer version (debug)",
|
|
1266
|
+
action: () => {
|
|
1267
|
+
showOverlay("none");
|
|
1268
|
+
checkForUpdateAndShow(true);
|
|
1269
|
+
},
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
type: "action",
|
|
1273
|
+
name: "Help",
|
|
1274
|
+
description: "Keybindings reference",
|
|
1275
|
+
action: () => showOverlay("help"),
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
type: "action",
|
|
1279
|
+
name: "Notifications",
|
|
1280
|
+
description: "View alerts",
|
|
1281
|
+
action: () => {
|
|
1282
|
+
refreshNotifications();
|
|
1283
|
+
showOverlay("notifications");
|
|
1284
|
+
},
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
type: "action",
|
|
1288
|
+
name: "Compare view",
|
|
1289
|
+
description: "Show side-by-side comparison",
|
|
1290
|
+
action: () => {
|
|
1291
|
+
refreshCompare();
|
|
1292
|
+
showOverlay("compare");
|
|
1293
|
+
},
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
type: "action",
|
|
1297
|
+
name: "Quit",
|
|
1298
|
+
description: "Exit ghfind",
|
|
1299
|
+
action: () => {
|
|
1300
|
+
saveSession({
|
|
1301
|
+
mode: currentMode,
|
|
1302
|
+
query: currentQueryInput,
|
|
1303
|
+
sort: currentSort,
|
|
1304
|
+
limit: currentLimit,
|
|
1305
|
+
trendingTab,
|
|
1306
|
+
});
|
|
1307
|
+
cleanup();
|
|
1308
|
+
},
|
|
1309
|
+
},
|
|
1310
|
+
];
|
|
1311
|
+
groups.push({
|
|
1312
|
+
type: "category",
|
|
1313
|
+
name: "System",
|
|
1314
|
+
description: "Help, notifications, compare, quit",
|
|
1315
|
+
icon: "\u2699\uFE0F",
|
|
1316
|
+
children: sysItems,
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
return groups;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function showLeaderMenu() {
|
|
1323
|
+
resetMenu();
|
|
1324
|
+
const topLevel = buildMenuHierarchy();
|
|
1325
|
+
pushMenuLevel(" ⚙ Menu ", "", topLevel, 0);
|
|
730
1326
|
leaderSelect.focus();
|
|
731
1327
|
showOverlay("leader");
|
|
732
1328
|
}
|
|
733
1329
|
|
|
1330
|
+
async function checkForUpdateAndShow(force: boolean = false) {
|
|
1331
|
+
if (!force && !shouldCheckUpdate()) return;
|
|
1332
|
+
if (cachedVersion === null) {
|
|
1333
|
+
try {
|
|
1334
|
+
const pkg = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf-8"));
|
|
1335
|
+
cachedVersion = pkg.version;
|
|
1336
|
+
} catch {
|
|
1337
|
+
debugLog("Failed to read package.json");
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
const currentVersion = cachedVersion;
|
|
1342
|
+
// ponytail: DEBUG_FORCE_UPDATE lets you test the panel without a real newer version
|
|
1343
|
+
const latest = process.env.DEBUG_FORCE_UPDATE || await checkForUpdate(currentVersion);
|
|
1344
|
+
markUpdateChecked(latest ?? undefined);
|
|
1345
|
+
if (latest) {
|
|
1346
|
+
updateSelectedOption = 0;
|
|
1347
|
+
updateText.content = ` A new version is available\n\n Current: ${currentVersion}\n Latest: ${latest}\n\n Run "npm install -g ghfind"`;
|
|
1348
|
+
renderUpdateOptions();
|
|
1349
|
+
showOverlay("update");
|
|
1350
|
+
} else {
|
|
1351
|
+
// ponytail: debug to file so it's not swallowed by TUI renderer
|
|
1352
|
+
debugLog(`No update available (current: ${currentVersion})`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
734
1356
|
function openUrlIfSelected() {
|
|
735
1357
|
const opt = resultsSelect.getSelectedOption();
|
|
736
1358
|
const repo = opt?.value as Repo | undefined;
|
|
@@ -743,7 +1365,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
743
1365
|
const repo = opt?.value as Repo | undefined;
|
|
744
1366
|
if (repo) {
|
|
745
1367
|
const added = toggleBookmark(repo);
|
|
746
|
-
setStatus(
|
|
1368
|
+
setStatus(
|
|
1369
|
+
added ? `Bookmarked ${repo.fullName}` : `Unbookmarked ${repo.fullName}`,
|
|
1370
|
+
);
|
|
747
1371
|
}
|
|
748
1372
|
}
|
|
749
1373
|
|
|
@@ -757,23 +1381,36 @@ export async function launchBrowser(): Promise<void> {
|
|
|
757
1381
|
setStatus(`Removed ${repo.fullName} from comparison`);
|
|
758
1382
|
} else {
|
|
759
1383
|
compareList.push(repo);
|
|
760
|
-
setStatus(
|
|
1384
|
+
setStatus(
|
|
1385
|
+
`${repo.fullName} added to comparison (${compareList.length} selected)`,
|
|
1386
|
+
);
|
|
761
1387
|
}
|
|
762
1388
|
}
|
|
763
1389
|
|
|
764
1390
|
function showShareIfRepo() {
|
|
765
1391
|
const opt = resultsSelect.getSelectedOption();
|
|
766
1392
|
const repo = opt?.value as Repo | undefined;
|
|
767
|
-
if (!repo) {
|
|
1393
|
+
if (!repo) {
|
|
1394
|
+
setStatus("No repo selected to share");
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
768
1397
|
showOverlay("share");
|
|
769
1398
|
}
|
|
770
1399
|
|
|
771
1400
|
function saveCurrentSearch() {
|
|
772
1401
|
if (currentQueryInput) {
|
|
773
|
-
const name =
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1402
|
+
const name =
|
|
1403
|
+
currentQueryInput.length > 40
|
|
1404
|
+
? currentQueryInput.slice(0, 37) + "..."
|
|
1405
|
+
: currentQueryInput;
|
|
1406
|
+
saveSearch(
|
|
1407
|
+
name,
|
|
1408
|
+
currentQueryInput,
|
|
1409
|
+
currentMode,
|
|
1410
|
+
currentSort,
|
|
1411
|
+
currentLimit,
|
|
1412
|
+
currentMode === "trending" ? trendingTab : undefined,
|
|
1413
|
+
);
|
|
777
1414
|
setStatus(`Saved as "${name}"`);
|
|
778
1415
|
} else {
|
|
779
1416
|
setStatus("No search to save");
|
|
@@ -807,9 +1444,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
807
1444
|
const readmeBox = new BoxRenderable(renderer, {
|
|
808
1445
|
visible: false,
|
|
809
1446
|
flexGrow: 1,
|
|
810
|
-
backgroundColor: colors.
|
|
811
|
-
|
|
812
|
-
|
|
1447
|
+
backgroundColor: colors.surface,
|
|
1448
|
+
borderBottom: true,
|
|
1449
|
+
borderBottomColor: colors.border,
|
|
813
1450
|
title: " README ",
|
|
814
1451
|
titleColor: colors.green,
|
|
815
1452
|
});
|
|
@@ -862,8 +1499,13 @@ export async function launchBrowser(): Promise<void> {
|
|
|
862
1499
|
`${repo.owner}/${repo.name}/master/README.md`,
|
|
863
1500
|
`${repo.owner}/${repo.name}/main/README.rst`,
|
|
864
1501
|
]) {
|
|
865
|
-
const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
|
|
866
|
-
|
|
1502
|
+
const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
|
|
1503
|
+
headers,
|
|
1504
|
+
});
|
|
1505
|
+
if (r.ok) {
|
|
1506
|
+
text = await r.text();
|
|
1507
|
+
break;
|
|
1508
|
+
}
|
|
867
1509
|
}
|
|
868
1510
|
if (!text) {
|
|
869
1511
|
readmeText.content = ` (no README found for ${repo.fullName})`;
|
|
@@ -889,10 +1531,12 @@ export async function launchBrowser(): Promise<void> {
|
|
|
889
1531
|
// ── Status bar ──────────────────────────────────────────────────────
|
|
890
1532
|
const statusBar = new TextRenderable(renderer, {
|
|
891
1533
|
content: " Ready. Press Enter to search.",
|
|
892
|
-
backgroundColor: colors.
|
|
1534
|
+
backgroundColor: colors.surface,
|
|
893
1535
|
color: colors.muted,
|
|
894
1536
|
height: 1,
|
|
895
1537
|
paddingX: 1,
|
|
1538
|
+
borderTop: true,
|
|
1539
|
+
borderTopColor: colors.border,
|
|
896
1540
|
});
|
|
897
1541
|
root.add(statusBar);
|
|
898
1542
|
|
|
@@ -918,7 +1562,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
918
1562
|
}
|
|
919
1563
|
|
|
920
1564
|
function setToolbar() {
|
|
921
|
-
toolbarText.content = formatToolbar(currentSort, currentLimit);
|
|
1565
|
+
toolbarText.content = formatToolbar(currentSort, currentLimit, totalCount);
|
|
922
1566
|
renderer.requestRender();
|
|
923
1567
|
}
|
|
924
1568
|
|
|
@@ -927,22 +1571,30 @@ export async function launchBrowser(): Promise<void> {
|
|
|
927
1571
|
detailText.content = "";
|
|
928
1572
|
return;
|
|
929
1573
|
}
|
|
930
|
-
const topics = repo.topics.length
|
|
1574
|
+
const topics = repo.topics.length
|
|
1575
|
+
? repo.topics.slice(0, 8).join(", ")
|
|
1576
|
+
: "—";
|
|
931
1577
|
const desc = repo.description ?? "(no description)";
|
|
1578
|
+
const stars = repo.stars.toLocaleString();
|
|
1579
|
+
const forks = repo.forks.toLocaleString();
|
|
1580
|
+
const lang = repo.language ?? "—";
|
|
1581
|
+
const updated = repo.updatedAt.slice(0, 10);
|
|
1582
|
+
const statusTags: string[] = [];
|
|
1583
|
+
if (repo.archived) statusTags.push("archived");
|
|
1584
|
+
if (repo.isFork) statusTags.push("fork");
|
|
1585
|
+
const tagStr = statusTags.length ? ` [${statusTags.join(", ")}]` : "";
|
|
1586
|
+
|
|
932
1587
|
const lines = [
|
|
933
|
-
`
|
|
934
|
-
|
|
935
|
-
`
|
|
936
|
-
`
|
|
937
|
-
`
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
"",
|
|
944
|
-
repo.url,
|
|
945
|
-
].filter((l) => l !== "");
|
|
1588
|
+
` ${repo.fullName}${tagStr}`,
|
|
1589
|
+
``,
|
|
1590
|
+
` ★ ${stars} ♡ ${forks} ${lang}`,
|
|
1591
|
+
` Updated ${updated}`,
|
|
1592
|
+
` Topics ${topics}`,
|
|
1593
|
+
``,
|
|
1594
|
+
` ${desc}`,
|
|
1595
|
+
``,
|
|
1596
|
+
` ${repo.url}`,
|
|
1597
|
+
];
|
|
946
1598
|
detailText.content = lines.join("\n");
|
|
947
1599
|
}
|
|
948
1600
|
|
|
@@ -956,7 +1608,11 @@ export async function launchBrowser(): Promise<void> {
|
|
|
956
1608
|
return bits === 0 ? " " : String.fromCodePoint(0x2800 + bits);
|
|
957
1609
|
}
|
|
958
1610
|
|
|
959
|
-
function buildChartString(
|
|
1611
|
+
function buildChartString(
|
|
1612
|
+
values: number[],
|
|
1613
|
+
termW: number,
|
|
1614
|
+
termH: number,
|
|
1615
|
+
): string[] {
|
|
960
1616
|
if (values.length === 0) return ["(no data)"];
|
|
961
1617
|
|
|
962
1618
|
// Braille resolution: 2 horizontal dots and 4 vertical dots per terminal cell.
|
|
@@ -970,10 +1626,12 @@ export async function launchBrowser(): Promise<void> {
|
|
|
970
1626
|
}
|
|
971
1627
|
const max = Math.max(...sampled, 1);
|
|
972
1628
|
// Map each sample to a dot row (0 = bottom, dotH-1 = top).
|
|
973
|
-
const dotRows = sampled.map(v => Math.round((v / max) * (dotH - 1)));
|
|
1629
|
+
const dotRows = sampled.map((v) => Math.round((v / max) * (dotH - 1)));
|
|
974
1630
|
|
|
975
1631
|
// Build a mask grid: dotGrid[row][col] = true if the silhouette passes through.
|
|
976
|
-
const dotGrid: boolean[][] = Array.from({ length: dotH }, () =>
|
|
1632
|
+
const dotGrid: boolean[][] = Array.from({ length: dotH }, () =>
|
|
1633
|
+
Array(dotW).fill(false),
|
|
1634
|
+
);
|
|
977
1635
|
|
|
978
1636
|
// Draw the silhouette line: mark the dot at each column.
|
|
979
1637
|
for (let c = 0; c < dotW; c++) {
|
|
@@ -998,29 +1656,54 @@ export async function launchBrowser(): Promise<void> {
|
|
|
998
1656
|
const r0 = tr * 4;
|
|
999
1657
|
const c0 = tc * 2;
|
|
1000
1658
|
let bits = 0;
|
|
1001
|
-
if (r0 + 0 < dotH && c0 + 0 < dotW && dotGrid[r0 + 0][c0 + 0])
|
|
1002
|
-
|
|
1003
|
-
if (r0 +
|
|
1004
|
-
|
|
1005
|
-
if (r0 +
|
|
1006
|
-
|
|
1007
|
-
if (r0 +
|
|
1008
|
-
|
|
1659
|
+
if (r0 + 0 < dotH && c0 + 0 < dotW && dotGrid[r0 + 0][c0 + 0])
|
|
1660
|
+
bits |= 0x01;
|
|
1661
|
+
if (r0 + 1 < dotH && c0 + 0 < dotW && dotGrid[r0 + 1][c0 + 0])
|
|
1662
|
+
bits |= 0x02;
|
|
1663
|
+
if (r0 + 2 < dotH && c0 + 0 < dotW && dotGrid[r0 + 2][c0 + 0])
|
|
1664
|
+
bits |= 0x04;
|
|
1665
|
+
if (r0 + 0 < dotH && c0 + 1 < dotW && dotGrid[r0 + 0][c0 + 1])
|
|
1666
|
+
bits |= 0x08;
|
|
1667
|
+
if (r0 + 1 < dotH && c0 + 1 < dotW && dotGrid[r0 + 1][c0 + 1])
|
|
1668
|
+
bits |= 0x10;
|
|
1669
|
+
if (r0 + 2 < dotH && c0 + 1 < dotW && dotGrid[r0 + 2][c0 + 1])
|
|
1670
|
+
bits |= 0x20;
|
|
1671
|
+
if (r0 + 3 < dotH && c0 + 0 < dotW && dotGrid[r0 + 3][c0 + 0])
|
|
1672
|
+
bits |= 0x40;
|
|
1673
|
+
if (r0 + 3 < dotH && c0 + 1 < dotW && dotGrid[r0 + 3][c0 + 1])
|
|
1674
|
+
bits |= 0x80;
|
|
1009
1675
|
chars.push(brailleChar(bits));
|
|
1010
1676
|
}
|
|
1011
1677
|
|
|
1012
1678
|
// Y-axis labels
|
|
1013
|
-
const labelRows = [
|
|
1014
|
-
|
|
1679
|
+
const labelRows = [
|
|
1680
|
+
0,
|
|
1681
|
+
Math.floor(termH / 4),
|
|
1682
|
+
Math.floor(termH / 2),
|
|
1683
|
+
Math.floor((3 * termH) / 4),
|
|
1684
|
+
termH - 1,
|
|
1685
|
+
];
|
|
1686
|
+
const labelVals = [
|
|
1687
|
+
max,
|
|
1688
|
+
Math.round(max * 0.75),
|
|
1689
|
+
Math.round(max / 2),
|
|
1690
|
+
Math.round(max / 4),
|
|
1691
|
+
0,
|
|
1692
|
+
];
|
|
1015
1693
|
const idx = labelRows.indexOf(tr);
|
|
1016
1694
|
let label = " ";
|
|
1017
|
-
if (idx >= 0)
|
|
1018
|
-
|
|
1695
|
+
if (idx >= 0) {
|
|
1696
|
+
const val = labelVals[idx];
|
|
1697
|
+
label = val >= 1000 ? `${(val/1000).toFixed(1).replace(/\.0$/, "")}k` : String(val);
|
|
1698
|
+
label = label.padStart(5) + " ";
|
|
1699
|
+
}
|
|
1700
|
+
lines.push(`${label}┆${chars.join("")}`);
|
|
1019
1701
|
}
|
|
1020
1702
|
|
|
1021
|
-
// X-axis
|
|
1022
|
-
|
|
1023
|
-
|
|
1703
|
+
// X-axis with week labels
|
|
1704
|
+
const xAxis = ` ┆${"─".repeat(termW)}`;
|
|
1705
|
+
const weekLabels = ` ┆ ${"1w".padEnd(Math.floor(termW/4))} ${"13w".padEnd(Math.floor(termW/4))} ${"26w".padEnd(Math.floor(termW/4))} ${"52w"}`;
|
|
1706
|
+
return [xAxis, weekLabels];
|
|
1024
1707
|
}
|
|
1025
1708
|
|
|
1026
1709
|
async function loadChart(repo: Repo) {
|
|
@@ -1028,10 +1711,13 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1028
1711
|
renderer.requestRender();
|
|
1029
1712
|
try {
|
|
1030
1713
|
const [chartRes, repoRes] = await Promise.all([
|
|
1031
|
-
fetch(
|
|
1032
|
-
{
|
|
1033
|
-
|
|
1034
|
-
|
|
1714
|
+
fetch(
|
|
1715
|
+
`https://api.github.com/repos/${repo.owner}/${repo.name}/stats/participation`,
|
|
1716
|
+
{ headers: { "User-Agent": "ghfind/1.0" } },
|
|
1717
|
+
),
|
|
1718
|
+
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}`, {
|
|
1719
|
+
headers: { "User-Agent": "ghfind/1.0" },
|
|
1720
|
+
}).then((r) => (r.ok ? r.json() : null)),
|
|
1035
1721
|
]);
|
|
1036
1722
|
|
|
1037
1723
|
let chartSection = "";
|
|
@@ -1042,22 +1728,28 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1042
1728
|
const chartW = 60;
|
|
1043
1729
|
const chartH = 10;
|
|
1044
1730
|
const chartLines = buildChartString(chartCommitData, chartW, chartH);
|
|
1045
|
-
chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join(
|
|
1731
|
+
chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join(
|
|
1732
|
+
"\n",
|
|
1733
|
+
);
|
|
1046
1734
|
}
|
|
1047
1735
|
}
|
|
1048
1736
|
|
|
1049
1737
|
const desc = repo.description ?? "";
|
|
1050
|
-
const lang = repo.language ?? "
|
|
1738
|
+
const lang = repo.language ?? "—";
|
|
1051
1739
|
const updated = repo.updatedAt ? repo.updatedAt.slice(0, 10) : "";
|
|
1052
|
-
const topics = repoRes?.topics?.length
|
|
1053
|
-
|
|
1054
|
-
const growthLine = currentMode === "trending" && repo.score > 0
|
|
1055
|
-
? ` Growth \u25b2 ${fmtStars(repo.score)} ${trendingPeriod}`
|
|
1740
|
+
const topics = repoRes?.topics?.length
|
|
1741
|
+
? repoRes.topics.slice(0, 8).join(", ")
|
|
1056
1742
|
: "";
|
|
1057
1743
|
|
|
1744
|
+
const growthLine =
|
|
1745
|
+
currentMode === "trending" && repo.score > 0
|
|
1746
|
+
? ` Growth \u25b2 ${fmtStars(repo.score)} ${trendingPeriod}`
|
|
1747
|
+
: "";
|
|
1748
|
+
|
|
1058
1749
|
const detailLines = [
|
|
1059
|
-
`
|
|
1060
|
-
|
|
1750
|
+
` ${repo.fullName}`,
|
|
1751
|
+
``,
|
|
1752
|
+
` ★ ${repo.stars.toLocaleString()} ♡ ${repo.forks.toLocaleString()} ${lang}`,
|
|
1061
1753
|
growthLine,
|
|
1062
1754
|
updated ? ` Updated ${updated}` : "",
|
|
1063
1755
|
topics ? ` Topics ${topics}` : "",
|
|
@@ -1065,12 +1757,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1065
1757
|
desc,
|
|
1066
1758
|
"",
|
|
1067
1759
|
` ${repo.url}`,
|
|
1068
|
-
].filter(l => l !== "");
|
|
1760
|
+
].filter((l) => l !== "");
|
|
1069
1761
|
|
|
1070
|
-
detailText.content = [
|
|
1071
|
-
...detailLines,
|
|
1072
|
-
chartSection,
|
|
1073
|
-
].join("\n");
|
|
1762
|
+
detailText.content = [...detailLines, chartSection].join("\n");
|
|
1074
1763
|
} catch {
|
|
1075
1764
|
detailText.content = " Failed to load chart";
|
|
1076
1765
|
}
|
|
@@ -1100,37 +1789,14 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1100
1789
|
|
|
1101
1790
|
// ── Trending helpers ────────────────────────────────────────────────
|
|
1102
1791
|
|
|
1103
|
-
function formatTrendingLine(r:
|
|
1104
|
-
const
|
|
1792
|
+
function formatTrendingLine(r: Repo, rank: number): string {
|
|
1793
|
+
const rankStr = String(rank).padStart(2, "0");
|
|
1105
1794
|
const name = `${r.owner}/${r.name}`.padEnd(30).slice(0, 30);
|
|
1106
1795
|
const lang = (r.language || "?").padEnd(12).slice(0, 12);
|
|
1107
1796
|
const stars = `★ ${fmtStars(r.stars)}`.padEnd(9).slice(0, 9);
|
|
1108
|
-
const arrow = r.
|
|
1109
|
-
const growth = `${arrow} ${fmtStars(r.
|
|
1110
|
-
return `[${
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
function trendingRepoToSelectValue(r: TrendingRepo): Repo {
|
|
1114
|
-
return {
|
|
1115
|
-
id: 0,
|
|
1116
|
-
fullName: `${r.owner}/${r.name}`,
|
|
1117
|
-
name: r.name,
|
|
1118
|
-
owner: r.owner,
|
|
1119
|
-
description: r.description,
|
|
1120
|
-
url: `https://github.com/${r.owner}/${r.name}`,
|
|
1121
|
-
stars: r.stars,
|
|
1122
|
-
forks: 0,
|
|
1123
|
-
watchers: 0,
|
|
1124
|
-
language: r.language,
|
|
1125
|
-
topics: [],
|
|
1126
|
-
archived: false,
|
|
1127
|
-
isFork: false,
|
|
1128
|
-
private: false,
|
|
1129
|
-
createdAt: "",
|
|
1130
|
-
updatedAt: "",
|
|
1131
|
-
pushedAt: "",
|
|
1132
|
-
score: r.starsToday,
|
|
1133
|
-
};
|
|
1797
|
+
const arrow = r.score > 0 ? "▲" : r.score < 0 ? "▼" : "—";
|
|
1798
|
+
const growth = `${arrow} ${fmtStars(r.score)} ${trendingPeriod}`;
|
|
1799
|
+
return `[${rankStr}] ${name} ${lang} ${stars} ${growth}`;
|
|
1134
1800
|
}
|
|
1135
1801
|
|
|
1136
1802
|
async function loadTrending() {
|
|
@@ -1140,31 +1806,59 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1140
1806
|
toolbarText.visible = false;
|
|
1141
1807
|
trendingTabBox.visible = true;
|
|
1142
1808
|
renderTrendingTabs();
|
|
1143
|
-
resultsSelect.options = [
|
|
1809
|
+
resultsSelect.options = [
|
|
1810
|
+
{ name: " Loading trending...", description: "", value: null },
|
|
1811
|
+
];
|
|
1144
1812
|
detailText.content = "";
|
|
1145
1813
|
statusMgr.set("loading", "Loading trending repos...");
|
|
1146
1814
|
renderer.requestRender();
|
|
1147
1815
|
|
|
1148
1816
|
try {
|
|
1149
|
-
const
|
|
1817
|
+
const searchModule = new SearchModule(new TrendingAdapter());
|
|
1818
|
+
const response = await searchModule.search(
|
|
1819
|
+
{ keywords: [], qualifiers: [], raw: "trending" },
|
|
1820
|
+
{ limit: 25, sort: "stars", json: false, verbose: false, trendingSince: tabSince(trendingTab) },
|
|
1821
|
+
);
|
|
1822
|
+
const fetched = response.repos;
|
|
1150
1823
|
const since = tabSince(trendingTab);
|
|
1151
|
-
trendingPeriod =
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1824
|
+
trendingPeriod =
|
|
1825
|
+
since === "daily"
|
|
1826
|
+
? "today"
|
|
1827
|
+
: since === "weekly"
|
|
1828
|
+
? "this week"
|
|
1829
|
+
: "this month";
|
|
1830
|
+
resultsSelect.options = fetched.map((r, i) => ({
|
|
1831
|
+
name: formatTrendingLine(r, i + 1),
|
|
1832
|
+
description: r.description ?? "",
|
|
1833
|
+
value: r,
|
|
1156
1834
|
}));
|
|
1157
1835
|
if (fetched.length > 0) {
|
|
1158
1836
|
resultsSelect.setSelectedIndex(0);
|
|
1159
|
-
updateDetail(
|
|
1837
|
+
updateDetail(fetched[0]);
|
|
1160
1838
|
} else {
|
|
1161
1839
|
throw new NoResultsError(trendingTab);
|
|
1162
1840
|
}
|
|
1163
|
-
statusMgr.set(
|
|
1164
|
-
|
|
1841
|
+
statusMgr.set(
|
|
1842
|
+
"success",
|
|
1843
|
+
`${fetched.length} trending repos — ${trendingTab}`,
|
|
1844
|
+
);
|
|
1845
|
+
appendHistory({
|
|
1846
|
+
query: `trending:${trendingTab}`,
|
|
1847
|
+
mode: "trending",
|
|
1848
|
+
tab: trendingTab,
|
|
1849
|
+
timestamp: Date.now(),
|
|
1850
|
+
resultCount: fetched.length,
|
|
1851
|
+
});
|
|
1165
1852
|
} catch (err) {
|
|
1166
|
-
const msg =
|
|
1167
|
-
|
|
1853
|
+
const msg =
|
|
1854
|
+
err instanceof SearchCliError
|
|
1855
|
+
? err.userMessage
|
|
1856
|
+
: err instanceof Error
|
|
1857
|
+
? err.message
|
|
1858
|
+
: String(err);
|
|
1859
|
+
resultsSelect.options = [
|
|
1860
|
+
{ name: " (error)", description: "", value: null },
|
|
1861
|
+
];
|
|
1168
1862
|
statusMgr.set("error", msg.slice(0, 60));
|
|
1169
1863
|
}
|
|
1170
1864
|
isLoading = false;
|
|
@@ -1176,7 +1870,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1176
1870
|
trendingTabBox.visible = false;
|
|
1177
1871
|
searchBox.visible = true;
|
|
1178
1872
|
toolbarText.visible = true;
|
|
1179
|
-
searchInput.placeholder =
|
|
1873
|
+
searchInput.placeholder =
|
|
1874
|
+
"Search GitHub repos (e.g. rust cli, or language:Rust stars:>100)";
|
|
1180
1875
|
searchInput.focus();
|
|
1181
1876
|
renderer.requestRender();
|
|
1182
1877
|
}
|
|
@@ -1191,7 +1886,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1191
1886
|
isLoading = true;
|
|
1192
1887
|
currentQueryInput = q;
|
|
1193
1888
|
if (!append) {
|
|
1194
|
-
resultsSelect.options = [
|
|
1889
|
+
resultsSelect.options = [
|
|
1890
|
+
{ name: " Searching...", description: "", value: null },
|
|
1891
|
+
];
|
|
1195
1892
|
detailText.content = "";
|
|
1196
1893
|
}
|
|
1197
1894
|
statusMgr.set("searching", `Searching for "${q}"`);
|
|
@@ -1229,15 +1926,30 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1229
1926
|
const idx = append ? resultsSelect.options.length - newRepos.length : 0;
|
|
1230
1927
|
updateDetail(currentRepos[idx]);
|
|
1231
1928
|
resultsSelect.setSelectedIndex(idx);
|
|
1232
|
-
statusMgr.set(
|
|
1929
|
+
statusMgr.set(
|
|
1930
|
+
"success",
|
|
1931
|
+
`Page ${currentPage} — ${currentRepos.length} of ${totalCount.toLocaleString()} results`,
|
|
1932
|
+
);
|
|
1233
1933
|
if (!append) {
|
|
1234
|
-
appendHistory({
|
|
1934
|
+
appendHistory({
|
|
1935
|
+
query: q,
|
|
1936
|
+
mode: "search",
|
|
1937
|
+
timestamp: Date.now(),
|
|
1938
|
+
resultCount: currentRepos.length,
|
|
1939
|
+
});
|
|
1235
1940
|
}
|
|
1236
1941
|
}
|
|
1237
1942
|
} catch (err) {
|
|
1238
1943
|
currentRepos = [];
|
|
1239
|
-
resultsSelect.options = [
|
|
1240
|
-
|
|
1944
|
+
resultsSelect.options = [
|
|
1945
|
+
{ name: " (error)", description: "", value: null },
|
|
1946
|
+
];
|
|
1947
|
+
const msg =
|
|
1948
|
+
err instanceof SearchCliError
|
|
1949
|
+
? err.userMessage
|
|
1950
|
+
: err instanceof Error
|
|
1951
|
+
? err.message
|
|
1952
|
+
: String(err);
|
|
1241
1953
|
statusMgr.set("error", msg.slice(0, 60));
|
|
1242
1954
|
}
|
|
1243
1955
|
|
|
@@ -1303,21 +2015,56 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1303
2015
|
renderer.keyInput.on("keypress", (key) => {
|
|
1304
2016
|
// ── Overlay-mode handling ────────────────────────────────────────
|
|
1305
2017
|
if (currentOverlay !== "none") {
|
|
1306
|
-
// Escape or q closes any overlay
|
|
1307
|
-
if (key.name === "escape"
|
|
2018
|
+
// Escape or q closes any overlay (except: leader uses Esc for back-nav, q to close)
|
|
2019
|
+
if (key.name === "escape") {
|
|
2020
|
+
if (currentOverlay === "leader") {
|
|
2021
|
+
if (!popMenuLevel()) {
|
|
2022
|
+
showOverlay("none");
|
|
2023
|
+
}
|
|
2024
|
+
setToolbar();
|
|
2025
|
+
renderer.requestRender();
|
|
2026
|
+
} else {
|
|
2027
|
+
showOverlay("none");
|
|
2028
|
+
renderer.requestRender();
|
|
2029
|
+
}
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
if (key.name === "q") {
|
|
1308
2033
|
showOverlay("none");
|
|
1309
2034
|
renderer.requestRender();
|
|
1310
2035
|
return;
|
|
1311
2036
|
}
|
|
1312
2037
|
|
|
1313
|
-
// Leader menu:
|
|
2038
|
+
// Leader menu: hierarchical submenu navigation
|
|
1314
2039
|
if (currentOverlay === "leader") {
|
|
1315
|
-
if (
|
|
2040
|
+
if (
|
|
2041
|
+
key.name === "enter" ||
|
|
2042
|
+
key.name === "return" ||
|
|
2043
|
+
key.name === "right" ||
|
|
2044
|
+
key.name === "l"
|
|
2045
|
+
) {
|
|
1316
2046
|
const sel = leaderSelect.getSelectedOption();
|
|
1317
|
-
const
|
|
1318
|
-
if (
|
|
2047
|
+
const entry = sel?.value as MenuEntry | undefined;
|
|
2048
|
+
if (!entry) return;
|
|
2049
|
+
if (entry.type === "category") {
|
|
2050
|
+
pushMenuLevel(
|
|
2051
|
+
` Menu > ${entry.name}`,
|
|
2052
|
+
entry.name,
|
|
2053
|
+
entry.children,
|
|
2054
|
+
0,
|
|
2055
|
+
);
|
|
2056
|
+
renderer.requestRender();
|
|
2057
|
+
} else {
|
|
2058
|
+
resetMenu();
|
|
1319
2059
|
showOverlay("none");
|
|
1320
|
-
|
|
2060
|
+
entry.action();
|
|
2061
|
+
renderer.requestRender();
|
|
2062
|
+
}
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
if (key.name === "left" || key.name === "h") {
|
|
2066
|
+
if (popMenuLevel()) {
|
|
2067
|
+
setToolbar();
|
|
1321
2068
|
renderer.requestRender();
|
|
1322
2069
|
}
|
|
1323
2070
|
return;
|
|
@@ -1355,7 +2102,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1355
2102
|
const entry = (sel.value as any).entry;
|
|
1356
2103
|
showOverlay("none");
|
|
1357
2104
|
if (entry.mode === "trending") {
|
|
1358
|
-
if (entry.tab)
|
|
2105
|
+
if (entry.tab)
|
|
2106
|
+
trendingTab = entry.tab as (typeof TAB_NAMES)[number];
|
|
1359
2107
|
loadTrending();
|
|
1360
2108
|
} else {
|
|
1361
2109
|
searchInput.value = entry.query;
|
|
@@ -1407,7 +2155,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1407
2155
|
showOverlay("none");
|
|
1408
2156
|
touchSavedSearch(s.name);
|
|
1409
2157
|
if (s.mode === "trending") {
|
|
1410
|
-
if (s.tab) trendingTab = s.tab as typeof TAB_NAMES[number];
|
|
2158
|
+
if (s.tab) trendingTab = s.tab as (typeof TAB_NAMES)[number];
|
|
1411
2159
|
loadTrending();
|
|
1412
2160
|
} else {
|
|
1413
2161
|
searchInput.value = s.query;
|
|
@@ -1512,6 +2260,51 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1512
2260
|
return;
|
|
1513
2261
|
}
|
|
1514
2262
|
|
|
2263
|
+
// Update panel
|
|
2264
|
+
if (currentOverlay === "update") {
|
|
2265
|
+
if (key.name === "escape" || key.name === "q") {
|
|
2266
|
+
showOverlay("none");
|
|
2267
|
+
renderer.requestRender();
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
if (key.name === "left" || key.name === "h") {
|
|
2271
|
+
updateSelectedOption = Math.max(0, updateSelectedOption - 1);
|
|
2272
|
+
renderUpdateOptions();
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
if (key.name === "right" || key.name === "l") {
|
|
2276
|
+
updateSelectedOption = Math.min(2, updateSelectedOption + 1);
|
|
2277
|
+
renderUpdateOptions();
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
if (key.name === "enter" || key.name === "return") {
|
|
2281
|
+
const action = ["now", "later", "never"][updateSelectedOption] as "now" | "later" | "never" | undefined;
|
|
2282
|
+
if (action === "now") {
|
|
2283
|
+
showOverlay("none");
|
|
2284
|
+
performUpdate().then((ok) => {
|
|
2285
|
+
if (ok) {
|
|
2286
|
+
setStatus("✓ Update complete — restart ghfind");
|
|
2287
|
+
} else {
|
|
2288
|
+
setStatus("✗ Update failed — check terminal output");
|
|
2289
|
+
}
|
|
2290
|
+
}).catch(() => {
|
|
2291
|
+
setStatus("✗ Update failed — check terminal output");
|
|
2292
|
+
});
|
|
2293
|
+
} else if (action === "never") {
|
|
2294
|
+
suppressUpdateNotices();
|
|
2295
|
+
showOverlay("none");
|
|
2296
|
+
setStatus("Update notices suppressed");
|
|
2297
|
+
} else {
|
|
2298
|
+
snoozeUpdateNotices(3);
|
|
2299
|
+
showOverlay("none");
|
|
2300
|
+
setStatus("Update reminder snoozed for 3 days");
|
|
2301
|
+
}
|
|
2302
|
+
renderer.requestRender();
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
1515
2308
|
return;
|
|
1516
2309
|
}
|
|
1517
2310
|
|
|
@@ -1523,7 +2316,13 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1523
2316
|
toggleGraph();
|
|
1524
2317
|
return;
|
|
1525
2318
|
}
|
|
1526
|
-
saveSession({
|
|
2319
|
+
saveSession({
|
|
2320
|
+
mode: currentMode,
|
|
2321
|
+
query: currentQueryInput,
|
|
2322
|
+
sort: currentSort,
|
|
2323
|
+
limit: currentLimit,
|
|
2324
|
+
trendingTab,
|
|
2325
|
+
});
|
|
1527
2326
|
cleanup();
|
|
1528
2327
|
return;
|
|
1529
2328
|
}
|
|
@@ -1534,9 +2333,14 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1534
2333
|
return;
|
|
1535
2334
|
}
|
|
1536
2335
|
|
|
1537
|
-
// Space
|
|
1538
|
-
|
|
1539
|
-
|
|
2336
|
+
// Space toggles leader menu (open if closed, close if open)
|
|
2337
|
+
// Skip when the search input is focused so spaces can be typed in queries
|
|
2338
|
+
if (key.name === "space" && !searchInput.focused) {
|
|
2339
|
+
if (currentOverlay === "leader") {
|
|
2340
|
+
showOverlay("none");
|
|
2341
|
+
} else {
|
|
2342
|
+
showLeaderMenu();
|
|
2343
|
+
}
|
|
1540
2344
|
return;
|
|
1541
2345
|
}
|
|
1542
2346
|
|
|
@@ -1554,9 +2358,10 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1554
2358
|
const currentWord = lastSpace >= 0 ? val.slice(lastSpace + 1) : val;
|
|
1555
2359
|
const suggestions = suggestFor(currentWord);
|
|
1556
2360
|
if (suggestions.length > 0) {
|
|
1557
|
-
const newVal =
|
|
1558
|
-
|
|
1559
|
-
|
|
2361
|
+
const newVal =
|
|
2362
|
+
lastSpace >= 0
|
|
2363
|
+
? val.slice(0, lastSpace + 1) + suggestions[0]
|
|
2364
|
+
: suggestions[0];
|
|
1560
2365
|
searchInput.value = newVal;
|
|
1561
2366
|
renderer.requestRender();
|
|
1562
2367
|
}
|
|
@@ -1588,14 +2393,20 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1588
2393
|
if (key.name === "left" || key.name === "h") {
|
|
1589
2394
|
if (currentMode === "trending") {
|
|
1590
2395
|
const idx = TAB_NAMES.indexOf(trendingTab);
|
|
1591
|
-
if (idx > 0) {
|
|
2396
|
+
if (idx > 0) {
|
|
2397
|
+
trendingTab = TAB_NAMES[idx - 1];
|
|
2398
|
+
loadTrending();
|
|
2399
|
+
}
|
|
1592
2400
|
}
|
|
1593
2401
|
return;
|
|
1594
2402
|
}
|
|
1595
2403
|
if (key.name === "right" || key.name === "l") {
|
|
1596
2404
|
if (currentMode === "trending") {
|
|
1597
2405
|
const idx = TAB_NAMES.indexOf(trendingTab);
|
|
1598
|
-
if (idx < TAB_NAMES.length - 1) {
|
|
2406
|
+
if (idx < TAB_NAMES.length - 1) {
|
|
2407
|
+
trendingTab = TAB_NAMES[idx + 1];
|
|
2408
|
+
loadTrending();
|
|
2409
|
+
}
|
|
1599
2410
|
}
|
|
1600
2411
|
return;
|
|
1601
2412
|
}
|
|
@@ -1620,7 +2431,22 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1620
2431
|
}
|
|
1621
2432
|
});
|
|
1622
2433
|
|
|
2434
|
+
// ── Update check (before start) ──────────────────────────────────────
|
|
2435
|
+
const pkgPath = join(
|
|
2436
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
2437
|
+
"..",
|
|
2438
|
+
"package.json",
|
|
2439
|
+
);
|
|
2440
|
+
let currentVersion = "0.0.0";
|
|
2441
|
+
try {
|
|
2442
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
2443
|
+
currentVersion = pkg.version;
|
|
2444
|
+
} catch {
|
|
2445
|
+
// non-critical
|
|
2446
|
+
}
|
|
1623
2447
|
// ── Start ──────────────────────────────────────────────────────────
|
|
2448
|
+
// Check for updates (non-blocking — doesn't delay TUI startup)
|
|
2449
|
+
checkForUpdateAndShow();
|
|
1624
2450
|
renderer.start();
|
|
1625
2451
|
if (currentMode === "trending") {
|
|
1626
2452
|
loadTrending();
|
|
@@ -1641,21 +2467,27 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1641
2467
|
function formatResultLine(repo: Repo): string {
|
|
1642
2468
|
const stars = formatStars(repo.stars);
|
|
1643
2469
|
const lang = (repo.language ?? "?").padEnd(12).slice(0, 12);
|
|
1644
|
-
|
|
2470
|
+
const name = repo.fullName.padEnd(40).slice(0, 40);
|
|
2471
|
+
return `${name} ${lang} ${stars}`;
|
|
1645
2472
|
}
|
|
1646
2473
|
|
|
1647
2474
|
function formatStars(n: number): string {
|
|
2475
|
+
if (n >= 1000000) return `★ ${(n / 1000000).toFixed(1).replace(/\.0$/, "")}M`;
|
|
1648
2476
|
if (n >= 1000) return `★ ${(n / 1000).toFixed(1).replace(/\.0$/, "")}k`;
|
|
1649
2477
|
return `★ ${n}`;
|
|
1650
2478
|
}
|
|
1651
2479
|
|
|
1652
|
-
function formatToolbar(sort: SortStrategy, limit: number): string {
|
|
2480
|
+
function formatToolbar(sort: SortStrategy, limit: number, totalCount: number): string {
|
|
1653
2481
|
const sortLabel = SORT_MODES.find((m) => m.key === sort)?.label ?? sort;
|
|
1654
|
-
return ` sort: ${sortLabel} limit: ${limit}`;
|
|
2482
|
+
return ` sort: ${sortLabel} limit: ${limit} results: ${totalCount.toLocaleString()}`;
|
|
1655
2483
|
}
|
|
1656
2484
|
|
|
1657
2485
|
function cleanup(): void {
|
|
1658
|
-
try {
|
|
2486
|
+
try {
|
|
2487
|
+
rotateHistory();
|
|
2488
|
+
} catch {
|
|
2489
|
+
/* non-critical */
|
|
2490
|
+
}
|
|
1659
2491
|
process.exit(0);
|
|
1660
2492
|
}
|
|
1661
2493
|
|