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/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
- renderer = await createCliRenderer();
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" = "none";
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
- borderBottom: true,
280
- borderBottomColor: colors.border,
281
- title: " SEARCH ",
309
+ bordered: true,
310
+ borderColor: colors.border,
311
+ title: "",
282
312
  titleColor: colors.blue,
283
313
  width: "100%",
284
314
  flexDirection: "row",
285
- paddingTop: 0,
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
- borderBottom: true,
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
- borderBottom: true,
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
- // ── Overlay system ──────────────────────────────────────────────────
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
- showMainContent();
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
- toolbarText.content = formatToolbar(currentSort, currentLimit, totalCount);
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: { "User-Agent": "ghfind/1.0" } },
1918
+ { headers },
1717
1919
  ),
1718
1920
  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)),
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
- const chartW = 60;
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
- "\n",
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
- showSearchMode();
1993
- doSearch(searchInput.value);
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
- const repo = opt?.value as Repo | undefined;
2000
- if (repo) {
2001
- if (graphFullscreen) loadChart(repo);
2002
- else updateDetail(repo);
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
- const repo = opt?.value as Repo | undefined;
2011
- if (repo) openUrl(repo.url);
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 (currentMode === "trending") {
2452
- loadTrending();
2453
- } else {
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
- showSearchMode();
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(sort: SortStrategy, limit: number, totalCount: number): string {
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
  }