github-search-cli 0.8.2 → 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 +103 -50
- package/bin/ghfind.js +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +106 -106
- 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 +1087 -270
- package/src/types.ts +2 -0
- package/src/update-check.ts +162 -0
- package/src/watch.ts +23 -11
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 ───────────────────────────────────────────────────────────
|
|
@@ -131,33 +196,48 @@ export async function launchBrowser(): Promise<void> {
|
|
|
131
196
|
let currentQueryInput = session?.query ?? "";
|
|
132
197
|
let isLoading = false;
|
|
133
198
|
let currentMode: "search" | "trending" = session?.mode ?? "search";
|
|
134
|
-
let trendingTab: typeof TAB_NAMES[number] =
|
|
199
|
+
let trendingTab: (typeof TAB_NAMES)[number] =
|
|
200
|
+
(session?.trendingTab as (typeof TAB_NAMES)[number]) ?? "This Week";
|
|
135
201
|
let trendingPeriod = "this week";
|
|
136
202
|
let graphFullscreen = false;
|
|
137
203
|
let chartCommitData: number[] = [];
|
|
138
|
-
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";
|
|
139
218
|
let currentPage = 1;
|
|
140
219
|
let totalCount = 0;
|
|
141
220
|
let deepDiveActive = false;
|
|
142
221
|
let compareList: Repo[] = [];
|
|
143
222
|
let quitArmed = false;
|
|
144
223
|
|
|
145
|
-
|
|
146
224
|
// ── Header bar ──────────────────────────────────────────────────────
|
|
147
225
|
const header = new TextRenderable(renderer, {
|
|
148
226
|
content:
|
|
149
|
-
" ghfind — GitHub repo browser [/]search [Space]menu [?]help [q]uit",
|
|
227
|
+
" ghfind — GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
|
|
150
228
|
backgroundColor: colors.bg,
|
|
151
229
|
color: colors.muted,
|
|
152
230
|
height: 1,
|
|
231
|
+
borderBottom: true,
|
|
232
|
+
borderBottomColor: colors.border,
|
|
153
233
|
});
|
|
154
|
-
root.add(header);
|
|
155
234
|
|
|
156
235
|
// ── Trending tab bar (hidden in search mode) ────────────────────────
|
|
157
236
|
const trendingTabBox = new BoxRenderable(renderer, {
|
|
158
237
|
flexDirection: "row",
|
|
159
238
|
height: 1,
|
|
160
|
-
backgroundColor: colors.
|
|
239
|
+
backgroundColor: colors.surface,
|
|
240
|
+
// No border — pure contract-edge separation via text contrast
|
|
161
241
|
paddingX: 1,
|
|
162
242
|
visible: false,
|
|
163
243
|
});
|
|
@@ -190,30 +270,45 @@ export async function launchBrowser(): Promise<void> {
|
|
|
190
270
|
}
|
|
191
271
|
});
|
|
192
272
|
}
|
|
273
|
+
root.add(header);
|
|
193
274
|
root.add(trendingTabBox);
|
|
194
275
|
|
|
195
276
|
// ── Search input row ────────────────────────────────────────────────
|
|
196
277
|
const searchBox = new BoxRenderable(renderer, {
|
|
197
|
-
visible: true,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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,
|
|
202
288
|
});
|
|
203
289
|
const searchInput = new InputRenderable(renderer, {
|
|
204
|
-
placeholder:
|
|
290
|
+
placeholder:
|
|
291
|
+
"Search GitHub repos (e.g. rust cli, or language:Rust stars:>100)",
|
|
205
292
|
value: "",
|
|
293
|
+
backgroundColor: colors.surface,
|
|
294
|
+
textColor: colors.text,
|
|
295
|
+
borderColor: colors.blue,
|
|
296
|
+
selectedBorderColor: colors.blue,
|
|
297
|
+
paddingX: 0,
|
|
298
|
+
flexGrow: 1,
|
|
206
299
|
});
|
|
207
300
|
searchBox.add(searchInput);
|
|
208
301
|
root.add(searchBox);
|
|
209
302
|
|
|
210
303
|
// ── Toolbar (sort + limit indicator) ────────────────────────────────
|
|
211
304
|
const toolbarText = new TextRenderable(renderer, {
|
|
212
|
-
content: formatToolbar(currentSort, currentLimit),
|
|
305
|
+
content: formatToolbar(currentSort, currentLimit, totalCount),
|
|
213
306
|
visible: true, // hidden in trending mode
|
|
214
|
-
backgroundColor: colors.
|
|
307
|
+
backgroundColor: colors.surface, // Surface panel
|
|
215
308
|
color: colors.muted,
|
|
216
309
|
height: 1,
|
|
310
|
+
borderBottom: true,
|
|
311
|
+
borderBottomColor: colors.border,
|
|
217
312
|
paddingX: 1,
|
|
218
313
|
});
|
|
219
314
|
root.add(toolbarText);
|
|
@@ -229,8 +324,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
229
324
|
|
|
230
325
|
// Results pane
|
|
231
326
|
const resultsBox = new BoxRenderable(renderer, {
|
|
232
|
-
|
|
233
|
-
|
|
327
|
+
borderBottom: true,
|
|
328
|
+
borderBottomColor: colors.border,
|
|
234
329
|
title: " Results ",
|
|
235
330
|
titleColor: colors.blue,
|
|
236
331
|
width: "50%",
|
|
@@ -239,11 +334,16 @@ export async function launchBrowser(): Promise<void> {
|
|
|
239
334
|
focusable: false,
|
|
240
335
|
});
|
|
241
336
|
const resultsSelect = new SelectRenderable(renderer, {
|
|
242
|
-
options: [
|
|
337
|
+
options: [
|
|
338
|
+
{
|
|
339
|
+
name: "",
|
|
340
|
+
description: "Type a query and press Enter to search",
|
|
341
|
+
value: null,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
243
344
|
showDescription: false,
|
|
244
345
|
showSelectionIndicator: false,
|
|
245
346
|
flexGrow: 1,
|
|
246
|
-
backgroundColor: colors.bg,
|
|
247
347
|
textColor: colors.text,
|
|
248
348
|
selectedBackgroundColor: colors.selectionBg,
|
|
249
349
|
selectedTextColor: colors.selectionText,
|
|
@@ -255,8 +355,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
255
355
|
|
|
256
356
|
// Detail / Graph pane
|
|
257
357
|
const detailBox = new BoxRenderable(renderer, {
|
|
258
|
-
|
|
259
|
-
|
|
358
|
+
borderBottom: true,
|
|
359
|
+
borderBottomColor: colors.border,
|
|
260
360
|
title: " Details ",
|
|
261
361
|
titleColor: colors.green,
|
|
262
362
|
width: "50%",
|
|
@@ -264,6 +364,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
264
364
|
paddingX: 1,
|
|
265
365
|
paddingY: 1,
|
|
266
366
|
focusable: false,
|
|
367
|
+
backgroundColor: colors.surface,
|
|
267
368
|
});
|
|
268
369
|
const detailText = new TextRenderable(renderer, {
|
|
269
370
|
content: "",
|
|
@@ -275,8 +376,6 @@ export async function launchBrowser(): Promise<void> {
|
|
|
275
376
|
body.add(detailBox);
|
|
276
377
|
root.add(body);
|
|
277
378
|
|
|
278
|
-
|
|
279
|
-
|
|
280
379
|
// ── Overlay system ──────────────────────────────────────────────────
|
|
281
380
|
|
|
282
381
|
// Hide main content so overlays can take 100% of the content area
|
|
@@ -308,7 +407,10 @@ export async function launchBrowser(): Promise<void> {
|
|
|
308
407
|
notifsBox.visible = false;
|
|
309
408
|
shareBox.visible = false;
|
|
310
409
|
leaderBox.visible = false;
|
|
410
|
+
leaderDim.visible = false;
|
|
311
411
|
readmeBox.visible = false;
|
|
412
|
+
updateBox.visible = false;
|
|
413
|
+
updateDim.visible = false;
|
|
312
414
|
|
|
313
415
|
if (type === "none") {
|
|
314
416
|
currentOverlay = "none";
|
|
@@ -317,18 +419,38 @@ export async function launchBrowser(): Promise<void> {
|
|
|
317
419
|
return;
|
|
318
420
|
}
|
|
319
421
|
currentOverlay = type;
|
|
320
|
-
|
|
321
|
-
if (type
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
else if (type === "
|
|
327
|
-
|
|
328
|
-
else if (type === "
|
|
329
|
-
|
|
330
|
-
else if (type === "
|
|
331
|
-
|
|
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
|
+
}
|
|
332
454
|
renderer.requestRender();
|
|
333
455
|
}
|
|
334
456
|
|
|
@@ -338,8 +460,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
338
460
|
visible: false,
|
|
339
461
|
flexGrow: 1,
|
|
340
462
|
backgroundColor: colors.bg,
|
|
341
|
-
|
|
342
|
-
|
|
463
|
+
borderBottom: true,
|
|
464
|
+
borderBottomColor: colors.border,
|
|
343
465
|
});
|
|
344
466
|
const helpScroll = new ScrollBoxRenderable(renderer, {
|
|
345
467
|
flexGrow: 1,
|
|
@@ -357,10 +479,12 @@ export async function launchBrowser(): Promise<void> {
|
|
|
357
479
|
});
|
|
358
480
|
for (const section of helpSections) {
|
|
359
481
|
const sectionBox = new BoxRenderable(renderer, {
|
|
360
|
-
|
|
361
|
-
|
|
482
|
+
borderBottom: true,
|
|
483
|
+
borderBottomColor: colors.border,
|
|
362
484
|
title: section.title,
|
|
363
|
-
titleColor:
|
|
485
|
+
titleColor:
|
|
486
|
+
(colors as Record<string, string>)[section.titleColor] ??
|
|
487
|
+
section.titleColor,
|
|
364
488
|
flexDirection: "column",
|
|
365
489
|
paddingY: 0,
|
|
366
490
|
marginLeft: 1,
|
|
@@ -369,16 +493,20 @@ export async function launchBrowser(): Promise<void> {
|
|
|
369
493
|
});
|
|
370
494
|
for (const row of section.rows) {
|
|
371
495
|
const keys = row.keys.padEnd(HELP_KEYS_COLUMN);
|
|
372
|
-
sectionBox.add(
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
496
|
+
sectionBox.add(
|
|
497
|
+
new TextRenderable(renderer, {
|
|
498
|
+
content: ` ${keys} ${row.action}`,
|
|
499
|
+
color: colors.text,
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
376
502
|
}
|
|
377
503
|
if (section.note) {
|
|
378
|
-
sectionBox.add(
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
504
|
+
sectionBox.add(
|
|
505
|
+
new TextRenderable(renderer, {
|
|
506
|
+
content: ` ${section.note}`,
|
|
507
|
+
color: colors.muted,
|
|
508
|
+
}),
|
|
509
|
+
);
|
|
382
510
|
}
|
|
383
511
|
helpScroll.add(sectionBox);
|
|
384
512
|
}
|
|
@@ -390,8 +518,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
390
518
|
visible: false,
|
|
391
519
|
flexGrow: 1,
|
|
392
520
|
backgroundColor: colors.bg,
|
|
393
|
-
|
|
394
|
-
|
|
521
|
+
borderBottom: true,
|
|
522
|
+
borderBottomColor: colors.border,
|
|
395
523
|
title: " Search History ",
|
|
396
524
|
titleColor: colors.blue,
|
|
397
525
|
});
|
|
@@ -412,7 +540,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
412
540
|
function refreshHistory() {
|
|
413
541
|
const entries = readHistory();
|
|
414
542
|
if (entries.length === 0) {
|
|
415
|
-
historySelect.options = [
|
|
543
|
+
historySelect.options = [
|
|
544
|
+
{ name: "(no history)", description: "", value: null },
|
|
545
|
+
];
|
|
416
546
|
} else {
|
|
417
547
|
historySelect.options = entries.map((e, i) => ({
|
|
418
548
|
name: `${e.query} — ${e.mode}${e.tab ? ` (${e.tab})` : ""}`,
|
|
@@ -428,8 +558,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
428
558
|
visible: false,
|
|
429
559
|
flexGrow: 1,
|
|
430
560
|
backgroundColor: colors.bg,
|
|
431
|
-
|
|
432
|
-
|
|
561
|
+
borderBottom: true,
|
|
562
|
+
borderBottomColor: colors.border,
|
|
433
563
|
title: " Bookmarks ",
|
|
434
564
|
titleColor: colors.green,
|
|
435
565
|
});
|
|
@@ -450,7 +580,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
450
580
|
function refreshBookmarks() {
|
|
451
581
|
const bookmarks = getBookmarks();
|
|
452
582
|
if (bookmarks.length === 0) {
|
|
453
|
-
bookmarksSelect.options = [
|
|
583
|
+
bookmarksSelect.options = [
|
|
584
|
+
{ name: "(no bookmarks)", description: "", value: null },
|
|
585
|
+
];
|
|
454
586
|
} else {
|
|
455
587
|
bookmarksSelect.options = bookmarks.map((b) => ({
|
|
456
588
|
name: `${b.repo.fullName} ★ ${b.repo.stars.toLocaleString()}`,
|
|
@@ -466,8 +598,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
466
598
|
visible: false,
|
|
467
599
|
flexGrow: 1,
|
|
468
600
|
backgroundColor: colors.bg,
|
|
469
|
-
|
|
470
|
-
|
|
601
|
+
borderBottom: true,
|
|
602
|
+
borderBottomColor: colors.border,
|
|
471
603
|
title: " Saved Searches ",
|
|
472
604
|
titleColor: colors.yellow,
|
|
473
605
|
});
|
|
@@ -490,8 +622,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
490
622
|
visible: false,
|
|
491
623
|
flexGrow: 1,
|
|
492
624
|
backgroundColor: colors.bg,
|
|
493
|
-
|
|
494
|
-
|
|
625
|
+
borderBottom: true,
|
|
626
|
+
borderBottomColor: colors.border,
|
|
495
627
|
title: " Topics ",
|
|
496
628
|
titleColor: colors.purple ?? colors.blue,
|
|
497
629
|
});
|
|
@@ -518,7 +650,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
518
650
|
value: t,
|
|
519
651
|
}));
|
|
520
652
|
} catch {
|
|
521
|
-
topicsSelect.options = [
|
|
653
|
+
topicsSelect.options = [
|
|
654
|
+
{ name: "(failed to load topics)", description: "", value: null },
|
|
655
|
+
];
|
|
522
656
|
}
|
|
523
657
|
topicsSelect.setSelectedIndex(0);
|
|
524
658
|
}
|
|
@@ -528,16 +662,28 @@ export async function launchBrowser(): Promise<void> {
|
|
|
528
662
|
visible: false,
|
|
529
663
|
flexGrow: 1,
|
|
530
664
|
backgroundColor: colors.bg,
|
|
531
|
-
|
|
532
|
-
|
|
665
|
+
borderBottom: true,
|
|
666
|
+
borderBottomColor: colors.border,
|
|
533
667
|
title: " Export ",
|
|
534
668
|
titleColor: colors.green,
|
|
535
669
|
});
|
|
536
670
|
const exportSelect = new SelectRenderable(renderer, {
|
|
537
671
|
options: [
|
|
538
|
-
{
|
|
539
|
-
|
|
540
|
-
|
|
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
|
+
},
|
|
541
687
|
{ name: " Plain text", description: "One repo per line", value: "text" },
|
|
542
688
|
],
|
|
543
689
|
showDescription: true,
|
|
@@ -557,8 +703,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
557
703
|
visible: false,
|
|
558
704
|
flexGrow: 1,
|
|
559
705
|
backgroundColor: colors.bg,
|
|
560
|
-
|
|
561
|
-
|
|
706
|
+
borderBottom: true,
|
|
707
|
+
borderBottomColor: colors.border,
|
|
562
708
|
title: " Comparison ",
|
|
563
709
|
titleColor: colors.yellow,
|
|
564
710
|
});
|
|
@@ -575,8 +721,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
575
721
|
visible: false,
|
|
576
722
|
flexGrow: 1,
|
|
577
723
|
backgroundColor: colors.bg,
|
|
578
|
-
|
|
579
|
-
|
|
724
|
+
borderBottom: true,
|
|
725
|
+
borderBottomColor: colors.border,
|
|
580
726
|
title: " Notifications ",
|
|
581
727
|
titleColor: colors.yellow,
|
|
582
728
|
});
|
|
@@ -604,16 +750,28 @@ export async function launchBrowser(): Promise<void> {
|
|
|
604
750
|
visible: false,
|
|
605
751
|
flexGrow: 1,
|
|
606
752
|
backgroundColor: colors.bg,
|
|
607
|
-
|
|
608
|
-
|
|
753
|
+
borderBottom: true,
|
|
754
|
+
borderBottomColor: colors.border,
|
|
609
755
|
title: " Share Repo ",
|
|
610
756
|
titleColor: colors.blue,
|
|
611
757
|
});
|
|
612
758
|
const shareSelect = new SelectRenderable(renderer, {
|
|
613
759
|
options: [
|
|
614
|
-
{
|
|
615
|
-
|
|
616
|
-
|
|
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
|
+
},
|
|
617
775
|
{ name: " Short", description: "owner/repo", value: "short" },
|
|
618
776
|
],
|
|
619
777
|
showDescription: true,
|
|
@@ -630,7 +788,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
630
788
|
|
|
631
789
|
function refreshCompare() {
|
|
632
790
|
if (compareList.length < 2) {
|
|
633
|
-
compareText.content =
|
|
791
|
+
compareText.content =
|
|
792
|
+
"Select at least 2 repos to compare (press c to add).";
|
|
634
793
|
} else {
|
|
635
794
|
compareText.content = buildComparisonTable(compareList);
|
|
636
795
|
}
|
|
@@ -639,7 +798,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
639
798
|
function refreshSavedSearches() {
|
|
640
799
|
const saved = getSavedSearches();
|
|
641
800
|
if (saved.length === 0) {
|
|
642
|
-
savedSelect.options = [
|
|
801
|
+
savedSelect.options = [
|
|
802
|
+
{ name: "(no saved searches)", description: "", value: null },
|
|
803
|
+
];
|
|
643
804
|
} else {
|
|
644
805
|
savedSelect.options = saved.map((s) => ({
|
|
645
806
|
name: `${s.name}`,
|
|
@@ -651,96 +812,547 @@ export async function launchBrowser(): Promise<void> {
|
|
|
651
812
|
}
|
|
652
813
|
|
|
653
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
|
+
|
|
654
827
|
const leaderBox = new BoxRenderable(renderer, {
|
|
655
828
|
visible: false,
|
|
656
|
-
|
|
657
|
-
|
|
829
|
+
position: "absolute",
|
|
830
|
+
width: "80%",
|
|
831
|
+
height: 18,
|
|
832
|
+
left: "10%",
|
|
833
|
+
top: "30%",
|
|
834
|
+
backgroundColor: colors.surfaceDim,
|
|
658
835
|
border: true,
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
836
|
+
borderStyle: "rounded",
|
|
837
|
+
borderColor: colors.borderAccent,
|
|
838
|
+
title: " ⚙ Menu ",
|
|
839
|
+
titleColor: colors.accent,
|
|
840
|
+
titleAlignment: "left",
|
|
662
841
|
flexDirection: "column",
|
|
842
|
+
paddingX: 1,
|
|
663
843
|
});
|
|
664
844
|
const leaderSelect = new SelectRenderable(renderer, {
|
|
665
845
|
options: [{ name: "(empty)", description: "", value: null }],
|
|
666
846
|
showDescription: true,
|
|
667
847
|
showSelectionIndicator: true,
|
|
668
848
|
flexGrow: 1,
|
|
669
|
-
backgroundColor: colors.
|
|
849
|
+
backgroundColor: colors.surfaceDim,
|
|
670
850
|
textColor: colors.text,
|
|
671
|
-
|
|
851
|
+
focusedBackgroundColor: colors.surfaceDim,
|
|
852
|
+
focusedTextColor: colors.text,
|
|
853
|
+
selectedBackgroundColor: colors.accent,
|
|
672
854
|
selectedTextColor: colors.selectionText,
|
|
855
|
+
descriptionColor: colors.muted,
|
|
856
|
+
selectedDescriptionColor: colors.selectionText,
|
|
673
857
|
itemSpacing: 0,
|
|
674
858
|
});
|
|
675
859
|
const leaderFooter = new TextRenderable(renderer, {
|
|
676
|
-
content: "
|
|
860
|
+
content: " ↑↓ select Enter run Esc/q close ",
|
|
677
861
|
color: colors.muted,
|
|
678
|
-
backgroundColor: colors.
|
|
862
|
+
backgroundColor: colors.surfaceDim,
|
|
679
863
|
height: 1,
|
|
680
|
-
|
|
864
|
+
borderTop: true,
|
|
865
|
+
borderTopColor: colors.border,
|
|
866
|
+
paddingX: 2,
|
|
681
867
|
});
|
|
682
868
|
leaderBox.add(leaderSelect);
|
|
683
869
|
leaderBox.add(leaderFooter);
|
|
870
|
+
root.add(leaderDim);
|
|
684
871
|
root.add(leaderBox);
|
|
685
872
|
|
|
686
|
-
|
|
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
|
+
});
|
|
687
883
|
|
|
688
|
-
|
|
689
|
-
const items: LeaderItem[] = [];
|
|
884
|
+
root.add(updateDim);
|
|
690
885
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
+
});
|
|
704
910
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
+
}
|
|
713
1072
|
|
|
714
|
-
|
|
1073
|
+
// ── Build hierarchical menu ─────────────────────────────────────
|
|
1074
|
+
function buildMenuHierarchy(): MenuCategory[] {
|
|
715
1075
|
const hasRepo = (() => {
|
|
716
1076
|
const opt = resultsSelect.getSelectedOption();
|
|
717
1077
|
return opt?.value && typeof (opt.value as any)?.fullName === "string";
|
|
718
1078
|
})();
|
|
719
1079
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
+
},
|
|
732
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
|
+
});
|
|
733
1259
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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);
|
|
740
1326
|
leaderSelect.focus();
|
|
741
1327
|
showOverlay("leader");
|
|
742
1328
|
}
|
|
743
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
|
+
|
|
744
1356
|
function openUrlIfSelected() {
|
|
745
1357
|
const opt = resultsSelect.getSelectedOption();
|
|
746
1358
|
const repo = opt?.value as Repo | undefined;
|
|
@@ -753,7 +1365,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
753
1365
|
const repo = opt?.value as Repo | undefined;
|
|
754
1366
|
if (repo) {
|
|
755
1367
|
const added = toggleBookmark(repo);
|
|
756
|
-
setStatus(
|
|
1368
|
+
setStatus(
|
|
1369
|
+
added ? `Bookmarked ${repo.fullName}` : `Unbookmarked ${repo.fullName}`,
|
|
1370
|
+
);
|
|
757
1371
|
}
|
|
758
1372
|
}
|
|
759
1373
|
|
|
@@ -767,23 +1381,36 @@ export async function launchBrowser(): Promise<void> {
|
|
|
767
1381
|
setStatus(`Removed ${repo.fullName} from comparison`);
|
|
768
1382
|
} else {
|
|
769
1383
|
compareList.push(repo);
|
|
770
|
-
setStatus(
|
|
1384
|
+
setStatus(
|
|
1385
|
+
`${repo.fullName} added to comparison (${compareList.length} selected)`,
|
|
1386
|
+
);
|
|
771
1387
|
}
|
|
772
1388
|
}
|
|
773
1389
|
|
|
774
1390
|
function showShareIfRepo() {
|
|
775
1391
|
const opt = resultsSelect.getSelectedOption();
|
|
776
1392
|
const repo = opt?.value as Repo | undefined;
|
|
777
|
-
if (!repo) {
|
|
1393
|
+
if (!repo) {
|
|
1394
|
+
setStatus("No repo selected to share");
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
778
1397
|
showOverlay("share");
|
|
779
1398
|
}
|
|
780
1399
|
|
|
781
1400
|
function saveCurrentSearch() {
|
|
782
1401
|
if (currentQueryInput) {
|
|
783
|
-
const name =
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
+
);
|
|
787
1414
|
setStatus(`Saved as "${name}"`);
|
|
788
1415
|
} else {
|
|
789
1416
|
setStatus("No search to save");
|
|
@@ -817,9 +1444,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
817
1444
|
const readmeBox = new BoxRenderable(renderer, {
|
|
818
1445
|
visible: false,
|
|
819
1446
|
flexGrow: 1,
|
|
820
|
-
backgroundColor: colors.
|
|
821
|
-
|
|
822
|
-
|
|
1447
|
+
backgroundColor: colors.surface,
|
|
1448
|
+
borderBottom: true,
|
|
1449
|
+
borderBottomColor: colors.border,
|
|
823
1450
|
title: " README ",
|
|
824
1451
|
titleColor: colors.green,
|
|
825
1452
|
});
|
|
@@ -872,8 +1499,13 @@ export async function launchBrowser(): Promise<void> {
|
|
|
872
1499
|
`${repo.owner}/${repo.name}/master/README.md`,
|
|
873
1500
|
`${repo.owner}/${repo.name}/main/README.rst`,
|
|
874
1501
|
]) {
|
|
875
|
-
const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
|
|
876
|
-
|
|
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
|
+
}
|
|
877
1509
|
}
|
|
878
1510
|
if (!text) {
|
|
879
1511
|
readmeText.content = ` (no README found for ${repo.fullName})`;
|
|
@@ -899,10 +1531,12 @@ export async function launchBrowser(): Promise<void> {
|
|
|
899
1531
|
// ── Status bar ──────────────────────────────────────────────────────
|
|
900
1532
|
const statusBar = new TextRenderable(renderer, {
|
|
901
1533
|
content: " Ready. Press Enter to search.",
|
|
902
|
-
backgroundColor: colors.
|
|
1534
|
+
backgroundColor: colors.surface,
|
|
903
1535
|
color: colors.muted,
|
|
904
1536
|
height: 1,
|
|
905
1537
|
paddingX: 1,
|
|
1538
|
+
borderTop: true,
|
|
1539
|
+
borderTopColor: colors.border,
|
|
906
1540
|
});
|
|
907
1541
|
root.add(statusBar);
|
|
908
1542
|
|
|
@@ -928,7 +1562,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
928
1562
|
}
|
|
929
1563
|
|
|
930
1564
|
function setToolbar() {
|
|
931
|
-
toolbarText.content = formatToolbar(currentSort, currentLimit);
|
|
1565
|
+
toolbarText.content = formatToolbar(currentSort, currentLimit, totalCount);
|
|
932
1566
|
renderer.requestRender();
|
|
933
1567
|
}
|
|
934
1568
|
|
|
@@ -937,22 +1571,30 @@ export async function launchBrowser(): Promise<void> {
|
|
|
937
1571
|
detailText.content = "";
|
|
938
1572
|
return;
|
|
939
1573
|
}
|
|
940
|
-
const topics = repo.topics.length
|
|
1574
|
+
const topics = repo.topics.length
|
|
1575
|
+
? repo.topics.slice(0, 8).join(", ")
|
|
1576
|
+
: "—";
|
|
941
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
|
+
|
|
942
1587
|
const lines = [
|
|
943
|
-
`
|
|
944
|
-
|
|
945
|
-
`
|
|
946
|
-
`
|
|
947
|
-
`
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
"",
|
|
954
|
-
repo.url,
|
|
955
|
-
].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
|
+
];
|
|
956
1598
|
detailText.content = lines.join("\n");
|
|
957
1599
|
}
|
|
958
1600
|
|
|
@@ -966,7 +1608,11 @@ export async function launchBrowser(): Promise<void> {
|
|
|
966
1608
|
return bits === 0 ? " " : String.fromCodePoint(0x2800 + bits);
|
|
967
1609
|
}
|
|
968
1610
|
|
|
969
|
-
function buildChartString(
|
|
1611
|
+
function buildChartString(
|
|
1612
|
+
values: number[],
|
|
1613
|
+
termW: number,
|
|
1614
|
+
termH: number,
|
|
1615
|
+
): string[] {
|
|
970
1616
|
if (values.length === 0) return ["(no data)"];
|
|
971
1617
|
|
|
972
1618
|
// Braille resolution: 2 horizontal dots and 4 vertical dots per terminal cell.
|
|
@@ -980,10 +1626,12 @@ export async function launchBrowser(): Promise<void> {
|
|
|
980
1626
|
}
|
|
981
1627
|
const max = Math.max(...sampled, 1);
|
|
982
1628
|
// Map each sample to a dot row (0 = bottom, dotH-1 = top).
|
|
983
|
-
const dotRows = sampled.map(v => Math.round((v / max) * (dotH - 1)));
|
|
1629
|
+
const dotRows = sampled.map((v) => Math.round((v / max) * (dotH - 1)));
|
|
984
1630
|
|
|
985
1631
|
// Build a mask grid: dotGrid[row][col] = true if the silhouette passes through.
|
|
986
|
-
const dotGrid: boolean[][] = Array.from({ length: dotH }, () =>
|
|
1632
|
+
const dotGrid: boolean[][] = Array.from({ length: dotH }, () =>
|
|
1633
|
+
Array(dotW).fill(false),
|
|
1634
|
+
);
|
|
987
1635
|
|
|
988
1636
|
// Draw the silhouette line: mark the dot at each column.
|
|
989
1637
|
for (let c = 0; c < dotW; c++) {
|
|
@@ -1008,29 +1656,54 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1008
1656
|
const r0 = tr * 4;
|
|
1009
1657
|
const c0 = tc * 2;
|
|
1010
1658
|
let bits = 0;
|
|
1011
|
-
if (r0 + 0 < dotH && c0 + 0 < dotW && dotGrid[r0 + 0][c0 + 0])
|
|
1012
|
-
|
|
1013
|
-
if (r0 +
|
|
1014
|
-
|
|
1015
|
-
if (r0 +
|
|
1016
|
-
|
|
1017
|
-
if (r0 +
|
|
1018
|
-
|
|
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;
|
|
1019
1675
|
chars.push(brailleChar(bits));
|
|
1020
1676
|
}
|
|
1021
1677
|
|
|
1022
1678
|
// Y-axis labels
|
|
1023
|
-
const labelRows = [
|
|
1024
|
-
|
|
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
|
+
];
|
|
1025
1693
|
const idx = labelRows.indexOf(tr);
|
|
1026
1694
|
let label = " ";
|
|
1027
|
-
if (idx >= 0)
|
|
1028
|
-
|
|
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("")}`);
|
|
1029
1701
|
}
|
|
1030
1702
|
|
|
1031
|
-
// X-axis
|
|
1032
|
-
|
|
1033
|
-
|
|
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];
|
|
1034
1707
|
}
|
|
1035
1708
|
|
|
1036
1709
|
async function loadChart(repo: Repo) {
|
|
@@ -1038,10 +1711,13 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1038
1711
|
renderer.requestRender();
|
|
1039
1712
|
try {
|
|
1040
1713
|
const [chartRes, repoRes] = await Promise.all([
|
|
1041
|
-
fetch(
|
|
1042
|
-
{
|
|
1043
|
-
|
|
1044
|
-
|
|
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)),
|
|
1045
1721
|
]);
|
|
1046
1722
|
|
|
1047
1723
|
let chartSection = "";
|
|
@@ -1052,22 +1728,28 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1052
1728
|
const chartW = 60;
|
|
1053
1729
|
const chartH = 10;
|
|
1054
1730
|
const chartLines = buildChartString(chartCommitData, chartW, chartH);
|
|
1055
|
-
chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join(
|
|
1731
|
+
chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join(
|
|
1732
|
+
"\n",
|
|
1733
|
+
);
|
|
1056
1734
|
}
|
|
1057
1735
|
}
|
|
1058
1736
|
|
|
1059
1737
|
const desc = repo.description ?? "";
|
|
1060
|
-
const lang = repo.language ?? "
|
|
1738
|
+
const lang = repo.language ?? "—";
|
|
1061
1739
|
const updated = repo.updatedAt ? repo.updatedAt.slice(0, 10) : "";
|
|
1062
|
-
const topics = repoRes?.topics?.length
|
|
1063
|
-
|
|
1064
|
-
const growthLine = currentMode === "trending" && repo.score > 0
|
|
1065
|
-
? ` Growth \u25b2 ${fmtStars(repo.score)} ${trendingPeriod}`
|
|
1740
|
+
const topics = repoRes?.topics?.length
|
|
1741
|
+
? repoRes.topics.slice(0, 8).join(", ")
|
|
1066
1742
|
: "";
|
|
1067
1743
|
|
|
1744
|
+
const growthLine =
|
|
1745
|
+
currentMode === "trending" && repo.score > 0
|
|
1746
|
+
? ` Growth \u25b2 ${fmtStars(repo.score)} ${trendingPeriod}`
|
|
1747
|
+
: "";
|
|
1748
|
+
|
|
1068
1749
|
const detailLines = [
|
|
1069
|
-
`
|
|
1070
|
-
|
|
1750
|
+
` ${repo.fullName}`,
|
|
1751
|
+
``,
|
|
1752
|
+
` ★ ${repo.stars.toLocaleString()} ♡ ${repo.forks.toLocaleString()} ${lang}`,
|
|
1071
1753
|
growthLine,
|
|
1072
1754
|
updated ? ` Updated ${updated}` : "",
|
|
1073
1755
|
topics ? ` Topics ${topics}` : "",
|
|
@@ -1075,12 +1757,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1075
1757
|
desc,
|
|
1076
1758
|
"",
|
|
1077
1759
|
` ${repo.url}`,
|
|
1078
|
-
].filter(l => l !== "");
|
|
1760
|
+
].filter((l) => l !== "");
|
|
1079
1761
|
|
|
1080
|
-
detailText.content = [
|
|
1081
|
-
...detailLines,
|
|
1082
|
-
chartSection,
|
|
1083
|
-
].join("\n");
|
|
1762
|
+
detailText.content = [...detailLines, chartSection].join("\n");
|
|
1084
1763
|
} catch {
|
|
1085
1764
|
detailText.content = " Failed to load chart";
|
|
1086
1765
|
}
|
|
@@ -1110,37 +1789,14 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1110
1789
|
|
|
1111
1790
|
// ── Trending helpers ────────────────────────────────────────────────
|
|
1112
1791
|
|
|
1113
|
-
function formatTrendingLine(r:
|
|
1114
|
-
const
|
|
1792
|
+
function formatTrendingLine(r: Repo, rank: number): string {
|
|
1793
|
+
const rankStr = String(rank).padStart(2, "0");
|
|
1115
1794
|
const name = `${r.owner}/${r.name}`.padEnd(30).slice(0, 30);
|
|
1116
1795
|
const lang = (r.language || "?").padEnd(12).slice(0, 12);
|
|
1117
1796
|
const stars = `★ ${fmtStars(r.stars)}`.padEnd(9).slice(0, 9);
|
|
1118
|
-
const arrow = r.
|
|
1119
|
-
const growth = `${arrow} ${fmtStars(r.
|
|
1120
|
-
return `[${
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
function trendingRepoToSelectValue(r: TrendingRepo): Repo {
|
|
1124
|
-
return {
|
|
1125
|
-
id: 0,
|
|
1126
|
-
fullName: `${r.owner}/${r.name}`,
|
|
1127
|
-
name: r.name,
|
|
1128
|
-
owner: r.owner,
|
|
1129
|
-
description: r.description,
|
|
1130
|
-
url: `https://github.com/${r.owner}/${r.name}`,
|
|
1131
|
-
stars: r.stars,
|
|
1132
|
-
forks: 0,
|
|
1133
|
-
watchers: 0,
|
|
1134
|
-
language: r.language,
|
|
1135
|
-
topics: [],
|
|
1136
|
-
archived: false,
|
|
1137
|
-
isFork: false,
|
|
1138
|
-
private: false,
|
|
1139
|
-
createdAt: "",
|
|
1140
|
-
updatedAt: "",
|
|
1141
|
-
pushedAt: "",
|
|
1142
|
-
score: r.starsToday,
|
|
1143
|
-
};
|
|
1797
|
+
const arrow = r.score > 0 ? "▲" : r.score < 0 ? "▼" : "—";
|
|
1798
|
+
const growth = `${arrow} ${fmtStars(r.score)} ${trendingPeriod}`;
|
|
1799
|
+
return `[${rankStr}] ${name} ${lang} ${stars} ${growth}`;
|
|
1144
1800
|
}
|
|
1145
1801
|
|
|
1146
1802
|
async function loadTrending() {
|
|
@@ -1150,31 +1806,59 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1150
1806
|
toolbarText.visible = false;
|
|
1151
1807
|
trendingTabBox.visible = true;
|
|
1152
1808
|
renderTrendingTabs();
|
|
1153
|
-
resultsSelect.options = [
|
|
1809
|
+
resultsSelect.options = [
|
|
1810
|
+
{ name: " Loading trending...", description: "", value: null },
|
|
1811
|
+
];
|
|
1154
1812
|
detailText.content = "";
|
|
1155
1813
|
statusMgr.set("loading", "Loading trending repos...");
|
|
1156
1814
|
renderer.requestRender();
|
|
1157
1815
|
|
|
1158
1816
|
try {
|
|
1159
|
-
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;
|
|
1160
1823
|
const since = tabSince(trendingTab);
|
|
1161
|
-
trendingPeriod =
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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,
|
|
1166
1834
|
}));
|
|
1167
1835
|
if (fetched.length > 0) {
|
|
1168
1836
|
resultsSelect.setSelectedIndex(0);
|
|
1169
|
-
updateDetail(
|
|
1837
|
+
updateDetail(fetched[0]);
|
|
1170
1838
|
} else {
|
|
1171
1839
|
throw new NoResultsError(trendingTab);
|
|
1172
1840
|
}
|
|
1173
|
-
statusMgr.set(
|
|
1174
|
-
|
|
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
|
+
});
|
|
1175
1852
|
} catch (err) {
|
|
1176
|
-
const msg =
|
|
1177
|
-
|
|
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
|
+
];
|
|
1178
1862
|
statusMgr.set("error", msg.slice(0, 60));
|
|
1179
1863
|
}
|
|
1180
1864
|
isLoading = false;
|
|
@@ -1186,7 +1870,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1186
1870
|
trendingTabBox.visible = false;
|
|
1187
1871
|
searchBox.visible = true;
|
|
1188
1872
|
toolbarText.visible = true;
|
|
1189
|
-
searchInput.placeholder =
|
|
1873
|
+
searchInput.placeholder =
|
|
1874
|
+
"Search GitHub repos (e.g. rust cli, or language:Rust stars:>100)";
|
|
1190
1875
|
searchInput.focus();
|
|
1191
1876
|
renderer.requestRender();
|
|
1192
1877
|
}
|
|
@@ -1201,7 +1886,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1201
1886
|
isLoading = true;
|
|
1202
1887
|
currentQueryInput = q;
|
|
1203
1888
|
if (!append) {
|
|
1204
|
-
resultsSelect.options = [
|
|
1889
|
+
resultsSelect.options = [
|
|
1890
|
+
{ name: " Searching...", description: "", value: null },
|
|
1891
|
+
];
|
|
1205
1892
|
detailText.content = "";
|
|
1206
1893
|
}
|
|
1207
1894
|
statusMgr.set("searching", `Searching for "${q}"`);
|
|
@@ -1239,15 +1926,30 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1239
1926
|
const idx = append ? resultsSelect.options.length - newRepos.length : 0;
|
|
1240
1927
|
updateDetail(currentRepos[idx]);
|
|
1241
1928
|
resultsSelect.setSelectedIndex(idx);
|
|
1242
|
-
statusMgr.set(
|
|
1929
|
+
statusMgr.set(
|
|
1930
|
+
"success",
|
|
1931
|
+
`Page ${currentPage} — ${currentRepos.length} of ${totalCount.toLocaleString()} results`,
|
|
1932
|
+
);
|
|
1243
1933
|
if (!append) {
|
|
1244
|
-
appendHistory({
|
|
1934
|
+
appendHistory({
|
|
1935
|
+
query: q,
|
|
1936
|
+
mode: "search",
|
|
1937
|
+
timestamp: Date.now(),
|
|
1938
|
+
resultCount: currentRepos.length,
|
|
1939
|
+
});
|
|
1245
1940
|
}
|
|
1246
1941
|
}
|
|
1247
1942
|
} catch (err) {
|
|
1248
1943
|
currentRepos = [];
|
|
1249
|
-
resultsSelect.options = [
|
|
1250
|
-
|
|
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);
|
|
1251
1953
|
statusMgr.set("error", msg.slice(0, 60));
|
|
1252
1954
|
}
|
|
1253
1955
|
|
|
@@ -1313,21 +2015,56 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1313
2015
|
renderer.keyInput.on("keypress", (key) => {
|
|
1314
2016
|
// ── Overlay-mode handling ────────────────────────────────────────
|
|
1315
2017
|
if (currentOverlay !== "none") {
|
|
1316
|
-
// Escape or q closes any overlay
|
|
1317
|
-
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") {
|
|
1318
2033
|
showOverlay("none");
|
|
1319
2034
|
renderer.requestRender();
|
|
1320
2035
|
return;
|
|
1321
2036
|
}
|
|
1322
2037
|
|
|
1323
|
-
// Leader menu:
|
|
2038
|
+
// Leader menu: hierarchical submenu navigation
|
|
1324
2039
|
if (currentOverlay === "leader") {
|
|
1325
|
-
if (
|
|
2040
|
+
if (
|
|
2041
|
+
key.name === "enter" ||
|
|
2042
|
+
key.name === "return" ||
|
|
2043
|
+
key.name === "right" ||
|
|
2044
|
+
key.name === "l"
|
|
2045
|
+
) {
|
|
1326
2046
|
const sel = leaderSelect.getSelectedOption();
|
|
1327
|
-
const
|
|
1328
|
-
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();
|
|
1329
2059
|
showOverlay("none");
|
|
1330
|
-
|
|
2060
|
+
entry.action();
|
|
2061
|
+
renderer.requestRender();
|
|
2062
|
+
}
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
if (key.name === "left" || key.name === "h") {
|
|
2066
|
+
if (popMenuLevel()) {
|
|
2067
|
+
setToolbar();
|
|
1331
2068
|
renderer.requestRender();
|
|
1332
2069
|
}
|
|
1333
2070
|
return;
|
|
@@ -1365,7 +2102,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1365
2102
|
const entry = (sel.value as any).entry;
|
|
1366
2103
|
showOverlay("none");
|
|
1367
2104
|
if (entry.mode === "trending") {
|
|
1368
|
-
if (entry.tab)
|
|
2105
|
+
if (entry.tab)
|
|
2106
|
+
trendingTab = entry.tab as (typeof TAB_NAMES)[number];
|
|
1369
2107
|
loadTrending();
|
|
1370
2108
|
} else {
|
|
1371
2109
|
searchInput.value = entry.query;
|
|
@@ -1417,7 +2155,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1417
2155
|
showOverlay("none");
|
|
1418
2156
|
touchSavedSearch(s.name);
|
|
1419
2157
|
if (s.mode === "trending") {
|
|
1420
|
-
if (s.tab) trendingTab = s.tab as typeof TAB_NAMES[number];
|
|
2158
|
+
if (s.tab) trendingTab = s.tab as (typeof TAB_NAMES)[number];
|
|
1421
2159
|
loadTrending();
|
|
1422
2160
|
} else {
|
|
1423
2161
|
searchInput.value = s.query;
|
|
@@ -1522,6 +2260,51 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1522
2260
|
return;
|
|
1523
2261
|
}
|
|
1524
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
|
+
|
|
1525
2308
|
return;
|
|
1526
2309
|
}
|
|
1527
2310
|
|
|
@@ -1533,7 +2316,13 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1533
2316
|
toggleGraph();
|
|
1534
2317
|
return;
|
|
1535
2318
|
}
|
|
1536
|
-
saveSession({
|
|
2319
|
+
saveSession({
|
|
2320
|
+
mode: currentMode,
|
|
2321
|
+
query: currentQueryInput,
|
|
2322
|
+
sort: currentSort,
|
|
2323
|
+
limit: currentLimit,
|
|
2324
|
+
trendingTab,
|
|
2325
|
+
});
|
|
1537
2326
|
cleanup();
|
|
1538
2327
|
return;
|
|
1539
2328
|
}
|
|
@@ -1569,9 +2358,10 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1569
2358
|
const currentWord = lastSpace >= 0 ? val.slice(lastSpace + 1) : val;
|
|
1570
2359
|
const suggestions = suggestFor(currentWord);
|
|
1571
2360
|
if (suggestions.length > 0) {
|
|
1572
|
-
const newVal =
|
|
1573
|
-
|
|
1574
|
-
|
|
2361
|
+
const newVal =
|
|
2362
|
+
lastSpace >= 0
|
|
2363
|
+
? val.slice(0, lastSpace + 1) + suggestions[0]
|
|
2364
|
+
: suggestions[0];
|
|
1575
2365
|
searchInput.value = newVal;
|
|
1576
2366
|
renderer.requestRender();
|
|
1577
2367
|
}
|
|
@@ -1603,14 +2393,20 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1603
2393
|
if (key.name === "left" || key.name === "h") {
|
|
1604
2394
|
if (currentMode === "trending") {
|
|
1605
2395
|
const idx = TAB_NAMES.indexOf(trendingTab);
|
|
1606
|
-
if (idx > 0) {
|
|
2396
|
+
if (idx > 0) {
|
|
2397
|
+
trendingTab = TAB_NAMES[idx - 1];
|
|
2398
|
+
loadTrending();
|
|
2399
|
+
}
|
|
1607
2400
|
}
|
|
1608
2401
|
return;
|
|
1609
2402
|
}
|
|
1610
2403
|
if (key.name === "right" || key.name === "l") {
|
|
1611
2404
|
if (currentMode === "trending") {
|
|
1612
2405
|
const idx = TAB_NAMES.indexOf(trendingTab);
|
|
1613
|
-
if (idx < TAB_NAMES.length - 1) {
|
|
2406
|
+
if (idx < TAB_NAMES.length - 1) {
|
|
2407
|
+
trendingTab = TAB_NAMES[idx + 1];
|
|
2408
|
+
loadTrending();
|
|
2409
|
+
}
|
|
1614
2410
|
}
|
|
1615
2411
|
return;
|
|
1616
2412
|
}
|
|
@@ -1635,7 +2431,22 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1635
2431
|
}
|
|
1636
2432
|
});
|
|
1637
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
|
+
}
|
|
1638
2447
|
// ── Start ──────────────────────────────────────────────────────────
|
|
2448
|
+
// Check for updates (non-blocking — doesn't delay TUI startup)
|
|
2449
|
+
checkForUpdateAndShow();
|
|
1639
2450
|
renderer.start();
|
|
1640
2451
|
if (currentMode === "trending") {
|
|
1641
2452
|
loadTrending();
|
|
@@ -1656,21 +2467,27 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1656
2467
|
function formatResultLine(repo: Repo): string {
|
|
1657
2468
|
const stars = formatStars(repo.stars);
|
|
1658
2469
|
const lang = (repo.language ?? "?").padEnd(12).slice(0, 12);
|
|
1659
|
-
|
|
2470
|
+
const name = repo.fullName.padEnd(40).slice(0, 40);
|
|
2471
|
+
return `${name} ${lang} ${stars}`;
|
|
1660
2472
|
}
|
|
1661
2473
|
|
|
1662
2474
|
function formatStars(n: number): string {
|
|
2475
|
+
if (n >= 1000000) return `★ ${(n / 1000000).toFixed(1).replace(/\.0$/, "")}M`;
|
|
1663
2476
|
if (n >= 1000) return `★ ${(n / 1000).toFixed(1).replace(/\.0$/, "")}k`;
|
|
1664
2477
|
return `★ ${n}`;
|
|
1665
2478
|
}
|
|
1666
2479
|
|
|
1667
|
-
function formatToolbar(sort: SortStrategy, limit: number): string {
|
|
2480
|
+
function formatToolbar(sort: SortStrategy, limit: number, totalCount: number): string {
|
|
1668
2481
|
const sortLabel = SORT_MODES.find((m) => m.key === sort)?.label ?? sort;
|
|
1669
|
-
return ` sort: ${sortLabel} limit: ${limit}`;
|
|
2482
|
+
return ` sort: ${sortLabel} limit: ${limit} results: ${totalCount.toLocaleString()}`;
|
|
1670
2483
|
}
|
|
1671
2484
|
|
|
1672
2485
|
function cleanup(): void {
|
|
1673
|
-
try {
|
|
2486
|
+
try {
|
|
2487
|
+
rotateHistory();
|
|
2488
|
+
} catch {
|
|
2489
|
+
/* non-critical */
|
|
2490
|
+
}
|
|
1674
2491
|
process.exit(0);
|
|
1675
2492
|
}
|
|
1676
2493
|
|