github-search-cli 9.9.0 → 9.10.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/CHANGELOG.md +51 -0
- package/README.md +38 -32
- package/dist/cli.js +450 -106
- package/docs/HOW-TO.md +3 -2
- package/package.json +2 -2
- package/release-notes/9.10.0.md +137 -0
- package/src/help.ts +21 -4
- package/src/tui/mouse.ts +349 -0
- package/src/tui.ts +378 -106
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 {
|
|
@@ -310,7 +317,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
310
317
|
|
|
311
318
|
const header = new TextRenderable(renderer, {
|
|
312
319
|
content:
|
|
313
|
-
" ghfind — GitHub repo browser [/]search [
|
|
320
|
+
" ghfind — GitHub repo browser [/]search [Esc]menu [\u2192]open [?]help [q]uit · click select · dbl-click open",
|
|
314
321
|
bg: colors.bg,
|
|
315
322
|
fg: colors.muted,
|
|
316
323
|
height: 1,
|
|
@@ -335,15 +342,26 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
335
342
|
const isActive = name === trendingTab;
|
|
336
343
|
const label = ` ${name} `;
|
|
337
344
|
const tt = new TextRenderable(renderer, {
|
|
345
|
+
...NON_INTERACTIVE_TEXT,
|
|
338
346
|
content: label,
|
|
339
347
|
fg: isActive ? colors.bg : colors.muted,
|
|
340
348
|
bg: isActive ? colors.blue : colors.bg,
|
|
341
349
|
height: 1,
|
|
342
350
|
});
|
|
351
|
+
// Click a tab to switch to it — same path as the 1-5 number keys.
|
|
352
|
+
tt.onMouseDown = (event) => {
|
|
353
|
+
event.stopPropagation();
|
|
354
|
+
if (name === trendingTab) return;
|
|
355
|
+
trendingTab = name;
|
|
356
|
+
loadTrending();
|
|
357
|
+
};
|
|
358
|
+
tt.onMouseOver = () => renderer.setMousePointer("pointer");
|
|
359
|
+
tt.onMouseOut = () => renderer.setMousePointer("default");
|
|
343
360
|
trendingTabTexts.push(tt);
|
|
344
361
|
trendingTabBox.add(tt);
|
|
345
362
|
if (i < TAB_NAMES.length - 1) {
|
|
346
363
|
const sp = new TextRenderable(renderer, {
|
|
364
|
+
...NON_INTERACTIVE_TEXT,
|
|
347
365
|
content: " ",
|
|
348
366
|
fg: colors.muted,
|
|
349
367
|
bg: colors.bg,
|
|
@@ -442,6 +460,9 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
442
460
|
content: "",
|
|
443
461
|
fg: colors.text,
|
|
444
462
|
wrapMode: "none",
|
|
463
|
+
// Non-interactive: a left-press here would otherwise be swallowed as a
|
|
464
|
+
// text-selection drag instead of reaching the click handlers.
|
|
465
|
+
selectable: false,
|
|
445
466
|
});
|
|
446
467
|
detailBox.add(detailText);
|
|
447
468
|
|
|
@@ -458,6 +479,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
458
479
|
});
|
|
459
480
|
root.add(landingBox);
|
|
460
481
|
const landingBanner = new TextRenderable(renderer, {
|
|
482
|
+
...NON_INTERACTIVE_TEXT,
|
|
461
483
|
content: [
|
|
462
484
|
" ██████╗ ██╗ ██╗███████╗██╗███╗ ██╗██████╗ ",
|
|
463
485
|
"██╔════╝ ██║ ██║██╔════╝██║████╗ ██║██╔══██╗",
|
|
@@ -540,8 +562,9 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
540
562
|
}
|
|
541
563
|
|
|
542
564
|
const landingHint = new TextRenderable(renderer, {
|
|
565
|
+
...NON_INTERACTIVE_TEXT,
|
|
543
566
|
content:
|
|
544
|
-
" \u2191\u2193 Navigate \u21E9 Select [b]ookmarks [?]help [q]uit",
|
|
567
|
+
" \u2191\u2193 Navigate \u21E9 Select [b]ookmarks [?]help [q]uit · click to pick a mode",
|
|
545
568
|
fg: colors.muted,
|
|
546
569
|
bg: colors.bg,
|
|
547
570
|
height: 1,
|
|
@@ -572,6 +595,34 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
572
595
|
}
|
|
573
596
|
}
|
|
574
597
|
|
|
598
|
+
// ── Landing mouse ───────────────────────────────────────────
|
|
599
|
+
// Click selects, double-click runs the card's action (same as Enter), hover
|
|
600
|
+
// shows a pointer cursor. `updateLandingCards` is the single source of truth
|
|
601
|
+
// for the card colours, so hover and selection cannot drift.
|
|
602
|
+
landingCards.forEach((card, i) => {
|
|
603
|
+
let lastClickAt = 0;
|
|
604
|
+
card.onMouseDown = (event) => {
|
|
605
|
+
event.stopPropagation();
|
|
606
|
+
const now = Date.now();
|
|
607
|
+
const isDouble =
|
|
608
|
+
landingSelected === i && now - lastClickAt < DOUBLE_CLICK_MS;
|
|
609
|
+
lastClickAt = now;
|
|
610
|
+
if (isDouble) {
|
|
611
|
+
landingOptions[i].action();
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
landingSelected = i;
|
|
615
|
+
updateLandingCards();
|
|
616
|
+
renderer.requestRender();
|
|
617
|
+
};
|
|
618
|
+
card.onMouseOver = () => {
|
|
619
|
+
renderer.setMousePointer("pointer");
|
|
620
|
+
};
|
|
621
|
+
card.onMouseOut = () => {
|
|
622
|
+
renderer.setMousePointer("default");
|
|
623
|
+
};
|
|
624
|
+
});
|
|
625
|
+
|
|
575
626
|
// Landing keyboard handler. Registered before the global handler below,
|
|
576
627
|
// and both listeners receive the same key event — so keys the landing
|
|
577
628
|
// screen consumes must be claimed with stopPropagation(), or the global
|
|
@@ -645,6 +696,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
645
696
|
updateDim.visible = false;
|
|
646
697
|
if (type === "none") {
|
|
647
698
|
currentOverlay = "none";
|
|
699
|
+
menuButton.visible = true;
|
|
648
700
|
if (currentMode === "landing") {
|
|
649
701
|
landingBox.visible = true;
|
|
650
702
|
body.visible = false;
|
|
@@ -658,6 +710,9 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
658
710
|
return;
|
|
659
711
|
}
|
|
660
712
|
currentOverlay = type;
|
|
713
|
+
// The button floats above the layout, so it would sit on top of every
|
|
714
|
+
// full-screen overlay (and stay clickable through the dim layer).
|
|
715
|
+
menuButton.visible = false;
|
|
661
716
|
// An overlay opened from the landing screen must hide the menu too
|
|
662
717
|
// (hideMainContent only covers the search/trending/toolbar boxes).
|
|
663
718
|
landingBox.visible = false;
|
|
@@ -945,6 +1000,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
945
1000
|
content: "",
|
|
946
1001
|
fg: colors.text,
|
|
947
1002
|
bg: colors.bg,
|
|
1003
|
+
selectable: false,
|
|
948
1004
|
});
|
|
949
1005
|
compareBox.add(compareText);
|
|
950
1006
|
root.add(compareBox);
|
|
@@ -1040,7 +1096,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1040
1096
|
savedSelect.setSelectedIndex(0);
|
|
1041
1097
|
}
|
|
1042
1098
|
|
|
1043
|
-
// ── Leader menu overlay (
|
|
1099
|
+
// ── Leader menu overlay (Esc key) ─────────────────────────────
|
|
1044
1100
|
// Dim overlay — sits behind the menu, above main content
|
|
1045
1101
|
const leaderDim = new BoxRenderable(renderer, {
|
|
1046
1102
|
visible: false,
|
|
@@ -1086,7 +1142,9 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1086
1142
|
itemSpacing: 0,
|
|
1087
1143
|
});
|
|
1088
1144
|
const leaderFooter = new TextRenderable(renderer, {
|
|
1089
|
-
|
|
1145
|
+
...NON_INTERACTIVE_TEXT,
|
|
1146
|
+
content:
|
|
1147
|
+
" ↑↓ select Enter run Esc/q close · click to run click outside to close ",
|
|
1090
1148
|
fg: colors.muted,
|
|
1091
1149
|
bg: colors.surfaceDim,
|
|
1092
1150
|
height: 1,
|
|
@@ -1236,11 +1294,77 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1236
1294
|
renderer.requestRender();
|
|
1237
1295
|
}
|
|
1238
1296
|
|
|
1297
|
+
// ── Update panel mouse ────────────────────────────────────────────
|
|
1298
|
+
// Click a card to select *and* run it in one gesture: these three options are
|
|
1299
|
+
// terminal actions (install now / snooze / suppress), and a modal that needs
|
|
1300
|
+
// select-then-Enter would be two clicks for the common case. `renderUpdateOptions`
|
|
1301
|
+
// stays the single source of truth for the colours.
|
|
1302
|
+
function runUpdateOption(index: number) {
|
|
1303
|
+
updateSelectedOption = index;
|
|
1304
|
+
renderUpdateOptions();
|
|
1305
|
+
const action = ["now", "later", "never"][index] as
|
|
1306
|
+
"now" | "later" | "never" | undefined;
|
|
1307
|
+
if (action === "now") {
|
|
1308
|
+
showOverlay("none");
|
|
1309
|
+
// ponytail: record the running version so the next launch detects the upgrade
|
|
1310
|
+
recordPreUpdateState(cachedVersion ?? "");
|
|
1311
|
+
performUpdate()
|
|
1312
|
+
.then((ok) => {
|
|
1313
|
+
if (ok) {
|
|
1314
|
+
setStatus("✓ Update complete — restart ghfind");
|
|
1315
|
+
} else {
|
|
1316
|
+
setStatus("✗ Update failed — check terminal output");
|
|
1317
|
+
}
|
|
1318
|
+
})
|
|
1319
|
+
.catch(() => {
|
|
1320
|
+
setStatus("✗ Update failed — check terminal output");
|
|
1321
|
+
});
|
|
1322
|
+
} else if (action === "never") {
|
|
1323
|
+
suppressUpdateNotices();
|
|
1324
|
+
showOverlay("none");
|
|
1325
|
+
setStatus("Update notices suppressed");
|
|
1326
|
+
} else {
|
|
1327
|
+
snoozeUpdateNotices(3);
|
|
1328
|
+
showOverlay("none");
|
|
1329
|
+
setStatus("Update reminder snoozed for 3 days");
|
|
1330
|
+
}
|
|
1331
|
+
renderer.requestRender();
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const updateCards: [BoxRenderable, number][] = [
|
|
1335
|
+
[updateNowBox, 0],
|
|
1336
|
+
[laterBox, 1],
|
|
1337
|
+
[neverBox, 2],
|
|
1338
|
+
];
|
|
1339
|
+
for (const [box, index] of updateCards) {
|
|
1340
|
+
attachClickableBox(renderer, box, {
|
|
1341
|
+
onActivate: () => runUpdateOption(index),
|
|
1342
|
+
idleBorder:
|
|
1343
|
+
updateSelectedOption === index ? colors.yellow : colors.border,
|
|
1344
|
+
activeBorder: colors.yellow,
|
|
1345
|
+
// Hovering must not fight the selection highlight, so it only moves the
|
|
1346
|
+
// pointer cursor; the border stays owned by renderUpdateOptions().
|
|
1347
|
+
onStateChange: () => {
|
|
1348
|
+
box.borderColor =
|
|
1349
|
+
updateSelectedOption === index ? colors.yellow : colors.border;
|
|
1350
|
+
},
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Clicking the dim layer behind the panel dismisses it without applying
|
|
1355
|
+
// anything (equivalent to Esc).
|
|
1356
|
+
attachClickOutside(updateDim, () => {
|
|
1357
|
+
showOverlay("none");
|
|
1358
|
+
renderer.requestRender();
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1239
1361
|
updateOptionsRow.add(updateNowBox);
|
|
1240
1362
|
updateOptionsRow.add(laterBox);
|
|
1241
1363
|
|
|
1242
1364
|
const updateFooter = new TextRenderable(renderer, {
|
|
1243
|
-
|
|
1365
|
+
...NON_INTERACTIVE_TEXT,
|
|
1366
|
+
content:
|
|
1367
|
+
" ↑↓/jk/PgUp/PgDn scroll ←→ select Enter Esc/q close · click a button click outside to close",
|
|
1244
1368
|
fg: colors.muted,
|
|
1245
1369
|
bg: colors.surfaceDim,
|
|
1246
1370
|
height: 1,
|
|
@@ -1964,6 +2088,48 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1964
2088
|
});
|
|
1965
2089
|
root.add(statusBar);
|
|
1966
2090
|
|
|
2091
|
+
// ── Menu button (bottom-right corner) ─────────────────────────────
|
|
2092
|
+
// Mouse-only affordance: Space types a space in the query, so a mouse user
|
|
2093
|
+
// needs a target to reach the command menu. Absolutely positioned so it
|
|
2094
|
+
// floats over the results pane instead of stealing a flex row, and added
|
|
2095
|
+
// after every other root child so it wins the hit test on overlap.
|
|
2096
|
+
const menuButton = new BoxRenderable(renderer, {
|
|
2097
|
+
position: "absolute",
|
|
2098
|
+
bottom: 1,
|
|
2099
|
+
right: 1,
|
|
2100
|
+
height: 1,
|
|
2101
|
+
paddingX: 1,
|
|
2102
|
+
backgroundColor: colors.surface,
|
|
2103
|
+
focusable: false,
|
|
2104
|
+
});
|
|
2105
|
+
const menuButtonLabel = new TextRenderable(renderer, {
|
|
2106
|
+
...NON_INTERACTIVE_TEXT,
|
|
2107
|
+
content: "☰ menu",
|
|
2108
|
+
fg: colors.muted,
|
|
2109
|
+
bg: colors.surface,
|
|
2110
|
+
height: 1,
|
|
2111
|
+
});
|
|
2112
|
+
menuButton.add(menuButtonLabel);
|
|
2113
|
+
root.add(menuButton);
|
|
2114
|
+
|
|
2115
|
+
menuButton.onMouseDown = (event) => {
|
|
2116
|
+
event.stopPropagation();
|
|
2117
|
+
// Toggle, so clicking the button twice returns to where you were.
|
|
2118
|
+
if (currentOverlay === "leader") showOverlay("none");
|
|
2119
|
+
else showLeaderMenu();
|
|
2120
|
+
renderer.requestRender();
|
|
2121
|
+
};
|
|
2122
|
+
menuButton.onMouseOver = () => {
|
|
2123
|
+
renderer.setMousePointer("pointer");
|
|
2124
|
+
menuButtonLabel.fg = colors.accent;
|
|
2125
|
+
renderer.requestRender();
|
|
2126
|
+
};
|
|
2127
|
+
menuButton.onMouseOut = () => {
|
|
2128
|
+
renderer.setMousePointer("default");
|
|
2129
|
+
menuButtonLabel.fg = colors.muted;
|
|
2130
|
+
renderer.requestRender();
|
|
2131
|
+
};
|
|
2132
|
+
|
|
1967
2133
|
const statusMgr = new StatusManager((text) => {
|
|
1968
2134
|
statusBar.content = ` ${text}`;
|
|
1969
2135
|
renderer.requestRender();
|
|
@@ -2713,6 +2879,192 @@ ${pack.description ?? ""}`;
|
|
|
2713
2879
|
}
|
|
2714
2880
|
});
|
|
2715
2881
|
|
|
2882
|
+
// ── Mouse on the results list ───────────────────────────────────────
|
|
2883
|
+
// Click selects (the `selectionChanged` handler above refreshes the detail
|
|
2884
|
+
// pane), double-click opens via the same path as Enter, and the wheel steps
|
|
2885
|
+
// the selection. `keepFocus` stops OpenTUI from auto-focusing the list away
|
|
2886
|
+
// from the query input, so typing after a click still edits the query.
|
|
2887
|
+
attachClickToSelect(renderer, resultsSelect, {
|
|
2888
|
+
keepFocus: true,
|
|
2889
|
+
onOpen: () => resultsSelect.selectCurrent(),
|
|
2890
|
+
});
|
|
2891
|
+
|
|
2892
|
+
/**
|
|
2893
|
+
* Enter-equivalent for every list overlay. Single source of truth for both
|
|
2894
|
+
* the keyboard handler below and the overlay mouse wiring, so a click can
|
|
2895
|
+
* never drift from pressing Enter.
|
|
2896
|
+
*/
|
|
2897
|
+
function activateOverlaySelection() {
|
|
2898
|
+
switch (currentOverlay) {
|
|
2899
|
+
case "history": {
|
|
2900
|
+
const sel = historySelect.getSelectedOption();
|
|
2901
|
+
if (sel?.value) {
|
|
2902
|
+
const entry = (sel.value as { entry?: HistoryEntry }).entry;
|
|
2903
|
+
showOverlay("none");
|
|
2904
|
+
if (!entry) return;
|
|
2905
|
+
if (entry.mode === "trending") {
|
|
2906
|
+
if (entry.tab)
|
|
2907
|
+
trendingTab = entry.tab as (typeof TAB_NAMES)[number];
|
|
2908
|
+
loadTrending();
|
|
2909
|
+
} else {
|
|
2910
|
+
searchInput.value = entry.query;
|
|
2911
|
+
showSearchMode();
|
|
2912
|
+
doSearch(entry.query);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
case "bookmarks": {
|
|
2918
|
+
const sel = bookmarksSelect.getSelectedOption();
|
|
2919
|
+
if (sel?.value)
|
|
2920
|
+
openUrl((sel.value as { repo?: { url?: string } }).repo?.url ?? "");
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
case "saved": {
|
|
2924
|
+
const sel = savedSelect.getSelectedOption();
|
|
2925
|
+
if (sel?.value) {
|
|
2926
|
+
const s = sel.value as SavedSearch;
|
|
2927
|
+
showOverlay("none");
|
|
2928
|
+
touchSavedSearch(s.name);
|
|
2929
|
+
if (s.mode === "trending") {
|
|
2930
|
+
if (s.tab) trendingTab = s.tab as (typeof TAB_NAMES)[number];
|
|
2931
|
+
loadTrending();
|
|
2932
|
+
} else {
|
|
2933
|
+
searchInput.value = s.query;
|
|
2934
|
+
showSearchMode();
|
|
2935
|
+
currentSort = s.sort;
|
|
2936
|
+
currentLimit = s.limit;
|
|
2937
|
+
setToolbar();
|
|
2938
|
+
doSearch(s.query);
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
case "topics": {
|
|
2944
|
+
const sel = topicsSelect.getSelectedOption();
|
|
2945
|
+
if (sel?.value) {
|
|
2946
|
+
const topic = sel.value as { name?: string };
|
|
2947
|
+
showOverlay("none");
|
|
2948
|
+
searchInput.value = `topic:${topic.name ?? ""}`;
|
|
2949
|
+
showSearchMode();
|
|
2950
|
+
doSearch(`topic:${topic.name}`);
|
|
2951
|
+
}
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
case "export": {
|
|
2955
|
+
const sel = exportSelect.getSelectedOption();
|
|
2956
|
+
if (sel?.value && currentRepos.length > 0) {
|
|
2957
|
+
const format = sel.value as ExportFormat;
|
|
2958
|
+
const path = exportToFile(currentRepos, format);
|
|
2959
|
+
showOverlay("none");
|
|
2960
|
+
setStatus(`✓ Exported to ${path}`);
|
|
2961
|
+
} else {
|
|
2962
|
+
setStatus("No results to export");
|
|
2963
|
+
showOverlay("none");
|
|
2964
|
+
}
|
|
2965
|
+
renderer.requestRender();
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
case "share": {
|
|
2969
|
+
const sel = shareSelect.getSelectedOption();
|
|
2970
|
+
if (sel?.value) {
|
|
2971
|
+
const opt = resultsSelect.getSelectedOption();
|
|
2972
|
+
const repo = opt?.value as Repo | undefined;
|
|
2973
|
+
if (repo) {
|
|
2974
|
+
const text = formatShare(repo, sel.value as ShareFormat);
|
|
2975
|
+
copyToClipboard(text).then((ok) => {
|
|
2976
|
+
showOverlay("none");
|
|
2977
|
+
setStatus(ok ? "✓ Copied!" : "Clipboard not available");
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
renderer.requestRender();
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
case "help": {
|
|
2985
|
+
showOverlay("none");
|
|
2986
|
+
renderer.requestRender();
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
default:
|
|
2990
|
+
// Leader / update / readme / org / compare / notifications have their
|
|
2991
|
+
// own activation paths (see the key handler) or are not list-driven.
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
/**
|
|
2997
|
+
* Wire the overlay lists. A click selects, a double-click activates (the
|
|
2998
|
+
* overlay's Enter-equivalent), the wheel scrolls, and a click on the panel
|
|
2999
|
+
* outside the rows dismisses the overlay like Esc.
|
|
3000
|
+
*/
|
|
3001
|
+
function attachOverlayMouse(
|
|
3002
|
+
box: Parameters<typeof attachClickOutside>[0],
|
|
3003
|
+
select: Parameters<typeof attachClickToSelect>[1],
|
|
3004
|
+
) {
|
|
3005
|
+
attachClickToSelect(renderer, select, {
|
|
3006
|
+
onOpen: () => activateOverlaySelection(),
|
|
3007
|
+
onClickMiss: () => {
|
|
3008
|
+
showOverlay("none");
|
|
3009
|
+
renderer.requestRender();
|
|
3010
|
+
},
|
|
3011
|
+
});
|
|
3012
|
+
attachClickOutside(box, () => {
|
|
3013
|
+
showOverlay("none");
|
|
3014
|
+
renderer.requestRender();
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
attachOverlayMouse(historyBox, historySelect);
|
|
3019
|
+
attachOverlayMouse(bookmarksBox, bookmarksSelect);
|
|
3020
|
+
attachOverlayMouse(savedBox, savedSelect);
|
|
3021
|
+
attachOverlayMouse(topicsBox, topicsSelect);
|
|
3022
|
+
attachOverlayMouse(exportBox, exportSelect);
|
|
3023
|
+
attachOverlayMouse(shareBox, shareSelect);
|
|
3024
|
+
|
|
3025
|
+
// Read-only overlays have no rows to click, so the panel itself is the
|
|
3026
|
+
// dismiss target (same as Esc). Their ScrollBox children handle the wheel
|
|
3027
|
+
// natively; claiming it here stops the renderer from also feeding the
|
|
3028
|
+
// focused-renderable fallback.
|
|
3029
|
+
for (const panel of [helpBox, orgBox, readmeBox, compareBox, notifsBox]) {
|
|
3030
|
+
attachClickOutside(panel, () => {
|
|
3031
|
+
showOverlay("none");
|
|
3032
|
+
renderer.requestRender();
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
for (const scroller of [helpScroll, orgScroll, readmeScroll, updateScroll]) {
|
|
3036
|
+
scroller.onMouseScroll = (event) => {
|
|
3037
|
+
event.stopPropagation();
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
// Leader menu: the list activates like Enter, but the dim layer behind the
|
|
3042
|
+
// floating menu is the click-outside target (the menu box itself is nested
|
|
3043
|
+
// inside it and stops propagation).
|
|
3044
|
+
attachClickToSelect(renderer, leaderSelect, {
|
|
3045
|
+
onOpen: () => {
|
|
3046
|
+
const entry = leaderSelect.getSelectedOption()?.value as
|
|
3047
|
+
MenuEntry | undefined;
|
|
3048
|
+
if (!entry) return;
|
|
3049
|
+
if (entry.type === "category") {
|
|
3050
|
+
pushMenuLevel(` Menu > ${entry.name}`, entry.name, entry.children, 0);
|
|
3051
|
+
} else {
|
|
3052
|
+
resetMenu();
|
|
3053
|
+
showOverlay("none");
|
|
3054
|
+
entry.action();
|
|
3055
|
+
}
|
|
3056
|
+
renderer.requestRender();
|
|
3057
|
+
},
|
|
3058
|
+
onClickMiss: () => {
|
|
3059
|
+
showOverlay("none");
|
|
3060
|
+
renderer.requestRender();
|
|
3061
|
+
},
|
|
3062
|
+
});
|
|
3063
|
+
attachClickOutside(leaderDim, () => {
|
|
3064
|
+
showOverlay("none");
|
|
3065
|
+
renderer.requestRender();
|
|
3066
|
+
});
|
|
3067
|
+
|
|
2716
3068
|
// ── Global keyboard shortcuts ──────────────────────────────────────
|
|
2717
3069
|
renderer.keyInput.on("keypress", (key) => {
|
|
2718
3070
|
// ── Token prompt (own keyboard scope) ──────────────────────────
|
|
@@ -2825,21 +3177,7 @@ ${pack.description ?? ""}`;
|
|
|
2825
3177
|
return;
|
|
2826
3178
|
}
|
|
2827
3179
|
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
|
-
}
|
|
3180
|
+
activateOverlaySelection();
|
|
2843
3181
|
return;
|
|
2844
3182
|
}
|
|
2845
3183
|
return;
|
|
@@ -2861,9 +3199,7 @@ ${pack.description ?? ""}`;
|
|
|
2861
3199
|
return;
|
|
2862
3200
|
}
|
|
2863
3201
|
if (key.name === "enter" || key.name === "return") {
|
|
2864
|
-
|
|
2865
|
-
if (sel?.value)
|
|
2866
|
-
openUrl((sel.value as { repo?: { url?: string } }).repo?.url ?? "");
|
|
3202
|
+
activateOverlaySelection();
|
|
2867
3203
|
return;
|
|
2868
3204
|
}
|
|
2869
3205
|
return;
|
|
@@ -2882,23 +3218,7 @@ ${pack.description ?? ""}`;
|
|
|
2882
3218
|
return;
|
|
2883
3219
|
}
|
|
2884
3220
|
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
|
-
}
|
|
3221
|
+
activateOverlaySelection();
|
|
2902
3222
|
return;
|
|
2903
3223
|
}
|
|
2904
3224
|
return;
|
|
@@ -2907,14 +3227,7 @@ ${pack.description ?? ""}`;
|
|
|
2907
3227
|
// Topics explorer overlay
|
|
2908
3228
|
if (currentOverlay === "topics") {
|
|
2909
3229
|
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
|
-
}
|
|
3230
|
+
activateOverlaySelection();
|
|
2918
3231
|
return;
|
|
2919
3232
|
}
|
|
2920
3233
|
return;
|
|
@@ -2923,17 +3236,7 @@ ${pack.description ?? ""}`;
|
|
|
2923
3236
|
// Export overlay
|
|
2924
3237
|
if (currentOverlay === "export") {
|
|
2925
3238
|
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();
|
|
3239
|
+
activateOverlaySelection();
|
|
2937
3240
|
return;
|
|
2938
3241
|
}
|
|
2939
3242
|
return;
|
|
@@ -2968,19 +3271,7 @@ ${pack.description ?? ""}`;
|
|
|
2968
3271
|
// Share overlay
|
|
2969
3272
|
if (currentOverlay === "share") {
|
|
2970
3273
|
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();
|
|
3274
|
+
activateOverlaySelection();
|
|
2984
3275
|
return;
|
|
2985
3276
|
}
|
|
2986
3277
|
return;
|
|
@@ -2988,8 +3279,7 @@ ${pack.description ?? ""}`;
|
|
|
2988
3279
|
|
|
2989
3280
|
// Help overlay: any key closes
|
|
2990
3281
|
if (currentOverlay === "help") {
|
|
2991
|
-
|
|
2992
|
-
renderer.requestRender();
|
|
3282
|
+
activateOverlaySelection();
|
|
2993
3283
|
return;
|
|
2994
3284
|
}
|
|
2995
3285
|
|
|
@@ -3029,33 +3319,7 @@ ${pack.description ?? ""}`;
|
|
|
3029
3319
|
return;
|
|
3030
3320
|
}
|
|
3031
3321
|
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();
|
|
3322
|
+
runUpdateOption(updateSelectedOption);
|
|
3059
3323
|
return;
|
|
3060
3324
|
}
|
|
3061
3325
|
return;
|
|
@@ -3091,11 +3355,19 @@ ${pack.description ?? ""}`;
|
|
|
3091
3355
|
return;
|
|
3092
3356
|
}
|
|
3093
3357
|
|
|
3094
|
-
//
|
|
3095
|
-
//
|
|
3096
|
-
//
|
|
3097
|
-
if (key.name === "
|
|
3098
|
-
|
|
3358
|
+
// Esc toggles the leader menu — the only binding for it. Space used to open it
|
|
3359
|
+
// when the query input was not focused, which made the same key mean two
|
|
3360
|
+
// things depending on invisible state; it is now reserved for typing.
|
|
3361
|
+
if (key.name === "escape") {
|
|
3362
|
+
if (currentOverlay !== "none") {
|
|
3363
|
+
// Only reachable for the leader menu — every other overlay returned
|
|
3364
|
+
// above, and its own branch already closed it.
|
|
3365
|
+
showOverlay("none");
|
|
3366
|
+
setToolbar();
|
|
3367
|
+
} else {
|
|
3368
|
+
showLeaderMenu();
|
|
3369
|
+
}
|
|
3370
|
+
renderer.requestRender();
|
|
3099
3371
|
return;
|
|
3100
3372
|
}
|
|
3101
3373
|
|