github-search-cli 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -50
- package/bin/ghfind.js +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +106 -106
- package/src/bookmarks.ts +4 -17
- package/src/cli.ts +90 -61
- package/src/config.ts +2 -2
- package/src/history.ts +16 -52
- package/src/notifications.ts +5 -12
- package/src/output.ts +124 -55
- package/src/saved-searches.ts +4 -12
- package/src/search-state.ts +229 -0
- package/src/search.ts +99 -2
- package/src/session.ts +6 -18
- package/src/storage.ts +106 -0
- package/src/themes.ts +47 -7
- package/src/trending.ts +27 -150
- package/src/tui.ts +1087 -270
- package/src/types.ts +2 -0
- package/src/update-check.ts +162 -0
- package/src/watch.ts +23 -11
package/src/trending.ts
CHANGED
|
@@ -11,11 +11,9 @@ import {
|
|
|
11
11
|
dim,
|
|
12
12
|
t,
|
|
13
13
|
} from "@opentui/core";
|
|
14
|
-
import type { Repo } from "./types";
|
|
14
|
+
import type { Repo, ParsedQuery } from "./types";
|
|
15
15
|
import { openUrl } from "./open-url";
|
|
16
|
-
import {
|
|
17
|
-
import type { GitHubSearchEnvelope } from "./search";
|
|
18
|
-
|
|
16
|
+
import { SearchModule, TrendingAdapter } from "./search";
|
|
19
17
|
// ─── Tokyo Night palette ──────────────────────────────────────────────
|
|
20
18
|
const C = {
|
|
21
19
|
bg: "#1a1b26",
|
|
@@ -40,7 +38,6 @@ const C = {
|
|
|
40
38
|
rankTop3: "#ff9e64", // bronze-ish orange
|
|
41
39
|
rankRest: "#565f89", // muted gray
|
|
42
40
|
};
|
|
43
|
-
|
|
44
41
|
// ─── Language → color mapping ─────────────────────────────────────────
|
|
45
42
|
const LANG_COLORS: Record<string, string> = {
|
|
46
43
|
Rust: "#e0af68",
|
|
@@ -62,26 +59,19 @@ const LANG_COLORS: Record<string, string> = {
|
|
|
62
59
|
Swift: "#f7768e",
|
|
63
60
|
Kotlin: "#7aa2f7",
|
|
64
61
|
};
|
|
65
|
-
|
|
66
62
|
function langColor(lang: string): string {
|
|
67
63
|
return LANG_COLORS[lang] ?? C.muted;
|
|
68
64
|
}
|
|
69
|
-
|
|
70
65
|
function rankColor(rank: number): string {
|
|
71
66
|
if (rank === 1) return C.rankTop1;
|
|
72
67
|
if (rank === 2) return C.rankTop2;
|
|
73
68
|
if (rank === 3) return C.rankTop3;
|
|
74
69
|
return C.rankRest;
|
|
75
70
|
}
|
|
76
|
-
|
|
77
71
|
// ─── Tab definitions ──────────────────────────────────────────────────
|
|
78
72
|
export const TAB_NAMES = ["Today", "This Week", "This Month"] as const;
|
|
79
73
|
export type TabName = (typeof TAB_NAMES)[number];
|
|
80
|
-
|
|
81
|
-
/** Map TabName to GitHub API date range.
|
|
82
|
-
* We use the search API with `created:>DATE&sort=stars` so only
|
|
83
|
-
* daily/weekly/monthly ranges are meaningful.
|
|
84
|
-
*/
|
|
74
|
+
/** Map TabName to github.com/trending?since= param */
|
|
85
75
|
export function tabSince(tab: TabName): "daily" | "weekly" | "monthly" {
|
|
86
76
|
switch (tab) {
|
|
87
77
|
case "Today": return "daily";
|
|
@@ -89,64 +79,7 @@ export function tabSince(tab: TabName): "daily" | "weekly" | "monthly" {
|
|
|
89
79
|
default: return "monthly";
|
|
90
80
|
}
|
|
91
81
|
}
|
|
92
|
-
|
|
93
|
-
// ─── Types ────────────────────────────────────────────────────────────
|
|
94
|
-
export interface TrendingRepo {
|
|
95
|
-
rank: number;
|
|
96
|
-
owner: string;
|
|
97
|
-
name: string;
|
|
98
|
-
stars: number;
|
|
99
|
-
starsToday: number;
|
|
100
|
-
language: string;
|
|
101
|
-
description: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ─── Mock data ────────────────────────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Fetch trending repos from the GitHub search API instead of HTML scraping.
|
|
108
|
-
* Uses `created:>DATE&sort=stars` queries since the API doesn't expose a
|
|
109
|
-
* dedicated trending endpoint.
|
|
110
|
-
*
|
|
111
|
-
* Note: `starsToday` is always 0 because the API doesn't provide period growth.
|
|
112
|
-
* The growth indicator is hidden when 0.
|
|
113
|
-
*/
|
|
114
|
-
export async function fetchTrendingRepos(since: "daily" | "weekly" | "monthly"): Promise<TrendingRepo[]> {
|
|
115
|
-
const now = Date.now();
|
|
116
|
-
const msPerDay = 86_400_000;
|
|
117
|
-
const days = since === "daily" ? 1 : since === "weekly" ? 7 : 30;
|
|
118
|
-
const date = new Date(now - days * msPerDay).toISOString().slice(0, 10);
|
|
119
|
-
|
|
120
|
-
const url = `https://api.github.com/search/repositories?q=created:>${date}&sort=stars&order=desc&per_page=25`;
|
|
121
|
-
let res: Response;
|
|
122
|
-
try {
|
|
123
|
-
res = await fetch(url, {
|
|
124
|
-
headers: {
|
|
125
|
-
Accept: "application/vnd.github+json",
|
|
126
|
-
"User-Agent": "ghfind",
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
} catch {
|
|
130
|
-
throw new NetworkError();
|
|
131
|
-
}
|
|
132
|
-
if (!res.ok) throw new NetworkError();
|
|
133
|
-
const data = (await res.json()) as GitHubSearchEnvelope;
|
|
134
|
-
|
|
135
|
-
return data.items.map((item, i) => ({
|
|
136
|
-
rank: i + 1,
|
|
137
|
-
owner: item.owner?.login ?? "?",
|
|
138
|
-
name: item.name,
|
|
139
|
-
stars: item.stargazers_count,
|
|
140
|
-
starsToday: 0, // API doesn't provide period growth
|
|
141
|
-
language: item.language ?? "",
|
|
142
|
-
description: item.description ?? "",
|
|
143
|
-
}));
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
82
|
// ─── Number formatting ────────────────────────────────────────────────
|
|
149
|
-
|
|
150
83
|
/** 1234 → "1.2k", 1234567 → "1.2M", 999999 → "1M" */
|
|
151
84
|
export function fmtStars(n: number): string {
|
|
152
85
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
|
|
@@ -154,15 +87,12 @@ export function fmtStars(n: number): string {
|
|
|
154
87
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
|
|
155
88
|
return String(n);
|
|
156
89
|
}
|
|
157
|
-
|
|
158
90
|
/** Format signed number: 1204 → "+1.2k" */
|
|
159
91
|
export function fmtSigned(n: number): string {
|
|
160
92
|
const abs = fmtStars(Math.abs(n));
|
|
161
93
|
return n >= 0 ? `+${abs}` : `-${abs}`;
|
|
162
94
|
}
|
|
163
|
-
|
|
164
95
|
// ─── Row formatting ───────────────────────────────────────────────────
|
|
165
|
-
|
|
166
96
|
/**
|
|
167
97
|
* Format one repo as a StyledText for per-chunk coloring.
|
|
168
98
|
*
|
|
@@ -178,25 +108,19 @@ export function fmtSigned(n: number): string {
|
|
|
178
108
|
* - Growth: green bold
|
|
179
109
|
* - Description: dim muted gray
|
|
180
110
|
*/
|
|
181
|
-
export function formatRepoLine(repo:
|
|
182
|
-
const
|
|
111
|
+
export function formatRepoLine(repo: Repo, rank: number): StyledText {
|
|
112
|
+
const rankStr = String(rank).padStart(2, "0");
|
|
183
113
|
const full = `${repo.owner}/${repo.name}`;
|
|
184
|
-
const lc = langColor(repo.language);
|
|
185
|
-
const rc = rankColor(
|
|
114
|
+
const lc = langColor(repo.language ?? "");
|
|
115
|
+
const rc = rankColor(rank);
|
|
186
116
|
const star = "★";
|
|
187
|
-
const arrow = repo.
|
|
188
|
-
const growth = `${arrow} ${fmtSigned(repo.
|
|
189
|
-
const desc = repo.description.length > 60
|
|
190
|
-
? repo.description.slice(0, 57) + "..."
|
|
191
|
-
: repo.description;
|
|
192
|
-
|
|
193
|
-
// Build the styled line using OpenTUI's `t` template literal tag.
|
|
194
|
-
// Each interpolated value is a TextChunk with its own fg/bg/attributes.
|
|
195
|
-
const line1 = t`${dim(fg(C.rankBg)(`[${rank}]`))} ${bold(fg("#c0caf5")(full))} ${fg(lc)(`● ${repo.language}`)} ${fg(C.gold)(`${star} ${fmtStars(repo.stars)}`)} ${bold(fg(C.green)(growth))}`;
|
|
117
|
+
const arrow = repo.score > 0 ? "▲" : repo.score < 0 ? "▼" : "—";
|
|
118
|
+
const growth = `${arrow} ${fmtSigned(repo.score)} today`;
|
|
119
|
+
const desc = (repo.description ?? "").length > 60
|
|
120
|
+
? (repo.description ?? "").slice(0, 57) + "..."
|
|
121
|
+
: (repo.description ?? "");
|
|
122
|
+
const line1 = t`${dim(fg(C.rankBg)(`[${rankStr}]`))} ${bold(fg("#c0caf5")(full))} ${fg(lc)(`● ${repo.language}`)} ${fg(C.gold)(`${star} ${fmtStars(repo.stars)}`)} ${bold(fg(C.green)(growth))}`;
|
|
196
123
|
const line2 = t` ${dim(fg(C.muted)(desc))}`;
|
|
197
|
-
|
|
198
|
-
// Combine into a single StyledText with a newline between lines.
|
|
199
|
-
// StyledText constructor accepts an array of TextChunk objects.
|
|
200
124
|
const chunks = [
|
|
201
125
|
...line1.chunks,
|
|
202
126
|
{ text: "\n", __isChunk: true as const, fg: C.text },
|
|
@@ -204,7 +128,6 @@ export function formatRepoLine(repo: TrendingRepo): StyledText {
|
|
|
204
128
|
];
|
|
205
129
|
return new StyledText(chunks);
|
|
206
130
|
}
|
|
207
|
-
|
|
208
131
|
// ─── Tab query builder ────────────────────────────────────────────────
|
|
209
132
|
/**
|
|
210
133
|
* Build a GitHub search query string for the given time range tab.
|
|
@@ -227,7 +150,6 @@ export function getTrendingQuery(tab: TabName): string {
|
|
|
227
150
|
const d = new Date(now - days * msPerDay);
|
|
228
151
|
return `created:>${d.toISOString().slice(0, 10)} sort:stars`;
|
|
229
152
|
}
|
|
230
|
-
|
|
231
153
|
// ─── Main TUI entry ───────────────────────────────────────────────────
|
|
232
154
|
export async function launchTrending(): Promise<void> {
|
|
233
155
|
let renderer;
|
|
@@ -241,23 +163,19 @@ export async function launchTrending(): Promise<void> {
|
|
|
241
163
|
process.exit(1);
|
|
242
164
|
return;
|
|
243
165
|
}
|
|
244
|
-
|
|
245
166
|
const root = renderer.root;
|
|
246
167
|
root.flexDirection = "column";
|
|
247
168
|
root.backgroundColor = C.bg;
|
|
248
|
-
|
|
249
169
|
// ── State ──
|
|
250
170
|
let selectedTab = 1; // "This Week" default
|
|
251
171
|
let currentPeriod = "this week";
|
|
252
|
-
let repos:
|
|
172
|
+
let repos: Repo[] = [];
|
|
253
173
|
let selectedRepoIdx = 0;
|
|
254
174
|
let isLoading = false;
|
|
255
|
-
|
|
256
175
|
// ── Fetch from github.com/trending ──
|
|
257
176
|
async function loadTab(index: number) {
|
|
258
177
|
if (index < 0 || index >= TAB_NAMES.length) return;
|
|
259
178
|
selectedTab = index;
|
|
260
|
-
// Map tab to data period label ("This Year" and "All Time" use monthly data)
|
|
261
179
|
const since = tabSince(TAB_NAMES[index]);
|
|
262
180
|
currentPeriod = since === "daily" ? "today" : since === "weekly" ? "this week" : "this month";
|
|
263
181
|
isLoading = true;
|
|
@@ -271,11 +189,18 @@ export async function launchTrending(): Promise<void> {
|
|
|
271
189
|
});
|
|
272
190
|
scrollBox.add(loadingText);
|
|
273
191
|
renderer.requestRender();
|
|
274
|
-
|
|
275
192
|
try {
|
|
276
|
-
const
|
|
193
|
+
const searchModule = new SearchModule(new TrendingAdapter());
|
|
194
|
+
const parsed: ParsedQuery = { keywords: [], qualifiers: [], raw: "trending" };
|
|
195
|
+
const response = await searchModule.search(parsed, {
|
|
196
|
+
limit: 25,
|
|
197
|
+
sort: "stars",
|
|
198
|
+
json: false,
|
|
199
|
+
verbose: false,
|
|
200
|
+
trendingSince: tabSince(TAB_NAMES[index]),
|
|
201
|
+
});
|
|
277
202
|
scrollBox.remove(loadingText);
|
|
278
|
-
repos =
|
|
203
|
+
repos = response.repos;
|
|
279
204
|
selectedRepoIdx = 0;
|
|
280
205
|
rebuildList();
|
|
281
206
|
} catch (err) {
|
|
@@ -292,15 +217,12 @@ export async function launchTrending(): Promise<void> {
|
|
|
292
217
|
renderTabBar();
|
|
293
218
|
renderer.requestRender();
|
|
294
219
|
}
|
|
295
|
-
|
|
296
220
|
// ── Switch tab ──
|
|
297
221
|
function switchTab(index: number) {
|
|
298
222
|
if (index < 0 || index >= TAB_NAMES.length || index === selectedTab) return;
|
|
299
223
|
loadTab(index);
|
|
300
224
|
}
|
|
301
|
-
|
|
302
225
|
// ── Outer frame ──
|
|
303
|
-
// Rounded border around the entire app for visual polish.
|
|
304
226
|
const outerBox = new BoxRenderable(renderer, {
|
|
305
227
|
flexGrow: 1,
|
|
306
228
|
flexDirection: "column",
|
|
@@ -311,7 +233,6 @@ export async function launchTrending(): Promise<void> {
|
|
|
311
233
|
paddingX: 0,
|
|
312
234
|
paddingY: 0,
|
|
313
235
|
});
|
|
314
|
-
|
|
315
236
|
// ── Header bar ──
|
|
316
237
|
const headerBox = new BoxRenderable(renderer, {
|
|
317
238
|
height: 1,
|
|
@@ -324,7 +245,6 @@ export async function launchTrending(): Promise<void> {
|
|
|
324
245
|
});
|
|
325
246
|
headerBox.add(headerText);
|
|
326
247
|
outerBox.add(headerBox);
|
|
327
|
-
|
|
328
248
|
// ── Tab bar ──
|
|
329
249
|
const tabBox = new BoxRenderable(renderer, {
|
|
330
250
|
flexDirection: "row",
|
|
@@ -333,7 +253,6 @@ export async function launchTrending(): Promise<void> {
|
|
|
333
253
|
paddingX: 1,
|
|
334
254
|
});
|
|
335
255
|
const tabTexts: TextRenderable[] = [];
|
|
336
|
-
|
|
337
256
|
function renderTabBar() {
|
|
338
257
|
for (const t of tabTexts) tabBox.remove(t);
|
|
339
258
|
tabTexts.length = 0;
|
|
@@ -362,8 +281,6 @@ export async function launchTrending(): Promise<void> {
|
|
|
362
281
|
}
|
|
363
282
|
renderTabBar();
|
|
364
283
|
outerBox.add(tabBox);
|
|
365
|
-
|
|
366
|
-
// Subtle horizontal divider between tab bar and list
|
|
367
284
|
const divider = new TextRenderable(renderer, {
|
|
368
285
|
content: "─".repeat(80),
|
|
369
286
|
color: C.border,
|
|
@@ -371,7 +288,6 @@ export async function launchTrending(): Promise<void> {
|
|
|
371
288
|
backgroundColor: C.bg,
|
|
372
289
|
});
|
|
373
290
|
outerBox.add(divider);
|
|
374
|
-
|
|
375
291
|
// ── Repo list (scrollable) ──
|
|
376
292
|
const scrollBox = new ScrollBoxRenderable(renderer, {
|
|
377
293
|
flexGrow: 1,
|
|
@@ -380,48 +296,38 @@ export async function launchTrending(): Promise<void> {
|
|
|
380
296
|
scrollX: false,
|
|
381
297
|
viewportCulling: true,
|
|
382
298
|
});
|
|
383
|
-
|
|
384
|
-
// We render rows as individual BoxRenderables inside the scroll area.
|
|
385
|
-
// Each row has alternating background and a left-border accent on selection.
|
|
386
299
|
const rowBoxes: BoxRenderable[] = [];
|
|
387
300
|
const rowTexts: TextRenderable[] = [];
|
|
388
|
-
|
|
389
301
|
function rebuildList() {
|
|
390
302
|
for (const rb of rowBoxes) scrollBox.remove(rb);
|
|
391
303
|
rowBoxes.length = 0;
|
|
392
304
|
rowTexts.length = 0;
|
|
393
|
-
|
|
394
305
|
repos.forEach((r, i) => {
|
|
395
306
|
const isEven = i % 2 === 0;
|
|
396
307
|
const isSelected = i === selectedRepoIdx;
|
|
397
308
|
const rowBg = isSelected ? C.selectionBg : (isEven ? C.bg : C.surface);
|
|
398
|
-
|
|
399
309
|
const rank = i + 1;
|
|
400
310
|
const rc = rankColor(rank);
|
|
401
311
|
const lc = langColor(r.language ?? "");
|
|
402
312
|
const desc = (r.description ?? "").length > 55
|
|
403
313
|
? (r.description ?? "").slice(0, 52) + "..."
|
|
404
314
|
: (r.description ?? "");
|
|
405
|
-
const arrow = r.
|
|
406
|
-
const growthStr = `${arrow} ${fmtStars(Math.abs(r.
|
|
407
|
-
|
|
315
|
+
const arrow = r.score > 0 ? "▲" : r.score < 0 ? "▼" : "—";
|
|
316
|
+
const growthStr = `${arrow} ${fmtStars(Math.abs(r.score))} ${currentPeriod}`;
|
|
408
317
|
const rankStr = `[${String(rank).padStart(2, "0")}]`;
|
|
409
318
|
const nameStr = `${r.owner}/${r.name}`.padEnd(30).slice(0, 30);
|
|
410
319
|
const langStr = `● ${r.language || "—"}`.padEnd(14).slice(0, 14);
|
|
411
320
|
const starsStr = `★ ${fmtStars(r.stars)}`.padEnd(12).slice(0, 12);
|
|
412
|
-
|
|
413
321
|
const rankText = new TextRenderable(renderer, {
|
|
414
322
|
content: t`${bold(fg(rc)(rankStr))}`,
|
|
415
323
|
backgroundColor: C.rankBg,
|
|
416
324
|
color: rc,
|
|
417
325
|
height: 1,
|
|
418
326
|
});
|
|
419
|
-
|
|
420
327
|
const descStr = desc.length > 60 ? desc.slice(0, 57) + "..." : desc;
|
|
421
328
|
const pad = " ";
|
|
422
329
|
const line1 = t`${bold(fg("#c0caf5")(nameStr))}${fg(lc)(langStr)}${fg(C.muted)(starsStr)}${bold(fg(C.green)(growthStr))}`;
|
|
423
330
|
const line2 = t`${fg(C.descText)(`${pad}${descStr}`)}`;
|
|
424
|
-
|
|
425
331
|
const chunks = [
|
|
426
332
|
...line1.chunks,
|
|
427
333
|
{ text: "\n", __isChunk: true as const, fg: C.text },
|
|
@@ -432,21 +338,18 @@ export async function launchTrending(): Promise<void> {
|
|
|
432
338
|
backgroundColor: rowBg,
|
|
433
339
|
height: 2,
|
|
434
340
|
});
|
|
435
|
-
|
|
436
341
|
const rowBox = new BoxRenderable(renderer, {
|
|
437
342
|
flexDirection: "row",
|
|
438
343
|
backgroundColor: rowBg,
|
|
439
344
|
});
|
|
440
345
|
rowBox.add(rankText);
|
|
441
346
|
rowBox.add(rowText);
|
|
442
|
-
|
|
443
347
|
rowBoxes.push(rowBox);
|
|
444
348
|
rowTexts.push(rowText);
|
|
445
349
|
scrollBox.add(rowBox);
|
|
446
350
|
});
|
|
447
351
|
}
|
|
448
352
|
outerBox.add(scrollBox);
|
|
449
|
-
|
|
450
353
|
// ── Footer bar ──
|
|
451
354
|
const footerBox = new BoxRenderable(renderer, {
|
|
452
355
|
height: 1,
|
|
@@ -460,24 +363,18 @@ export async function launchTrending(): Promise<void> {
|
|
|
460
363
|
});
|
|
461
364
|
footerBox.add(footerText);
|
|
462
365
|
outerBox.add(footerBox);
|
|
463
|
-
|
|
464
366
|
root.add(outerBox);
|
|
465
|
-
|
|
466
367
|
// ── Helpers ──
|
|
467
368
|
function openSelectedRepo() {
|
|
468
369
|
if (selectedRepoIdx < 0 || selectedRepoIdx >= repos.length) return;
|
|
469
370
|
const r = repos[selectedRepoIdx];
|
|
470
371
|
openUrl(`https://github.com/${r.owner}/${r.name}`);
|
|
471
372
|
}
|
|
472
|
-
|
|
473
|
-
// Toggle selection highlight on the row boxes without rebuilding everything.
|
|
474
|
-
// OpenTUI key names for arrows: "up", "down", "left", "right"
|
|
475
373
|
function moveSelection(delta: number) {
|
|
476
374
|
const oldIdx = selectedRepoIdx;
|
|
477
375
|
const newIdx = Math.max(0, Math.min(repos.length - 1, oldIdx + delta));
|
|
478
376
|
if (newIdx === oldIdx) return;
|
|
479
377
|
selectedRepoIdx = newIdx;
|
|
480
|
-
|
|
481
378
|
const setBg = (idx: number, selected: boolean) => {
|
|
482
379
|
if (idx < 0 || idx >= rowBoxes.length) return;
|
|
483
380
|
const bg = selected ? C.selectionBg : (idx % 2 === 0 ? C.bg : C.surface);
|
|
@@ -488,24 +385,17 @@ export async function launchTrending(): Promise<void> {
|
|
|
488
385
|
setBg(newIdx, true);
|
|
489
386
|
renderer.requestRender();
|
|
490
387
|
}
|
|
491
|
-
|
|
492
388
|
// ── Global key events ──
|
|
493
389
|
renderer.keyInput.on("keypress", (key) => {
|
|
494
390
|
const name = key.name || "";
|
|
495
|
-
|
|
496
|
-
// Quit
|
|
497
391
|
if (name === "q") {
|
|
498
392
|
process.exit(0);
|
|
499
393
|
return;
|
|
500
394
|
}
|
|
501
|
-
|
|
502
|
-
// Number keys 1-3 switch tabs
|
|
503
395
|
if (/^[1-3]$/.test(name)) {
|
|
504
396
|
switchTab(parseInt(name, 10) - 1);
|
|
505
397
|
return;
|
|
506
398
|
}
|
|
507
|
-
|
|
508
|
-
// Tab navigation: left/right arrows or h/l
|
|
509
399
|
if (name === "left" || name === "h") {
|
|
510
400
|
switchTab(selectedTab - 1);
|
|
511
401
|
return;
|
|
@@ -514,8 +404,6 @@ export async function launchTrending(): Promise<void> {
|
|
|
514
404
|
switchTab(selectedTab + 1);
|
|
515
405
|
return;
|
|
516
406
|
}
|
|
517
|
-
|
|
518
|
-
// Repo navigation: up/down arrows or j/k
|
|
519
407
|
if (name === "up" || name === "k") {
|
|
520
408
|
moveSelection(-1);
|
|
521
409
|
return;
|
|
@@ -524,36 +412,25 @@ export async function launchTrending(): Promise<void> {
|
|
|
524
412
|
moveSelection(1);
|
|
525
413
|
return;
|
|
526
414
|
}
|
|
527
|
-
|
|
528
|
-
// Enter or 'o' opens selected repo
|
|
529
415
|
if (name === "enter" || name === "return" || name === "o") {
|
|
530
416
|
openSelectedRepo();
|
|
531
417
|
return;
|
|
532
418
|
}
|
|
533
|
-
|
|
534
|
-
// '/' jumps to top
|
|
535
419
|
if (name === "/") {
|
|
536
420
|
moveSelection(-selectedRepoIdx);
|
|
537
421
|
return;
|
|
538
422
|
}
|
|
539
|
-
|
|
540
|
-
// 'r' refreshes from API
|
|
541
423
|
if (name === "r") {
|
|
542
424
|
loadTab(selectedTab);
|
|
543
425
|
return;
|
|
544
426
|
}
|
|
545
427
|
});
|
|
546
|
-
|
|
547
428
|
// ── Start ──
|
|
548
429
|
renderer.start();
|
|
549
430
|
renderer.requestRender();
|
|
550
|
-
// Fetch real trending data on launch
|
|
551
431
|
loadTab(selectedTab);
|
|
552
432
|
}
|
|
553
|
-
|
|
554
433
|
// ── Auto-run ──────────────────────────────────────────────────────────
|
|
555
|
-
// Only launch when run directly (bun run src/trending.ts), not when bundled
|
|
556
|
-
// into cli.js or imported as a module.
|
|
557
434
|
if (import.meta.main && !process.env.GHFIND_BUNDLED) {
|
|
558
435
|
launchTrending();
|
|
559
|
-
}
|
|
436
|
+
}
|