github-search-cli 9.4.4 → 9.5.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 +102 -21
- package/README.md +43 -30
- package/dist/cli.js +431 -89
- package/package.json +1 -1
- package/release-notes/9.5.0.md +30 -0
- package/release-notes/9.5.1.md +36 -0
- package/src/cli.ts +57 -5
- package/src/help.ts +8 -0
- package/src/tui.ts +108 -28
- package/src/user.ts +356 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-search-cli",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.5.1",
|
|
4
4
|
"description": "Search GitHub from your terminal — repo search, trending repos, npm package search, bookmarks, deep-dive, compare, export. Keyboard-driven TUI + pipeable CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# What's New in ghfind 9.5.0
|
|
2
|
+
|
|
3
|
+
User profiles from the terminal, plus an org-lookup fix.
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
**`ghfind user <name>`** — the user-side counterpart to `ghfind org <name>`
|
|
8
|
+
|
|
9
|
+
- Fetches a GitHub user's metadata and public repos from the REST API and
|
|
10
|
+
aggregates them into one summary: public repo count, total stars/forks of
|
|
11
|
+
the fetched repos, followers/following, top languages by repo count,
|
|
12
|
+
top 5 repos by stars, and the 5 most recently pushed repos
|
|
13
|
+
- `ghfind user torvalds`, `ghfind user @torvalds` (leading `@` stripped)
|
|
14
|
+
- Same output flags as `org`: `--json`, `--csv`, `--markdown`, `--count`,
|
|
15
|
+
`--limit`, `--token`
|
|
16
|
+
- Organization / Bot accounts are labeled with an `[org]` / `[bot]` badge in
|
|
17
|
+
text output; site admins get `[staff]`
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `ghfind org <name>` joined multi-word names with dashes (`"in tech"` →
|
|
22
|
+
`in-tech`) — now joined with spaces, which the API handles. Single-token
|
|
23
|
+
org lookups are unaffected.
|
|
24
|
+
|
|
25
|
+
### Internal
|
|
26
|
+
|
|
27
|
+
- `src/user.ts` mirrors the org-profile module shape: pure normalize/aggregate
|
|
28
|
+
functions, thin formatters, graceful degradation when the repos request
|
|
29
|
+
fails (user metadata is still returned)
|
|
30
|
+
- 27 new tests in `tests/user.test.ts`
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# What's New in ghfind 9.5.1
|
|
2
|
+
|
|
3
|
+
Two TUI bugs in the landing menu and the update panel, plus test-isolation work.
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
**Landing-screen keys no longer leak into the main view**
|
|
8
|
+
|
|
9
|
+
- Pressing `/` on the landing menu focused the search input on top of it
|
|
10
|
+
- `Space`, `t`, `c`, `?`, and `q` all double-fired through the second key
|
|
11
|
+
listener — help opened and instantly closed, quit ran cleanup twice
|
|
12
|
+
- The landing handler now claims its keys with `stopPropagation()` via the
|
|
13
|
+
`landingKeysActive` / `landingConsumesKey` seam, and the global handler
|
|
14
|
+
ignores everything while the landing menu owns the screen
|
|
15
|
+
- Also fixed while routing keys: `b` on the landing screen opens bookmarks
|
|
16
|
+
(hint bar updated), and overlays opened from landing hide the menu instead
|
|
17
|
+
of drawing on top of it
|
|
18
|
+
|
|
19
|
+
**Update panel no longer clips the changelog**
|
|
20
|
+
|
|
21
|
+
- The modal was hard-capped at 16 rows with the release notes in a single
|
|
22
|
+
unsized text block, so anything past the first ~10 lines was invisible
|
|
23
|
+
- The notes area is now a scrollable region (same mechanics as the
|
|
24
|
+
README/org viewers): `↑↓`/`j`/`k` move a fifth of a page,
|
|
25
|
+
`PageUp`/`PageDown` and `Home`/`End` jump, and a scrollbar appears when
|
|
26
|
+
content overflows
|
|
27
|
+
- The panel is taller (24 rows), scroll position resets each time it opens,
|
|
28
|
+
and notes render at the panel's real width so wrapped lines don't clip
|
|
29
|
+
horizontally on narrow terminals
|
|
30
|
+
|
|
31
|
+
### Internal
|
|
32
|
+
|
|
33
|
+
- Seven test suites built temp dirs from a `Date.now()` stamp
|
|
34
|
+
(`tmpdir()/<name>-<ms>`), a name two concurrent `bun test` processes can
|
|
35
|
+
collide on. They now use `mkdtempSync`, and the login
|
|
36
|
+
`readGhCliToken` / `persistToken` dirs are cleaned up after use
|
package/src/cli.ts
CHANGED
|
@@ -36,6 +36,13 @@ import {
|
|
|
36
36
|
formatOrgMarkdown,
|
|
37
37
|
formatOrgText,
|
|
38
38
|
} from "./org";
|
|
39
|
+
import {
|
|
40
|
+
fetchUserProfile,
|
|
41
|
+
formatUserCsv,
|
|
42
|
+
formatUserJson,
|
|
43
|
+
formatUserMarkdown,
|
|
44
|
+
formatUserText,
|
|
45
|
+
} from "./user";
|
|
39
46
|
import { checkAndNotify, allCachedReleases } from "./releases";
|
|
40
47
|
import { runInitWizard } from "./init";
|
|
41
48
|
import { runLoginWizard } from "./login";
|
|
@@ -79,6 +86,7 @@ interface CLIFlags {
|
|
|
79
86
|
compare: string[]; // repo fullNames to compare
|
|
80
87
|
doctor: boolean;
|
|
81
88
|
org?: string; // org profile lookup (ghfind org <name>)
|
|
89
|
+
user?: string; // user profile lookup (ghfind user <name>)
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
function parseArgs(args: string[]): CLIFlags {
|
|
@@ -184,12 +192,22 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
184
192
|
flags.completion = args[++i];
|
|
185
193
|
break;
|
|
186
194
|
case "org": {
|
|
187
|
-
// Collect org name until next flag (
|
|
195
|
+
// Collect org name until next flag (single token — GitHub org logins
|
|
196
|
+
// contain no spaces; a quoted multi-word name is an error)
|
|
188
197
|
const parts: string[] = [];
|
|
189
198
|
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
190
199
|
parts.push(args[++i]);
|
|
191
200
|
}
|
|
192
|
-
flags.org = parts.join("
|
|
201
|
+
flags.org = parts.join(" ");
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "user": {
|
|
205
|
+
// Collect user name until next flag (single token, same as org)
|
|
206
|
+
const userParts: string[] = [];
|
|
207
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
208
|
+
userParts.push(args[++i]);
|
|
209
|
+
}
|
|
210
|
+
flags.user = userParts.join(" ");
|
|
193
211
|
break;
|
|
194
212
|
}
|
|
195
213
|
case "--compare": {
|
|
@@ -243,10 +261,11 @@ Usage:
|
|
|
243
261
|
ghfind <query> --pipe <target> Pipe to clone/open
|
|
244
262
|
ghfind --trending --json Trending repos as JSON
|
|
245
263
|
ghfind --releases Check bookmarks for new releases
|
|
246
|
-
ghfind --compare <
|
|
264
|
+
ghfind --compare <r1> <r2> Compare two+ repos side-by-side
|
|
247
265
|
ghfind org <name> --json Org profile: repos, stars, top languages
|
|
248
|
-
ghfind
|
|
249
|
-
ghfind pkg <query>
|
|
266
|
+
ghfind user <name> --json User profile: repos, stars, top languages
|
|
267
|
+
ghfind pkg <query> --json Search npm packages, output JSON
|
|
268
|
+
ghfind pkg <query> Search npm packages, text list
|
|
250
269
|
ghfind --watch <query> Watch mode (poll every Ns)
|
|
251
270
|
ghfind init Run setup wizard
|
|
252
271
|
ghfind login Import the gh CLI token or paste one
|
|
@@ -334,6 +353,12 @@ Options:
|
|
|
334
353
|
return;
|
|
335
354
|
}
|
|
336
355
|
|
|
356
|
+
// User profile mode — same shape as org profile
|
|
357
|
+
if (flags.user !== undefined) {
|
|
358
|
+
await runUserProfile(flags, outputFormat);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
337
362
|
const isNonInteractive =
|
|
338
363
|
outputFormat ||
|
|
339
364
|
flags.count ||
|
|
@@ -370,6 +395,33 @@ function buildSearchContext(flags: CLIFlags): SearchContext {
|
|
|
370
395
|
};
|
|
371
396
|
}
|
|
372
397
|
|
|
398
|
+
/** `ghfind user <name>` — aggregate user profile: metadata, stars, top languages, top repos. */
|
|
399
|
+
async function runUserProfile(
|
|
400
|
+
flags: CLIFlags,
|
|
401
|
+
outputFormat?: ExportFormat,
|
|
402
|
+
): Promise<void> {
|
|
403
|
+
try {
|
|
404
|
+
const profile = await fetchUserProfile(flags.user!, {
|
|
405
|
+
token: flags.token,
|
|
406
|
+
limit: flags.limit,
|
|
407
|
+
});
|
|
408
|
+
if (outputFormat === "json") {
|
|
409
|
+
console.log(formatUserJson(profile));
|
|
410
|
+
} else if (outputFormat === "csv") {
|
|
411
|
+
console.log(formatUserCsv(profile));
|
|
412
|
+
} else if (outputFormat === "markdown") {
|
|
413
|
+
console.log(formatUserMarkdown(profile));
|
|
414
|
+
} else if (flags.count) {
|
|
415
|
+
console.log(profile.publicRepoCount);
|
|
416
|
+
} else {
|
|
417
|
+
console.log(formatUserText(profile));
|
|
418
|
+
}
|
|
419
|
+
} catch (err) {
|
|
420
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
373
425
|
/** `ghfind org <name>` — aggregate org profile: metadata, stars, top languages, top repos. */
|
|
374
426
|
async function runOrgProfile(
|
|
375
427
|
flags: CLIFlags,
|
package/src/help.ts
CHANGED
|
@@ -82,6 +82,10 @@ export function buildHelpSections(): HelpSection[] {
|
|
|
82
82
|
{ keys: "Packages", action: "Switch to npm packages" },
|
|
83
83
|
{ keys: "Export", action: "Export results to file" },
|
|
84
84
|
{ keys: "Share", action: "Share repo as text" },
|
|
85
|
+
{
|
|
86
|
+
keys: "Org profile",
|
|
87
|
+
action: "Org summary: repos, stars, top languages",
|
|
88
|
+
},
|
|
85
89
|
{ keys: "Topics", action: "Browse GitHub topics" },
|
|
86
90
|
{ keys: "Saved", action: "View saved searches" },
|
|
87
91
|
{ keys: "Notifs", action: "Check GitHub notifications" },
|
|
@@ -110,6 +114,10 @@ export function buildHelpSections(): HelpSection[] {
|
|
|
110
114
|
{ keys: "d", action: "Delete entry (history, saved)" },
|
|
111
115
|
{ keys: "↑ ↓ / j k", action: "Navigate overlay list" },
|
|
112
116
|
{ keys: "← → / h l", action: "Navigate tabs / panels" },
|
|
117
|
+
{
|
|
118
|
+
keys: "PgUp/PgDn",
|
|
119
|
+
action: "Scroll update notes; Home/End jump to ends",
|
|
120
|
+
},
|
|
113
121
|
],
|
|
114
122
|
},
|
|
115
123
|
{
|
package/src/tui.ts
CHANGED
|
@@ -122,6 +122,11 @@ import {
|
|
|
122
122
|
recordPreUpdateState,
|
|
123
123
|
} from "./update-check";
|
|
124
124
|
import { debugLog } from "./storage";
|
|
125
|
+
import {
|
|
126
|
+
landingConsumesKey,
|
|
127
|
+
landingKeysActive,
|
|
128
|
+
moveSelection,
|
|
129
|
+
} from "./landing";
|
|
125
130
|
const colors: Record<string, string> = {
|
|
126
131
|
bg: "#3D3B3B",
|
|
127
132
|
surface: "#4a4848",
|
|
@@ -494,7 +499,8 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
494
499
|
}
|
|
495
500
|
|
|
496
501
|
const landingHint = new TextRenderable(renderer, {
|
|
497
|
-
content:
|
|
502
|
+
content:
|
|
503
|
+
" \u2191\u2193 Navigate \u21E9 Select [b]ookmarks [?]help [q]uit",
|
|
498
504
|
fg: colors.muted,
|
|
499
505
|
bg: colors.bg,
|
|
500
506
|
height: 1,
|
|
@@ -525,29 +531,42 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
525
531
|
}
|
|
526
532
|
}
|
|
527
533
|
|
|
528
|
-
// Landing keyboard handler
|
|
534
|
+
// Landing keyboard handler. Registered before the global handler below,
|
|
535
|
+
// and both listeners receive the same key event — so keys the landing
|
|
536
|
+
// screen consumes must be claimed with stopPropagation(), or the global
|
|
537
|
+
// handler re-reads them (e.g. "/" focusing the search input on top of the
|
|
538
|
+
// menu, or "?" opening help and instantly closing it again).
|
|
529
539
|
renderer.keyInput.on("keypress", (key) => {
|
|
530
|
-
if (currentMode
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
landingOptions
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
540
|
+
if (!landingKeysActive(currentMode, currentOverlay)) return;
|
|
541
|
+
if (key.name === "up" || key.name === "k") {
|
|
542
|
+
landingSelected = moveSelection(
|
|
543
|
+
landingSelected,
|
|
544
|
+
-1,
|
|
545
|
+
landingOptions.length,
|
|
546
|
+
);
|
|
547
|
+
updateLandingCards();
|
|
548
|
+
renderer.requestRender();
|
|
549
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
550
|
+
landingSelected = moveSelection(
|
|
551
|
+
landingSelected,
|
|
552
|
+
1,
|
|
553
|
+
landingOptions.length,
|
|
554
|
+
);
|
|
555
|
+
updateLandingCards();
|
|
556
|
+
renderer.requestRender();
|
|
557
|
+
} else if (key.name === "enter" || key.name === "return") {
|
|
558
|
+
landingOptions[landingSelected].action();
|
|
559
|
+
} else if (key.name === "b") {
|
|
560
|
+
refreshBookmarks();
|
|
561
|
+
showOverlay("bookmarks");
|
|
562
|
+
} else if (key.name === "?" || key.name === "h") {
|
|
563
|
+
showOverlay(currentOverlay === "help" ? "none" : "help");
|
|
564
|
+
} else if (key.name === "q") {
|
|
565
|
+
cleanup();
|
|
566
|
+
} else {
|
|
567
|
+
return; // not a landing key — let the global handler see it
|
|
550
568
|
}
|
|
569
|
+
if (landingConsumesKey(key.name)) key.stopPropagation();
|
|
551
570
|
});
|
|
552
571
|
|
|
553
572
|
// Hide main content so overlays can take 100% of the content area
|
|
@@ -598,6 +617,9 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
598
617
|
return;
|
|
599
618
|
}
|
|
600
619
|
currentOverlay = type;
|
|
620
|
+
// An overlay opened from the landing screen must hide the menu too
|
|
621
|
+
// (hideMainContent only covers the search/trending/toolbar boxes).
|
|
622
|
+
landingBox.visible = false;
|
|
601
623
|
// Leader menu is a floating overlay — main content stays visible behind it
|
|
602
624
|
if (type !== "leader") {
|
|
603
625
|
hideMainContent();
|
|
@@ -630,6 +652,8 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
630
652
|
} else if (type === "update") {
|
|
631
653
|
updateDim.visible = true;
|
|
632
654
|
updateBox.visible = true;
|
|
655
|
+
// Fresh notes start at the top of the changelog
|
|
656
|
+
updateScroll.scrollTop = 0;
|
|
633
657
|
// Selection is driven by updateSelectedOption + the global key handler
|
|
634
658
|
// (see the "Update panel" block); there is no focusable Select here.
|
|
635
659
|
}
|
|
@@ -1049,9 +1073,9 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1049
1073
|
visible: false,
|
|
1050
1074
|
position: "absolute",
|
|
1051
1075
|
width: "55%",
|
|
1052
|
-
height:
|
|
1076
|
+
height: 24,
|
|
1053
1077
|
left: "22.5%",
|
|
1054
|
-
top: "
|
|
1078
|
+
top: "20%",
|
|
1055
1079
|
backgroundColor: colors.surfaceDim,
|
|
1056
1080
|
border: true,
|
|
1057
1081
|
borderStyle: "rounded",
|
|
@@ -1069,6 +1093,25 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1069
1093
|
fg: colors.accent,
|
|
1070
1094
|
bg: colors.surfaceDim,
|
|
1071
1095
|
});
|
|
1096
|
+
// ponytail: notes render as plain text with \n; keep it a column so lines stack
|
|
1097
|
+
const updateScroll = new ScrollBoxRenderable(renderer, {
|
|
1098
|
+
flexGrow: 1,
|
|
1099
|
+
backgroundColor: colors.surfaceDim,
|
|
1100
|
+
scrollY: true,
|
|
1101
|
+
scrollX: false,
|
|
1102
|
+
viewportOptions: { backgroundColor: colors.surfaceDim },
|
|
1103
|
+
contentOptions: {
|
|
1104
|
+
backgroundColor: colors.surfaceDim,
|
|
1105
|
+
flexDirection: "column",
|
|
1106
|
+
},
|
|
1107
|
+
scrollbarOptions: {
|
|
1108
|
+
width: 1,
|
|
1109
|
+
trackOptions: {
|
|
1110
|
+
backgroundColor: colors.surfaceDim,
|
|
1111
|
+
foregroundColor: colors.muted,
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
});
|
|
1072
1115
|
|
|
1073
1116
|
// Horizontal option boxes
|
|
1074
1117
|
let updateSelectedOption = 0;
|
|
@@ -1146,13 +1189,14 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1146
1189
|
updateOptionsRow.add(laterBox);
|
|
1147
1190
|
|
|
1148
1191
|
const updateFooter = new TextRenderable(renderer, {
|
|
1149
|
-
content: " ←→ select Enter Esc/q close",
|
|
1192
|
+
content: " ↑↓/jk/PgUp/PgDn scroll ←→ select Enter Esc/q close",
|
|
1150
1193
|
fg: colors.muted,
|
|
1151
1194
|
bg: colors.surfaceDim,
|
|
1152
1195
|
height: 1,
|
|
1153
1196
|
paddingX: 1,
|
|
1154
1197
|
});
|
|
1155
|
-
updateBox.add(
|
|
1198
|
+
updateBox.add(updateScroll);
|
|
1199
|
+
updateScroll.add(updateText);
|
|
1156
1200
|
updateBox.add(updateOptionsRow);
|
|
1157
1201
|
updateBox.add(updateFooter);
|
|
1158
1202
|
root.add(updateDim);
|
|
@@ -1558,8 +1602,14 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1558
1602
|
if (state.lastInstalledVersion === currentVersion) return;
|
|
1559
1603
|
// We just upgraded — show notes for the new version
|
|
1560
1604
|
const notes = fetchReleaseNotes(currentVersion);
|
|
1605
|
+
// Box is 55% of the terminal; subtract border + padding so wrapped
|
|
1606
|
+
// lines never clip horizontally.
|
|
1607
|
+
const notesWidth = Math.max(
|
|
1608
|
+
30,
|
|
1609
|
+
Math.floor((process.stdout.columns || 80) * 0.55) - 8,
|
|
1610
|
+
);
|
|
1561
1611
|
const notesBlock = notes
|
|
1562
|
-
? renderMarkdown(notes, { width:
|
|
1612
|
+
? renderMarkdown(notes, { width: notesWidth })
|
|
1563
1613
|
: " No release notes found for this version.";
|
|
1564
1614
|
updateSelectedOption = 0;
|
|
1565
1615
|
updateText.content = ` Updated ghfind ${state.lastInstalledVersion} → ${currentVersion}\n\n${notesBlock}`;
|
|
@@ -1580,8 +1630,12 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1580
1630
|
if (latest) {
|
|
1581
1631
|
updateSelectedOption = 0;
|
|
1582
1632
|
const notes = fetchReleaseNotes(latest);
|
|
1633
|
+
const notesWidth = Math.max(
|
|
1634
|
+
30,
|
|
1635
|
+
Math.floor((process.stdout.columns || 80) * 0.55) - 8,
|
|
1636
|
+
);
|
|
1583
1637
|
const notesBlock = notes
|
|
1584
|
-
? renderMarkdown(notes, { width:
|
|
1638
|
+
? renderMarkdown(notes, { width: notesWidth })
|
|
1585
1639
|
: ` A new version is available\n\n Current: ${currentVersion}\n Latest: ${latest}\n\n Run "npm install -g ghfind"`;
|
|
1586
1640
|
updateText.content = ` A new version is available\n\n Current: ${currentVersion}\n Latest: ${latest}\n\n${notesBlock}`;
|
|
1587
1641
|
renderUpdateOptions();
|
|
@@ -2755,6 +2809,24 @@ ${pack.description ?? ""}`;
|
|
|
2755
2809
|
renderer.requestRender();
|
|
2756
2810
|
return;
|
|
2757
2811
|
}
|
|
2812
|
+
// Notes area is scrollable — ↑↓/jk move a fifth of a page,
|
|
2813
|
+
// PageUp/PageDown/Home/End jump, all via the ScrollBox's native
|
|
2814
|
+
// handler (same semantics as the readme/org viewers); ←/→ still
|
|
2815
|
+
// drive the button row, not the scrollbar.
|
|
2816
|
+
if (
|
|
2817
|
+
key.name === "pageup" ||
|
|
2818
|
+
key.name === "pagedown" ||
|
|
2819
|
+
key.name === "home" ||
|
|
2820
|
+
key.name === "end" ||
|
|
2821
|
+
key.name === "up" ||
|
|
2822
|
+
key.name === "down" ||
|
|
2823
|
+
key.name === "j" ||
|
|
2824
|
+
key.name === "k"
|
|
2825
|
+
) {
|
|
2826
|
+
updateScroll.handleKeyPress(key);
|
|
2827
|
+
renderer.requestRender();
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2758
2830
|
if (key.name === "left" || key.name === "h") {
|
|
2759
2831
|
updateSelectedOption = Math.max(0, updateSelectedOption - 1);
|
|
2760
2832
|
renderUpdateOptions();
|
|
@@ -2803,6 +2875,14 @@ ${pack.description ?? ""}`;
|
|
|
2803
2875
|
|
|
2804
2876
|
// ── Main view handling (no overlay active) ────────────────────────
|
|
2805
2877
|
|
|
2878
|
+
// The landing screen owns the keyboard: its handler above already
|
|
2879
|
+
// consumed its keys, and everything else must be ignored here so hidden
|
|
2880
|
+
// main-view bindings (search focus, leader menu, theme, compare, quit)
|
|
2881
|
+
// can't fire through the landing menu.
|
|
2882
|
+
if (currentMode === "landing") {
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2806
2886
|
// q quits
|
|
2807
2887
|
if (key.name === "q") {
|
|
2808
2888
|
if (graphFullscreen) {
|