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/README.md +183 -188
- package/demo.svg +66 -0
- package/package.json +2 -1
- package/src/cli.ts +13 -3
- package/src/config.ts +1 -2
- package/src/errors.ts +2 -2
- package/src/explore.ts +17 -32
- package/src/export.ts +9 -1
- package/src/help.ts +120 -46
- package/src/init.ts +9 -1
- package/src/normalizer.ts +17 -17
- package/src/open-url.ts +17 -0
- package/src/output.ts +2 -1
- package/src/pipe.ts +2 -7
- package/src/provider.ts +32 -9
- package/src/query.ts +5 -7
- package/src/ranking.ts +5 -3
- package/src/session.ts +4 -2
- package/src/trending.ts +41 -65
- package/src/tui.ts +81 -24
- package/src/watch.ts +13 -1
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 {
|
|
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
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
+
if (failCount < MAX_FAILS) {
|
|
63
|
+
intervalId = setInterval(tick, opts.intervalMs);
|
|
64
|
+
}
|
|
53
65
|
}
|