github-search-cli 9.1.0 → 9.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -11
- package/bin/ghfind.js +3 -3
- package/completions/ghfind.bash +5 -1
- package/completions/ghfind.fish +2 -0
- package/completions/ghfind.zsh +2 -0
- package/demo.svg +51 -46
- package/dist/{chunk-TUMYR2IC.js → chunk-IVUU2KGR.js} +1 -2
- package/dist/cli.js +1014 -238
- package/dist/{config-DFL44OVL.js → config-GNZOQRTH.js} +1 -1
- package/package.json +2 -4
- package/scripts/postinstall.js +10 -7
- package/src/cli.ts +116 -7
- package/src/config.ts +1 -1
- package/src/help.ts +45 -15
- package/src/init.ts +1 -1
- package/src/package-browser.ts +305 -0
- package/src/package.ts +137 -0
- package/src/search.ts +1 -0
- package/src/themes.ts +60 -36
- package/src/trending.ts +31 -5
- package/src/tui.ts +390 -52
- package/src/types.ts +16 -2
- package/vendor/bun.exe +0 -0
package/src/tui.ts
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
TAB_NAMES,
|
|
49
49
|
fmtStars,
|
|
50
50
|
} from "./trending";
|
|
51
|
-
import { loadConfig } from "./config";
|
|
51
|
+
import { loadConfig, saveConfig } from "./config";
|
|
52
52
|
import { buildHelpSections, HELP_KEYS_COLUMN } from "./help";
|
|
53
53
|
import {
|
|
54
54
|
SearchCliError,
|
|
@@ -81,10 +81,16 @@ import { fetchDeepDive, buildDeepDiveText } from "./deepdive";
|
|
|
81
81
|
import { buildComparisonTable } from "./compare";
|
|
82
82
|
import { fetchTopics, type TopicItem } from "./explore";
|
|
83
83
|
import { exportToFile, type ExportFormat } from "./output";
|
|
84
|
-
import { loadTheme } from "./themes";
|
|
84
|
+
import { listThemes, loadTheme } from "./themes";
|
|
85
|
+
import {
|
|
86
|
+
createPackageSearch,
|
|
87
|
+
sortPackages,
|
|
88
|
+
PACKAGE_SORT_MODES,
|
|
89
|
+
type Package,
|
|
90
|
+
type PackageSortMode,
|
|
91
|
+
} from "./package";
|
|
85
92
|
import { StatusManager } from "./status";
|
|
86
93
|
import {
|
|
87
|
-
addNotification,
|
|
88
94
|
getNotifications,
|
|
89
95
|
dismissNotification,
|
|
90
96
|
dismissAll,
|
|
@@ -114,7 +120,6 @@ const colors = {
|
|
|
114
120
|
surfaceAlt: "#525050",
|
|
115
121
|
text: "#e0e4f0",
|
|
116
122
|
muted: "#a8a6a6",
|
|
117
|
-
blue: "#89b4fa",
|
|
118
123
|
green: "#a6e3a1",
|
|
119
124
|
yellow: "#f9e2af",
|
|
120
125
|
red: "#f38ba8",
|
|
@@ -150,10 +155,20 @@ const SORT_MODES: { key: SortStrategy; label: string }[] = [
|
|
|
150
155
|
];
|
|
151
156
|
|
|
152
157
|
// ─── Main ─────────────────────────────────────────────────────────────
|
|
153
|
-
export async function launchBrowser(): Promise<void> {
|
|
158
|
+
export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
154
159
|
let renderer;
|
|
155
160
|
try {
|
|
156
|
-
|
|
161
|
+
const isTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
162
|
+
if (isTTY) {
|
|
163
|
+
renderer = await createCliRenderer();
|
|
164
|
+
} else {
|
|
165
|
+
renderer = await createCliRenderer({
|
|
166
|
+
width: 80,
|
|
167
|
+
height: 24,
|
|
168
|
+
screenMode: "main-screen",
|
|
169
|
+
autoFocus: false,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
157
172
|
} catch (err) {
|
|
158
173
|
const isNode = process.versions.bun === undefined;
|
|
159
174
|
if (isNode) {
|
|
@@ -176,6 +191,12 @@ export async function launchBrowser(): Promise<void> {
|
|
|
176
191
|
|
|
177
192
|
const root = renderer.root;
|
|
178
193
|
root.flexDirection = "column";
|
|
194
|
+
root.border = true;
|
|
195
|
+
root.borderTop = true;
|
|
196
|
+
root.borderBottom = true;
|
|
197
|
+
root.borderLeft = true;
|
|
198
|
+
root.borderRight = true;
|
|
199
|
+
root.borderColor = colors.borderAccent;
|
|
179
200
|
root.backgroundColor = colors.bg;
|
|
180
201
|
|
|
181
202
|
// ── Config ──
|
|
@@ -183,7 +204,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
183
204
|
const githubToken = config.githubToken || process.env.GITHUB_TOKEN;
|
|
184
205
|
|
|
185
206
|
// Apply theme
|
|
186
|
-
const theme = loadTheme(config.theme);
|
|
207
|
+
const theme = loadTheme(theme_override || config.theme);
|
|
187
208
|
Object.assign(colors, theme);
|
|
188
209
|
|
|
189
210
|
// ── Session restore ──
|
|
@@ -195,7 +216,10 @@ export async function launchBrowser(): Promise<void> {
|
|
|
195
216
|
let currentLimit = session?.limit ?? config.defaultLimit;
|
|
196
217
|
let currentQueryInput = session?.query ?? "";
|
|
197
218
|
let isLoading = false;
|
|
198
|
-
let currentMode: "search" | "trending" = session?.mode ?? "search";
|
|
219
|
+
let currentMode: "landing" | "search" | "trending" | "packages" = session?.mode ?? "search";
|
|
220
|
+
let packages: Package[] = [];
|
|
221
|
+
let currentPackageSort: PackageSortMode = "best-match";
|
|
222
|
+
let packageResultsRaw: Package[] = [];
|
|
199
223
|
let trendingTab: (typeof TAB_NAMES)[number] =
|
|
200
224
|
(session?.trendingTab as (typeof TAB_NAMES)[number]) ?? "This Week";
|
|
201
225
|
let trendingPeriod = "this week";
|
|
@@ -214,14 +238,14 @@ export async function launchBrowser(): Promise<void> {
|
|
|
214
238
|
| "share"
|
|
215
239
|
| "leader"
|
|
216
240
|
| "readme"
|
|
217
|
-
| "update"
|
|
241
|
+
| "update"
|
|
242
|
+
| "landing" = "none";
|
|
218
243
|
let currentPage = 1;
|
|
219
244
|
let totalCount = 0;
|
|
220
245
|
let deepDiveActive = false;
|
|
221
246
|
let compareList: Repo[] = [];
|
|
222
247
|
let quitArmed = false;
|
|
223
248
|
|
|
224
|
-
// ── Header bar ──────────────────────────────────────────────────────
|
|
225
249
|
const header = new TextRenderable(renderer, {
|
|
226
250
|
content:
|
|
227
251
|
" ghfind — GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
|
|
@@ -272,18 +296,23 @@ export async function launchBrowser(): Promise<void> {
|
|
|
272
296
|
}
|
|
273
297
|
root.add(header);
|
|
274
298
|
root.add(trendingTabBox);
|
|
299
|
+
// ── Gap between header/tabs and search ────────────────────────────────
|
|
300
|
+
const gapBox = new BoxRenderable(renderer, {
|
|
301
|
+
height: 1,
|
|
302
|
+
visible: true,
|
|
303
|
+
});
|
|
304
|
+
root.add(gapBox);
|
|
275
305
|
|
|
276
306
|
// ── Search input row ────────────────────────────────────────────────
|
|
277
307
|
const searchBox = new BoxRenderable(renderer, {
|
|
278
308
|
visible: true,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
title: "
|
|
309
|
+
bordered: true,
|
|
310
|
+
borderColor: colors.border,
|
|
311
|
+
title: "",
|
|
282
312
|
titleColor: colors.blue,
|
|
283
313
|
width: "100%",
|
|
284
314
|
flexDirection: "row",
|
|
285
|
-
|
|
286
|
-
paddingBottom: 0,
|
|
315
|
+
marginTop: 1,
|
|
287
316
|
backgroundColor: colors.surface,
|
|
288
317
|
});
|
|
289
318
|
const searchInput = new InputRenderable(renderer, {
|
|
@@ -324,7 +353,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
324
353
|
|
|
325
354
|
// Results pane
|
|
326
355
|
const resultsBox = new BoxRenderable(renderer, {
|
|
327
|
-
|
|
356
|
+
bordered: true,
|
|
357
|
+
borderColor: colors.border,
|
|
328
358
|
borderBottomColor: colors.border,
|
|
329
359
|
title: " Results ",
|
|
330
360
|
titleColor: colors.blue,
|
|
@@ -355,7 +385,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
355
385
|
|
|
356
386
|
// Detail / Graph pane
|
|
357
387
|
const detailBox = new BoxRenderable(renderer, {
|
|
358
|
-
|
|
388
|
+
bordered: true,
|
|
389
|
+
borderColor: colors.border,
|
|
359
390
|
borderBottomColor: colors.border,
|
|
360
391
|
title: " Details ",
|
|
361
392
|
titleColor: colors.green,
|
|
@@ -369,18 +400,145 @@ export async function launchBrowser(): Promise<void> {
|
|
|
369
400
|
const detailText = new TextRenderable(renderer, {
|
|
370
401
|
content: "",
|
|
371
402
|
color: colors.text,
|
|
403
|
+
wrapMode: "none",
|
|
372
404
|
});
|
|
373
405
|
detailBox.add(detailText);
|
|
374
406
|
|
|
375
407
|
body.add(resultsBox);
|
|
376
408
|
body.add(detailBox);
|
|
377
409
|
root.add(body);
|
|
410
|
+
// ── Landing screen ────────────────────────────────────────────────
|
|
411
|
+
const landingBox = new BoxRenderable(renderer, {
|
|
412
|
+
visible: false,
|
|
413
|
+
flexGrow: 1,
|
|
414
|
+
flexDirection: "column",
|
|
415
|
+
alignItems: "center",
|
|
416
|
+
justifyContent: "center",
|
|
417
|
+
});
|
|
418
|
+
root.add(landingBox);
|
|
419
|
+
const landingBanner = new TextRenderable(renderer, {
|
|
420
|
+
content: [
|
|
421
|
+
" ██████╗ ██╗ ██╗███████╗██╗███╗ ██╗██████╗ ",
|
|
422
|
+
"██╔════╝ ██║ ██║██╔════╝██║████╗ ██║██╔══██╗",
|
|
423
|
+
"██║ ███╗███████║█████╗ ██║██╔██╗ ██║██║ ██║",
|
|
424
|
+
"██║ ██║██╔══██║██╔══╝ ██║██║╚██╗██║██║ ██║",
|
|
425
|
+
"╚██████╔╝██║ ██║██║ ██║██║ ╚████║██████╔",
|
|
426
|
+
" ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝",
|
|
427
|
+
].join("\n"),
|
|
428
|
+
color: colors.accent,
|
|
429
|
+
backgroundColor: colors.bg,
|
|
430
|
+
height: 6,
|
|
431
|
+
});
|
|
432
|
+
landingBox.add(landingBanner);
|
|
433
|
+
// Three option cards
|
|
434
|
+
const landingOptions = [
|
|
435
|
+
{
|
|
436
|
+
icon: "\u{1F50D}",
|
|
437
|
+
title: "Repo Search",
|
|
438
|
+
desc: "Search GitHub repositories by query, language, stars, and more",
|
|
439
|
+
action: () => { currentMode = "search"; showLanding(false); showSearchMode(); },
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
icon: "\u{1F4E6}",
|
|
443
|
+
title: "Package Search",
|
|
444
|
+
desc: "Search npm packages by name, description, or tags",
|
|
445
|
+
action: () => { currentMode = "packages"; showLanding(false); showPackagesMode(); },
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
icon: "\u{1F525}",
|
|
449
|
+
title: "Trending",
|
|
450
|
+
desc: "Browse the hottest repos on GitHub right now",
|
|
451
|
+
action: () => { currentMode = "trending"; showLanding(false); loadTrending(); },
|
|
452
|
+
},
|
|
453
|
+
];
|
|
454
|
+
let landingSelected = 0;
|
|
455
|
+
|
|
456
|
+
const landingCards: BoxRenderable[] = [];
|
|
457
|
+
for (let i = 0; i < landingOptions.length; i++) {
|
|
458
|
+
const opt = landingOptions[i];
|
|
459
|
+
const card = new BoxRenderable(renderer, {
|
|
460
|
+
flexDirection: "column",
|
|
461
|
+
height: 5,
|
|
462
|
+
width: "60%",
|
|
463
|
+
marginTop: 1,
|
|
464
|
+
border: true,
|
|
465
|
+
borderColor: i === 0 ? colors.accent : colors.border,
|
|
466
|
+
paddingX: 1,
|
|
467
|
+
focusable: false,
|
|
468
|
+
});
|
|
469
|
+
landingCards.push(card);
|
|
470
|
+
landingBox.add(card);
|
|
471
|
+
|
|
472
|
+
const spacerTop = new TextRenderable(renderer, { content: "", height: 1 });
|
|
473
|
+
const titleLine = new TextRenderable(renderer, {
|
|
474
|
+
content: opt.title,
|
|
475
|
+
color: i === 0 ? colors.accent : colors.text,
|
|
476
|
+
height: 1,
|
|
477
|
+
});
|
|
478
|
+
const spacerBot = new TextRenderable(renderer, { content: "", height: 1 });
|
|
479
|
+
card.add(spacerTop);
|
|
480
|
+
card.add(titleLine);
|
|
481
|
+
card.add(spacerBot);
|
|
482
|
+
}
|
|
378
483
|
|
|
379
|
-
|
|
484
|
+
const landingHint = new TextRenderable(renderer, {
|
|
485
|
+
content: " \u2191\u2193 Navigate \u21E9 Select [?]help [q]uit",
|
|
486
|
+
color: colors.muted,
|
|
487
|
+
backgroundColor: colors.bg,
|
|
488
|
+
height: 1,
|
|
489
|
+
marginTop: 1,
|
|
490
|
+
});
|
|
491
|
+
landingBox.add(landingHint);
|
|
492
|
+
|
|
493
|
+
function showLanding(visible: boolean) {
|
|
494
|
+
landingBox.visible = visible;
|
|
495
|
+
if (visible) {
|
|
496
|
+
currentMode = "landing";
|
|
497
|
+
hideMainContent();
|
|
498
|
+
landingSelected = 0;
|
|
499
|
+
updateLandingCards();
|
|
500
|
+
renderer.requestRender();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function updateLandingCards() {
|
|
505
|
+
for (let i = 0; i < landingCards.length; i++) {
|
|
506
|
+
const isSel = i === landingSelected;
|
|
507
|
+
landingCards[i].borderColor = isSel ? colors.accent : colors.border;
|
|
508
|
+
const children = landingCards[i].children ?? [];
|
|
509
|
+
const titleChild = children[1] as TextRenderable;
|
|
510
|
+
if (titleChild) {
|
|
511
|
+
titleChild.color = isSel ? colors.accent : colors.text;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Landing keyboard handler
|
|
517
|
+
renderer.keyInput.on("keypress", (key) => {
|
|
518
|
+
if (currentMode === "landing") {
|
|
519
|
+
if (key.name === "up" || key.name === "k") {
|
|
520
|
+
landingSelected = Math.max(0, landingSelected - 1);
|
|
521
|
+
updateLandingCards();
|
|
522
|
+
renderer.requestRender();
|
|
523
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
524
|
+
landingSelected = Math.min(landingOptions.length - 1, landingSelected + 1);
|
|
525
|
+
updateLandingCards();
|
|
526
|
+
renderer.requestRender();
|
|
527
|
+
} else if (key.name === "enter" || key.name === "return") {
|
|
528
|
+
landingOptions[landingSelected].action();
|
|
529
|
+
} else if (key.name === "?" || key.name === "h") {
|
|
530
|
+
showOverlay(currentOverlay === "help" ? "none" : "help");
|
|
531
|
+
} else if (key.name === "q") {
|
|
532
|
+
cleanup();
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
});
|
|
380
537
|
|
|
381
538
|
// Hide main content so overlays can take 100% of the content area
|
|
382
539
|
function hideMainContent() {
|
|
383
540
|
trendingTabBox.visible = false;
|
|
541
|
+
gapBox.visible = false;
|
|
384
542
|
searchBox.visible = false;
|
|
385
543
|
toolbarText.visible = false;
|
|
386
544
|
body.visible = false;
|
|
@@ -391,6 +549,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
391
549
|
trendingTabBox.visible = true;
|
|
392
550
|
} else {
|
|
393
551
|
searchBox.visible = true;
|
|
552
|
+
gapBox.visible = true;
|
|
394
553
|
toolbarText.visible = true;
|
|
395
554
|
}
|
|
396
555
|
body.visible = true;
|
|
@@ -411,10 +570,18 @@ export async function launchBrowser(): Promise<void> {
|
|
|
411
570
|
readmeBox.visible = false;
|
|
412
571
|
updateBox.visible = false;
|
|
413
572
|
updateDim.visible = false;
|
|
414
|
-
|
|
415
573
|
if (type === "none") {
|
|
416
574
|
currentOverlay = "none";
|
|
417
|
-
|
|
575
|
+
if (currentMode === "landing") {
|
|
576
|
+
landingBox.visible = true;
|
|
577
|
+
body.visible = false;
|
|
578
|
+
trendingTabBox.visible = false;
|
|
579
|
+
gapBox.visible = false;
|
|
580
|
+
searchBox.visible = false;
|
|
581
|
+
toolbarText.visible = false;
|
|
582
|
+
} else {
|
|
583
|
+
showMainContent();
|
|
584
|
+
}
|
|
418
585
|
renderer.requestRender();
|
|
419
586
|
return;
|
|
420
587
|
}
|
|
@@ -1079,7 +1246,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1079
1246
|
|
|
1080
1247
|
const groups: MenuCategory[] = [];
|
|
1081
1248
|
|
|
1082
|
-
// Search group
|
|
1249
|
+
// Search group
|
|
1083
1250
|
const searchItems: MenuEntry[] = [];
|
|
1084
1251
|
if (currentMode === "search") {
|
|
1085
1252
|
searchItems.push(
|
|
@@ -1119,6 +1286,29 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1119
1286
|
},
|
|
1120
1287
|
},
|
|
1121
1288
|
);
|
|
1289
|
+
} else if (currentMode === "packages") {
|
|
1290
|
+
searchItems.push(
|
|
1291
|
+
{
|
|
1292
|
+
type: "action",
|
|
1293
|
+
name: "Sort",
|
|
1294
|
+
description: `Cycle package sort (current: ${currentPackageSort})`,
|
|
1295
|
+
action: () => changePackageSort(),
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
type: "action",
|
|
1299
|
+
name: "Limit",
|
|
1300
|
+
description: `Cycle result limit (current: ${currentLimit})`,
|
|
1301
|
+
action: () => changeLimit(),
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
type: "action",
|
|
1305
|
+
name: "Refresh",
|
|
1306
|
+
description: "Re-run current package search",
|
|
1307
|
+
action: () => {
|
|
1308
|
+
if (currentQueryInput) doPackageSearch(currentQueryInput);
|
|
1309
|
+
},
|
|
1310
|
+
},
|
|
1311
|
+
);
|
|
1122
1312
|
}
|
|
1123
1313
|
if (searchItems.length > 0) {
|
|
1124
1314
|
groups.push({
|
|
@@ -1130,7 +1320,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1130
1320
|
});
|
|
1131
1321
|
}
|
|
1132
1322
|
|
|
1133
|
-
// Repo group
|
|
1323
|
+
// Repo group
|
|
1134
1324
|
const repoItems: MenuEntry[] = [];
|
|
1135
1325
|
if (hasRepo) {
|
|
1136
1326
|
repoItems.push(
|
|
@@ -1197,8 +1387,17 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1197
1387
|
});
|
|
1198
1388
|
}
|
|
1199
1389
|
|
|
1200
|
-
// Navigate group
|
|
1390
|
+
// Navigate group
|
|
1201
1391
|
const navItems: MenuEntry[] = [];
|
|
1392
|
+
navItems.push({
|
|
1393
|
+
type: "action",
|
|
1394
|
+
name: "Packages",
|
|
1395
|
+
description: "Search npm packages",
|
|
1396
|
+
action: () => {
|
|
1397
|
+
showOverlay("none");
|
|
1398
|
+
showPackagesMode();
|
|
1399
|
+
},
|
|
1400
|
+
});
|
|
1202
1401
|
if (currentMode === "search") {
|
|
1203
1402
|
navItems.push({
|
|
1204
1403
|
type: "action",
|
|
@@ -1562,7 +1761,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1562
1761
|
}
|
|
1563
1762
|
|
|
1564
1763
|
function setToolbar() {
|
|
1565
|
-
|
|
1764
|
+
const sort = currentMode === "packages" ? currentPackageSort : currentSort;
|
|
1765
|
+
toolbarText.content = formatToolbar(sort, currentLimit, totalCount);
|
|
1566
1766
|
renderer.requestRender();
|
|
1567
1767
|
}
|
|
1568
1768
|
|
|
@@ -1583,7 +1783,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1583
1783
|
if (repo.archived) statusTags.push("archived");
|
|
1584
1784
|
if (repo.isFork) statusTags.push("fork");
|
|
1585
1785
|
const tagStr = statusTags.length ? ` [${statusTags.join(", ")}]` : "";
|
|
1586
|
-
|
|
1786
|
+
|
|
1587
1787
|
const lines = [
|
|
1588
1788
|
` ${repo.fullName}${tagStr}`,
|
|
1589
1789
|
``,
|
|
@@ -1703,37 +1903,47 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1703
1903
|
// X-axis with week labels
|
|
1704
1904
|
const xAxis = ` ┆${"─".repeat(termW)}`;
|
|
1705
1905
|
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];
|
|
1906
|
+
return [...lines, xAxis, weekLabels];
|
|
1707
1907
|
}
|
|
1708
1908
|
|
|
1709
1909
|
async function loadChart(repo: Repo) {
|
|
1710
1910
|
detailText.content = " Loading chart...";
|
|
1711
1911
|
renderer.requestRender();
|
|
1712
1912
|
try {
|
|
1913
|
+
const headers: Record<string, string> = { "User-Agent": "ghfind/1.0" };
|
|
1914
|
+
if (githubToken) headers.Authorization = `Bearer ${githubToken}`;
|
|
1713
1915
|
const [chartRes, repoRes] = await Promise.all([
|
|
1714
1916
|
fetch(
|
|
1715
1917
|
`https://api.github.com/repos/${repo.owner}/${repo.name}/stats/participation`,
|
|
1716
|
-
{ headers
|
|
1918
|
+
{ headers },
|
|
1717
1919
|
),
|
|
1718
1920
|
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}`, {
|
|
1719
|
-
headers
|
|
1720
|
-
}).then((r) => (r.ok ? r.json() : null)),
|
|
1921
|
+
headers,
|
|
1922
|
+
}).then((r) => (r.ok ? r.clone().json() : null)),
|
|
1721
1923
|
]);
|
|
1722
|
-
|
|
1723
1924
|
let chartSection = "";
|
|
1724
1925
|
if (chartRes.ok) {
|
|
1725
1926
|
const data = await chartRes.json();
|
|
1726
1927
|
if (data?.all && data.all.length >= 2) {
|
|
1727
1928
|
chartCommitData = data.all;
|
|
1728
|
-
|
|
1929
|
+
// Chart width: leave room for y-axis labels (6 chars) and padding.
|
|
1930
|
+
// In fullscreen graph mode, detailBox takes 100% width.
|
|
1931
|
+
// In normal mode (50%), use half the terminal width.
|
|
1932
|
+
const termW = graphFullscreen
|
|
1933
|
+
? (process.stdout.columns || 120) - 12
|
|
1934
|
+
: Math.floor((process.stdout.columns || 120) / 2) - 12;
|
|
1935
|
+
const chartW = Math.max(20, Math.min(60, termW));
|
|
1729
1936
|
const chartH = 10;
|
|
1730
1937
|
const chartLines = buildChartString(chartCommitData, chartW, chartH);
|
|
1731
|
-
chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join(
|
|
1732
|
-
|
|
1733
|
-
);
|
|
1938
|
+
chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join("\n");
|
|
1939
|
+
} else if (data?.message) {
|
|
1940
|
+
chartSection = ["", `Chart unavailable: ${data.message}`].join("\n");
|
|
1734
1941
|
}
|
|
1942
|
+
} else {
|
|
1943
|
+
chartSection = ["", `Chart unavailable (API ${chartRes.status})`].join("\n");
|
|
1735
1944
|
}
|
|
1736
1945
|
|
|
1946
|
+
|
|
1737
1947
|
const desc = repo.description ?? "";
|
|
1738
1948
|
const lang = repo.language ?? "—";
|
|
1739
1949
|
const updated = repo.updatedAt ? repo.updatedAt.slice(0, 10) : "";
|
|
@@ -1805,6 +2015,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1805
2015
|
searchBox.visible = false;
|
|
1806
2016
|
toolbarText.visible = false;
|
|
1807
2017
|
trendingTabBox.visible = true;
|
|
2018
|
+
body.visible = true;
|
|
1808
2019
|
renderTrendingTabs();
|
|
1809
2020
|
resultsSelect.options = [
|
|
1810
2021
|
{ name: " Loading trending...", description: "", value: null },
|
|
@@ -1868,14 +2079,72 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1868
2079
|
function showSearchMode() {
|
|
1869
2080
|
currentMode = "search";
|
|
1870
2081
|
trendingTabBox.visible = false;
|
|
2082
|
+
gapBox.visible = false;
|
|
1871
2083
|
searchBox.visible = true;
|
|
1872
2084
|
toolbarText.visible = true;
|
|
2085
|
+
body.visible = true;
|
|
1873
2086
|
searchInput.placeholder =
|
|
1874
2087
|
"Search GitHub repos (e.g. rust cli, or language:Rust stars:>100)";
|
|
1875
2088
|
searchInput.focus();
|
|
1876
2089
|
renderer.requestRender();
|
|
1877
2090
|
}
|
|
1878
2091
|
|
|
2092
|
+
function showPackagesMode() {
|
|
2093
|
+
currentMode = "packages";
|
|
2094
|
+
trendingTabBox.visible = false;
|
|
2095
|
+
gapBox.visible = false;
|
|
2096
|
+
searchBox.visible = true;
|
|
2097
|
+
toolbarText.visible = true;
|
|
2098
|
+
body.visible = true;
|
|
2099
|
+
searchInput.placeholder = "Search npm packages (e.g. react, vue, typescript)";
|
|
2100
|
+
searchInput.focus();
|
|
2101
|
+
packages = [];
|
|
2102
|
+
resultsSelect.options = [
|
|
2103
|
+
{ name: "", description: "Type a query and press Enter to search", value: null },
|
|
2104
|
+
];
|
|
2105
|
+
detailText.content = "";
|
|
2106
|
+
renderer.requestRender();
|
|
2107
|
+
}
|
|
2108
|
+
async function doPackageSearch(queryText: string) {
|
|
2109
|
+
const q = queryText.trim();
|
|
2110
|
+
if (q === "" || isLoading) return;
|
|
2111
|
+
currentQueryInput = q;
|
|
2112
|
+
isLoading = true;
|
|
2113
|
+
resultsSelect.options = [
|
|
2114
|
+
{ name: " Searching packages...", description: "", value: null },
|
|
2115
|
+
];
|
|
2116
|
+
detailText.content = "";
|
|
2117
|
+
statusMgr.set("searching", `Searching npm for "${q}"`);
|
|
2118
|
+
renderer.requestRender();
|
|
2119
|
+
try {
|
|
2120
|
+
const search = createPackageSearch();
|
|
2121
|
+
const result = await search.searchPackage(q, currentLimit);
|
|
2122
|
+
totalCount = result.totalCount;
|
|
2123
|
+
packageResultsRaw = result.packages;
|
|
2124
|
+
packages = sortPackages(packageResultsRaw, currentPackageSort);
|
|
2125
|
+
setToolbar();
|
|
2126
|
+
if (packages.length > 0) {
|
|
2127
|
+
resultsSelect.options = packages.map((p) => ({
|
|
2128
|
+
name: `${p.name}@${p.version}`,
|
|
2129
|
+
description: p.description ?? "",
|
|
2130
|
+
value: p,
|
|
2131
|
+
}));
|
|
2132
|
+
resultsSelect.setSelectedIndex(0);
|
|
2133
|
+
statusMgr.set("success", `${packages.length} packages found`);
|
|
2134
|
+
} else {
|
|
2135
|
+
throw new NoResultsError(q);
|
|
2136
|
+
}
|
|
2137
|
+
} catch (err) {
|
|
2138
|
+
const msg =
|
|
2139
|
+
err instanceof Error ? err.message : String(err);
|
|
2140
|
+
resultsSelect.options = [
|
|
2141
|
+
{ name: " (error)", description: "", value: null },
|
|
2142
|
+
];
|
|
2143
|
+
statusMgr.set("error", msg.slice(0, 60));
|
|
2144
|
+
}
|
|
2145
|
+
isLoading = false;
|
|
2146
|
+
renderer.requestRender();
|
|
2147
|
+
}
|
|
1879
2148
|
async function doSearch(queryText: string, append = false) {
|
|
1880
2149
|
const q = queryText.trim();
|
|
1881
2150
|
if (q === "" || isLoading) return;
|
|
@@ -1975,6 +2244,23 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1975
2244
|
}
|
|
1976
2245
|
}
|
|
1977
2246
|
|
|
2247
|
+
function changePackageSort() {
|
|
2248
|
+
const idx = PACKAGE_SORT_MODES.findIndex((m) => m.key === currentPackageSort);
|
|
2249
|
+
const next = PACKAGE_SORT_MODES[(idx + 1) % PACKAGE_SORT_MODES.length];
|
|
2250
|
+
currentPackageSort = next.key;
|
|
2251
|
+
setToolbar();
|
|
2252
|
+
if (packageResultsRaw.length > 0) {
|
|
2253
|
+
packages = sortPackages(packageResultsRaw, currentPackageSort);
|
|
2254
|
+
resultsSelect.options = packages.map((p) => ({
|
|
2255
|
+
name: `${p.name}@${p.version}`,
|
|
2256
|
+
description: p.description ?? "",
|
|
2257
|
+
value: p,
|
|
2258
|
+
}));
|
|
2259
|
+
resultsSelect.setSelectedIndex(0);
|
|
2260
|
+
renderer.requestRender();
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
|
|
1978
2264
|
function changeLimit() {
|
|
1979
2265
|
const limits = [10, 25, 50, 100];
|
|
1980
2266
|
const idx = limits.indexOf(currentLimit);
|
|
@@ -1989,17 +2275,30 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1989
2275
|
// Enter in search input → show results
|
|
1990
2276
|
searchInput.on("enter", () => {
|
|
1991
2277
|
if (searchInput.value.trim() === "" || isLoading) return;
|
|
1992
|
-
|
|
1993
|
-
|
|
2278
|
+
if (currentMode === "packages") {
|
|
2279
|
+
doPackageSearch(searchInput.value);
|
|
2280
|
+
} else {
|
|
2281
|
+
doSearch(searchInput.value);
|
|
2282
|
+
}
|
|
1994
2283
|
});
|
|
1995
2284
|
|
|
1996
2285
|
// Navigate results → update detail pane
|
|
1997
2286
|
resultsSelect.on("selectionChanged", () => {
|
|
1998
2287
|
const opt = resultsSelect.getSelectedOption();
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
if (
|
|
2002
|
-
|
|
2288
|
+
if (currentMode === "packages") {
|
|
2289
|
+
const pack = opt?.value as Package | undefined;
|
|
2290
|
+
if (pack) {
|
|
2291
|
+
detailText.content = `Package: ${pack.name}@${pack.version}
|
|
2292
|
+
Downloads: ${pack.downloads.toLocaleString()}
|
|
2293
|
+
Score: ${pack.score.toFixed(2)}
|
|
2294
|
+
${pack.description ?? ""}`;
|
|
2295
|
+
}
|
|
2296
|
+
} else {
|
|
2297
|
+
const repo = opt?.value as Repo | undefined;
|
|
2298
|
+
if (repo) {
|
|
2299
|
+
if (graphFullscreen) loadChart(repo);
|
|
2300
|
+
else updateDetail(repo);
|
|
2301
|
+
}
|
|
2003
2302
|
}
|
|
2004
2303
|
renderer.requestRender();
|
|
2005
2304
|
});
|
|
@@ -2007,8 +2306,13 @@ export async function launchBrowser(): Promise<void> {
|
|
|
2007
2306
|
// Enter on a result → open URL
|
|
2008
2307
|
resultsSelect.on("itemSelected", () => {
|
|
2009
2308
|
const opt = resultsSelect.getSelectedOption();
|
|
2010
|
-
|
|
2011
|
-
|
|
2309
|
+
if (currentMode === "packages") {
|
|
2310
|
+
const pack = opt?.value as Package | undefined;
|
|
2311
|
+
if (pack?.url) openUrl(pack.url);
|
|
2312
|
+
} else {
|
|
2313
|
+
const repo = opt?.value as Repo | undefined;
|
|
2314
|
+
if (repo) openUrl(repo.url);
|
|
2315
|
+
}
|
|
2012
2316
|
});
|
|
2013
2317
|
|
|
2014
2318
|
// ── Global keyboard shortcuts ──────────────────────────────────────
|
|
@@ -2342,6 +2646,31 @@ export async function launchBrowser(): Promise<void> {
|
|
|
2342
2646
|
showLeaderMenu();
|
|
2343
2647
|
}
|
|
2344
2648
|
return;
|
|
2649
|
+
// 't' toggles theme and persists to config
|
|
2650
|
+
if (key.name === "t" && !searchInput.focused) {
|
|
2651
|
+
const current = config.theme;
|
|
2652
|
+
const themes = listThemes();
|
|
2653
|
+
const idx = themes.indexOf(current);
|
|
2654
|
+
const next = themes[(idx + 1) % themes.length];
|
|
2655
|
+
const nextTheme = loadTheme(next);
|
|
2656
|
+
Object.assign(colors, nextTheme);
|
|
2657
|
+
config.theme = next;
|
|
2658
|
+
saveConfig(config);
|
|
2659
|
+
renderer.requestRender();
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
// 'c' toggles compare on selected repo (when not typing in search)
|
|
2665
|
+
if (key.name === "c" && !searchInput.focused) {
|
|
2666
|
+
if (currentOverlay === "compare") {
|
|
2667
|
+
showOverlay("none");
|
|
2668
|
+
} else {
|
|
2669
|
+
toggleCompareOnSelected();
|
|
2670
|
+
refreshCompare();
|
|
2671
|
+
showOverlay("compare");
|
|
2672
|
+
}
|
|
2673
|
+
return;
|
|
2345
2674
|
}
|
|
2346
2675
|
|
|
2347
2676
|
// '?' / Ctrl+H toggle help overlay (fast path)
|
|
@@ -2446,18 +2775,23 @@ export async function launchBrowser(): Promise<void> {
|
|
|
2446
2775
|
}
|
|
2447
2776
|
// ── Start ──────────────────────────────────────────────────────────
|
|
2448
2777
|
// Check for updates (non-blocking — doesn't delay TUI startup)
|
|
2778
|
+
if (!session || !session.mode) {
|
|
2779
|
+
showLanding(true);
|
|
2780
|
+
}
|
|
2449
2781
|
checkForUpdateAndShow();
|
|
2450
2782
|
renderer.start();
|
|
2451
|
-
if (
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
searchInput.value = currentQueryInput;
|
|
2455
|
-
if (currentQueryInput) {
|
|
2456
|
-
doSearch(currentQueryInput);
|
|
2783
|
+
if (session && session.mode) {
|
|
2784
|
+
if (currentMode === "trending") {
|
|
2785
|
+
loadTrending();
|
|
2457
2786
|
} else {
|
|
2458
|
-
|
|
2787
|
+
searchInput.value = currentQueryInput;
|
|
2788
|
+
if (currentQueryInput) {
|
|
2789
|
+
doSearch(currentQueryInput);
|
|
2790
|
+
} else {
|
|
2791
|
+
showSearchMode();
|
|
2792
|
+
}
|
|
2793
|
+
searchInput.focus();
|
|
2459
2794
|
}
|
|
2460
|
-
searchInput.focus();
|
|
2461
2795
|
}
|
|
2462
2796
|
renderer.requestRender();
|
|
2463
2797
|
}
|
|
@@ -2477,7 +2811,11 @@ function formatStars(n: number): string {
|
|
|
2477
2811
|
return `★ ${n}`;
|
|
2478
2812
|
}
|
|
2479
2813
|
|
|
2480
|
-
function formatToolbar(
|
|
2814
|
+
function formatToolbar(
|
|
2815
|
+
sort: SortStrategy | PackageSortMode,
|
|
2816
|
+
limit: number,
|
|
2817
|
+
totalCount: number,
|
|
2818
|
+
): string {
|
|
2481
2819
|
const sortLabel = SORT_MODES.find((m) => m.key === sort)?.label ?? sort;
|
|
2482
2820
|
return ` sort: ${sortLabel} limit: ${limit} results: ${totalCount.toLocaleString()}`;
|
|
2483
2821
|
}
|
|
@@ -2494,6 +2832,6 @@ function cleanup(): void {
|
|
|
2494
2832
|
// ── Auto-run ──────────────────────────────────────────────────────────
|
|
2495
2833
|
// Only launch when run directly (bun run src/tui.ts), not when bundled
|
|
2496
2834
|
// into cli.js or imported as a module.
|
|
2497
|
-
if (import.meta.main && !process.env.GHFIND_BUNDLED) {
|
|
2835
|
+
if (import.meta.main && !process.env.GHFIND_BUNDLED && !process.env.GHFIND_CLI_RUN) {
|
|
2498
2836
|
launchBrowser();
|
|
2499
2837
|
}
|