github-search-cli 0.6.0 → 0.7.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/src/tui.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  TextRenderable,
30
30
  InputRenderable,
31
31
  SelectRenderable,
32
+ ScrollBoxRenderable,
32
33
  } from "@opentui/core";
33
34
  import type { Repo, SearchOptions, SortStrategy } from "./types.ts";
34
35
  import { parseQuery, applyFlagFilters, validateQuery, suggestFor } from "./query.ts";
@@ -36,9 +37,9 @@ import { GitHubSearchProvider, type Logger } from "./provider.ts";
36
37
  import { rankRepos } from "./ranking.ts";
37
38
  import { fetchTrendingRepos, tabSince, TAB_NAMES, fmtStars, type TrendingRepo } from "./trending.ts";
38
39
  import { loadConfig } from "./config.ts";
39
- import { buildHelpText } from "./help.ts";
40
+ import { buildHelpSections, HELP_KEYS_COLUMN } from "./help.ts";
40
41
  import { SearchCliError, NetworkError, RateLimitError, BadQueryError, NoResultsError } from "./errors.ts";
41
- import { appendHistory, readHistory, deleteHistoryEntry, clearHistory } from "./history.ts";
42
+ import { appendHistory, readHistory, deleteHistoryEntry, clearHistory, rotateHistory } from "./history.ts";
42
43
  import { getBookmarks, isBookmarked, toggleBookmark, removeBookmark } from "./bookmarks.ts";
43
44
  import { getSavedSearches, saveSearch, deleteSavedSearch, touchSavedSearch } from "./saved-searches.ts";
44
45
  import { saveSession, restoreSession } from "./session.ts";
@@ -51,6 +52,7 @@ import { StatusManager } from "./status.ts";
51
52
  import { addNotification, getNotifications, dismissNotification, dismissAll } from "./notifications.ts";
52
53
  import { formatShare, copyToClipboard, type ShareFormat } from "./share.ts";
53
54
  import { nextTip } from "./tips.ts";
55
+ import { openUrl } from "./open-url.ts";
54
56
 
55
57
  // ─── Theme ────────────────────────────────────────────────────────────
