github-search-cli 9.9.0 → 9.10.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/CHANGELOG.md +94 -0
- package/README.md +49 -51
- package/dist/cli.js +562 -121
- package/docs/HOW-TO.md +3 -2
- package/package.json +2 -2
- package/release-notes/9.10.0.md +137 -0
- package/release-notes/9.10.1.md +59 -0
- package/src/cli.ts +77 -7
- package/src/help.ts +21 -4
- package/src/history.ts +4 -5
- package/src/mcp-server.ts +4 -2
- package/src/output.ts +11 -1
- package/src/package-browser.ts +2 -0
- package/src/status.ts +40 -0
- package/src/trending.ts +3 -1
- package/src/tui/mouse.ts +349 -0
- package/src/tui.ts +428 -114
- package/src/watch.ts +18 -6
- package/src/export.ts +0 -73
- package/src/pipe.ts +0 -60
- package/src/search-state.ts +0 -278
package/src/tui.ts
CHANGED
|
@@ -57,6 +57,13 @@ import {
|
|
|
57
57
|
type Logger,
|
|
58
58
|
} from "./search";
|
|
59
59
|
import { tabSince, TAB_NAMES, fmtStars } from "./trending";
|
|
60
|
+
import {
|
|
61
|
+
attachClickToSelect,
|
|
62
|
+
attachClickOutside,
|
|
63
|
+
attachClickableBox,
|
|
64
|
+
NON_INTERACTIVE_TEXT,
|
|
65
|
+
DOUBLE_CLICK_MS,
|
|
66
|
+
} from "./tui/mouse";
|
|
60
67
|
import { loadConfig, saveConfig } from "./config";
|
|
61
68
|
import { buildHelpSections, HELP_KEYS_COLUMN } from "./help";
|
|
62
69
|
import {
|
|
@@ -101,7 +108,7 @@ import {
|
|
|
101
108
|
PACKAGE_SORT_MODES,
|
|
102
109
|
type PackageSortMode,
|
|
103
110
|
} from "./package";
|
|
104
|
-
import { StatusManager } from "./status";
|
|
111
|
+
import { type StatusManager, createStatusManager } from "./status";
|
|
105
112
|
import {
|
|
106
113
|
getNotifications,
|
|
107
114
|
dismissNotification,
|
|
@@ -192,6 +199,15 @@ function createTuiLogger(): Logger {
|
|
|
192
199
|
|
|
193
200
|
const logger: Logger = createTuiLogger();
|
|
194
201
|
|
|
202
|
+
// The live StatusManager for the current renderer. Kept at module scope so the
|
|
203
|
+
// hoisted cleanup() can dispose the spinner interval on the quit path; a stale
|
|
204
|
+
// interval would otherwise outlive the destroyed renderable it writes to.
|
|
205
|
+
let activeStatusMgr: StatusManager | null = null;
|
|
206
|
+
// The live renderer, so the hoisted cleanup() can release the terminal
|
|
207
|
+
// (raw mode, alternate screen) before process.exit() — same reasoning as
|
|
208
|
+
// activeStatusMgr above.
|
|
209
|
+
let activeRenderer: CliRenderer | null = null;
|
|
210
|
+
|
|
195
211
|
// ─── Sort cycle ───────────────────────────────────────────────────────
|
|
196
212
|
const SORT_MODES: { key: SortStrategy; label: string }[] = [
|
|
197
213
|
{ key: "best-match", label: "best-match" },
|
|
@@ -201,7 +217,10 @@ const SORT_MODES: { key: SortStrategy; label: string }[] = [
|
|
|
201
217
|
];
|
|
202
218
|
|
|
203
219
|
// ─── Main ─────────────────────────────────────────────────────────────
|
|
204
|
-
export async function launchBrowser(
|
|
220
|
+
export async function launchBrowser(
|
|
221
|
+
theme_override?: string,
|
|
222
|
+
startMode?: "trending",
|
|
223
|
+
): Promise<void> {
|
|
205
224
|
// Ask the terminal for its real background color (OSC 11) BEFORE the
|
|
206
225
|
// renderer takes over stdin — after that, reading the reply gets messy.
|
|
207
226
|
// Used below so the app backdrop matches the user's terminal theme.
|
|
@@ -240,6 +259,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
240
259
|
}
|
|
241
260
|
|
|
242
261
|
const root = renderer.root;
|
|
262
|
+
activeRenderer = renderer;
|
|
243
263
|
root.flexDirection = "column";
|
|
244
264
|
|
|
245
265
|
// ── Config ──
|
|
@@ -310,7 +330,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
310
330
|
|
|
311
331
|
const header = new TextRenderable(renderer, {
|
|
312
332
|
content:
|
|
313
|
-
" ghfind — GitHub repo browser [/]search [
|
|
333
|
+
" ghfind — GitHub repo browser [/]search [Esc]menu [\u2192]open [?]help [q]uit · click select · dbl-click open",
|
|
314
334
|
bg: colors.bg,
|
|
315
335
|
fg: colors.muted,
|
|
316
336
|
height: 1,
|
|
@@ -335,15 +355,26 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
335
355
|
const isActive = name === trendingTab;
|
|
336
356
|
const label = ` ${name} `;
|
|
337
357
|
const tt = new TextRenderable(renderer, {
|
|
358
|
+
...NON_INTERACTIVE_TEXT,
|
|
338
359
|
content: label,
|
|
339
360
|
fg: isActive ? colors.bg : colors.muted,
|
|
340
361
|
bg: isActive ? colors.blue : colors.bg,
|
|
341
362
|
height: 1,
|
|
342
363
|
});
|
|
364
|
+
// Click a tab to switch to it — same path as the 1-5 number keys.
|
|
365
|
+
tt.onMouseDown = (event) => {
|
|
366
|
+
event.stopPropagation();
|
|
367
|
+
if (name === trendingTab) return;
|
|
368
|
+
trendingTab = name;
|
|
369
|
+
loadTrending();
|
|
370
|
+
};
|
|
371
|
+
tt.onMouseOver = () => renderer.setMousePointer("pointer");
|
|
372
|
+
tt.onMouseOut = () => renderer.setMousePointer("default");
|
|
343
373
|
trendingTabTexts.push(tt);
|
|
344
374
|
trendingTabBox.add(tt);
|
|
345
375
|
if (i < TAB_NAMES.length - 1) {
|
|
346
376
|
const sp = new TextRenderable(renderer, {
|
|
377
|
+
...NON_INTERACTIVE_TEXT,
|
|
347
378
|
content: " ",
|
|
348
379
|
fg: colors.muted,
|
|
349
380
|
bg: colors.bg,
|
|
@@ -442,6 +473,9 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
442
473
|
content: "",
|
|
443
474
|
fg: colors.text,
|
|
444
475
|
wrapMode: "none",
|
|
476
|
+
// Non-interactive: a left-press here would otherwise be swallowed as a
|
|
477
|
+
// text-selection drag instead of reaching the click handlers.
|
|
478
|
+
selectable: false,
|
|
445
479
|
});
|
|
446
480
|
detailBox.add(detailText);
|
|
447
481
|
|
|
@@ -458,6 +492,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
458
492
|
});
|
|
459
493
|
root.add(landingBox);
|
|
460
494
|
const landingBanner = new TextRenderable(renderer, {
|
|
495
|
+
...NON_INTERACTIVE_TEXT,
|
|
461
496
|
content: [
|
|
462
497
|
" ██████╗ ██╗ ██╗███████╗██╗███╗ ██╗██████╗ ",
|
|
463
498
|
"██╔════╝ ██║ ██║██╔════╝██║████╗ ██║██╔══██╗",
|
|
@@ -540,8 +575,9 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
540
575
|
}
|
|
541
576
|
|
|
542
577
|
const landingHint = new TextRenderable(renderer, {
|
|
578
|
+
...NON_INTERACTIVE_TEXT,
|
|
543
579
|
content:
|
|
544
|
-
" \u2191\u2193 Navigate \u21E9 Select [b]ookmarks [?]help [q]uit",
|
|
580
|
+
" \u2191\u2193 Navigate \u21E9 Select [b]ookmarks [?]help [q]uit · click to pick a mode",
|
|
545
581
|
fg: colors.muted,
|
|
546
582
|
bg: colors.bg,
|
|
547
583
|
height: 1,
|
|
@@ -572,6 +608,34 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
572
608
|
}
|
|
573
609
|
}
|
|
574
610
|
|
|
611
|
+
// ── Landing mouse ───────────────────────────────────────────
|
|
612
|
+
// Click selects, double-click runs the card's action (same as Enter), hover
|
|
613
|
+
// shows a pointer cursor. `updateLandingCards` is the single source of truth
|
|
614
|
+
// for the card colours, so hover and selection cannot drift.
|
|
615
|
+
landingCards.forEach((card, i) => {
|
|
616
|
+
let lastClickAt = 0;
|
|
617
|
+
card.onMouseDown = (event) => {
|
|
618
|
+
event.stopPropagation();
|
|
619
|
+
const now = Date.now();
|
|
620
|
+
const isDouble =
|
|
621
|
+
landingSelected === i && now - lastClickAt < DOUBLE_CLICK_MS;
|
|
622
|
+
lastClickAt = now;
|
|
623
|
+
if (isDouble) {
|
|
624
|
+
landingOptions[i].action();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
landingSelected = i;
|
|
628
|
+
updateLandingCards();
|
|
629
|
+
renderer.requestRender();
|
|
630
|
+
};
|
|
631
|
+
card.onMouseOver = () => {
|
|
632
|
+
renderer.setMousePointer("pointer");
|
|
633
|
+
};
|
|
634
|
+
card.onMouseOut = () => {
|
|
635
|
+
renderer.setMousePointer("default");
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
|
|
575
639
|
// Landing keyboard handler. Registered before the global handler below,
|
|
576
640
|
// and both listeners receive the same key event — so keys the landing
|
|
577
641
|
// screen consumes must be claimed with stopPropagation(), or the global
|
|
@@ -641,10 +705,15 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
641
705
|
leaderBox.visible = false;
|
|
642
706
|
leaderDim.visible = false;
|
|
643
707
|
readmeBox.visible = false;
|
|
708
|
+
// The org panel is a flexGrow sibling of `body` just like every other
|
|
709
|
+
// overlay — omitting it here meant it stayed visible after Esc, permanently
|
|
710
|
+
// stealing half the content height and every click in the lower half.
|
|
711
|
+
orgBox.visible = false;
|
|
644
712
|
updateBox.visible = false;
|
|
645
713
|
updateDim.visible = false;
|
|
646
714
|
if (type === "none") {
|
|
647
715
|
currentOverlay = "none";
|
|
716
|
+
menuButton.visible = true;
|
|
648
717
|
if (currentMode === "landing") {
|
|
649
718
|
landingBox.visible = true;
|
|
650
719
|
body.visible = false;
|
|
@@ -658,6 +727,9 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
658
727
|
return;
|
|
659
728
|
}
|
|
660
729
|
currentOverlay = type;
|
|
730
|
+
// The button floats above the layout, so it would sit on top of every
|
|
731
|
+
// full-screen overlay (and stay clickable through the dim layer).
|
|
732
|
+
menuButton.visible = false;
|
|
661
733
|
// An overlay opened from the landing screen must hide the menu too
|
|
662
734
|
// (hideMainContent only covers the search/trending/toolbar boxes).
|
|
663
735
|
landingBox.visible = false;
|
|
@@ -945,6 +1017,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
945
1017
|
content: "",
|
|
946
1018
|
fg: colors.text,
|
|
947
1019
|
bg: colors.bg,
|
|
1020
|
+
selectable: false,
|
|
948
1021
|
});
|
|
949
1022
|
compareBox.add(compareText);
|
|
950
1023
|
root.add(compareBox);
|
|
@@ -1040,7 +1113,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1040
1113
|
savedSelect.setSelectedIndex(0);
|
|
1041
1114
|
}
|
|
1042
1115
|
|
|
1043
|
-
// ── Leader menu overlay (
|
|
1116
|
+
// ── Leader menu overlay (Esc key) ─────────────────────────────
|
|
1044
1117
|
// Dim overlay — sits behind the menu, above main content
|
|
1045
1118
|
const leaderDim = new BoxRenderable(renderer, {
|
|
1046
1119
|
visible: false,
|
|
@@ -1086,7 +1159,9 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1086
1159
|
itemSpacing: 0,
|
|
1087
1160
|
});
|
|
1088
1161
|
const leaderFooter = new TextRenderable(renderer, {
|
|
1089
|
-
|
|
1162
|
+
...NON_INTERACTIVE_TEXT,
|
|
1163
|
+
content:
|
|
1164
|
+
" ↑↓ select Enter run Esc/q close · click to run click outside to close ",
|
|
1090
1165
|
fg: colors.muted,
|
|
1091
1166
|
bg: colors.surfaceDim,
|
|
1092
1167
|
height: 1,
|
|
@@ -1236,11 +1311,77 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1236
1311
|
renderer.requestRender();
|
|
1237
1312
|
}
|
|
1238
1313
|
|
|
1314
|
+
// ── Update panel mouse ────────────────────────────────────────────
|
|
1315
|
+
// Click a card to select *and* run it in one gesture: these three options are
|
|
1316
|
+
// terminal actions (install now / snooze / suppress), and a modal that needs
|
|
1317
|
+
// select-then-Enter would be two clicks for the common case. `renderUpdateOptions`
|
|
1318
|
+
// stays the single source of truth for the colours.
|
|
1319
|
+
function runUpdateOption(index: number) {
|
|
1320
|
+
updateSelectedOption = index;
|
|
1321
|
+
renderUpdateOptions();
|
|
1322
|
+
const action = ["now", "later", "never"][index] as
|
|
1323
|
+
"now" | "later" | "never" | undefined;
|
|
1324
|
+
if (action === "now") {
|
|
1325
|
+
showOverlay("none");
|
|
1326
|
+
// ponytail: record the running version so the next launch detects the upgrade
|
|
1327
|
+
recordPreUpdateState(cachedVersion ?? "");
|
|
1328
|
+
performUpdate()
|
|
1329
|
+
.then((ok) => {
|
|
1330
|
+
if (ok) {
|
|
1331
|
+
setStatus("✓ Update complete — restart ghfind");
|
|
1332
|
+
} else {
|
|
1333
|
+
setStatus("✗ Update failed — check terminal output");
|
|
1334
|
+
}
|
|
1335
|
+
})
|
|
1336
|
+
.catch(() => {
|
|
1337
|
+
setStatus("✗ Update failed — check terminal output");
|
|
1338
|
+
});
|
|
1339
|
+
} else if (action === "never") {
|
|
1340
|
+
suppressUpdateNotices();
|
|
1341
|
+
showOverlay("none");
|
|
1342
|
+
setStatus("Update notices suppressed");
|
|
1343
|
+
} else {
|
|
1344
|
+
snoozeUpdateNotices(3);
|
|
1345
|
+
showOverlay("none");
|
|
1346
|
+
setStatus("Update reminder snoozed for 3 days");
|
|
1347
|
+
}
|
|
1348
|
+
renderer.requestRender();
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const updateCards: [BoxRenderable, number][] = [
|
|
1352
|
+
[updateNowBox, 0],
|
|
1353
|
+
[laterBox, 1],
|
|
1354
|
+
[neverBox, 2],
|
|
1355
|
+
];
|
|
1356
|
+
for (const [box, index] of updateCards) {
|
|
1357
|
+
attachClickableBox(renderer, box, {
|
|
1358
|
+
onActivate: () => runUpdateOption(index),
|
|
1359
|
+
idleBorder:
|
|
1360
|
+
updateSelectedOption === index ? colors.yellow : colors.border,
|
|
1361
|
+
activeBorder: colors.yellow,
|
|
1362
|
+
// Hovering must not fight the selection highlight, so it only moves the
|
|
1363
|
+
// pointer cursor; the border stays owned by renderUpdateOptions().
|
|
1364
|
+
onStateChange: () => {
|
|
1365
|
+
box.borderColor =
|
|
1366
|
+
updateSelectedOption === index ? colors.yellow : colors.border;
|
|
1367
|
+
},
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// Clicking the dim layer behind the panel dismisses it without applying
|
|
1372
|
+
// anything (equivalent to Esc).
|
|
1373
|
+
attachClickOutside(updateDim, () => {
|
|
1374
|
+
showOverlay("none");
|
|
1375
|
+
renderer.requestRender();
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1239
1378
|
updateOptionsRow.add(updateNowBox);
|
|
1240
1379
|
updateOptionsRow.add(laterBox);
|
|
1241
1380
|
|
|
1242
1381
|
const updateFooter = new TextRenderable(renderer, {
|
|
1243
|
-
|
|
1382
|
+
...NON_INTERACTIVE_TEXT,
|
|
1383
|
+
content:
|
|
1384
|
+
" ↑↓/jk/PgUp/PgDn scroll ←→ select Enter Esc/q close · click a button click outside to close",
|
|
1244
1385
|
fg: colors.muted,
|
|
1245
1386
|
bg: colors.surfaceDim,
|
|
1246
1387
|
height: 1,
|
|
@@ -1805,6 +1946,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1805
1946
|
},
|
|
1806
1947
|
});
|
|
1807
1948
|
const orgText = new TextRenderable(renderer, {
|
|
1949
|
+
...NON_INTERACTIVE_TEXT,
|
|
1808
1950
|
content: "",
|
|
1809
1951
|
fg: colors.text,
|
|
1810
1952
|
bg: colors.bg,
|
|
@@ -1812,6 +1954,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1812
1954
|
orgScroll.add(orgText);
|
|
1813
1955
|
orgBox.add(orgScroll);
|
|
1814
1956
|
const orgFooter = new TextRenderable(renderer, {
|
|
1957
|
+
...NON_INTERACTIVE_TEXT,
|
|
1815
1958
|
content: " ↑↓/jk scroll Esc/q close",
|
|
1816
1959
|
fg: colors.muted,
|
|
1817
1960
|
bg: colors.bg,
|
|
@@ -1964,10 +2107,58 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1964
2107
|
});
|
|
1965
2108
|
root.add(statusBar);
|
|
1966
2109
|
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2110
|
+
// ── Menu button (bottom-right corner) ─────────────────────────────
|
|
2111
|
+
// Mouse-only affordance: Space types a space in the query, so a mouse user
|
|
2112
|
+
// needs a target to reach the command menu. Absolutely positioned so it
|
|
2113
|
+
// floats over the results pane instead of stealing a flex row, and added
|
|
2114
|
+
// after every other root child so it wins the hit test on overlap.
|
|
2115
|
+
const menuButton = new BoxRenderable(renderer, {
|
|
2116
|
+
position: "absolute",
|
|
2117
|
+
bottom: 1,
|
|
2118
|
+
right: 1,
|
|
2119
|
+
height: 1,
|
|
2120
|
+
paddingX: 1,
|
|
2121
|
+
backgroundColor: colors.surface,
|
|
2122
|
+
focusable: false,
|
|
2123
|
+
});
|
|
2124
|
+
const menuButtonLabel = new TextRenderable(renderer, {
|
|
2125
|
+
...NON_INTERACTIVE_TEXT,
|
|
2126
|
+
content: "☰ menu",
|
|
2127
|
+
fg: colors.muted,
|
|
2128
|
+
bg: colors.surface,
|
|
2129
|
+
height: 1,
|
|
1970
2130
|
});
|
|
2131
|
+
menuButton.add(menuButtonLabel);
|
|
2132
|
+
root.add(menuButton);
|
|
2133
|
+
|
|
2134
|
+
menuButton.onMouseDown = (event) => {
|
|
2135
|
+
event.stopPropagation();
|
|
2136
|
+
// Toggle, so clicking the button twice returns to where you were.
|
|
2137
|
+
if (currentOverlay === "leader") showOverlay("none");
|
|
2138
|
+
else showLeaderMenu();
|
|
2139
|
+
renderer.requestRender();
|
|
2140
|
+
};
|
|
2141
|
+
menuButton.onMouseOver = () => {
|
|
2142
|
+
renderer.setMousePointer("pointer");
|
|
2143
|
+
menuButtonLabel.fg = colors.accent;
|
|
2144
|
+
renderer.requestRender();
|
|
2145
|
+
};
|
|
2146
|
+
menuButton.onMouseOut = () => {
|
|
2147
|
+
renderer.setMousePointer("default");
|
|
2148
|
+
menuButtonLabel.fg = colors.muted;
|
|
2149
|
+
renderer.requestRender();
|
|
2150
|
+
};
|
|
2151
|
+
|
|
2152
|
+
// Held at module scope so cleanup() (a hoisted function declaration, called
|
|
2153
|
+
// from key handlers defined above the manager's creation) can still reach it.
|
|
2154
|
+
activeStatusMgr = createStatusManager(statusBar, () =>
|
|
2155
|
+
renderer.requestRender(),
|
|
2156
|
+
);
|
|
2157
|
+
const statusMgr = activeStatusMgr;
|
|
2158
|
+
// The renderer tears down its renderables on Ctrl+C / SIGTERM / terminal
|
|
2159
|
+
// close, but the 120ms spinner interval is ours. Without this, the next tick
|
|
2160
|
+
// assigns to a destroyed TextBuffer and throws out of a timer callback.
|
|
2161
|
+
renderer.on("destroy", () => statusMgr.dispose());
|
|
1971
2162
|
setTimeout(() => {
|
|
1972
2163
|
if (currentOverlay === "none") {
|
|
1973
2164
|
setStatus(nextTip());
|
|
@@ -2713,6 +2904,192 @@ ${pack.description ?? ""}`;
|
|
|
2713
2904
|
}
|
|
2714
2905
|
});
|
|
2715
2906
|
|
|
2907
|
+
// ── Mouse on the results list ───────────────────────────────────────
|
|
2908
|
+
// Click selects (the `selectionChanged` handler above refreshes the detail
|
|
2909
|
+
// pane), double-click opens via the same path as Enter, and the wheel steps
|
|
2910
|
+
// the selection. `keepFocus` stops OpenTUI from auto-focusing the list away
|
|
2911
|
+
// from the query input, so typing after a click still edits the query.
|
|
2912
|
+
attachClickToSelect(renderer, resultsSelect, {
|
|
2913
|
+
keepFocus: true,
|
|
2914
|
+
onOpen: () => resultsSelect.selectCurrent(),
|
|
2915
|
+
});
|
|
2916
|
+
|
|
2917
|
+
/**
|
|
2918
|
+
* Enter-equivalent for every list overlay. Single source of truth for both
|
|
2919
|
+
* the keyboard handler below and the overlay mouse wiring, so a click can
|
|
2920
|
+
* never drift from pressing Enter.
|
|
2921
|
+
*/
|
|
2922
|
+
function activateOverlaySelection() {
|
|
2923
|
+
switch (currentOverlay) {
|
|
2924
|
+
case "history": {
|
|
2925
|
+
const sel = historySelect.getSelectedOption();
|
|
2926
|
+
if (sel?.value) {
|
|
2927
|
+
const entry = (sel.value as { entry?: HistoryEntry }).entry;
|
|
2928
|
+
showOverlay("none");
|
|
2929
|
+
if (!entry) return;
|
|
2930
|
+
if (entry.mode === "trending") {
|
|
2931
|
+
if (entry.tab)
|
|
2932
|
+
trendingTab = entry.tab as (typeof TAB_NAMES)[number];
|
|
2933
|
+
loadTrending();
|
|
2934
|
+
} else {
|
|
2935
|
+
searchInput.value = entry.query;
|
|
2936
|
+
showSearchMode();
|
|
2937
|
+
doSearch(entry.query);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
case "bookmarks": {
|
|
2943
|
+
const sel = bookmarksSelect.getSelectedOption();
|
|
2944
|
+
if (sel?.value)
|
|
2945
|
+
openUrl((sel.value as { repo?: { url?: string } }).repo?.url ?? "");
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
case "saved": {
|
|
2949
|
+
const sel = savedSelect.getSelectedOption();
|
|
2950
|
+
if (sel?.value) {
|
|
2951
|
+
const s = sel.value as SavedSearch;
|
|
2952
|
+
showOverlay("none");
|
|
2953
|
+
touchSavedSearch(s.name);
|
|
2954
|
+
if (s.mode === "trending") {
|
|
2955
|
+
if (s.tab) trendingTab = s.tab as (typeof TAB_NAMES)[number];
|
|
2956
|
+
loadTrending();
|
|
2957
|
+
} else {
|
|
2958
|
+
searchInput.value = s.query;
|
|
2959
|
+
showSearchMode();
|
|
2960
|
+
currentSort = s.sort;
|
|
2961
|
+
currentLimit = s.limit;
|
|
2962
|
+
setToolbar();
|
|
2963
|
+
doSearch(s.query);
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
case "topics": {
|
|
2969
|
+
const sel = topicsSelect.getSelectedOption();
|
|
2970
|
+
if (sel?.value) {
|
|
2971
|
+
const topic = sel.value as { name?: string };
|
|
2972
|
+
showOverlay("none");
|
|
2973
|
+
searchInput.value = `topic:${topic.name ?? ""}`;
|
|
2974
|
+
showSearchMode();
|
|
2975
|
+
doSearch(`topic:${topic.name}`);
|
|
2976
|
+
}
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
case "export": {
|
|
2980
|
+
const sel = exportSelect.getSelectedOption();
|
|
2981
|
+
if (sel?.value && currentRepos.length > 0) {
|
|
2982
|
+
const format = sel.value as ExportFormat;
|
|
2983
|
+
const path = exportToFile(currentRepos, format);
|
|
2984
|
+
showOverlay("none");
|
|
2985
|
+
setStatus(`✓ Exported to ${path}`);
|
|
2986
|
+
} else {
|
|
2987
|
+
setStatus("No results to export");
|
|
2988
|
+
showOverlay("none");
|
|
2989
|
+
}
|
|
2990
|
+
renderer.requestRender();
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
case "share": {
|
|
2994
|
+
const sel = shareSelect.getSelectedOption();
|
|
2995
|
+
if (sel?.value) {
|
|
2996
|
+
const opt = resultsSelect.getSelectedOption();
|
|
2997
|
+
const repo = opt?.value as Repo | undefined;
|
|
2998
|
+
if (repo) {
|
|
2999
|
+
const text = formatShare(repo, sel.value as ShareFormat);
|
|
3000
|
+
copyToClipboard(text).then((ok) => {
|
|
3001
|
+
showOverlay("none");
|
|
3002
|
+
setStatus(ok ? "✓ Copied!" : "Clipboard not available");
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
renderer.requestRender();
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
case "help": {
|
|
3010
|
+
showOverlay("none");
|
|
3011
|
+
renderer.requestRender();
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
default:
|
|
3015
|
+
// Leader / update / readme / org / compare / notifications have their
|
|
3016
|
+
// own activation paths (see the key handler) or are not list-driven.
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
/**
|
|
3022
|
+
* Wire the overlay lists. A click selects, a double-click activates (the
|
|
3023
|
+
* overlay's Enter-equivalent), the wheel scrolls, and a click on the panel
|
|
3024
|
+
* outside the rows dismisses the overlay like Esc.
|
|
3025
|
+
*/
|
|
3026
|
+
function attachOverlayMouse(
|
|
3027
|
+
box: Parameters<typeof attachClickOutside>[0],
|
|
3028
|
+
select: Parameters<typeof attachClickToSelect>[1],
|
|
3029
|
+
) {
|
|
3030
|
+
attachClickToSelect(renderer, select, {
|
|
3031
|
+
onOpen: () => activateOverlaySelection(),
|
|
3032
|
+
onClickMiss: () => {
|
|
3033
|
+
showOverlay("none");
|
|
3034
|
+
renderer.requestRender();
|
|
3035
|
+
},
|
|
3036
|
+
});
|
|
3037
|
+
attachClickOutside(box, () => {
|
|
3038
|
+
showOverlay("none");
|
|
3039
|
+
renderer.requestRender();
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
attachOverlayMouse(historyBox, historySelect);
|
|
3044
|
+
attachOverlayMouse(bookmarksBox, bookmarksSelect);
|
|
3045
|
+
attachOverlayMouse(savedBox, savedSelect);
|
|
3046
|
+
attachOverlayMouse(topicsBox, topicsSelect);
|
|
3047
|
+
attachOverlayMouse(exportBox, exportSelect);
|
|
3048
|
+
attachOverlayMouse(shareBox, shareSelect);
|
|
3049
|
+
|
|
3050
|
+
// Read-only overlays have no rows to click, so the panel itself is the
|
|
3051
|
+
// dismiss target (same as Esc). Their ScrollBox children handle the wheel
|
|
3052
|
+
// natively; claiming it here stops the renderer from also feeding the
|
|
3053
|
+
// focused-renderable fallback.
|
|
3054
|
+
for (const panel of [helpBox, orgBox, readmeBox, compareBox, notifsBox]) {
|
|
3055
|
+
attachClickOutside(panel, () => {
|
|
3056
|
+
showOverlay("none");
|
|
3057
|
+
renderer.requestRender();
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
3060
|
+
for (const scroller of [helpScroll, orgScroll, readmeScroll, updateScroll]) {
|
|
3061
|
+
scroller.onMouseScroll = (event) => {
|
|
3062
|
+
event.stopPropagation();
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
// Leader menu: the list activates like Enter, but the dim layer behind the
|
|
3067
|
+
// floating menu is the click-outside target (the menu box itself is nested
|
|
3068
|
+
// inside it and stops propagation).
|
|
3069
|
+
attachClickToSelect(renderer, leaderSelect, {
|
|
3070
|
+
onOpen: () => {
|
|
3071
|
+
const entry = leaderSelect.getSelectedOption()?.value as
|
|
3072
|
+
MenuEntry | undefined;
|
|
3073
|
+
if (!entry) return;
|
|
3074
|
+
if (entry.type === "category") {
|
|
3075
|
+
pushMenuLevel(` Menu > ${entry.name}`, entry.name, entry.children, 0);
|
|
3076
|
+
} else {
|
|
3077
|
+
resetMenu();
|
|
3078
|
+
showOverlay("none");
|
|
3079
|
+
entry.action();
|
|
3080
|
+
}
|
|
3081
|
+
renderer.requestRender();
|
|
3082
|
+
},
|
|
3083
|
+
onClickMiss: () => {
|
|
3084
|
+
showOverlay("none");
|
|
3085
|
+
renderer.requestRender();
|
|
3086
|
+
},
|
|
3087
|
+
});
|
|
3088
|
+
attachClickOutside(leaderDim, () => {
|
|
3089
|
+
showOverlay("none");
|
|
3090
|
+
renderer.requestRender();
|
|
3091
|
+
});
|
|
3092
|
+
|
|
2716
3093
|
// ── Global keyboard shortcuts ──────────────────────────────────────
|
|
2717
3094
|
renderer.keyInput.on("keypress", (key) => {
|
|
2718
3095
|
// ── Token prompt (own keyboard scope) ──────────────────────────
|
|
@@ -2825,21 +3202,7 @@ ${pack.description ?? ""}`;
|
|
|
2825
3202
|
return;
|
|
2826
3203
|
}
|
|
2827
3204
|
if (key.name === "enter" || key.name === "return") {
|
|
2828
|
-
|
|
2829
|
-
if (sel?.value) {
|
|
2830
|
-
const entry = (sel.value as { entry?: HistoryEntry }).entry;
|
|
2831
|
-
showOverlay("none");
|
|
2832
|
-
if (!entry) return;
|
|
2833
|
-
if (entry.mode === "trending") {
|
|
2834
|
-
if (entry.tab)
|
|
2835
|
-
trendingTab = entry.tab as (typeof TAB_NAMES)[number];
|
|
2836
|
-
loadTrending();
|
|
2837
|
-
} else {
|
|
2838
|
-
searchInput.value = entry.query;
|
|
2839
|
-
showSearchMode();
|
|
2840
|
-
doSearch(entry.query);
|
|
2841
|
-
}
|
|
2842
|
-
}
|
|
3205
|
+
activateOverlaySelection();
|
|
2843
3206
|
return;
|
|
2844
3207
|
}
|
|
2845
3208
|
return;
|
|
@@ -2861,9 +3224,7 @@ ${pack.description ?? ""}`;
|
|
|
2861
3224
|
return;
|
|
2862
3225
|
}
|
|
2863
3226
|
if (key.name === "enter" || key.name === "return") {
|
|
2864
|
-
|
|
2865
|
-
if (sel?.value)
|
|
2866
|
-
openUrl((sel.value as { repo?: { url?: string } }).repo?.url ?? "");
|
|
3227
|
+
activateOverlaySelection();
|
|
2867
3228
|
return;
|
|
2868
3229
|
}
|
|
2869
3230
|
return;
|
|
@@ -2882,23 +3243,7 @@ ${pack.description ?? ""}`;
|
|
|
2882
3243
|
return;
|
|
2883
3244
|
}
|
|
2884
3245
|
if (key.name === "enter" || key.name === "return") {
|
|
2885
|
-
|
|
2886
|
-
if (sel?.value) {
|
|
2887
|
-
const s = sel.value as SavedSearch;
|
|
2888
|
-
showOverlay("none");
|
|
2889
|
-
touchSavedSearch(s.name);
|
|
2890
|
-
if (s.mode === "trending") {
|
|
2891
|
-
if (s.tab) trendingTab = s.tab as (typeof TAB_NAMES)[number];
|
|
2892
|
-
loadTrending();
|
|
2893
|
-
} else {
|
|
2894
|
-
searchInput.value = s.query;
|
|
2895
|
-
showSearchMode();
|
|
2896
|
-
currentSort = s.sort;
|
|
2897
|
-
currentLimit = s.limit;
|
|
2898
|
-
setToolbar();
|
|
2899
|
-
doSearch(s.query);
|
|
2900
|
-
}
|
|
2901
|
-
}
|
|
3246
|
+
activateOverlaySelection();
|
|
2902
3247
|
return;
|
|
2903
3248
|
}
|
|
2904
3249
|
return;
|
|
@@ -2907,14 +3252,7 @@ ${pack.description ?? ""}`;
|
|
|
2907
3252
|
// Topics explorer overlay
|
|
2908
3253
|
if (currentOverlay === "topics") {
|
|
2909
3254
|
if (key.name === "enter" || key.name === "return") {
|
|
2910
|
-
|
|
2911
|
-
if (sel?.value) {
|
|
2912
|
-
const topic = sel.value as { name?: string };
|
|
2913
|
-
showOverlay("none");
|
|
2914
|
-
searchInput.value = `topic:${topic.name ?? ""}`;
|
|
2915
|
-
showSearchMode();
|
|
2916
|
-
doSearch(`topic:${topic.name}`);
|
|
2917
|
-
}
|
|
3255
|
+
activateOverlaySelection();
|
|
2918
3256
|
return;
|
|
2919
3257
|
}
|
|
2920
3258
|
return;
|
|
@@ -2923,17 +3261,7 @@ ${pack.description ?? ""}`;
|
|
|
2923
3261
|
// Export overlay
|
|
2924
3262
|
if (currentOverlay === "export") {
|
|
2925
3263
|
if (key.name === "enter" || key.name === "return") {
|
|
2926
|
-
|
|
2927
|
-
if (sel?.value && currentRepos.length > 0) {
|
|
2928
|
-
const format = sel.value as ExportFormat;
|
|
2929
|
-
const path = exportToFile(currentRepos, format);
|
|
2930
|
-
showOverlay("none");
|
|
2931
|
-
setStatus(`✓ Exported to ${path}`);
|
|
2932
|
-
} else {
|
|
2933
|
-
setStatus("No results to export");
|
|
2934
|
-
showOverlay("none");
|
|
2935
|
-
}
|
|
2936
|
-
renderer.requestRender();
|
|
3264
|
+
activateOverlaySelection();
|
|
2937
3265
|
return;
|
|
2938
3266
|
}
|
|
2939
3267
|
return;
|
|
@@ -2968,19 +3296,7 @@ ${pack.description ?? ""}`;
|
|
|
2968
3296
|
// Share overlay
|
|
2969
3297
|
if (currentOverlay === "share") {
|
|
2970
3298
|
if (key.name === "enter" || key.name === "return") {
|
|
2971
|
-
|
|
2972
|
-
if (sel?.value) {
|
|
2973
|
-
const opt = resultsSelect.getSelectedOption();
|
|
2974
|
-
const repo = opt?.value as Repo | undefined;
|
|
2975
|
-
if (repo) {
|
|
2976
|
-
const text = formatShare(repo, sel.value as ShareFormat);
|
|
2977
|
-
copyToClipboard(text).then((ok) => {
|
|
2978
|
-
showOverlay("none");
|
|
2979
|
-
setStatus(ok ? "✓ Copied!" : "Clipboard not available");
|
|
2980
|
-
});
|
|
2981
|
-
}
|
|
2982
|
-
}
|
|
2983
|
-
renderer.requestRender();
|
|
3299
|
+
activateOverlaySelection();
|
|
2984
3300
|
return;
|
|
2985
3301
|
}
|
|
2986
3302
|
return;
|
|
@@ -2988,8 +3304,7 @@ ${pack.description ?? ""}`;
|
|
|
2988
3304
|
|
|
2989
3305
|
// Help overlay: any key closes
|
|
2990
3306
|
if (currentOverlay === "help") {
|
|
2991
|
-
|
|
2992
|
-
renderer.requestRender();
|
|
3307
|
+
activateOverlaySelection();
|
|
2993
3308
|
return;
|
|
2994
3309
|
}
|
|
2995
3310
|
|
|
@@ -3029,33 +3344,7 @@ ${pack.description ?? ""}`;
|
|
|
3029
3344
|
return;
|
|
3030
3345
|
}
|
|
3031
3346
|
if (key.name === "enter" || key.name === "return") {
|
|
3032
|
-
|
|
3033
|
-
"now" | "later" | "never" | undefined;
|
|
3034
|
-
if (action === "now") {
|
|
3035
|
-
showOverlay("none");
|
|
3036
|
-
// ponytail: record the running version so the next launch detects the upgrade
|
|
3037
|
-
recordPreUpdateState(cachedVersion ?? "");
|
|
3038
|
-
performUpdate()
|
|
3039
|
-
.then((ok) => {
|
|
3040
|
-
if (ok) {
|
|
3041
|
-
setStatus("✓ Update complete — restart ghfind");
|
|
3042
|
-
} else {
|
|
3043
|
-
setStatus("✗ Update failed — check terminal output");
|
|
3044
|
-
}
|
|
3045
|
-
})
|
|
3046
|
-
.catch(() => {
|
|
3047
|
-
setStatus("✗ Update failed — check terminal output");
|
|
3048
|
-
});
|
|
3049
|
-
} else if (action === "never") {
|
|
3050
|
-
suppressUpdateNotices();
|
|
3051
|
-
showOverlay("none");
|
|
3052
|
-
setStatus("Update notices suppressed");
|
|
3053
|
-
} else {
|
|
3054
|
-
snoozeUpdateNotices(3);
|
|
3055
|
-
showOverlay("none");
|
|
3056
|
-
setStatus("Update reminder snoozed for 3 days");
|
|
3057
|
-
}
|
|
3058
|
-
renderer.requestRender();
|
|
3347
|
+
runUpdateOption(updateSelectedOption);
|
|
3059
3348
|
return;
|
|
3060
3349
|
}
|
|
3061
3350
|
return;
|
|
@@ -3091,11 +3380,19 @@ ${pack.description ?? ""}`;
|
|
|
3091
3380
|
return;
|
|
3092
3381
|
}
|
|
3093
3382
|
|
|
3094
|
-
//
|
|
3095
|
-
//
|
|
3096
|
-
//
|
|
3097
|
-
if (key.name === "
|
|
3098
|
-
|
|
3383
|
+
// Esc toggles the leader menu — the only binding for it. Space used to open it
|
|
3384
|
+
// when the query input was not focused, which made the same key mean two
|
|
3385
|
+
// things depending on invisible state; it is now reserved for typing.
|
|
3386
|
+
if (key.name === "escape") {
|
|
3387
|
+
if (currentOverlay !== "none") {
|
|
3388
|
+
// Only reachable for the leader menu — every other overlay returned
|
|
3389
|
+
// above, and its own branch already closed it.
|
|
3390
|
+
showOverlay("none");
|
|
3391
|
+
setToolbar();
|
|
3392
|
+
} else {
|
|
3393
|
+
showLeaderMenu();
|
|
3394
|
+
}
|
|
3395
|
+
renderer.requestRender();
|
|
3099
3396
|
return;
|
|
3100
3397
|
}
|
|
3101
3398
|
|
|
@@ -3246,12 +3543,18 @@ ${pack.description ?? ""}`;
|
|
|
3246
3543
|
// ── Update check (before start) ──────────────────────────────────────
|
|
3247
3544
|
const _currentVersion = getVersion();
|
|
3248
3545
|
// ── Start ──────────────────────────────────────────────────────────
|
|
3249
|
-
//
|
|
3546
|
+
// Default is the landing menu. Saved sessions restore preferences
|
|
3250
3547
|
// (sort/limit/trending tab) and pre-fill the query once a mode is picked,
|
|
3251
3548
|
// but never auto-run a search — a stale or corrupted session (e.g. an MCP
|
|
3252
3549
|
// handshake blob saved as a query) used to hijack startup and skip the
|
|
3253
|
-
// menu on every launch.
|
|
3254
|
-
|
|
3550
|
+
// menu on every launch. An explicit `ghfind --trending` is not a stale
|
|
3551
|
+
// session though: it is the mode the user asked for, so it skips the menu.
|
|
3552
|
+
if (startMode === "trending") {
|
|
3553
|
+
showLanding(false);
|
|
3554
|
+
loadTrending();
|
|
3555
|
+
} else {
|
|
3556
|
+
showLanding(true);
|
|
3557
|
+
}
|
|
3255
3558
|
checkForUpdateAndShow();
|
|
3256
3559
|
checkPostUpgradePanel();
|
|
3257
3560
|
renderer.start();
|
|
@@ -3277,11 +3580,22 @@ function formatToolbar(
|
|
|
3277
3580
|
}
|
|
3278
3581
|
|
|
3279
3582
|
function cleanup(): void {
|
|
3583
|
+
// Stop the spinner before exiting: process.exit() would kill the process
|
|
3584
|
+
// anyway, but a tick already queued between here and the exit can still fire.
|
|
3585
|
+
activeStatusMgr?.dispose();
|
|
3586
|
+
activeStatusMgr = null;
|
|
3280
3587
|
try {
|
|
3281
3588
|
rotateHistory();
|
|
3282
3589
|
} catch {
|
|
3283
3590
|
/* non-critical */
|
|
3284
3591
|
}
|
|
3592
|
+
// Release the terminal (raw mode, alternate screen) before the hard exit.
|
|
3593
|
+
try {
|
|
3594
|
+
activeRenderer?.destroy();
|
|
3595
|
+
} catch {
|
|
3596
|
+
/* non-critical */
|
|
3597
|
+
}
|
|
3598
|
+
activeRenderer = null;
|
|
3285
3599
|
process.exit(0);
|
|
3286
3600
|
}
|
|
3287
3601
|
|