github-search-cli 9.4.1 → 9.4.3
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 +65 -1
- package/README.md +268 -237
- package/bin/ghfind.js +98 -89
- package/completions/ghfind.bash +1 -1
- package/completions/ghfind.fish +3 -1
- package/completions/ghfind.zsh +6 -0
- package/dist/{chunk-IVUU2KGR.js → chunk-GYIL4S5X.js} +4 -1
- package/dist/cli.js +1179 -403
- package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
- package/package.json +16 -5
- package/release-notes/9.3.0.md +32 -29
- package/scripts/build.ts +34 -19
- package/scripts/generate-svg-demo.js +218 -168
- package/scripts/postinstall.js +176 -176
- package/src/bookmarks.ts +69 -69
- package/src/cli.ts +650 -566
- package/src/compare.ts +69 -47
- package/src/config.ts +70 -65
- package/src/deepdive.ts +149 -118
- package/src/doctor.ts +287 -287
- package/src/error-report.ts +8 -12
- package/src/errors.ts +62 -59
- package/src/explore.ts +34 -29
- package/src/export.ts +73 -63
- package/src/help.ts +157 -157
- package/src/history.ts +69 -55
- package/src/init.ts +60 -55
- package/src/landing.ts +106 -104
- package/src/login.ts +186 -0
- package/src/markdown-render.ts +1 -6
- package/src/notifications.ts +75 -59
- package/src/open-url.ts +16 -16
- package/src/org.ts +299 -0
- package/src/output.ts +153 -137
- package/src/package-browser.ts +317 -305
- package/src/package.ts +149 -137
- package/src/pipe.ts +60 -54
- package/src/releases.ts +182 -144
- package/src/saved-searches.ts +72 -50
- package/src/search-state.ts +283 -229
- package/src/search.ts +678 -653
- package/src/session.ts +26 -24
- package/src/share.ts +61 -54
- package/src/status.ts +68 -61
- package/src/storage.ts +136 -106
- package/src/themes.ts +259 -116
- package/src/tips.ts +42 -42
- package/src/toast.ts +70 -69
- package/src/trending-parser.ts +104 -0
- package/src/trending.ts +481 -462
- package/src/tui.ts +3015 -2867
- package/src/types.ts +160 -158
- package/src/update-check.ts +231 -215
- package/src/version.ts +33 -0
- package/src/watch.ts +95 -74
package/src/trending.ts
CHANGED
|
@@ -1,462 +1,481 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/** trending.ts — "Trending Repositories" view for the GitHub repo browser CLI. */
|
|
3
|
-
import {
|
|
4
|
-
createCliRenderer,
|
|
5
|
-
BoxRenderable,
|
|
6
|
-
TextRenderable,
|
|
7
|
-
ScrollBoxRenderable,
|
|
8
|
-
StyledText,
|
|
9
|
-
bold,
|
|
10
|
-
fg,
|
|
11
|
-
dim,
|
|
12
|
-
t,
|
|
13
|
-
} from "@opentui/core";
|
|
14
|
-
import type {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (rank ===
|
|
69
|
-
return C.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
export
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
paddingX:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** trending.ts — "Trending Repositories" view for the GitHub repo browser CLI. */
|
|
3
|
+
import {
|
|
4
|
+
createCliRenderer,
|
|
5
|
+
BoxRenderable,
|
|
6
|
+
TextRenderable,
|
|
7
|
+
ScrollBoxRenderable,
|
|
8
|
+
StyledText,
|
|
9
|
+
bold,
|
|
10
|
+
fg,
|
|
11
|
+
dim,
|
|
12
|
+
t,
|
|
13
|
+
} from "@opentui/core";
|
|
14
|
+
import type { CliRenderer } from "@opentui/core";
|
|
15
|
+
import type { Repo, ParsedQuery } from "./types";
|
|
16
|
+
import { openUrl } from "./open-url";
|
|
17
|
+
import { SearchModule, TrendingAdapter } from "./search";
|
|
18
|
+
import { detectTerminalBackground, deriveSurfaceLayers } from "./themes";
|
|
19
|
+
// ─── Tokyo Night palette ──────────────────────────────────────────────
|
|
20
|
+
const C = {
|
|
21
|
+
bg: "#1a1b26",
|
|
22
|
+
surface: "#16161e",
|
|
23
|
+
text: "#a9b1d6",
|
|
24
|
+
muted: "#565f89",
|
|
25
|
+
cyan: "#7dcfff",
|
|
26
|
+
purple: "#bb9af7",
|
|
27
|
+
green: "#9ece6a",
|
|
28
|
+
gold: "#e0af68",
|
|
29
|
+
orange: "#ff9e64",
|
|
30
|
+
red: "#f7768e",
|
|
31
|
+
blue: "#7aa2f7",
|
|
32
|
+
lightBlue: "#b4f9f8",
|
|
33
|
+
selectionBg: "#3b6ce8",
|
|
34
|
+
selectionText: "#c0caf5",
|
|
35
|
+
border: "#363b54",
|
|
36
|
+
descText: "#6c7086", // lighter muted for descriptions
|
|
37
|
+
rankBg: "#2a2e42",
|
|
38
|
+
rankTop1: "#e0af68", // gold
|
|
39
|
+
rankTop2: "#9ece6a", // silver-ish green
|
|
40
|
+
rankTop3: "#ff9e64", // bronze-ish orange
|
|
41
|
+
rankRest: "#565f89", // muted gray
|
|
42
|
+
};
|
|
43
|
+
// ─── Language → color mapping ─────────────────────────────────────────
|
|
44
|
+
const LANG_COLORS: Record<string, string> = {
|
|
45
|
+
Rust: "#e0af68",
|
|
46
|
+
TypeScript: "#7dcfff",
|
|
47
|
+
JavaScript: "#e0d068",
|
|
48
|
+
Python: "#7aa2f7",
|
|
49
|
+
Go: "#73daca",
|
|
50
|
+
Java: "#f7768e",
|
|
51
|
+
Ruby: "#f7768e",
|
|
52
|
+
C: "#f7768e",
|
|
53
|
+
"C++": "#f7768e",
|
|
54
|
+
"C#": "#f7768e",
|
|
55
|
+
Shell: "#9ece6a",
|
|
56
|
+
Zig: "#e0af68",
|
|
57
|
+
Lua: "#7dcfff",
|
|
58
|
+
Haskell: "#bb9af7",
|
|
59
|
+
Elixir: "#bb9af7",
|
|
60
|
+
Dart: "#73daca",
|
|
61
|
+
Swift: "#f7768e",
|
|
62
|
+
Kotlin: "#7aa2f7",
|
|
63
|
+
};
|
|
64
|
+
function langColor(lang: string): string {
|
|
65
|
+
return LANG_COLORS[lang] ?? C.muted;
|
|
66
|
+
}
|
|
67
|
+
function rankColor(rank: number): string {
|
|
68
|
+
if (rank === 1) return C.rankTop1;
|
|
69
|
+
if (rank === 2) return C.rankTop2;
|
|
70
|
+
if (rank === 3) return C.rankTop3;
|
|
71
|
+
return C.rankRest;
|
|
72
|
+
}
|
|
73
|
+
// ─── Tab definitions ──────────────────────────────────────────────────
|
|
74
|
+
export const TAB_NAMES = ["Today", "This Week", "This Month"] as const;
|
|
75
|
+
export type TabName = (typeof TAB_NAMES)[number];
|
|
76
|
+
/** Map TabName to github.com/trending?since= param */
|
|
77
|
+
export function tabSince(tab: TabName): "daily" | "weekly" | "monthly" {
|
|
78
|
+
switch (tab) {
|
|
79
|
+
case "Today":
|
|
80
|
+
return "daily";
|
|
81
|
+
case "This Week":
|
|
82
|
+
return "weekly";
|
|
83
|
+
default:
|
|
84
|
+
return "monthly";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ─── Number formatting ────────────────────────────────────────────────
|
|
88
|
+
/** 1234 → "1.2k", 1234567 → "1.2M", 999999 → "1M" */
|
|
89
|
+
export function fmtStars(n: number): string {
|
|
90
|
+
if (n >= 1_000_000)
|
|
91
|
+
return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
|
|
92
|
+
if (n >= 999_500) return "1M"; // ponytail: round 999500+ up before showing 1000k
|
|
93
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
|
|
94
|
+
return String(n);
|
|
95
|
+
}
|
|
96
|
+
/** Format signed number: 1204 → "+1.2k" */
|
|
97
|
+
export function fmtSigned(n: number): string {
|
|
98
|
+
const abs = fmtStars(Math.abs(n));
|
|
99
|
+
return n >= 0 ? `+${abs}` : `-${abs}`;
|
|
100
|
+
}
|
|
101
|
+
// ─── Row formatting ───────────────────────────────────────────────────
|
|
102
|
+
/**
|
|
103
|
+
* Format one repo as a StyledText for per-chunk coloring.
|
|
104
|
+
*
|
|
105
|
+
* Layout (two lines):
|
|
106
|
+
* "[01] owner/name ● Rust ★ 89.2k ▲ +235 today"
|
|
107
|
+
* " Build smaller, faster desktop apps."
|
|
108
|
+
*
|
|
109
|
+
* Uses OpenTUI's StyledText + bold/fg/dim helpers for per-element colors:
|
|
110
|
+
* - Rank badge: colored background via bracket style, rank color
|
|
111
|
+
* - owner/name: bold white
|
|
112
|
+
* - Language dot: language-specific color
|
|
113
|
+
* - Stars: gold
|
|
114
|
+
* - Growth: green bold
|
|
115
|
+
* - Description: dim muted gray
|
|
116
|
+
*/
|
|
117
|
+
export function formatRepoLine(repo: Repo, rank: number): StyledText {
|
|
118
|
+
const rankStr = String(rank).padStart(2, "0");
|
|
119
|
+
const full = `${repo.owner}/${repo.name}`;
|
|
120
|
+
const lc = langColor(repo.language ?? "");
|
|
121
|
+
const rc = rankColor(rank);
|
|
122
|
+
const star = "★";
|
|
123
|
+
const arrow = repo.score > 0 ? "▲" : repo.score < 0 ? "▼" : "—";
|
|
124
|
+
const growth = `${arrow} ${fmtSigned(repo.score)} today`;
|
|
125
|
+
const desc =
|
|
126
|
+
(repo.description ?? "").length > 60
|
|
127
|
+
? (repo.description ?? "").slice(0, 57) + "..."
|
|
128
|
+
: (repo.description ?? "");
|
|
129
|
+
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))}`;
|
|
130
|
+
const line2 = t` ${dim(fg(C.muted)(desc))}`;
|
|
131
|
+
const chunks = [...line1.chunks, fg(C.text)("\n"), ...line2.chunks];
|
|
132
|
+
return new StyledText(chunks);
|
|
133
|
+
}
|
|
134
|
+
// ─── Tab query builder ────────────────────────────────────────────────
|
|
135
|
+
/**
|
|
136
|
+
* Build a GitHub search query string for the given time range tab.
|
|
137
|
+
*
|
|
138
|
+
* Examples:
|
|
139
|
+
* "Today" → "created:>2026-07-09 sort:stars"
|
|
140
|
+
* "This Week" → "created:>2026-07-03 sort:stars"
|
|
141
|
+
* "All Time" → "sort:stars"
|
|
142
|
+
*/
|
|
143
|
+
export function getTrendingQuery(tab: TabName): string {
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
const msPerDay = 86_400_000;
|
|
146
|
+
const ranges: Record<TabName, number> = {
|
|
147
|
+
Today: 1,
|
|
148
|
+
"This Week": 7,
|
|
149
|
+
"This Month": 30,
|
|
150
|
+
};
|
|
151
|
+
const days = ranges[tab];
|
|
152
|
+
if (days === 0) return "sort:stars";
|
|
153
|
+
const d = new Date(now - days * msPerDay);
|
|
154
|
+
return `created:>${d.toISOString().slice(0, 10)} sort:stars`;
|
|
155
|
+
}
|
|
156
|
+
// ─── Main TUI entry ───────────────────────────────────────────────────
|
|
157
|
+
export async function launchTrending(): Promise<void> {
|
|
158
|
+
// Match the app backdrop to the terminal's own background (OSC 11), before
|
|
159
|
+
// the renderer takes over stdin. Falls back to the Tokyo Night bg. Surface
|
|
160
|
+
// and border layers are blended toward that background for a seamless look.
|
|
161
|
+
const terminalBg = await detectTerminalBackground();
|
|
162
|
+
if (terminalBg) {
|
|
163
|
+
C.bg = terminalBg;
|
|
164
|
+
Object.assign(C, deriveSurfaceLayers(terminalBg));
|
|
165
|
+
}
|
|
166
|
+
let renderer: CliRenderer;
|
|
167
|
+
try {
|
|
168
|
+
renderer = await createCliRenderer();
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error(
|
|
171
|
+
"Failed to start the interactive browser.\n",
|
|
172
|
+
`Reason: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
173
|
+
);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const root = renderer.root;
|
|
178
|
+
root.flexDirection = "column";
|
|
179
|
+
renderer.setBackgroundColor(C.bg);
|
|
180
|
+
// ── State ──
|
|
181
|
+
let selectedTab = 1; // "This Week" default
|
|
182
|
+
let currentPeriod = "this week";
|
|
183
|
+
let repos: Repo[] = [];
|
|
184
|
+
let selectedRepoIdx = 0;
|
|
185
|
+
let isLoading = false;
|
|
186
|
+
// ── Fetch from github.com/trending ──
|
|
187
|
+
async function loadTab(index: number) {
|
|
188
|
+
if (index < 0 || index >= TAB_NAMES.length) return;
|
|
189
|
+
selectedTab = index;
|
|
190
|
+
const since = tabSince(TAB_NAMES[index]);
|
|
191
|
+
currentPeriod =
|
|
192
|
+
since === "daily"
|
|
193
|
+
? "today"
|
|
194
|
+
: since === "weekly"
|
|
195
|
+
? "this week"
|
|
196
|
+
: "this month";
|
|
197
|
+
isLoading = true;
|
|
198
|
+
for (const rb of rowBoxes) scrollBox.remove(rb);
|
|
199
|
+
rowBoxes.length = 0;
|
|
200
|
+
rowTexts.length = 0;
|
|
201
|
+
const loadingText = new TextRenderable(renderer, {
|
|
202
|
+
content: t`${dim(fg(C.muted)(` Loading trending repos...`))}`,
|
|
203
|
+
bg: C.bg,
|
|
204
|
+
height: 1,
|
|
205
|
+
});
|
|
206
|
+
scrollBox.add(loadingText);
|
|
207
|
+
renderer.requestRender();
|
|
208
|
+
try {
|
|
209
|
+
const searchModule = new SearchModule(new TrendingAdapter());
|
|
210
|
+
const parsed: ParsedQuery = {
|
|
211
|
+
keywords: [],
|
|
212
|
+
qualifiers: [],
|
|
213
|
+
raw: "trending",
|
|
214
|
+
};
|
|
215
|
+
const response = await searchModule.search(parsed, {
|
|
216
|
+
limit: 25,
|
|
217
|
+
sort: "stars",
|
|
218
|
+
json: false,
|
|
219
|
+
verbose: false,
|
|
220
|
+
trendingSince: tabSince(TAB_NAMES[index]),
|
|
221
|
+
});
|
|
222
|
+
scrollBox.remove(loadingText);
|
|
223
|
+
repos = response.repos;
|
|
224
|
+
selectedRepoIdx = 0;
|
|
225
|
+
rebuildList();
|
|
226
|
+
} catch (err) {
|
|
227
|
+
scrollBox.remove(loadingText);
|
|
228
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
229
|
+
const errText = new TextRenderable(renderer, {
|
|
230
|
+
content: t`${fg(C.red)(` Error: ${msg.slice(0, 70)}`)}`,
|
|
231
|
+
bg: C.bg,
|
|
232
|
+
height: 1,
|
|
233
|
+
});
|
|
234
|
+
scrollBox.add(errText);
|
|
235
|
+
}
|
|
236
|
+
isLoading = false;
|
|
237
|
+
renderTabBar();
|
|
238
|
+
renderer.requestRender();
|
|
239
|
+
}
|
|
240
|
+
// ── Switch tab ──
|
|
241
|
+
function switchTab(index: number) {
|
|
242
|
+
if (index < 0 || index >= TAB_NAMES.length || index === selectedTab) return;
|
|
243
|
+
loadTab(index);
|
|
244
|
+
}
|
|
245
|
+
// ── Outer frame ──
|
|
246
|
+
const outerBox = new BoxRenderable(renderer, {
|
|
247
|
+
flexGrow: 1,
|
|
248
|
+
flexDirection: "column",
|
|
249
|
+
backgroundColor: C.bg,
|
|
250
|
+
border: true,
|
|
251
|
+
borderColor: C.border,
|
|
252
|
+
borderStyle: "rounded",
|
|
253
|
+
paddingX: 0,
|
|
254
|
+
paddingY: 0,
|
|
255
|
+
});
|
|
256
|
+
// ── Header bar ──
|
|
257
|
+
const headerBox = new BoxRenderable(renderer, {
|
|
258
|
+
height: 1,
|
|
259
|
+
backgroundColor: C.bg,
|
|
260
|
+
paddingX: 1,
|
|
261
|
+
});
|
|
262
|
+
const headerText = new TextRenderable(renderer, {
|
|
263
|
+
content: t`${bold(fg(C.purple)("⟠ tulipsearch"))}${fg(C.muted)(" — Trending")}`,
|
|
264
|
+
height: 1,
|
|
265
|
+
});
|
|
266
|
+
headerBox.add(headerText);
|
|
267
|
+
outerBox.add(headerBox);
|
|
268
|
+
// ── Tab bar ──
|
|
269
|
+
const tabBox = new BoxRenderable(renderer, {
|
|
270
|
+
flexDirection: "row",
|
|
271
|
+
height: 1,
|
|
272
|
+
backgroundColor: C.bg,
|
|
273
|
+
paddingX: 1,
|
|
274
|
+
});
|
|
275
|
+
const tabTexts: TextRenderable[] = [];
|
|
276
|
+
const underlineBox = new BoxRenderable(renderer, {
|
|
277
|
+
height: 1,
|
|
278
|
+
backgroundColor: C.bg,
|
|
279
|
+
paddingX: 1,
|
|
280
|
+
});
|
|
281
|
+
function tabWidth(name: string): number {
|
|
282
|
+
return name.length + 2; // " name " padding
|
|
283
|
+
}
|
|
284
|
+
function renderTabBar() {
|
|
285
|
+
for (const t of tabTexts) tabBox.remove(t);
|
|
286
|
+
tabTexts.length = 0;
|
|
287
|
+
TAB_NAMES.forEach((name, i) => {
|
|
288
|
+
const isActive = i === selectedTab;
|
|
289
|
+
const label = ` ${name} `;
|
|
290
|
+
const tt = new TextRenderable(renderer, {
|
|
291
|
+
content: isActive
|
|
292
|
+
? t`${bold(fg(C.bg)(label))}`
|
|
293
|
+
: t`${fg(C.muted)(label)}`,
|
|
294
|
+
fg: isActive ? C.bg : C.muted,
|
|
295
|
+
bg: isActive ? C.cyan : C.bg,
|
|
296
|
+
height: 1,
|
|
297
|
+
});
|
|
298
|
+
tabTexts.push(tt);
|
|
299
|
+
tabBox.add(tt);
|
|
300
|
+
if (i < TAB_NAMES.length - 1) {
|
|
301
|
+
const sp = new TextRenderable(renderer, {
|
|
302
|
+
content: " ",
|
|
303
|
+
fg: C.muted,
|
|
304
|
+
bg: C.bg,
|
|
305
|
+
height: 1,
|
|
306
|
+
});
|
|
307
|
+
tabTexts.push(sp);
|
|
308
|
+
tabBox.add(sp);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
// ── cyan underline bar under active tab ──
|
|
312
|
+
let offset = 1; // paddingX
|
|
313
|
+
for (let i = 0; i < selectedTab; i++) {
|
|
314
|
+
offset += tabWidth(TAB_NAMES[i]) + 2; // tab + separator
|
|
315
|
+
}
|
|
316
|
+
const w = tabWidth(TAB_NAMES[selectedTab]);
|
|
317
|
+
const dots = "┉".repeat(w);
|
|
318
|
+
underlineBox.remove(underlineText);
|
|
319
|
+
underlineText.content = t`${fg(C.cyan)(dots)}`;
|
|
320
|
+
underlineText.left = offset;
|
|
321
|
+
}
|
|
322
|
+
const underlineText = new TextRenderable(renderer, {
|
|
323
|
+
content: "",
|
|
324
|
+
fg: C.cyan,
|
|
325
|
+
bg: C.bg,
|
|
326
|
+
height: 1,
|
|
327
|
+
});
|
|
328
|
+
underlineBox.add(underlineText);
|
|
329
|
+
renderTabBar();
|
|
330
|
+
outerBox.add(tabBox);
|
|
331
|
+
outerBox.add(underlineBox);
|
|
332
|
+
const divider = new TextRenderable(renderer, {
|
|
333
|
+
content: "─".repeat(80),
|
|
334
|
+
fg: C.border,
|
|
335
|
+
height: 1,
|
|
336
|
+
bg: C.bg,
|
|
337
|
+
});
|
|
338
|
+
outerBox.add(divider);
|
|
339
|
+
// ── Repo list (scrollable) ──
|
|
340
|
+
const scrollBox = new ScrollBoxRenderable(renderer, {
|
|
341
|
+
flexGrow: 1,
|
|
342
|
+
backgroundColor: C.bg,
|
|
343
|
+
scrollY: true,
|
|
344
|
+
scrollX: false,
|
|
345
|
+
viewportCulling: true,
|
|
346
|
+
});
|
|
347
|
+
const rowBoxes: BoxRenderable[] = [];
|
|
348
|
+
const rowTexts: TextRenderable[] = [];
|
|
349
|
+
function rebuildList() {
|
|
350
|
+
for (const rb of rowBoxes) scrollBox.remove(rb);
|
|
351
|
+
rowBoxes.length = 0;
|
|
352
|
+
rowTexts.length = 0;
|
|
353
|
+
repos.forEach((r, i) => {
|
|
354
|
+
const isEven = i % 2 === 0;
|
|
355
|
+
const isSelected = i === selectedRepoIdx;
|
|
356
|
+
const rowBg = isSelected ? C.selectionBg : isEven ? C.bg : C.surface;
|
|
357
|
+
const rank = i + 1;
|
|
358
|
+
const rc = rankColor(rank);
|
|
359
|
+
const lc = langColor(r.language ?? "");
|
|
360
|
+
const desc =
|
|
361
|
+
(r.description ?? "").length > 55
|
|
362
|
+
? (r.description ?? "").slice(0, 52) + "..."
|
|
363
|
+
: (r.description ?? "");
|
|
364
|
+
const arrow = r.score > 0 ? "▲" : r.score < 0 ? "▼" : "—";
|
|
365
|
+
const growthStr = `${arrow} ${fmtStars(Math.abs(r.score))} ${currentPeriod}`;
|
|
366
|
+
const rankStr = `[${String(rank).padStart(2, "0")}]`;
|
|
367
|
+
const nameStr = `${r.owner}/${r.name}`.padEnd(30).slice(0, 30);
|
|
368
|
+
const langStr = `● ${r.language || "—"}`.padEnd(14).slice(0, 14);
|
|
369
|
+
const starsStr = `★ ${fmtStars(r.stars)}`.padEnd(12).slice(0, 12);
|
|
370
|
+
const rankText = new TextRenderable(renderer, {
|
|
371
|
+
content: t`${bold(fg(rc)(rankStr))}`,
|
|
372
|
+
bg: C.rankBg,
|
|
373
|
+
fg: rc,
|
|
374
|
+
height: 1,
|
|
375
|
+
});
|
|
376
|
+
const descStr = desc.length > 60 ? desc.slice(0, 57) + "..." : desc;
|
|
377
|
+
const pad = " ";
|
|
378
|
+
const line1 = t`${bold(fg("#c0caf5")(nameStr))}${fg(lc)(langStr)}${fg(C.muted)(starsStr)}${bold(fg(C.green)(growthStr))}`;
|
|
379
|
+
const line2 = t`${fg(C.descText)(`${pad}${descStr}`)}`;
|
|
380
|
+
const chunks = [...line1.chunks, fg(C.text)("\n"), ...line2.chunks];
|
|
381
|
+
const rowText = new TextRenderable(renderer, {
|
|
382
|
+
content: new StyledText(chunks),
|
|
383
|
+
bg: rowBg,
|
|
384
|
+
height: 2,
|
|
385
|
+
});
|
|
386
|
+
const rowBox = new BoxRenderable(renderer, {
|
|
387
|
+
flexDirection: "row",
|
|
388
|
+
backgroundColor: rowBg,
|
|
389
|
+
});
|
|
390
|
+
rowBox.add(rankText);
|
|
391
|
+
rowBox.add(rowText);
|
|
392
|
+
rowBoxes.push(rowBox);
|
|
393
|
+
rowTexts.push(rowText);
|
|
394
|
+
scrollBox.add(rowBox);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
outerBox.add(scrollBox);
|
|
398
|
+
// ── Footer bar ──
|
|
399
|
+
const footerBox = new BoxRenderable(renderer, {
|
|
400
|
+
height: 1,
|
|
401
|
+
backgroundColor: C.bg,
|
|
402
|
+
paddingX: 1,
|
|
403
|
+
});
|
|
404
|
+
const footerText = new TextRenderable(renderer, {
|
|
405
|
+
content: " ↑↓ navigate ↵ open / search 1-3 tab q quit r refresh",
|
|
406
|
+
fg: C.muted,
|
|
407
|
+
height: 1,
|
|
408
|
+
});
|
|
409
|
+
footerBox.add(footerText);
|
|
410
|
+
outerBox.add(footerBox);
|
|
411
|
+
root.add(outerBox);
|
|
412
|
+
// ── Helpers ──
|
|
413
|
+
function openSelectedRepo() {
|
|
414
|
+
if (selectedRepoIdx < 0 || selectedRepoIdx >= repos.length) return;
|
|
415
|
+
const r = repos[selectedRepoIdx];
|
|
416
|
+
openUrl(`https://github.com/${r.owner}/${r.name}`);
|
|
417
|
+
}
|
|
418
|
+
function moveSelection(delta: number) {
|
|
419
|
+
const oldIdx = selectedRepoIdx;
|
|
420
|
+
const newIdx = Math.max(0, Math.min(repos.length - 1, oldIdx + delta));
|
|
421
|
+
if (newIdx === oldIdx) return;
|
|
422
|
+
selectedRepoIdx = newIdx;
|
|
423
|
+
const setBg = (idx: number, selected: boolean) => {
|
|
424
|
+
if (idx < 0 || idx >= rowBoxes.length) return;
|
|
425
|
+
const bg = selected ? C.selectionBg : idx % 2 === 0 ? C.bg : C.surface;
|
|
426
|
+
rowBoxes[idx].backgroundColor = bg;
|
|
427
|
+
if (rowTexts[idx]) rowTexts[idx].bg = bg;
|
|
428
|
+
};
|
|
429
|
+
setBg(oldIdx, false);
|
|
430
|
+
setBg(newIdx, true);
|
|
431
|
+
renderer.requestRender();
|
|
432
|
+
}
|
|
433
|
+
// ── Global key events ──
|
|
434
|
+
renderer.keyInput.on("keypress", (key) => {
|
|
435
|
+
const name = key.name || "";
|
|
436
|
+
if (name === "q") {
|
|
437
|
+
process.exit(0);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (/^[1-3]$/.test(name)) {
|
|
441
|
+
switchTab(parseInt(name, 10) - 1);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (name === "left" || name === "h") {
|
|
445
|
+
switchTab(selectedTab - 1);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (name === "right" || name === "l") {
|
|
449
|
+
switchTab(selectedTab + 1);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (name === "up" || name === "k") {
|
|
453
|
+
moveSelection(-1);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (name === "down" || name === "j") {
|
|
457
|
+
moveSelection(1);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (name === "enter" || name === "return" || name === "o") {
|
|
461
|
+
openSelectedRepo();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (name === "/") {
|
|
465
|
+
moveSelection(-selectedRepoIdx);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (name === "r") {
|
|
469
|
+
loadTab(selectedTab);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
// ── Start ──
|
|
474
|
+
renderer.start();
|
|
475
|
+
renderer.requestRender();
|
|
476
|
+
loadTab(selectedTab);
|
|
477
|
+
}
|
|
478
|
+
// ── Auto-run ──────────────────────────────────────────────────────────
|
|
479
|
+
if (import.meta.main && !process.env.GHFIND_BUNDLED) {
|
|
480
|
+
launchTrending();
|
|
481
|
+
}
|