github-search-cli 0.7.0 → 0.8.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/README.md +163 -183
- package/bin/ghfind.js +55 -0
- package/completions/{search-cli.bash → ghfind.bash} +4 -4
- package/completions/ghfind.fish +19 -0
- package/completions/{search-cli.zsh → ghfind.zsh} +4 -4
- package/demo.svg +65 -65
- package/package.json +20 -9
- package/src/bookmarks.ts +2 -2
- package/src/cli.ts +32 -45
- package/src/compare.ts +1 -1
- package/src/config.ts +8 -8
- package/src/deepdive.ts +4 -10
- package/src/errors.ts +1 -4
- package/src/explore.ts +0 -1
- package/src/export.ts +2 -3
- package/src/help.ts +112 -120
- package/src/history.ts +2 -2
- package/src/init.ts +6 -6
- package/src/notifications.ts +2 -6
- package/src/open-url.ts +16 -17
- package/src/output.ts +2 -9
- package/src/pipe.ts +2 -8
- package/src/saved-searches.ts +2 -2
- package/src/search.ts +555 -0
- package/src/session.ts +4 -5
- package/src/share.ts +27 -22
- package/src/trending.ts +10 -15
- package/src/tui.ts +408 -261
- package/src/types.ts +3 -11
- package/src/watch.ts +3 -6
- package/vendor/bun.exe +0 -0
- package/bin/search-cli.js +0 -17
- package/completions/search-cli.fish +0 -19
- package/src/cache.ts +0 -64
- package/src/normalizer.ts +0 -66
- package/src/provider.ts +0 -165
- package/src/query.ts +0 -249
- package/src/ranking.ts +0 -57
package/src/tui.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* ghfind — interactive GitHub repository browser (OpenTUI)
|
|
4
4
|
*
|
|
5
5
|
* Full-screen, keyboard-driven browser. This is the only entry point.
|
|
6
6
|
* Pipeline: user query → provider → normalizer → ranking → rendered in TUI
|
|
7
7
|
*
|
|
8
8
|
* Layout:
|
|
9
|
-
* ┌─
|
|
9
|
+
* ┌─ ghfind [s]sort [l]limit [r]refresh [q]uit ────────────────┐
|
|
10
10
|
* │ > [query input ........................................] │
|
|
11
11
|
* │ sort: best-match limit: 50 │
|
|
12
12
|
* ├─── Results ────────────────┬─── Details ──────────────────┤
|
|
@@ -31,28 +31,26 @@ import {
|
|
|
31
31
|
SelectRenderable,
|
|
32
32
|
ScrollBoxRenderable,
|
|
33
33
|
} from "@opentui/core";
|
|
34
|
-
import type { Repo, SearchOptions, SortStrategy } from "./types
|
|
35
|
-
import { parseQuery, applyFlagFilters, validateQuery, suggestFor } from "./
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
import { nextTip } from "./tips.ts";
|
|
55
|
-
import { openUrl } from "./open-url.ts";
|
|
34
|
+
import type { Repo, SearchOptions, SortStrategy } from "./types";
|
|
35
|
+
import { parseQuery, applyFlagFilters, validateQuery, suggestFor, rankRepos, createGitHubSearch, type Logger } from "./search";
|
|
36
|
+
import { fetchTrendingRepos, tabSince, TAB_NAMES, fmtStars, type TrendingRepo } from "./trending";
|
|
37
|
+
import { loadConfig } from "./config";
|
|
38
|
+
import { buildHelpSections, HELP_KEYS_COLUMN } from "./help";
|
|
39
|
+
import { SearchCliError, NetworkError, RateLimitError, BadQueryError, NoResultsError } from "./errors";
|
|
40
|
+
import { appendHistory, readHistory, deleteHistoryEntry, clearHistory, rotateHistory } from "./history";
|
|
41
|
+
import { getBookmarks, isBookmarked, toggleBookmark, removeBookmark } from "./bookmarks";
|
|
42
|
+
import { getSavedSearches, saveSearch, deleteSavedSearch, touchSavedSearch } from "./saved-searches";
|
|
43
|
+
import { saveSession, restoreSession } from "./session";
|
|
44
|
+
import { fetchDeepDive, buildDeepDiveText } from "./deepdive";
|
|
45
|
+
import { buildComparisonTable } from "./compare";
|
|
46
|
+
import { fetchTopics, type TopicItem } from "./explore";
|
|
47
|
+
import { exportToFile, type ExportFormat } from "./export";
|
|
48
|
+
import { loadTheme } from "./themes";
|
|
49
|
+
import { StatusManager } from "./status";
|
|
50
|
+
import { addNotification, getNotifications, dismissNotification, dismissAll } from "./notifications";
|
|
51
|
+
import { formatShare, copyToClipboard, type ShareFormat } from "./share";
|
|
52
|
+
import { nextTip } from "./tips";
|
|
53
|
+
import { openUrl } from "./open-url";
|
|
56
54
|
|
|
57
55
|
// ─── Theme ────────────────────────────────────────────────────────────
|
|
58
56
|
const colors = {
|
|
@@ -127,18 +125,19 @@ export async function launchBrowser(): Promise<void> {
|
|
|
127
125
|
let trendingPeriod = "this week";
|
|
128
126
|
let graphFullscreen = false;
|
|
129
127
|
let chartCommitData: number[] = [];
|
|
130
|
-
let currentOverlay: "none" | "history" | "bookmarks" | "saved" | "help" | "topics" | "export" | "compare" | "notifications" | "share" = "none";
|
|
128
|
+
let currentOverlay: "none" | "history" | "bookmarks" | "saved" | "help" | "topics" | "export" | "compare" | "notifications" | "share" | "leader" | "readme" = "none";
|
|
131
129
|
let currentPage = 1;
|
|
132
130
|
let totalCount = 0;
|
|
133
131
|
let deepDiveActive = false;
|
|
134
132
|
let compareList: Repo[] = [];
|
|
135
133
|
let quitArmed = false;
|
|
136
134
|
|
|
135
|
+
|
|
137
136
|
// ── Header bar ──────────────────────────────────────────────────────
|
|
138
137
|
const header = new TextRenderable(renderer, {
|
|
139
138
|
content:
|
|
140
|
-
"
|
|
141
|
-
backgroundColor: colors.
|
|
139
|
+
" ghfind — GitHub repo browser [/]search [Space]menu [?]help [q]uit",
|
|
140
|
+
backgroundColor: colors.bg,
|
|
142
141
|
color: colors.muted,
|
|
143
142
|
height: 1,
|
|
144
143
|
});
|
|
@@ -266,6 +265,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
266
265
|
body.add(detailBox);
|
|
267
266
|
root.add(body);
|
|
268
267
|
|
|
268
|
+
|
|
269
|
+
|
|
269
270
|
// ── Overlay system ──────────────────────────────────────────────────
|
|
270
271
|
|
|
271
272
|
// Hide main content so overlays can take 100% of the content area
|
|
@@ -296,6 +297,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
296
297
|
compareBox.visible = false;
|
|
297
298
|
notifsBox.visible = false;
|
|
298
299
|
shareBox.visible = false;
|
|
300
|
+
leaderBox.visible = false;
|
|
301
|
+
readmeBox.visible = false;
|
|
299
302
|
|
|
300
303
|
if (type === "none") {
|
|
301
304
|
currentOverlay = "none";
|
|
@@ -314,6 +317,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
314
317
|
else if (type === "compare") { compareBox.visible = true; }
|
|
315
318
|
else if (type === "notifications") { notifsBox.visible = true; }
|
|
316
319
|
else if (type === "share") { shareBox.visible = true; }
|
|
320
|
+
else if (type === "leader") { leaderBox.visible = true; }
|
|
321
|
+
else if (type === "readme") { readmeBox.visible = true; }
|
|
317
322
|
renderer.requestRender();
|
|
318
323
|
}
|
|
319
324
|
|
|
@@ -635,10 +640,256 @@ export async function launchBrowser(): Promise<void> {
|
|
|
635
640
|
savedSelect.setSelectedIndex(0);
|
|
636
641
|
}
|
|
637
642
|
|
|
643
|
+
// ── Leader menu overlay (Space key) ─────────────────────────────
|
|
644
|
+
const leaderBox = new BoxRenderable(renderer, {
|
|
645
|
+
visible: false,
|
|
646
|
+
flexGrow: 1,
|
|
647
|
+
backgroundColor: colors.bg,
|
|
648
|
+
border: true,
|
|
649
|
+
borderColor: colors.border,
|
|
650
|
+
title: " Menu ",
|
|
651
|
+
titleColor: colors.blue,
|
|
652
|
+
flexDirection: "column",
|
|
653
|
+
});
|
|
654
|
+
const leaderSelect = new SelectRenderable(renderer, {
|
|
655
|
+
options: [{ name: "(empty)", description: "", value: null }],
|
|
656
|
+
showDescription: true,
|
|
657
|
+
showSelectionIndicator: true,
|
|
658
|
+
flexGrow: 1,
|
|
659
|
+
backgroundColor: colors.bg,
|
|
660
|
+
textColor: colors.text,
|
|
661
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
662
|
+
selectedTextColor: colors.selectionText,
|
|
663
|
+
itemSpacing: 0,
|
|
664
|
+
});
|
|
665
|
+
const leaderFooter = new TextRenderable(renderer, {
|
|
666
|
+
content: " ↑↓ select Enter run Esc/q close",
|
|
667
|
+
color: colors.muted,
|
|
668
|
+
backgroundColor: colors.bg,
|
|
669
|
+
height: 1,
|
|
670
|
+
paddingX: 1,
|
|
671
|
+
});
|
|
672
|
+
leaderBox.add(leaderSelect);
|
|
673
|
+
leaderBox.add(leaderFooter);
|
|
674
|
+
root.add(leaderBox);
|
|
675
|
+
|
|
676
|
+
interface LeaderItem { name: string; description: string; action: () => void; }
|
|
677
|
+
|
|
678
|
+
function showLeaderMenu() {
|
|
679
|
+
const items: LeaderItem[] = [];
|
|
680
|
+
|
|
681
|
+
if (currentMode === "search") {
|
|
682
|
+
items.push(
|
|
683
|
+
{ name: " Sort", description: `Cycle sort (current: ${currentSort})`, action: () => changeSort() },
|
|
684
|
+
{ name: " Limit", description: `Cycle result limit (current: ${currentLimit})`, action: () => changeLimit() },
|
|
685
|
+
{ name: " Refresh", description: "Re-run current search", action: () => { if (currentQueryInput) doSearch(currentQueryInput); } },
|
|
686
|
+
{ name: " Toggle trending", description: "Browse GitHub trending repos", action: () => loadTrending() },
|
|
687
|
+
{ name: " Deep-dive", description: "Languages, contributors, README excerpt", action: () => { showOverlay("none"); triggerDeepDive(); } },
|
|
688
|
+
{ name: " Readme", description: "Full README viewer", action: () => { showReadme(); } },
|
|
689
|
+
{ name: " Activity graph", description: "Toggle commit chart fullscreen", action: () => toggleGraph() },
|
|
690
|
+
{ name: " Bookmark", description: "Save / unsave selected repo", action: () => toggleBookmarkOnSelected() },
|
|
691
|
+
{ name: " Compare", description: "Add/remove repo to comparison", action: () => toggleCompareOnSelected() },
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (currentMode === "trending") {
|
|
696
|
+
items.push(
|
|
697
|
+
{ name: " Refresh", description: "Reload trending repos", action: () => loadTrending() },
|
|
698
|
+
{ name: " Search mode", description: "Switch to query search", action: () => showSearchMode() },
|
|
699
|
+
{ name: " Readme", description: "Full README viewer", action: () => { showReadme(); } },
|
|
700
|
+
{ name: " Bookmark", description: "Save / unsave selected repo", action: () => toggleBookmarkOnSelected() },
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Common items (always available when repos exist)
|
|
705
|
+
const hasRepo = (() => {
|
|
706
|
+
const opt = resultsSelect.getSelectedOption();
|
|
707
|
+
return opt?.value && typeof (opt.value as any)?.fullName === "string";
|
|
708
|
+
})();
|
|
709
|
+
|
|
710
|
+
items.push(
|
|
711
|
+
{ name: " Open in browser", description: "Open selected repo in browser", action: () => openUrlIfSelected() },
|
|
712
|
+
{ name: " History", description: "Search history", action: () => { refreshHistory(); showOverlay("history"); } },
|
|
713
|
+
{ name: " Saved searches", description: "Load a saved search", action: () => { refreshSavedSearches(); showOverlay("saved"); } },
|
|
714
|
+
{ name: " Export", description: "Export results to JSON/CSV/Markdown", action: () => showOverlay("export") },
|
|
715
|
+
{ name: " Share repo", description: "Copy repo link to clipboard", action: () => showShareIfRepo() },
|
|
716
|
+
{ name: " Notifications", description: "View alerts", action: () => { refreshNotifications(); showOverlay("notifications"); } },
|
|
717
|
+
{ name: " Topics", description: "Browse popular GitHub topics", action: () => { refreshTopics(); showOverlay("topics"); } },
|
|
718
|
+
{ name: " Compare view", description: "Show side-by-side comparison", action: () => { refreshCompare(); showOverlay("compare"); } },
|
|
719
|
+
{ name: " Bookmarks panel", description: "Browse saved repos", action: () => { refreshBookmarks(); showOverlay("bookmarks"); } },
|
|
720
|
+
{ name: " Save search", description: "Save current query", action: () => saveCurrentSearch() },
|
|
721
|
+
{ name: " Help", description: "Keybindings reference", action: () => showOverlay("help") },
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
leaderSelect.options = items.map((it) => ({
|
|
725
|
+
name: it.name,
|
|
726
|
+
description: it.description,
|
|
727
|
+
value: it,
|
|
728
|
+
}));
|
|
729
|
+
leaderSelect.setSelectedIndex(0);
|
|
730
|
+
leaderSelect.focus();
|
|
731
|
+
showOverlay("leader");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function openUrlIfSelected() {
|
|
735
|
+
const opt = resultsSelect.getSelectedOption();
|
|
736
|
+
const repo = opt?.value as Repo | undefined;
|
|
737
|
+
if (repo) openUrl(repo.url);
|
|
738
|
+
else setStatus("No repo selected");
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function toggleBookmarkOnSelected() {
|
|
742
|
+
const opt = resultsSelect.getSelectedOption();
|
|
743
|
+
const repo = opt?.value as Repo | undefined;
|
|
744
|
+
if (repo) {
|
|
745
|
+
const added = toggleBookmark(repo);
|
|
746
|
+
setStatus(added ? `Bookmarked ${repo.fullName}` : `Unbookmarked ${repo.fullName}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function toggleCompareOnSelected() {
|
|
751
|
+
const opt = resultsSelect.getSelectedOption();
|
|
752
|
+
const repo = opt?.value as Repo | undefined;
|
|
753
|
+
if (!repo) return;
|
|
754
|
+
const idx = compareList.findIndex((r) => r.fullName === repo.fullName);
|
|
755
|
+
if (idx >= 0) {
|
|
756
|
+
compareList.splice(idx, 1);
|
|
757
|
+
setStatus(`Removed ${repo.fullName} from comparison`);
|
|
758
|
+
} else {
|
|
759
|
+
compareList.push(repo);
|
|
760
|
+
setStatus(`${repo.fullName} added to comparison (${compareList.length} selected)`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function showShareIfRepo() {
|
|
765
|
+
const opt = resultsSelect.getSelectedOption();
|
|
766
|
+
const repo = opt?.value as Repo | undefined;
|
|
767
|
+
if (!repo) { setStatus("No repo selected to share"); return; }
|
|
768
|
+
showOverlay("share");
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function saveCurrentSearch() {
|
|
772
|
+
if (currentQueryInput) {
|
|
773
|
+
const name = currentQueryInput.length > 40
|
|
774
|
+
? currentQueryInput.slice(0, 37) + "..."
|
|
775
|
+
: currentQueryInput;
|
|
776
|
+
saveSearch(name, currentQueryInput, currentMode, currentSort, currentLimit, currentMode === "trending" ? trendingTab : undefined);
|
|
777
|
+
setStatus(`Saved as "${name}"`);
|
|
778
|
+
} else {
|
|
779
|
+
setStatus("No search to save");
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function triggerDeepDive() {
|
|
784
|
+
const opt = resultsSelect.getSelectedOption();
|
|
785
|
+
const repo = opt?.value as Repo | undefined;
|
|
786
|
+
if (!repo) return;
|
|
787
|
+
deepDiveActive = !deepDiveActive;
|
|
788
|
+
if (deepDiveActive) {
|
|
789
|
+
detailText.content = " Loading deep-dive...";
|
|
790
|
+
renderer.requestRender();
|
|
791
|
+
fetchDeepDive(repo, githubToken)
|
|
792
|
+
.then((data) => {
|
|
793
|
+
detailText.content = buildDeepDiveText(data);
|
|
794
|
+
renderer.requestRender();
|
|
795
|
+
})
|
|
796
|
+
.catch(() => {
|
|
797
|
+
detailText.content = " Failed to load deep-dive";
|
|
798
|
+
renderer.requestRender();
|
|
799
|
+
});
|
|
800
|
+
} else {
|
|
801
|
+
updateDetail(repo);
|
|
802
|
+
renderer.requestRender();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// ── README viewer overlay ────────────────────────────────────
|
|
807
|
+
const readmeBox = new BoxRenderable(renderer, {
|
|
808
|
+
visible: false,
|
|
809
|
+
flexGrow: 1,
|
|
810
|
+
backgroundColor: colors.bg,
|
|
811
|
+
border: true,
|
|
812
|
+
borderColor: colors.green,
|
|
813
|
+
title: " README ",
|
|
814
|
+
titleColor: colors.green,
|
|
815
|
+
});
|
|
816
|
+
const readmeScroll = new ScrollBoxRenderable(renderer, {
|
|
817
|
+
flexGrow: 1,
|
|
818
|
+
backgroundColor: colors.bg,
|
|
819
|
+
scrollY: true,
|
|
820
|
+
scrollX: false,
|
|
821
|
+
paddingX: 1,
|
|
822
|
+
viewportOptions: { backgroundColor: colors.bg },
|
|
823
|
+
contentOptions: { backgroundColor: colors.bg, flexDirection: "column" },
|
|
824
|
+
scrollbarOptions: {
|
|
825
|
+
backgroundColor: colors.bg,
|
|
826
|
+
foregroundColor: colors.muted,
|
|
827
|
+
width: 1,
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
const readmeText = new TextRenderable(renderer, {
|
|
831
|
+
content: "",
|
|
832
|
+
color: colors.text,
|
|
833
|
+
backgroundColor: colors.bg,
|
|
834
|
+
});
|
|
835
|
+
readmeScroll.add(readmeText);
|
|
836
|
+
readmeBox.add(readmeScroll);
|
|
837
|
+
const readmeFooter = new TextRenderable(renderer, {
|
|
838
|
+
content: " ↑↓/jk scroll Esc/q close",
|
|
839
|
+
color: colors.muted,
|
|
840
|
+
backgroundColor: colors.bg,
|
|
841
|
+
height: 1,
|
|
842
|
+
paddingX: 1,
|
|
843
|
+
});
|
|
844
|
+
readmeBox.add(readmeFooter);
|
|
845
|
+
root.add(readmeBox);
|
|
846
|
+
|
|
847
|
+
async function showReadme() {
|
|
848
|
+
const opt = resultsSelect.getSelectedOption();
|
|
849
|
+
const repo = opt?.value as Repo | undefined;
|
|
850
|
+
if (!repo) {
|
|
851
|
+
setStatus("No repo selected");
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
readmeText.content = ` Loading README for ${repo.fullName}...`;
|
|
855
|
+
showOverlay("readme");
|
|
856
|
+
try {
|
|
857
|
+
const headers: Record<string, string> = { "User-Agent": "ghfind/1.0" };
|
|
858
|
+
if (githubToken) headers.Authorization = `Bearer ${githubToken}`;
|
|
859
|
+
let text = "";
|
|
860
|
+
for (const path of [
|
|
861
|
+
`${repo.owner}/${repo.name}/main/README.md`,
|
|
862
|
+
`${repo.owner}/${repo.name}/master/README.md`,
|
|
863
|
+
`${repo.owner}/${repo.name}/main/README.rst`,
|
|
864
|
+
]) {
|
|
865
|
+
const r = await fetch(`https://raw.githubusercontent.com/${path}`, { headers });
|
|
866
|
+
if (r.ok) { text = await r.text(); break; }
|
|
867
|
+
}
|
|
868
|
+
if (!text) {
|
|
869
|
+
readmeText.content = ` (no README found for ${repo.fullName})`;
|
|
870
|
+
} else {
|
|
871
|
+
// ponytail: just strip markdown formatting, show full text
|
|
872
|
+
const stripped = text
|
|
873
|
+
.replace(/```[\s\S]*?```/g, "[code block]")
|
|
874
|
+
.replace(/#{1,6}\s/g, "")
|
|
875
|
+
.replace(/\*\*(.+?)\*\*/g, "$1")
|
|
876
|
+
.replace(/\[(.+?)\]\(.+?\)/g, "$1")
|
|
877
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
878
|
+
.split("\n")
|
|
879
|
+
.map((l) => l.trimEnd())
|
|
880
|
+
.join("\n");
|
|
881
|
+
readmeText.content = stripped;
|
|
882
|
+
}
|
|
883
|
+
} catch {
|
|
884
|
+
readmeText.content = " Failed to load README";
|
|
885
|
+
}
|
|
886
|
+
renderer.requestRender();
|
|
887
|
+
}
|
|
888
|
+
|
|
638
889
|
// ── Status bar ──────────────────────────────────────────────────────
|
|
639
890
|
const statusBar = new TextRenderable(renderer, {
|
|
640
891
|
content: " Ready. Press Enter to search.",
|
|
641
|
-
backgroundColor: colors.
|
|
892
|
+
backgroundColor: colors.bg,
|
|
642
893
|
color: colors.muted,
|
|
643
894
|
height: 1,
|
|
644
895
|
paddingX: 1,
|
|
@@ -698,50 +949,77 @@ export async function launchBrowser(): Promise<void> {
|
|
|
698
949
|
// ── Graph / Chart ──────────────────────────────────────────────────
|
|
699
950
|
|
|
700
951
|
/** Unicode blocks for 8 vertical levels within one character cell. */
|
|
701
|
-
|
|
952
|
+
// Braille dot encoding: each byte is 8 dots (2 cols × 4 rows per char).
|
|
953
|
+
// Bit layout (LSB first): 0-2=col0 rows, 3-5=col1 rows, 6=col0 row3, 7=col1 row3.
|
|
954
|
+
// Unicode offset is 0x2800, so braille code point = 0x2800 + bitmask.
|
|
955
|
+
function brailleChar(bits: number): string {
|
|
956
|
+
return bits === 0 ? " " : String.fromCodePoint(0x2800 + bits);
|
|
957
|
+
}
|
|
702
958
|
|
|
703
|
-
function buildChartString(values: number[],
|
|
959
|
+
function buildChartString(values: number[], termW: number, termH: number): string[] {
|
|
704
960
|
if (values.length === 0) return ["(no data)"];
|
|
705
|
-
|
|
961
|
+
|
|
962
|
+
// Braille resolution: 2 horizontal dots and 4 vertical dots per terminal cell.
|
|
963
|
+
const dotW = termW * 2;
|
|
964
|
+
const dotH = termH * 4;
|
|
965
|
+
|
|
966
|
+
// Sample values to fit braille dot columns.
|
|
706
967
|
const sampled: number[] = [];
|
|
707
|
-
for (let i = 0; i <
|
|
708
|
-
sampled.push(values[Math.floor((i /
|
|
968
|
+
for (let i = 0; i < dotW; i++) {
|
|
969
|
+
sampled.push(values[Math.floor((i / dotW) * values.length)]);
|
|
709
970
|
}
|
|
710
971
|
const max = Math.max(...sampled, 1);
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
if (full < height && part > 0) grid[height - 1 - full][c] = BLOCKS[part];
|
|
972
|
+
// Map each sample to a dot row (0 = bottom, dotH-1 = top).
|
|
973
|
+
const dotRows = sampled.map(v => Math.round((v / max) * (dotH - 1)));
|
|
974
|
+
|
|
975
|
+
// Build a mask grid: dotGrid[row][col] = true if the silhouette passes through.
|
|
976
|
+
const dotGrid: boolean[][] = Array.from({ length: dotH }, () => Array(dotW).fill(false));
|
|
977
|
+
|
|
978
|
+
// Draw the silhouette line: mark the dot at each column.
|
|
979
|
+
for (let c = 0; c < dotW; c++) {
|
|
980
|
+
dotGrid[dotH - 1 - dotRows[c]][c] = true;
|
|
721
981
|
}
|
|
722
|
-
// Fill
|
|
723
|
-
for (let c = 0; c <
|
|
724
|
-
const r1 =
|
|
725
|
-
const r2 =
|
|
726
|
-
const lo = Math.
|
|
727
|
-
const hi = Math.
|
|
982
|
+
// Fill gaps between adjacent columns so the line is continuous.
|
|
983
|
+
for (let c = 0; c < dotW - 1; c++) {
|
|
984
|
+
const r1 = dotRows[c];
|
|
985
|
+
const r2 = dotRows[c + 1];
|
|
986
|
+
const lo = Math.min(r1, r2);
|
|
987
|
+
const hi = Math.max(r1, r2);
|
|
728
988
|
for (let r = lo; r <= hi; r++) {
|
|
729
|
-
|
|
989
|
+
dotGrid[dotH - 1 - r][c] = true;
|
|
730
990
|
}
|
|
731
991
|
}
|
|
732
992
|
|
|
733
|
-
//
|
|
993
|
+
// Convert 4-row × 2-col blocks of dots into braille characters.
|
|
734
994
|
const lines: string[] = [];
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
995
|
+
for (let tr = 0; tr < termH; tr++) {
|
|
996
|
+
const chars: string[] = [];
|
|
997
|
+
for (let tc = 0; tc < termW; tc++) {
|
|
998
|
+
const r0 = tr * 4;
|
|
999
|
+
const c0 = tc * 2;
|
|
1000
|
+
let bits = 0;
|
|
1001
|
+
if (r0 + 0 < dotH && c0 + 0 < dotW && dotGrid[r0 + 0][c0 + 0]) bits |= 0x01;
|
|
1002
|
+
if (r0 + 1 < dotH && c0 + 0 < dotW && dotGrid[r0 + 1][c0 + 0]) bits |= 0x02;
|
|
1003
|
+
if (r0 + 2 < dotH && c0 + 0 < dotW && dotGrid[r0 + 2][c0 + 0]) bits |= 0x04;
|
|
1004
|
+
if (r0 + 0 < dotH && c0 + 1 < dotW && dotGrid[r0 + 0][c0 + 1]) bits |= 0x08;
|
|
1005
|
+
if (r0 + 1 < dotH && c0 + 1 < dotW && dotGrid[r0 + 1][c0 + 1]) bits |= 0x10;
|
|
1006
|
+
if (r0 + 2 < dotH && c0 + 1 < dotW && dotGrid[r0 + 2][c0 + 1]) bits |= 0x20;
|
|
1007
|
+
if (r0 + 3 < dotH && c0 + 0 < dotW && dotGrid[r0 + 3][c0 + 0]) bits |= 0x40;
|
|
1008
|
+
if (r0 + 3 < dotH && c0 + 1 < dotW && dotGrid[r0 + 3][c0 + 1]) bits |= 0x80;
|
|
1009
|
+
chars.push(brailleChar(bits));
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Y-axis labels
|
|
1013
|
+
const labelRows = [0, Math.floor(termH / 4), Math.floor(termH / 2), Math.floor(3 * termH / 4), termH - 1];
|
|
1014
|
+
const labelVals = [max, Math.round(max * 0.75), Math.round(max / 2), Math.round(max / 4), 0];
|
|
1015
|
+
const idx = labelRows.indexOf(tr);
|
|
739
1016
|
let label = " ";
|
|
740
1017
|
if (idx >= 0) label = String(labelVals[idx]).padStart(4) + " ";
|
|
741
|
-
lines.push(`${label}\u2524${
|
|
1018
|
+
lines.push(`${label}\u2524${chars.join("")}`);
|
|
742
1019
|
}
|
|
1020
|
+
|
|
743
1021
|
// X-axis
|
|
744
|
-
lines.push(` \u2514${String.fromCharCode(0x2500).repeat(
|
|
1022
|
+
lines.push(` \u2514${String.fromCharCode(0x2500).repeat(termW)}`);
|
|
745
1023
|
return lines;
|
|
746
1024
|
}
|
|
747
1025
|
|
|
@@ -751,9 +1029,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
751
1029
|
try {
|
|
752
1030
|
const [chartRes, repoRes] = await Promise.all([
|
|
753
1031
|
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/stats/participation`,
|
|
754
|
-
{ headers: { "User-Agent": "
|
|
1032
|
+
{ headers: { "User-Agent": "ghfind/1.0" } }),
|
|
755
1033
|
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}`,
|
|
756
|
-
{ headers: { "User-Agent": "
|
|
1034
|
+
{ headers: { "User-Agent": "ghfind/1.0" } }).then(r => r.ok ? r.json() : null),
|
|
757
1035
|
]);
|
|
758
1036
|
|
|
759
1037
|
let chartSection = "";
|
|
@@ -761,7 +1039,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
761
1039
|
const data = await chartRes.json();
|
|
762
1040
|
if (data?.all && data.all.length >= 2) {
|
|
763
1041
|
chartCommitData = data.all;
|
|
764
|
-
const chartW =
|
|
1042
|
+
const chartW = 60;
|
|
765
1043
|
const chartH = 10;
|
|
766
1044
|
const chartLines = buildChartString(chartCommitData, chartW, chartH);
|
|
767
1045
|
chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join("\n");
|
|
@@ -921,7 +1199,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
921
1199
|
try {
|
|
922
1200
|
const parsed = applyFlagFilters(parseQuery(q), {});
|
|
923
1201
|
validateQuery(parsed);
|
|
924
|
-
const provider =
|
|
1202
|
+
const provider = createGitHubSearch(logger);
|
|
925
1203
|
const options: SearchOptions = {
|
|
926
1204
|
limit: currentLimit,
|
|
927
1205
|
sort: currentSort,
|
|
@@ -996,8 +1274,9 @@ export async function launchBrowser(): Promise<void> {
|
|
|
996
1274
|
|
|
997
1275
|
// ── Wire events ────────────────────────────────────────────────────
|
|
998
1276
|
|
|
999
|
-
// Enter in search input →
|
|
1277
|
+
// Enter in search input → show results
|
|
1000
1278
|
searchInput.on("enter", () => {
|
|
1279
|
+
if (searchInput.value.trim() === "" || isLoading) return;
|
|
1001
1280
|
showSearchMode();
|
|
1002
1281
|
doSearch(searchInput.value);
|
|
1003
1282
|
});
|
|
@@ -1031,9 +1310,29 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1031
1310
|
return;
|
|
1032
1311
|
}
|
|
1033
1312
|
|
|
1313
|
+
// Leader menu: Enter dispatches action, up/down navigates
|
|
1314
|
+
if (currentOverlay === "leader") {
|
|
1315
|
+
if (key.name === "enter" || key.name === "return") {
|
|
1316
|
+
const sel = leaderSelect.getSelectedOption();
|
|
1317
|
+
const item = sel?.value as LeaderItem | undefined;
|
|
1318
|
+
if (item) {
|
|
1319
|
+
showOverlay("none");
|
|
1320
|
+
item.action();
|
|
1321
|
+
renderer.requestRender();
|
|
1322
|
+
}
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
// Let up/down/j/k fall through to SelectRenderable
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// README viewer: scrollable, Esc/q already handled above
|
|
1329
|
+
if (currentOverlay === "readme") {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1034
1333
|
// History overlay
|
|
1035
1334
|
if (currentOverlay === "history") {
|
|
1036
|
-
if (key.
|
|
1335
|
+
if (key.name === "d") {
|
|
1037
1336
|
const sel = historySelect.getSelectedOption();
|
|
1038
1337
|
if (sel?.value) {
|
|
1039
1338
|
deleteHistoryEntry((sel.value as any).index);
|
|
@@ -1050,7 +1349,6 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1050
1349
|
renderer.requestRender();
|
|
1051
1350
|
return;
|
|
1052
1351
|
}
|
|
1053
|
-
// SelectRenderable handles ↑↓; Enter runs the search
|
|
1054
1352
|
if (key.name === "enter" || key.name === "return") {
|
|
1055
1353
|
const sel = historySelect.getSelectedOption();
|
|
1056
1354
|
if (sel?.value) {
|
|
@@ -1067,7 +1365,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1067
1365
|
}
|
|
1068
1366
|
return;
|
|
1069
1367
|
}
|
|
1070
|
-
return;
|
|
1368
|
+
return;
|
|
1071
1369
|
}
|
|
1072
1370
|
|
|
1073
1371
|
// Bookmarks overlay
|
|
@@ -1160,19 +1458,13 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1160
1458
|
return;
|
|
1161
1459
|
}
|
|
1162
1460
|
|
|
1163
|
-
// Compare overlay: Esc closes
|
|
1461
|
+
// Compare overlay: Esc/q closes
|
|
1164
1462
|
if (currentOverlay === "compare") {
|
|
1165
1463
|
return;
|
|
1166
1464
|
}
|
|
1167
1465
|
|
|
1168
1466
|
// Notifications overlay
|
|
1169
1467
|
if (currentOverlay === "notifications") {
|
|
1170
|
-
if (key.ctrl && key.name === "c") {
|
|
1171
|
-
dismissAll();
|
|
1172
|
-
refreshNotifications();
|
|
1173
|
-
renderer.requestRender();
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
1468
|
if (key.name === "d") {
|
|
1177
1469
|
const notifs = getNotifications();
|
|
1178
1470
|
if (notifs.length > 0) {
|
|
@@ -1183,6 +1475,12 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1183
1475
|
renderer.requestRender();
|
|
1184
1476
|
return;
|
|
1185
1477
|
}
|
|
1478
|
+
if (key.ctrl && key.name === "c") {
|
|
1479
|
+
dismissAll();
|
|
1480
|
+
refreshNotifications();
|
|
1481
|
+
renderer.requestRender();
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1186
1484
|
return;
|
|
1187
1485
|
}
|
|
1188
1486
|
|
|
@@ -1213,89 +1511,38 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1213
1511
|
renderer.requestRender();
|
|
1214
1512
|
return;
|
|
1215
1513
|
}
|
|
1514
|
+
|
|
1515
|
+
return;
|
|
1216
1516
|
}
|
|
1217
1517
|
|
|
1218
1518
|
// ── Main view handling (no overlay active) ────────────────────────
|
|
1219
1519
|
|
|
1220
|
-
//
|
|
1520
|
+
// q quits
|
|
1221
1521
|
if (key.name === "q") {
|
|
1522
|
+
if (graphFullscreen) {
|
|
1523
|
+
toggleGraph();
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1222
1526
|
saveSession({ mode: currentMode, query: currentQueryInput, sort: currentSort, limit: currentLimit, trendingTab });
|
|
1223
1527
|
cleanup();
|
|
1224
1528
|
return;
|
|
1225
1529
|
}
|
|
1226
1530
|
|
|
1227
|
-
//
|
|
1228
|
-
if (key.name === "
|
|
1229
|
-
|
|
1230
|
-
showOverlay("help");
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
// Ctrl+R — search history overlay
|
|
1235
|
-
if (key.ctrl && key.name === "r") {
|
|
1236
|
-
refreshHistory();
|
|
1237
|
-
showOverlay("history");
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
// Ctrl+S — save current search
|
|
1242
|
-
if (key.ctrl && key.name === "s") {
|
|
1243
|
-
if (currentQueryInput) {
|
|
1244
|
-
const name = currentQueryInput.length > 40
|
|
1245
|
-
? currentQueryInput.slice(0, 37) + "..."
|
|
1246
|
-
: currentQueryInput;
|
|
1247
|
-
saveSearch(name, currentQueryInput, currentMode, currentSort, currentLimit, currentMode === "trending" ? trendingTab : undefined);
|
|
1248
|
-
setStatus(`✓ Saved as "${name}"`);
|
|
1249
|
-
} else {
|
|
1250
|
-
setStatus("No search to save");
|
|
1251
|
-
}
|
|
1252
|
-
renderer.requestRender();
|
|
1253
|
-
return;
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
// Ctrl+O — saved searches panel
|
|
1257
|
-
if (key.ctrl && key.name === "o") {
|
|
1258
|
-
refreshSavedSearches();
|
|
1259
|
-
showOverlay("saved");
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
// Ctrl+N — notifications panel
|
|
1264
|
-
if (key.ctrl && key.name === "n") {
|
|
1265
|
-
refreshNotifications();
|
|
1266
|
-
showOverlay("notifications");
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
// Ctrl+P — share repo
|
|
1271
|
-
if (key.ctrl && key.name === "p") {
|
|
1272
|
-
const opt = resultsSelect.getSelectedOption();
|
|
1273
|
-
const repo = opt?.value as Repo | undefined;
|
|
1274
|
-
if (!repo) {
|
|
1275
|
-
setStatus("No repo selected to share");
|
|
1276
|
-
renderer.requestRender();
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
showOverlay("share");
|
|
1531
|
+
// Esc exits graph mode
|
|
1532
|
+
if (key.name === "escape" && graphFullscreen) {
|
|
1533
|
+
toggleGraph();
|
|
1280
1534
|
return;
|
|
1281
1535
|
}
|
|
1282
1536
|
|
|
1283
|
-
//
|
|
1284
|
-
if (key.name === "
|
|
1285
|
-
|
|
1286
|
-
const repo = opt?.value as Repo | undefined;
|
|
1287
|
-
if (repo) {
|
|
1288
|
-
const added = toggleBookmark(repo);
|
|
1289
|
-
setStatus(added ? `Bookmarked ${repo.fullName}` : `Unbookmarked ${repo.fullName}`);
|
|
1290
|
-
renderer.requestRender();
|
|
1291
|
-
}
|
|
1537
|
+
// Space opens leader menu
|
|
1538
|
+
if (key.name === "space") {
|
|
1539
|
+
showLeaderMenu();
|
|
1292
1540
|
return;
|
|
1293
1541
|
}
|
|
1294
1542
|
|
|
1295
|
-
// '
|
|
1296
|
-
if (key.name === "
|
|
1297
|
-
|
|
1298
|
-
showOverlay("bookmarks");
|
|
1543
|
+
// '?' / Ctrl+H toggle help overlay (fast path)
|
|
1544
|
+
if (key.name === "?" || (key.name === "h" && key.ctrl)) {
|
|
1545
|
+
showOverlay("help");
|
|
1299
1546
|
return;
|
|
1300
1547
|
}
|
|
1301
1548
|
|
|
@@ -1317,27 +1564,11 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1317
1564
|
return;
|
|
1318
1565
|
}
|
|
1319
1566
|
|
|
1320
|
-
// '/' focuses search
|
|
1567
|
+
// '/' focuses search
|
|
1321
1568
|
if (key.name === "/") {
|
|
1569
|
+
searchInput.focus();
|
|
1322
1570
|
showSearchMode();
|
|
1323
|
-
|
|
1324
|
-
}
|
|
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
|
-
|
|
1330
|
-
if (key.name === "g") {
|
|
1331
|
-
toggleGraph();
|
|
1332
|
-
return;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
if (key.name === "t") {
|
|
1336
|
-
if (currentMode === "trending") {
|
|
1337
|
-
showSearchMode();
|
|
1338
|
-
} else {
|
|
1339
|
-
loadTrending();
|
|
1340
|
-
}
|
|
1571
|
+
renderer.requestRender();
|
|
1341
1572
|
return;
|
|
1342
1573
|
}
|
|
1343
1574
|
|
|
@@ -1353,38 +1584,7 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1353
1584
|
return;
|
|
1354
1585
|
}
|
|
1355
1586
|
|
|
1356
|
-
|
|
1357
|
-
if (currentMode === "trending") {
|
|
1358
|
-
loadTrending();
|
|
1359
|
-
} else {
|
|
1360
|
-
doSearch(searchInput.value || currentQueryInput);
|
|
1361
|
-
}
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
if (key.name === "s") {
|
|
1366
|
-
if (currentMode === "search") changeSort();
|
|
1367
|
-
return;
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
if (key.name === "l") {
|
|
1371
|
-
if (currentMode === "search") {
|
|
1372
|
-
changeLimit();
|
|
1373
|
-
} else if (currentMode === "trending") {
|
|
1374
|
-
const idx = TAB_NAMES.indexOf(trendingTab);
|
|
1375
|
-
if (idx < TAB_NAMES.length - 1) { trendingTab = TAB_NAMES[idx + 1]; loadTrending(); }
|
|
1376
|
-
}
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
if (key.name === "o") {
|
|
1381
|
-
const opt = resultsSelect.getSelectedOption();
|
|
1382
|
-
const repo = opt?.value as Repo | undefined;
|
|
1383
|
-
if (repo) openUrl(repo.url);
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
// Left/right/h arrows switch trending tabs
|
|
1587
|
+
// Left/right arrows (and h/l) switch trending tabs
|
|
1388
1588
|
if (key.name === "left" || key.name === "h") {
|
|
1389
1589
|
if (currentMode === "trending") {
|
|
1390
1590
|
const idx = TAB_NAMES.indexOf(trendingTab);
|
|
@@ -1392,75 +1592,16 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1392
1592
|
}
|
|
1393
1593
|
return;
|
|
1394
1594
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
if (key.name === "d") {
|
|
1400
|
-
const opt = resultsSelect.getSelectedOption();
|
|
1401
|
-
const repo = opt?.value as Repo | undefined;
|
|
1402
|
-
if (!repo) return;
|
|
1403
|
-
deepDiveActive = !deepDiveActive;
|
|
1404
|
-
if (deepDiveActive) {
|
|
1405
|
-
detailText.content = " Loading deep-dive...";
|
|
1406
|
-
renderer.requestRender();
|
|
1407
|
-
fetchDeepDive(repo, githubToken)
|
|
1408
|
-
.then((data) => {
|
|
1409
|
-
detailText.content = buildDeepDiveText(data);
|
|
1410
|
-
renderer.requestRender();
|
|
1411
|
-
})
|
|
1412
|
-
.catch(() => {
|
|
1413
|
-
detailText.content = " Failed to load deep-dive";
|
|
1414
|
-
renderer.requestRender();
|
|
1415
|
-
});
|
|
1416
|
-
} else {
|
|
1417
|
-
updateDetail(repo);
|
|
1418
|
-
renderer.requestRender();
|
|
1419
|
-
}
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
// 'c' — toggle compare list for selected repo
|
|
1424
|
-
if (key.name === "c" && !key.shift) {
|
|
1425
|
-
const opt = resultsSelect.getSelectedOption();
|
|
1426
|
-
const repo = opt?.value as Repo | undefined;
|
|
1427
|
-
if (!repo) return;
|
|
1428
|
-
const idx = compareList.findIndex((r) => r.fullName === repo.fullName);
|
|
1429
|
-
if (idx >= 0) {
|
|
1430
|
-
compareList.splice(idx, 1);
|
|
1431
|
-
setStatus(`Removed ${repo.fullName} from comparison`);
|
|
1432
|
-
} else {
|
|
1433
|
-
compareList.push(repo);
|
|
1434
|
-
setStatus(`${repo.fullName} added to comparison (${compareList.length} selected)`);
|
|
1595
|
+
if (key.name === "right" || key.name === "l") {
|
|
1596
|
+
if (currentMode === "trending") {
|
|
1597
|
+
const idx = TAB_NAMES.indexOf(trendingTab);
|
|
1598
|
+
if (idx < TAB_NAMES.length - 1) { trendingTab = TAB_NAMES[idx + 1]; loadTrending(); }
|
|
1435
1599
|
}
|
|
1436
|
-
renderer.requestRender();
|
|
1437
|
-
return;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
// 'C' (Shift+c) — show comparison view
|
|
1441
|
-
if (key.name === "c" && key.shift) {
|
|
1442
|
-
refreshCompare();
|
|
1443
|
-
showOverlay("compare");
|
|
1444
|
-
return;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
// 'E' (Shift+e) — topic explorer
|
|
1448
|
-
if (key.name === "e" && key.shift) {
|
|
1449
|
-
refreshTopics();
|
|
1450
|
-
showOverlay("topics");
|
|
1451
1600
|
return;
|
|
1452
1601
|
}
|
|
1453
1602
|
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
// Ctrl+E — export
|
|
1457
|
-
if (key.ctrl && key.name === "e") {
|
|
1458
|
-
showOverlay("export");
|
|
1459
|
-
return;
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
// PageDown / Ctrl+F — next page
|
|
1463
|
-
if (key.name === "pagedown" || (key.ctrl && key.name === "f")) {
|
|
1603
|
+
// PageDown — next page (search mode)
|
|
1604
|
+
if (key.name === "pagedown") {
|
|
1464
1605
|
if (currentMode === "search" && currentQueryInput) {
|
|
1465
1606
|
currentPage++;
|
|
1466
1607
|
doSearch(currentQueryInput, true);
|
|
@@ -1468,8 +1609,8 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1468
1609
|
return;
|
|
1469
1610
|
}
|
|
1470
1611
|
|
|
1471
|
-
// PageUp
|
|
1472
|
-
if (key.name === "pageup"
|
|
1612
|
+
// PageUp — scroll to top of results
|
|
1613
|
+
if (key.name === "pageup") {
|
|
1473
1614
|
if (currentRepos.length > 0) {
|
|
1474
1615
|
resultsSelect.setSelectedIndex(0);
|
|
1475
1616
|
updateDetail(currentRepos[0]);
|
|
@@ -1485,8 +1626,12 @@ export async function launchBrowser(): Promise<void> {
|
|
|
1485
1626
|
loadTrending();
|
|
1486
1627
|
} else {
|
|
1487
1628
|
searchInput.value = currentQueryInput;
|
|
1629
|
+
if (currentQueryInput) {
|
|
1630
|
+
doSearch(currentQueryInput);
|
|
1631
|
+
} else {
|
|
1632
|
+
showSearchMode();
|
|
1633
|
+
}
|
|
1488
1634
|
searchInput.focus();
|
|
1489
|
-
if (currentQueryInput) doSearch(currentQueryInput);
|
|
1490
1635
|
}
|
|
1491
1636
|
renderer.requestRender();
|
|
1492
1637
|
}
|
|
@@ -1506,7 +1651,7 @@ function formatStars(n: number): string {
|
|
|
1506
1651
|
|
|
1507
1652
|
function formatToolbar(sort: SortStrategy, limit: number): string {
|
|
1508
1653
|
const sortLabel = SORT_MODES.find((m) => m.key === sort)?.label ?? sort;
|
|
1509
|
-
return ` sort: ${sortLabel} limit: ${limit}
|
|
1654
|
+
return ` sort: ${sortLabel} limit: ${limit}`;
|
|
1510
1655
|
}
|
|
1511
1656
|
|
|
1512
1657
|
function cleanup(): void {
|
|
@@ -1515,6 +1660,8 @@ function cleanup(): void {
|
|
|
1515
1660
|
}
|
|
1516
1661
|
|
|
1517
1662
|
// ── Auto-run ──────────────────────────────────────────────────────────
|
|
1518
|
-
|
|
1663
|
+
// Only launch when run directly (bun run src/tui.ts), not when bundled
|
|
1664
|
+
// into cli.js or imported as a module.
|
|
1665
|
+
if (import.meta.main && !process.env.GHFIND_BUNDLED) {
|
|
1519
1666
|
launchBrowser();
|
|
1520
1667
|
}
|