github-search-cli 9.1.0 → 9.1.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 +6 -1
- 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 +976 -208
- package/dist/{config-DFL44OVL.js → config-GNZOQRTH.js} +1 -1
- package/package.json +1 -1
- package/src/cli.ts +88 -6
- package/src/config.ts +1 -1
- package/src/help.ts +142 -112
- package/src/init.ts +1 -1
- package/src/package-browser.ts +305 -0
- package/src/package.ts +137 -0
- package/src/themes.ts +60 -36
- package/src/tui.ts +403 -52
- package/src/types.ts +16 -2
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,171 @@ 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: "row",
|
|
461
|
+
height: 1,
|
|
462
|
+
width: "60%",
|
|
463
|
+
marginTop: 1,
|
|
464
|
+
backgroundColor: undefined,
|
|
465
|
+
borderColor: i === 0 ? colors.accent : colors.border,
|
|
466
|
+
borderWidth: 1,
|
|
467
|
+
paddingX: 1,
|
|
468
|
+
alignItems: "center",
|
|
469
|
+
focusable: false,
|
|
470
|
+
});
|
|
471
|
+
landingCards.push(card);
|
|
472
|
+
landingBox.add(card);
|
|
473
|
+
|
|
474
|
+
const titleLine = new TextRenderable(renderer, {
|
|
475
|
+
content: opt.title,
|
|
476
|
+
color: i === 0 ? colors.accent : colors.text,
|
|
477
|
+
backgroundColor: undefined,
|
|
478
|
+
height: 1,
|
|
479
|
+
flexGrow: 1,
|
|
480
|
+
});
|
|
481
|
+
card.add(titleLine);
|
|
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 opt = landingOptions[i];
|
|
507
|
+
const isSel = i === landingSelected;
|
|
508
|
+
landingCards[i].backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
509
|
+
landingCards[i].borderColor = isSel ? colors.accent : colors.border;
|
|
510
|
+
landingCards[i].borderWidth = isSel ? 1 : 0;
|
|
511
|
+
const children = landingCards[i].children ?? [];
|
|
512
|
+
if (children[0]) {
|
|
513
|
+
const iconCol = children[0] as BoxRenderable;
|
|
514
|
+
const iconChildren = iconCol.children ?? [];
|
|
515
|
+
const iconTextChild = iconChildren[0] as TextRenderable;
|
|
516
|
+
if (iconTextChild) {
|
|
517
|
+
iconTextChild.color = isSel ? colors.accent : colors.text;
|
|
518
|
+
iconTextChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
519
|
+
}
|
|
520
|
+
const iconSpacerChild = iconChildren[1] as TextRenderable;
|
|
521
|
+
if (iconSpacerChild) {
|
|
522
|
+
iconSpacerChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (children[1]) {
|
|
526
|
+
const contentCol = children[1] as BoxRenderable;
|
|
527
|
+
const contentChildren = contentCol.children ?? [];
|
|
528
|
+
const titleChild = contentChildren[0] as TextRenderable;
|
|
529
|
+
if (titleChild) {
|
|
530
|
+
titleChild.color = isSel ? colors.accent : colors.text;
|
|
531
|
+
titleChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
532
|
+
}
|
|
533
|
+
const descChild = contentChildren[1] as TextRenderable;
|
|
534
|
+
if (descChild) {
|
|
535
|
+
descChild.backgroundColor = isSel ? colors.selectionBg : colors.surface;
|
|
536
|
+
}
|
|
537
|
+
const hintChild = contentChildren[2] as TextRenderable;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Landing keyboard handler
|
|
543
|
+
renderer.keyInput.on("keypress", (key) => {
|
|
544
|
+
if (currentMode === "landing") {
|
|
545
|
+
if (key.name === "up" || key.name === "k") {
|
|
546
|
+
landingSelected = Math.max(0, landingSelected - 1);
|
|
547
|
+
updateLandingCards();
|
|
548
|
+
renderer.requestRender();
|
|
549
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
550
|
+
landingSelected = Math.min(landingOptions.length - 1, landingSelected + 1);
|
|
551
|
+
updateLandingCards();
|
|
552
|
+
renderer.requestRender();
|
|
553
|
+
} else if (key.name === "enter" || key.name === "return") {
|
|
554
|
+
landingOptions[landingSelected].action();
|
|
555
|
+
} else if (key.name === "?" || key.name === "h") {
|
|
556
|
+
showOverlay(currentOverlay === "help" ? "none" : "help");
|
|
557
|
+
} else if (key.name === "q") {
|
|
558
|
+
cleanup();
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
});
|
|
380
563
|
|
|
381
564
|
// Hide main content so overlays can take 100% of the content area
|
|
382
565
|
function hideMainContent() {
|
|
383
566
|
trendingTabBox.visible = false;
|
|
567
|
+
gapBox.visible = false;
|
|
384
568
|
searchBox.visible = false;
|
|
385
569
|
toolbarText.visible = false;
|
|
386
570
|
body.visible = false;
|
|
@@ -391,6 +575,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
391
575
|
trendingTabBox.visible = true;
|
|
392
576
|
} else {
|
|
393
577
|
searchBox.visible = true;
|
|
578
|
+
gapBox.visible = true;
|
|
394
579
|
toolbarText.visible = true;
|
|
395
580
|
}
|
|
396
581
|
body.visible = true;
|
|
@@ -411,10 +596,18 @@ export async function launchBrowser(): Promise<void> {
|
|
|
411
596
|
readmeBox.visible = false;
|
|
412
597
|
updateBox.visible = false;
|
|
413
598
|
updateDim.visible = false;
|
|
414
|
-
|
|
415
599
|
if (type === "none") {
|
|
416
600
|
currentOverlay = "none";
|
|
417
|
-
|
|
601
|
+
if (currentMode === "landing") {
|
|
602
|
+
landingBox.visible = true;
|
|
603
|
+
body.visible = false;
|
|
604
|
+
trendingTabBox.visible = false;
|
|
605
|
+
gapBox.visible = false;
|
|
606
|
+
searchBox.visible = false;
|
|
607
|
+
toolbarText.visible = false;
|
|
608
|
+
} else {
|
|
609
|
+
showMainContent();
|
|
610
|
+
}
|
|
418
611
|
renderer.requestRender();
|
|
419
612
|
return;
|
|
420
613
|
}
|
|
@@ -1079,7 +1272,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1079
1272
|
|
|
1080
1273
|
const groups: MenuCategory[] = [];
|
|
1081
1274
|
|
|
1082
|
-
// Search group
|
|
1275
|
+
// Search group
|
|
1083
1276
|
const searchItems: MenuEntry[] = [];
|
|
1084
1277
|
if (currentMode === "search") {
|
|
1085
1278
|
searchItems.push(
|
|
@@ -1119,6 +1312,29 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1119
1312
|
},
|
|
1120
1313
|
},
|
|
1121
1314
|
);
|
|
1315
|
+
} else if (currentMode === "packages") {
|
|
1316
|
+
searchItems.push(
|
|
1317
|
+
{
|
|
1318
|
+
type: "action",
|
|
1319
|
+
name: "Sort",
|
|
1320
|
+
description: `Cycle package sort (current: ${currentPackageSort})`,
|
|
1321
|
+
action: () => changePackageSort(),
|
|
1322
|
+
},
|
|
1323
|
+
{
|
|
1324
|
+
type: "action",
|
|
1325
|
+
name: "Limit",
|
|
1326
|
+
description: `Cycle result limit (current: ${currentLimit})`,
|
|
1327
|
+
action: () => changeLimit(),
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
type: "action",
|
|
1331
|
+
name: "Refresh",
|
|
1332
|
+
description: "Re-run current package search",
|
|
1333
|
+
action: () => {
|
|
1334
|
+
if (currentQueryInput) doPackageSearch(currentQueryInput);
|
|
1335
|
+
},
|
|
1336
|
+
},
|
|
1337
|
+
);
|
|
1122
1338
|
}
|
|
1123
1339
|
if (searchItems.length > 0) {
|
|
1124
1340
|
groups.push({
|
|
@@ -1130,7 +1346,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1130
1346
|
});
|
|
1131
1347
|
}
|
|
1132
1348
|
|
|
1133
|
-
// Repo group
|
|
1349
|
+
// Repo group
|
|
1134
1350
|
const repoItems: MenuEntry[] = [];
|
|
1135
1351
|
if (hasRepo) {
|
|
1136
1352
|
repoItems.push(
|
|
@@ -1197,8 +1413,17 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1197
1413
|
});
|
|
1198
1414
|
}
|
|
1199
1415
|
|
|
1200
|
-
// Navigate group
|
|
1416
|
+
// Navigate group
|
|
1201
1417
|
const navItems: MenuEntry[] = [];
|
|
1418
|
+
navItems.push({
|
|
1419
|
+
type: "action",
|
|
1420
|
+
name: "Packages",
|
|
1421
|
+
description: "Search npm packages",
|
|
1422
|
+
action: () => {
|
|
1423
|
+
showOverlay("none");
|
|
1424
|
+
showPackagesMode();
|
|
1425
|
+
},
|
|
1426
|
+
});
|
|
1202
1427
|
if (currentMode === "search") {
|
|
1203
1428
|
navItems.push({
|
|
1204
1429
|
type: "action",
|
|
@@ -1562,7 +1787,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1562
1787
|
}
|
|
1563
1788
|
|
|
1564
1789
|
function setToolbar() {
|
|
1565
|
-
|
|
1790
|
+
const sort = currentMode === "packages" ? currentPackageSort : currentSort;
|
|
1791
|
+
toolbarText.content = formatToolbar(sort, currentLimit, totalCount);
|
|
1566
1792
|
renderer.requestRender();
|
|
1567
1793
|
}
|
|
1568
1794
|
|
|
@@ -1583,7 +1809,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1583
1809
|
if (repo.archived) statusTags.push("archived");
|
|
1584
1810
|
if (repo.isFork) statusTags.push("fork");
|
|
1585
1811
|
const tagStr = statusTags.length ? ` [${statusTags.join(", ")}]` : "";
|
|
1586
|
-
|
|
1812
|
+
|
|
1587
1813
|
const lines = [
|
|
1588
1814
|
` ${repo.fullName}${tagStr}`,
|
|
1589
1815
|
``,
|
|
@@ -1703,37 +1929,47 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1703
1929
|
// X-axis with week labels
|
|
1704
1930
|
const xAxis = ` ┆${"─".repeat(termW)}`;
|
|
1705
1931
|
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];
|
|
1932
|
+
return [...lines, xAxis, weekLabels];
|
|
1707
1933
|
}
|
|
1708
1934
|
|
|
1709
1935
|
async function loadChart(repo: Repo) {
|
|
1710
1936
|
detailText.content = " Loading chart...";
|
|
1711
1937
|
renderer.requestRender();
|
|
1712
1938
|
try {
|
|
1939
|
+
const headers: Record<string, string> = { "User-Agent": "ghfind/1.0" };
|
|
1940
|
+
if (githubToken) headers.Authorization = `Bearer ${githubToken}`;
|
|
1713
1941
|
const [chartRes, repoRes] = await Promise.all([
|
|
1714
1942
|
fetch(
|
|
1715
1943
|
`https://api.github.com/repos/${repo.owner}/${repo.name}/stats/participation`,
|
|
1716
|
-
{ headers
|
|
1944
|
+
{ headers },
|
|
1717
1945
|
),
|
|
1718
1946
|
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}`, {
|
|
1719
|
-
headers
|
|
1720
|
-
}).then((r) => (r.ok ? r.json() : null)),
|
|
1947
|
+
headers,
|
|
1948
|
+
}).then((r) => (r.ok ? r.clone().json() : null)),
|
|
1721
1949
|
]);
|
|
1722
|
-
|
|
1723
1950
|
let chartSection = "";
|
|
1724
1951
|
if (chartRes.ok) {
|
|
1725
1952
|
const data = await chartRes.json();
|
|
1726
1953
|
if (data?.all && data.all.length >= 2) {
|
|
1727
1954
|
chartCommitData = data.all;
|
|
1728
|
-
|
|
1955
|
+
// Chart width: leave room for y-axis labels (6 chars) and padding.
|
|
1956
|
+
// In fullscreen graph mode, detailBox takes 100% width.
|
|
1957
|
+
// In normal mode (50%), use half the terminal width.
|
|
1958
|
+
const termW = graphFullscreen
|
|
1959
|
+
? (process.stdout.columns || 120) - 12
|
|
1960
|
+
: Math.floor((process.stdout.columns || 120) / 2) - 12;
|
|
1961
|
+
const chartW = Math.max(20, Math.min(60, termW));
|
|
1729
1962
|
const chartH = 10;
|
|
1730
1963
|
const chartLines = buildChartString(chartCommitData, chartW, chartH);
|
|
1731
|
-
chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join(
|
|
1732
|
-
|
|
1733
|
-
);
|
|
1964
|
+
chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join("\n");
|
|
1965
|
+
} else if (data?.message) {
|
|
1966
|
+
chartSection = ["", `Chart unavailable: ${data.message}`].join("\n");
|
|
1734
1967
|
}
|
|
1968
|
+
} else {
|
|
1969
|
+
chartSection = ["", `Chart unavailable (API ${chartRes.status})`].join("\n");
|
|
1735
1970
|
}
|
|
1736
1971
|
|
|
1972
|
+
|
|
1737
1973
|
const desc = repo.description ?? "";
|
|
1738
1974
|
const lang = repo.language ?? "—";
|
|
1739
1975
|
const updated = repo.updatedAt ? repo.updatedAt.slice(0, 10) : "";
|
|
@@ -1868,14 +2104,72 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1868
2104
|
function showSearchMode() {
|
|
1869
2105
|
currentMode = "search";
|
|
1870
2106
|
trendingTabBox.visible = false;
|
|
2107
|
+
gapBox.visible = false;
|
|
1871
2108
|
searchBox.visible = true;
|
|
1872
2109
|
toolbarText.visible = true;
|
|
2110
|
+
body.visible = true;
|
|
1873
2111
|
searchInput.placeholder =
|
|
1874
2112
|
"Search GitHub repos (e.g. rust cli, or language:Rust stars:>100)";
|
|
1875
2113
|
searchInput.focus();
|
|
1876
2114
|
renderer.requestRender();
|
|
1877
2115
|
}
|
|
1878
2116
|
|
|
2117
|
+
function showPackagesMode() {
|
|
2118
|
+
currentMode = "packages";
|
|
2119
|
+
trendingTabBox.visible = false;
|
|
2120
|
+
gapBox.visible = false;
|
|
2121
|
+
searchBox.visible = true;
|
|
2122
|
+
toolbarText.visible = true;
|
|
2123
|
+
body.visible = true;
|
|
2124
|
+
searchInput.placeholder = "Search npm packages (e.g. react, vue, typescript)";
|
|
2125
|
+
searchInput.focus();
|
|
2126
|
+
packages = [];
|
|
2127
|
+
resultsSelect.options = [
|
|
2128
|
+
{ name: "", description: "Type a query and press Enter to search", value: null },
|
|
2129
|
+
];
|
|
2130
|
+
detailText.content = "";
|
|
2131
|
+
renderer.requestRender();
|
|
2132
|
+
}
|
|
2133
|
+
async function doPackageSearch(queryText: string) {
|
|
2134
|
+
const q = queryText.trim();
|
|
2135
|
+
if (q === "" || isLoading) return;
|
|
2136
|
+
currentQueryInput = q;
|
|
2137
|
+
isLoading = true;
|
|
2138
|
+
resultsSelect.options = [
|
|
2139
|
+
{ name: " Searching packages...", description: "", value: null },
|
|
2140
|
+
];
|
|
2141
|
+
detailText.content = "";
|
|
2142
|
+
statusMgr.set("searching", `Searching npm for "${q}"`);
|
|
2143
|
+
renderer.requestRender();
|
|
2144
|
+
try {
|
|
2145
|
+
const search = createPackageSearch();
|
|
2146
|
+
const result = await search.searchPackage(q, currentLimit);
|
|
2147
|
+
totalCount = result.totalCount;
|
|
2148
|
+
packageResultsRaw = result.packages;
|
|
2149
|
+
packages = sortPackages(packageResultsRaw, currentPackageSort);
|
|
2150
|
+
setToolbar();
|
|
2151
|
+
if (packages.length > 0) {
|
|
2152
|
+
resultsSelect.options = packages.map((p) => ({
|
|
2153
|
+
name: `${p.name}@${p.version}`,
|
|
2154
|
+
description: p.description ?? "",
|
|
2155
|
+
value: p,
|
|
2156
|
+
}));
|
|
2157
|
+
resultsSelect.setSelectedIndex(0);
|
|
2158
|
+
statusMgr.set("success", `${packages.length} packages found`);
|
|
2159
|
+
} else {
|
|
2160
|
+
throw new NoResultsError(q);
|
|
2161
|
+
}
|
|
2162
|
+
} catch (err) {
|
|
2163
|
+
const msg =
|
|
2164
|
+
err instanceof Error ? err.message : String(err);
|
|
2165
|
+
resultsSelect.options = [
|
|
2166
|
+
{ name: " (error)", description: "", value: null },
|
|
2167
|
+
];
|
|
2168
|
+
statusMgr.set("error", msg.slice(0, 60));
|
|
2169
|
+
}
|
|
2170
|
+
isLoading = false;
|
|
2171
|
+
renderer.requestRender();
|
|
2172
|
+
}
|
|
1879
2173
|
async function doSearch(queryText: string, append = false) {
|
|
1880
2174
|
const q = queryText.trim();
|
|
1881
2175
|
if (q === "" || isLoading) return;
|
|
@@ -1975,6 +2269,23 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1975
2269
|
}
|
|
1976
2270
|
}
|
|
1977
2271
|
|
|
2272
|
+
function changePackageSort() {
|
|
2273
|
+
const idx = PACKAGE_SORT_MODES.findIndex((m) => m.key === currentPackageSort);
|
|
2274
|
+
const next = PACKAGE_SORT_MODES[(idx + 1) % PACKAGE_SORT_MODES.length];
|
|
2275
|
+
currentPackageSort = next.key;
|
|
2276
|
+
setToolbar();
|
|
2277
|
+
if (packageResultsRaw.length > 0) {
|
|
2278
|
+
packages = sortPackages(packageResultsRaw, currentPackageSort);
|
|
2279
|
+
resultsSelect.options = packages.map((p) => ({
|
|
2280
|
+
name: `${p.name}@${p.version}`,
|
|
2281
|
+
description: p.description ?? "",
|
|
2282
|
+
value: p,
|
|
2283
|
+
}));
|
|
2284
|
+
resultsSelect.setSelectedIndex(0);
|
|
2285
|
+
renderer.requestRender();
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
1978
2289
|
function changeLimit() {
|
|
1979
2290
|
const limits = [10, 25, 50, 100];
|
|
1980
2291
|
const idx = limits.indexOf(currentLimit);
|
|
@@ -1989,17 +2300,30 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1989
2300
|
// Enter in search input → show results
|
|
1990
2301
|
searchInput.on("enter", () => {
|
|
1991
2302
|
if (searchInput.value.trim() === "" || isLoading) return;
|
|
1992
|
-
|
|
1993
|
-
|
|
2303
|
+
if (currentMode === "packages") {
|
|
2304
|
+
doPackageSearch(searchInput.value);
|
|
2305
|
+
} else {
|
|
2306
|
+
doSearch(searchInput.value);
|
|
2307
|
+
}
|
|
1994
2308
|
});
|
|
1995
2309
|
|
|
1996
2310
|
// Navigate results → update detail pane
|
|
1997
2311
|
resultsSelect.on("selectionChanged", () => {
|
|
1998
2312
|
const opt = resultsSelect.getSelectedOption();
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
if (
|
|
2002
|
-
|
|
2313
|
+
if (currentMode === "packages") {
|
|
2314
|
+
const pack = opt?.value as Package | undefined;
|
|
2315
|
+
if (pack) {
|
|
2316
|
+
detailText.content = `Package: ${pack.name}@${pack.version}
|
|
2317
|
+
Downloads: ${pack.downloads.toLocaleString()}
|
|
2318
|
+
Score: ${pack.score.toFixed(2)}
|
|
2319
|
+
${pack.description ?? ""}`;
|
|
2320
|
+
}
|
|
2321
|
+
} else {
|
|
2322
|
+
const repo = opt?.value as Repo | undefined;
|
|
2323
|
+
if (repo) {
|
|
2324
|
+
if (graphFullscreen) loadChart(repo);
|
|
2325
|
+
else updateDetail(repo);
|
|
2326
|
+
}
|
|
2003
2327
|
}
|
|
2004
2328
|
renderer.requestRender();
|
|
2005
2329
|
});
|
|
@@ -2007,8 +2331,13 @@ export async function launchBrowser(): Promise<void> {
|
|
|
2007
2331
|
// Enter on a result → open URL
|
|
2008
2332
|
resultsSelect.on("itemSelected", () => {
|
|
2009
2333
|
const opt = resultsSelect.getSelectedOption();
|
|
2010
|
-
|
|
2011
|
-
|
|
2334
|
+
if (currentMode === "packages") {
|
|
2335
|
+
const pack = opt?.value as Package | undefined;
|
|
2336
|
+
if (pack?.url) openUrl(pack.url);
|
|
2337
|
+
} else {
|
|
2338
|
+
const repo = opt?.value as Repo | undefined;
|
|
2339
|
+
if (repo) openUrl(repo.url);
|
|
2340
|
+
}
|
|
2012
2341
|
});
|
|
2013
2342
|
|
|
2014
2343
|
// ── Global keyboard shortcuts ──────────────────────────────────────
|
|
@@ -2342,6 +2671,19 @@ export async function launchBrowser(): Promise<void> {
|
|
|
2342
2671
|
showLeaderMenu();
|
|
2343
2672
|
}
|
|
2344
2673
|
return;
|
|
2674
|
+
// 't' toggles theme and persists to config
|
|
2675
|
+
if (key.name === "t" && !searchInput.focused) {
|
|
2676
|
+
const current = config.theme;
|
|
2677
|
+
const themes = listThemes();
|
|
2678
|
+
const idx = themes.indexOf(current);
|
|
2679
|
+
const next = themes[(idx + 1) % themes.length];
|
|
2680
|
+
const nextTheme = loadTheme(next);
|
|
2681
|
+
Object.assign(colors, nextTheme);
|
|
2682
|
+
config.theme = next;
|
|
2683
|
+
saveConfig(config);
|
|
2684
|
+
renderer.requestRender();
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2345
2687
|
}
|
|
2346
2688
|
|
|
2347
2689
|
// '?' / Ctrl+H toggle help overlay (fast path)
|
|
@@ -2446,18 +2788,23 @@ export async function launchBrowser(): Promise<void> {
|
|
|
2446
2788
|
}
|
|
2447
2789
|
// ── Start ──────────────────────────────────────────────────────────
|
|
2448
2790
|
// Check for updates (non-blocking — doesn't delay TUI startup)
|
|
2791
|
+
if (!session || !session.mode) {
|
|
2792
|
+
showLanding(true);
|
|
2793
|
+
}
|
|
2449
2794
|
checkForUpdateAndShow();
|
|
2450
2795
|
renderer.start();
|
|
2451
|
-
if (
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
searchInput.value = currentQueryInput;
|
|
2455
|
-
if (currentQueryInput) {
|
|
2456
|
-
doSearch(currentQueryInput);
|
|
2796
|
+
if (session && session.mode) {
|
|
2797
|
+
if (currentMode === "trending") {
|
|
2798
|
+
loadTrending();
|
|
2457
2799
|
} else {
|
|
2458
|
-
|
|
2800
|
+
searchInput.value = currentQueryInput;
|
|
2801
|
+
if (currentQueryInput) {
|
|
2802
|
+
doSearch(currentQueryInput);
|
|
2803
|
+
} else {
|
|
2804
|
+
showSearchMode();
|
|
2805
|
+
}
|
|
2806
|
+
searchInput.focus();
|
|
2459
2807
|
}
|
|
2460
|
-
searchInput.focus();
|
|
2461
2808
|
}
|
|
2462
2809
|
renderer.requestRender();
|
|
2463
2810
|
}
|
|
@@ -2477,7 +2824,11 @@ function formatStars(n: number): string {
|
|
|
2477
2824
|
return `★ ${n}`;
|
|
2478
2825
|
}
|
|
2479
2826
|
|
|
2480
|
-
function formatToolbar(
|
|
2827
|
+
function formatToolbar(
|
|
2828
|
+
sort: SortStrategy | PackageSortMode,
|
|
2829
|
+
limit: number,
|
|
2830
|
+
totalCount: number,
|
|
2831
|
+
): string {
|
|
2481
2832
|
const sortLabel = SORT_MODES.find((m) => m.key === sort)?.label ?? sort;
|
|
2482
2833
|
return ` sort: ${sortLabel} limit: ${limit} results: ${totalCount.toLocaleString()}`;
|
|
2483
2834
|
}
|
|
@@ -2494,6 +2845,6 @@ function cleanup(): void {
|
|
|
2494
2845
|
// ── Auto-run ──────────────────────────────────────────────────────────
|
|
2495
2846
|
// Only launch when run directly (bun run src/tui.ts), not when bundled
|
|
2496
2847
|
// into cli.js or imported as a module.
|
|
2497
|
-
if (import.meta.main && !process.env.GHFIND_BUNDLED) {
|
|
2848
|
+
if (import.meta.main && !process.env.GHFIND_BUNDLED && !process.env.GHFIND_CLI_RUN) {
|
|
2498
2849
|
launchBrowser();
|
|
2499
2850
|
}
|