56
58
  const colors = {
@@ -130,6 +132,7 @@ export async function launchBrowser(): Promise<void> {
130
132
  let totalCount = 0;
131
133
  let deepDiveActive = false;
132
134
  let compareList: Repo[] = [];
135
+ let quitArmed = false;
133
136
 
134
137
  // ── Header bar ──────────────────────────────────────────────────────
135
138
  const header = new TextRenderable(renderer, {
@@ -264,7 +267,26 @@ export async function launchBrowser(): Promise<void> {
264
267
  root.add(body);
265
268
 
266
269
  // ── Overlay system ──────────────────────────────────────────────────
267
- function hideAllOverlays() {
270
+
271
+ // Hide main content so overlays can take 100% of the content area
272
+ function hideMainContent() {
273
+ trendingTabBox.visible = false;
274
+ searchBox.visible = false;
275
+ toolbarText.visible = false;
276
+ body.visible = false;
277
+ }
278
+
279
+ function showMainContent() {
280
+ if (currentMode === "trending") {
281
+ trendingTabBox.visible = true;
282
+ } else {
283
+ searchBox.visible = true;
284
+ toolbarText.visible = true;
285
+ }
286
+ body.visible = true;
287
+ }
288
+
289
+ function showOverlay(type: typeof currentOverlay) {
268
290
  helpBox.visible = false;
269
291
  historyBox.visible = false;
270
292
  bookmarksBox.visible = false;
@@ -274,13 +296,15 @@ export async function launchBrowser(): Promise<void> {
274
296
  compareBox.visible = false;
275
297
  notifsBox.visible = false;
276
298
  shareBox.visible = false;
277
- currentOverlay = "none";
278
- }
279
299
 
280
- function showOverlay(type: typeof currentOverlay) {
281
- hideAllOverlays();
282
- if (type === "none") return;
300
+ if (type === "none") {
301
+ currentOverlay = "none";
302
+ showMainContent();
303
+ renderer.requestRender();
304
+ return;
305
+ }
283
306
  currentOverlay = type;
307
+ hideMainContent();
284
308
  if (type === "help") { helpBox.visible = true; }
285
309
  else if (type === "history") { historyBox.visible = true; }
286
310
  else if (type === "bookmarks") { bookmarksBox.visible = true; }
@@ -293,7 +317,8 @@ export async function launchBrowser(): Promise<void> {
293
317
  renderer.requestRender();
294
318
  }
295
319
 
296
- // ── Help overlay ────────────────────────────────────────────
320
+ // ── Help overlay (OpenTUI components) ────────────────────────
321
+ const helpSections = buildHelpSections();
297
322
  const helpBox = new BoxRenderable(renderer, {
298
323
  visible: false,
299
324
  flexGrow: 1,
@@ -301,12 +326,48 @@ export async function launchBrowser(): Promise<void> {
301
326
  border: true,
302
327
  borderColor: colors.border,
303
328
  });
304
- const helpTextEl = new TextRenderable(renderer, {
305
- content: buildHelpText(),
306
- color: colors.text,
329
+ const helpScroll = new ScrollBoxRenderable(renderer, {
330
+ flexGrow: 1,
307
331
  backgroundColor: colors.bg,
332
+ scrollY: true,
333
+ scrollX: false,
334
+ paddingX: 0,
335
+ viewportOptions: { backgroundColor: colors.bg },
336
+ contentOptions: { backgroundColor: colors.bg, flexDirection: "column" },
337
+ scrollbarOptions: {
338
+ backgroundColor: colors.bg,
339
+ foregroundColor: colors.muted,
340
+ width: 1,
341
+ },
308
342
  });
309
- helpBox.add(helpTextEl);
343
+ for (const section of helpSections) {
344
+ const sectionBox = new BoxRenderable(renderer, {
345
+ border: true,
346
+ borderColor: colors.border,
347
+ title: section.title,
348
+ titleColor: (colors as Record<string, string>)[section.titleColor] ?? section.titleColor,
349
+ flexDirection: "column",
350
+ paddingY: 0,
351
+ marginLeft: 1,
352
+ marginRight: 1,
353
+ marginTop: 1,
354
+ });
355
+ for (const row of section.rows) {
356
+ const keys = row.keys.padEnd(HELP_KEYS_COLUMN);
357
+ sectionBox.add(new TextRenderable(renderer, {
358
+ content: ` ${keys} ${row.action}`,
359
+ color: colors.text,
360
+ }));
361
+ }
362
+ if (section.note) {
363
+ sectionBox.add(new TextRenderable(renderer, {
364
+ content: ` ${section.note}`,
365
+ color: colors.muted,
366
+ }));
367
+ }
368
+ helpScroll.add(sectionBox);
369
+ }
370
+ helpBox.add(helpScroll);
310
371
  root.add(helpBox);
311
372
 
312
373
  // ── History overlay ─────────────────────────────────────────
@@ -1262,6 +1323,10 @@ export async function launchBrowser(): Promise<void> {
1262
1323
  return;
1263
1324
  }
1264
1325
 
1326
+ // ponytail: guard single-letter bindings behind !searchInput.focused
1327
+ // so typing a repo name containing t/g/r/s/l/o/d/c doesn't trigger actions
1328
+ if (!searchInput.focused) {
1329
+
1265
1330
  if (key.name === "g") {
1266
1331
  toggleGraph();
1267
1332
  return;
@@ -1386,6 +1451,8 @@ export async function launchBrowser(): Promise<void> {
1386
1451
  return;
1387
1452
  }
1388
1453
 
1454
+ }
1455
+
1389
1456
  // Ctrl+E — export
1390
1457
  if (key.ctrl && key.name === "e") {
1391
1458
  showOverlay("export");
@@ -1442,18 +1509,8 @@ function formatToolbar(sort: SortStrategy, limit: number): string {
1442
1509
  return ` sort: ${sortLabel} limit: ${limit} (s=cycle sort l=cycle limit)`;
1443
1510
  }
1444
1511
 
1445
- function openUrl(url: string): void {
1446
- try {
1447
- const p = process.platform;
1448
- if (p === "win32") Bun.spawn(["cmd", "/c", "start", "", url]);
1449
- else if (p === "darwin") Bun.spawn(["open", url]);
1450
- else Bun.spawn(["xdg-open", url]);
1451
- } catch {
1452
- // non-critical
1453
- }
1454
- }
1455
-
1456
1512
  function cleanup(): void {
1513
+ try { rotateHistory(); } catch { /* non-critical */ }
1457
1514
  process.exit(0);
1458
1515
  }
1459
1516
 
package/src/watch.ts CHANGED
@@ -17,6 +17,8 @@ export interface WatchOptions {
17
17
  trending?: boolean;
18
18
  }
19
19
 
20
+ const MAX_FAILS = 5;
21
+
20
22
  /** Run watch mode. Calls onChange with each tick's results. */
21
23
  export async function runWatch(
22
24
  opts: WatchOptions,
@@ -24,6 +26,8 @@ export async function runWatch(
24
26
  onError: (err: Error) => void,
25
27
  ): Promise<void> {
26
28
  let previous: Repo[] = [];
29
+ let failCount = 0;
30
+ let intervalId: ReturnType<typeof setInterval> | null = null;
27
31
 
28
32
  const tick = async () => {
29
33
  try {
@@ -39,15 +43,23 @@ export async function runWatch(
39
43
  const response = await provider.search(parsed, options);
40
44
  const repos = rankRepos(response.repos, options.sort);
41
45
 
46
+ failCount = 0; // reset on success
42
47
  const delta = previous.length > 0 ? repos.length - previous.length : 0;
43
48
  previous = repos;
44
49
  onChange(repos, delta);
45
50
  } catch (err) {
51
+ failCount++;
46
52
  onError(err instanceof Error ? err : new Error(String(err)));
53
+ if (failCount >= MAX_FAILS) {
54
+ onError(new Error(`Watch stopped after ${MAX_FAILS} consecutive failures`));
55
+ if (intervalId) clearInterval(intervalId);
56
+ }
47
57
  }
48
58
  };
49
59
 
50
60
  // Immediate first tick, then interval
51
61
  await tick();
52
- setInterval(tick, opts.intervalMs);
62
+ if (failCount < MAX_FAILS) {
63
+ intervalId = setInterval(tick, opts.intervalMs);
64
+ }
53
65
  }