github-search-cli 0.7.0 → 0.8.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 +66 -104
- package/bin/ghfind.js +14 -0
- package/completions/{search-cli.bash → ghfind.bash} +4 -4
- package/completions/ghfind.fish +19 -0
- package/completions/{search-cli.zsh → ghfind.zsh} +27 -27
- package/package.json +17 -9
- package/src/bookmarks.ts +69 -69
- package/src/cli.ts +239 -252
- package/src/compare.ts +46 -46
- package/src/config.ts +65 -65
- package/src/deepdive.ts +118 -124
- package/src/errors.ts +59 -62
- package/src/explore.ts +29 -30
- package/src/export.ts +63 -64
- package/src/help.ts +22 -30
- package/src/history.ts +91 -91
- package/src/init.ts +55 -55
- package/src/notifications.ts +65 -69
- package/src/open-url.ts +0 -1
- package/src/output.ts +67 -74
- package/src/pipe.ts +54 -60
- package/src/saved-searches.ts +58 -58
- package/src/search.ts +555 -0
- package/src/session.ts +36 -37
- package/src/share.ts +54 -49
- package/src/status.ts +61 -61
- package/src/themes.ts +52 -52
- package/src/tips.ts +27 -27
- package/src/trending.ts +556 -563
- package/src/tui.ts +1665 -1520
- package/src/types.ts +139 -147
- package/src/watch.ts +62 -65
- 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,1520 +1,1665 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Full-screen, keyboard-driven browser. This is the only entry point.
|
|
6
|
-
* Pipeline: user query → provider → normalizer → ranking → rendered in TUI
|
|
7
|
-
*
|
|
8
|
-
* Layout:
|
|
9
|
-
* ┌─
|
|
10
|
-
* │ > [query input ........................................] │
|
|
11
|
-
* │ sort: best-match limit: 50 │
|
|
12
|
-
* ├─── Results ────────────────┬─── Details ──────────────────┤
|
|
13
|
-
* │ owner/repo ★69k │ Name owner/repo │
|
|
14
|
-
* │ owner/repo ★42k │ Stars 69,420 │
|
|
15
|
-
* │ owner/repo ★12k │ Forks 1,337 │
|
|
16
|
-
* │ owner/repo ★5k │ Language TypeScript │
|
|
17
|
-
* │ owner/repo ★2k │ Updated 2026-07-09 │
|
|
18
|
-
* │ owner/repo ★1k │ Topics ai, agent, tui │
|
|
19
|
-
* │ │ │
|
|
20
|
-
* │ │ A description... │
|
|
21
|
-
* │ │ https://github.com/... │
|
|
22
|
-
* ├──────────────────────────┴────────────────────────────────┤
|
|
23
|
-
* │ 42 results of 15000 (↑↓ nav Enter open o browser) │
|
|
24
|
-
* └───────────────────────────────────────────────────────────┘
|
|
25
|
-
*/
|
|
26
|
-
import {
|
|
27
|
-
createCliRenderer,
|
|
28
|
-
BoxRenderable,
|
|
29
|
-
TextRenderable,
|
|
30
|
-
InputRenderable,
|
|
31
|
-
SelectRenderable,
|
|
32
|
-
ScrollBoxRenderable,
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
{ key: "
|
|
84
|
-
{ key: "
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
let
|
|
121
|
-
let
|
|
122
|
-
let
|
|
123
|
-
let
|
|
124
|
-
let
|
|
125
|
-
let
|
|
126
|
-
let
|
|
127
|
-
let
|
|
128
|
-
let
|
|
129
|
-
let
|
|
130
|
-
let
|
|
131
|
-
let
|
|
132
|
-
let
|
|
133
|
-
let
|
|
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
|
-
const
|
|
163
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
body.add(
|
|
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
|
-
else if (type === "
|
|
313
|
-
else if (type === "
|
|
314
|
-
else if (type === "
|
|
315
|
-
else if (type === "
|
|
316
|
-
else if (type === "
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
const
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
if
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
if (
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ghfind — interactive GitHub repository browser (OpenTUI)
|
|
4
|
+
*
|
|
5
|
+
* Full-screen, keyboard-driven browser. This is the only entry point.
|
|
6
|
+
* Pipeline: user query → provider → normalizer → ranking → rendered in TUI
|
|
7
|
+
*
|
|
8
|
+
* Layout:
|
|
9
|
+
* ┌─ ghfind [s]sort [l]limit [r]refresh [q]uit ────────────────┐
|
|
10
|
+
* │ > [query input ........................................] │
|
|
11
|
+
* │ sort: best-match limit: 50 │
|
|
12
|
+
* ├─── Results ────────────────┬─── Details ──────────────────┤
|
|
13
|
+
* │ owner/repo ★69k │ Name owner/repo │
|
|
14
|
+
* │ owner/repo ★42k │ Stars 69,420 │
|
|
15
|
+
* │ owner/repo ★12k │ Forks 1,337 │
|
|
16
|
+
* │ owner/repo ★5k │ Language TypeScript │
|
|
17
|
+
* │ owner/repo ★2k │ Updated 2026-07-09 │
|
|
18
|
+
* │ owner/repo ★1k │ Topics ai, agent, tui │
|
|
19
|
+
* │ │ │
|
|
20
|
+
* │ │ A description... │
|
|
21
|
+
* │ │ https://github.com/... │
|
|
22
|
+
* ├──────────────────────────┴────────────────────────────────┤
|
|
23
|
+
* │ 42 results of 15000 (↑↓ nav Enter open o browser) │
|
|
24
|
+
* └───────────────────────────────────────────────────────────┘
|
|
25
|
+
*/
|
|
26
|
+
import {
|
|
27
|
+
createCliRenderer,
|
|
28
|
+
BoxRenderable,
|
|
29
|
+
TextRenderable,
|
|
30
|
+
InputRenderable,
|
|
31
|
+
SelectRenderable,
|
|
32
|
+
ScrollBoxRenderable,
|
|
33
|
+
} from "@opentui/core";
|
|
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";
|
|
54
|
+
|
|
55
|
+
// ─── Theme ────────────────────────────────────────────────────────────
|
|
56
|
+
const colors = {
|
|
57
|
+
bg: "#1e1e2e",
|
|
58
|
+
surface: "#181825",
|
|
59
|
+
text: "#cdd6f4",
|
|
60
|
+
muted: "#6c7086",
|
|
61
|
+
blue: "#89b4fa",
|
|
62
|
+
green: "#a6e3a1",
|
|
63
|
+
yellow: "#f9e2af",
|
|
64
|
+
red: "#f38ba8",
|
|
65
|
+
teal: "#94e2d5",
|
|
66
|
+
border: "#45475a",
|
|
67
|
+
selectionBg: "#2d5bcf",
|
|
68
|
+
selectionText: "#ffffff",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ─── Logger ───────────────────────────────────────────────────────────
|
|
72
|
+
const logger: Logger = {
|
|
73
|
+
debug: () => {},
|
|
74
|
+
info: () => {},
|
|
75
|
+
warn: () => {},
|
|
76
|
+
error: () => {},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ─── Sort cycle ───────────────────────────────────────────────────────
|
|
80
|
+
const SORT_MODES: { key: SortStrategy; label: string }[] = [
|
|
81
|
+
{ key: "best-match", label: "best-match" },
|
|
82
|
+
{ key: "stars", label: "stars" },
|
|
83
|
+
{ key: "updated", label: "updated" },
|
|
84
|
+
{ key: "forks", label: "forks" },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
// ─── Main ─────────────────────────────────────────────────────────────
|
|
88
|
+
export async function launchBrowser(): Promise<void> {
|
|
89
|
+
let renderer;
|
|
90
|
+
try {
|
|
91
|
+
renderer = await createCliRenderer();
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error(
|
|
94
|
+
"Failed to start the interactive browser. OpenTUI native backend unavailable.\n",
|
|
95
|
+
`Reason: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
96
|
+
"Install Zig or use a platform with native OpenTUI support.",
|
|
97
|
+
);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const root = renderer.root;
|
|
103
|
+
root.flexDirection = "column";
|
|
104
|
+
root.backgroundColor = colors.bg;
|
|
105
|
+
|
|
106
|
+
// ── Config ──
|
|
107
|
+
const config = loadConfig();
|
|
108
|
+
const githubToken = config.githubToken || process.env.GITHUB_TOKEN;
|
|
109
|
+
|
|
110
|
+
// Apply theme
|
|
111
|
+
const theme = loadTheme(config.theme);
|
|
112
|
+
Object.assign(colors, theme);
|
|
113
|
+
|
|
114
|
+
// ── Session restore ──
|
|
115
|
+
const session = restoreSession();
|
|
116
|
+
|
|
117
|
+
// ── State ───────────────────────────────────────────────────────────
|
|
118
|
+
let currentRepos: Repo[] = [];
|
|
119
|
+
let currentSort: SortStrategy = session?.sort ?? config.defaultSort;
|
|
120
|
+
let currentLimit = session?.limit ?? config.defaultLimit;
|
|
121
|
+
let currentQueryInput = session?.query ?? "";
|
|
122
|
+
let isLoading = false;
|
|
123
|
+
let currentMode: "search" | "trending" = session?.mode ?? "search";
|
|
124
|
+
let trendingTab: typeof TAB_NAMES[number] = (session?.trendingTab as typeof TAB_NAMES[number]) ?? "This Week";
|
|
125
|
+
let trendingPeriod = "this week";
|
|
126
|
+
let graphFullscreen = false;
|
|
127
|
+
let chartCommitData: number[] = [];
|
|
128
|
+
let currentOverlay: "none" | "history" | "bookmarks" | "saved" | "help" | "topics" | "export" | "compare" | "notifications" | "share" | "leader" | "readme" = "none";
|
|
129
|
+
let currentPage = 1;
|
|
130
|
+
let totalCount = 0;
|
|
131
|
+
let deepDiveActive = false;
|
|
132
|
+
let compareList: Repo[] = [];
|
|
133
|
+
let quitArmed = false;
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
// ── Header bar ──────────────────────────────────────────────────────
|
|
137
|
+
const header = new TextRenderable(renderer, {
|
|
138
|
+
content:
|
|
139
|
+
" ghfind — GitHub repo browser [/]search [Space]menu [?]help [q]uit",
|
|
140
|
+
backgroundColor: colors.bg,
|
|
141
|
+
color: colors.muted,
|
|
142
|
+
height: 1,
|
|
143
|
+
});
|
|
144
|
+
root.add(header);
|
|
145
|
+
|
|
146
|
+
// ── Trending tab bar (hidden in search mode) ────────────────────────
|
|
147
|
+
const trendingTabBox = new BoxRenderable(renderer, {
|
|
148
|
+
flexDirection: "row",
|
|
149
|
+
height: 1,
|
|
150
|
+
backgroundColor: colors.bg,
|
|
151
|
+
paddingX: 1,
|
|
152
|
+
visible: false,
|
|
153
|
+
});
|
|
154
|
+
// Hidden by default; shown when currentMode === "trending"
|
|
155
|
+
const trendingTabTexts: TextRenderable[] = [];
|
|
156
|
+
|
|
157
|
+
function renderTrendingTabs() {
|
|
158
|
+
for (const t of trendingTabTexts) trendingTabBox.remove(t);
|
|
159
|
+
trendingTabTexts.length = 0;
|
|
160
|
+
TAB_NAMES.forEach((name, i) => {
|
|
161
|
+
const isActive = name === trendingTab;
|
|
162
|
+
const label = ` ${name} `;
|
|
163
|
+
const tt = new TextRenderable(renderer, {
|
|
164
|
+
content: label,
|
|
165
|
+
color: isActive ? colors.bg : colors.muted,
|
|
166
|
+
backgroundColor: isActive ? colors.blue : colors.bg,
|
|
167
|
+
height: 1,
|
|
168
|
+
});
|
|
169
|
+
trendingTabTexts.push(tt);
|
|
170
|
+
trendingTabBox.add(tt);
|
|
171
|
+
if (i < TAB_NAMES.length - 1) {
|
|
172
|
+
const sp = new TextRenderable(renderer, {
|
|
173
|
+
content: " ",
|
|
174
|
+
color: colors.muted,
|
|
175
|
+
backgroundColor: colors.bg,
|
|
176
|
+
height: 1,
|
|
177
|
+
});
|
|
178
|
+
trendingTabTexts.push(sp);
|
|
179
|
+
trendingTabBox.add(sp);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
root.add(trendingTabBox);
|
|
184
|
+
|
|
185
|
+
// ── Search input row ────────────────────────────────────────────────
|
|
186
|
+
const searchBox = new BoxRenderable(renderer, {
|
|
187
|
+
visible: true, // hidden in trending mode
|
|
188
|
+
border: true,
|
|
189
|
+
borderColor: colors.blue,
|
|
190
|
+
paddingX: 1,
|
|
191
|
+
height: 3,
|
|
192
|
+
});
|
|
193
|
+
const searchInput = new InputRenderable(renderer, {
|
|
194
|
+
placeholder: "Search GitHub repos (e.g. rust cli, or language:Rust stars:>100)",
|
|
195
|
+
value: "",
|
|
196
|
+
});
|
|
197
|
+
searchBox.add(searchInput);
|
|
198
|
+
root.add(searchBox);
|
|
199
|
+
|
|
200
|
+
// ── Toolbar (sort + limit indicator) ────────────────────────────────
|
|
201
|
+
const toolbarText = new TextRenderable(renderer, {
|
|
202
|
+
content: formatToolbar(currentSort, currentLimit),
|
|
203
|
+
visible: true, // hidden in trending mode
|
|
204
|
+
backgroundColor: colors.bg,
|
|
205
|
+
color: colors.muted,
|
|
206
|
+
height: 1,
|
|
207
|
+
paddingX: 1,
|
|
208
|
+
});
|
|
209
|
+
root.add(toolbarText);
|
|
210
|
+
|
|
211
|
+
// ── Body: results + detail ──────────────────────────────────────────
|
|
212
|
+
const body = new BoxRenderable(renderer, {
|
|
213
|
+
flexDirection: "row",
|
|
214
|
+
flexGrow: 1,
|
|
215
|
+
gap: 1,
|
|
216
|
+
paddingY: 1,
|
|
217
|
+
paddingX: 1,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Results pane
|
|
221
|
+
const resultsBox = new BoxRenderable(renderer, {
|
|
222
|
+
border: true,
|
|
223
|
+
borderColor: colors.border,
|
|
224
|
+
title: " Results ",
|
|
225
|
+
titleColor: colors.blue,
|
|
226
|
+
width: "50%",
|
|
227
|
+
flexDirection: "column",
|
|
228
|
+
paddingLeft: 0,
|
|
229
|
+
focusable: false,
|
|
230
|
+
});
|
|
231
|
+
const resultsSelect = new SelectRenderable(renderer, {
|
|
232
|
+
options: [{ name: "", description: "Type a query and press Enter to search", value: null }],
|
|
233
|
+
showDescription: false,
|
|
234
|
+
showSelectionIndicator: false,
|
|
235
|
+
flexGrow: 1,
|
|
236
|
+
backgroundColor: colors.bg,
|
|
237
|
+
textColor: colors.text,
|
|
238
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
239
|
+
selectedTextColor: colors.selectionText,
|
|
240
|
+
focusedBackgroundColor: colors.bg,
|
|
241
|
+
focusedTextColor: colors.text,
|
|
242
|
+
itemSpacing: 0,
|
|
243
|
+
});
|
|
244
|
+
resultsBox.add(resultsSelect);
|
|
245
|
+
|
|
246
|
+
// Detail / Graph pane
|
|
247
|
+
const detailBox = new BoxRenderable(renderer, {
|
|
248
|
+
border: true,
|
|
249
|
+
borderColor: colors.border,
|
|
250
|
+
title: " Details ",
|
|
251
|
+
titleColor: colors.green,
|
|
252
|
+
width: "50%",
|
|
253
|
+
flexDirection: "column",
|
|
254
|
+
paddingX: 1,
|
|
255
|
+
paddingY: 1,
|
|
256
|
+
focusable: false,
|
|
257
|
+
});
|
|
258
|
+
const detailText = new TextRenderable(renderer, {
|
|
259
|
+
content: "",
|
|
260
|
+
color: colors.text,
|
|
261
|
+
});
|
|
262
|
+
detailBox.add(detailText);
|
|
263
|
+
|
|
264
|
+
body.add(resultsBox);
|
|
265
|
+
body.add(detailBox);
|
|
266
|
+
root.add(body);
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
// ── Overlay system ──────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
// Hide main content so overlays can take 100% of the content area
|
|
273
|
+
function hideMainContent() {
|
|
274
|
+
trendingTabBox.visible = false;
|
|
275
|
+
searchBox.visible = false;
|
|
276
|
+
toolbarText.visible = false;
|
|
277
|
+
body.visible = false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function showMainContent() {
|
|
281
|
+
if (currentMode === "trending") {
|
|
282
|
+
trendingTabBox.visible = true;
|
|
283
|
+
} else {
|
|
284
|
+
searchBox.visible = true;
|
|
285
|
+
toolbarText.visible = true;
|
|
286
|
+
}
|
|
287
|
+
body.visible = true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function showOverlay(type: typeof currentOverlay) {
|
|
291
|
+
helpBox.visible = false;
|
|
292
|
+
historyBox.visible = false;
|
|
293
|
+
bookmarksBox.visible = false;
|
|
294
|
+
savedBox.visible = false;
|
|
295
|
+
topicsBox.visible = false;
|
|
296
|
+
exportBox.visible = false;
|
|
297
|
+
compareBox.visible = false;
|
|
298
|
+
notifsBox.visible = false;
|
|
299
|
+
shareBox.visible = false;
|
|
300
|
+
leaderBox.visible = false;
|
|
301
|
+
readmeBox.visible = false;
|
|
302
|
+
|
|
303
|
+
if (type === "none") {
|
|
304
|
+
currentOverlay = "none";
|
|
305
|
+
showMainContent();
|
|
306
|
+
renderer.requestRender();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
currentOverlay = type;
|
|
310
|
+
hideMainContent();
|
|
311
|
+
if (type === "help") { helpBox.visible = true; }
|
|
312
|
+
else if (type === "history") { historyBox.visible = true; }
|
|
313
|
+
else if (type === "bookmarks") { bookmarksBox.visible = true; }
|
|
314
|
+
else if (type === "saved") { savedBox.visible = true; }
|
|
315
|
+
else if (type === "topics") { topicsBox.visible = true; }
|
|
316
|
+
else if (type === "export") { exportBox.visible = true; }
|
|
317
|
+
else if (type === "compare") { compareBox.visible = true; }
|
|
318
|
+
else if (type === "notifications") { notifsBox.visible = true; }
|
|
319
|
+
else if (type === "share") { shareBox.visible = true; }
|
|
320
|
+
else if (type === "leader") { leaderBox.visible = true; }
|
|
321
|
+
else if (type === "readme") { readmeBox.visible = true; }
|
|
322
|
+
renderer.requestRender();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── Help overlay (OpenTUI components) ────────────────────────
|
|
326
|
+
const helpSections = buildHelpSections();
|
|
327
|
+
const helpBox = new BoxRenderable(renderer, {
|
|
328
|
+
visible: false,
|
|
329
|
+
flexGrow: 1,
|
|
330
|
+
backgroundColor: colors.bg,
|
|
331
|
+
border: true,
|
|
332
|
+
borderColor: colors.border,
|
|
333
|
+
});
|
|
334
|
+
const helpScroll = new ScrollBoxRenderable(renderer, {
|
|
335
|
+
flexGrow: 1,
|
|
336
|
+
backgroundColor: colors.bg,
|
|
337
|
+
scrollY: true,
|
|
338
|
+
scrollX: false,
|
|
339
|
+
paddingX: 0,
|
|
340
|
+
viewportOptions: { backgroundColor: colors.bg },
|
|
341
|
+
contentOptions: { backgroundColor: colors.bg, flexDirection: "column" },
|
|
342
|
+
scrollbarOptions: {
|
|
343
|
+
backgroundColor: colors.bg,
|
|
344
|
+
foregroundColor: colors.muted,
|
|
345
|
+
width: 1,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
for (const section of helpSections) {
|
|
349
|
+
const sectionBox = new BoxRenderable(renderer, {
|
|
350
|
+
border: true,
|
|
351
|
+
borderColor: colors.border,
|
|
352
|
+
title: section.title,
|
|
353
|
+
titleColor: (colors as Record<string, string>)[section.titleColor] ?? section.titleColor,
|
|
354
|
+
flexDirection: "column",
|
|
355
|
+
paddingY: 0,
|
|
356
|
+
marginLeft: 1,
|
|
357
|
+
marginRight: 1,
|
|
358
|
+
marginTop: 1,
|
|
359
|
+
});
|
|
360
|
+
for (const row of section.rows) {
|
|
361
|
+
const keys = row.keys.padEnd(HELP_KEYS_COLUMN);
|
|
362
|
+
sectionBox.add(new TextRenderable(renderer, {
|
|
363
|
+
content: ` ${keys} ${row.action}`,
|
|
364
|
+
color: colors.text,
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
if (section.note) {
|
|
368
|
+
sectionBox.add(new TextRenderable(renderer, {
|
|
369
|
+
content: ` ${section.note}`,
|
|
370
|
+
color: colors.muted,
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
helpScroll.add(sectionBox);
|
|
374
|
+
}
|
|
375
|
+
helpBox.add(helpScroll);
|
|
376
|
+
root.add(helpBox);
|
|
377
|
+
|
|
378
|
+
// ── History overlay ─────────────────────────────────────────
|
|
379
|
+
const historyBox = new BoxRenderable(renderer, {
|
|
380
|
+
visible: false,
|
|
381
|
+
flexGrow: 1,
|
|
382
|
+
backgroundColor: colors.bg,
|
|
383
|
+
border: true,
|
|
384
|
+
borderColor: colors.border,
|
|
385
|
+
title: " Search History ",
|
|
386
|
+
titleColor: colors.blue,
|
|
387
|
+
});
|
|
388
|
+
const historySelect = new SelectRenderable(renderer, {
|
|
389
|
+
options: [{ name: "(no history)", description: "", value: null }],
|
|
390
|
+
showDescription: false,
|
|
391
|
+
showSelectionIndicator: true,
|
|
392
|
+
flexGrow: 1,
|
|
393
|
+
backgroundColor: colors.bg,
|
|
394
|
+
textColor: colors.text,
|
|
395
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
396
|
+
selectedTextColor: colors.selectionText,
|
|
397
|
+
itemSpacing: 0,
|
|
398
|
+
});
|
|
399
|
+
historyBox.add(historySelect);
|
|
400
|
+
root.add(historyBox);
|
|
401
|
+
|
|
402
|
+
function refreshHistory() {
|
|
403
|
+
const entries = readHistory();
|
|
404
|
+
if (entries.length === 0) {
|
|
405
|
+
historySelect.options = [{ name: "(no history)", description: "", value: null }];
|
|
406
|
+
} else {
|
|
407
|
+
historySelect.options = entries.map((e, i) => ({
|
|
408
|
+
name: `${e.query} — ${e.mode}${e.tab ? ` (${e.tab})` : ""}`,
|
|
409
|
+
description: `${e.resultCount} results`,
|
|
410
|
+
value: { index: i, entry: e },
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
historySelect.setSelectedIndex(0);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ── Bookmarks overlay ───────────────────────────────────────
|
|
417
|
+
const bookmarksBox = new BoxRenderable(renderer, {
|
|
418
|
+
visible: false,
|
|
419
|
+
flexGrow: 1,
|
|
420
|
+
backgroundColor: colors.bg,
|
|
421
|
+
border: true,
|
|
422
|
+
borderColor: colors.border,
|
|
423
|
+
title: " Bookmarks ",
|
|
424
|
+
titleColor: colors.green,
|
|
425
|
+
});
|
|
426
|
+
const bookmarksSelect = new SelectRenderable(renderer, {
|
|
427
|
+
options: [{ name: "(no bookmarks)", description: "", value: null }],
|
|
428
|
+
showDescription: false,
|
|
429
|
+
showSelectionIndicator: true,
|
|
430
|
+
flexGrow: 1,
|
|
431
|
+
backgroundColor: colors.bg,
|
|
432
|
+
textColor: colors.text,
|
|
433
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
434
|
+
selectedTextColor: colors.selectionText,
|
|
435
|
+
itemSpacing: 0,
|
|
436
|
+
});
|
|
437
|
+
bookmarksBox.add(bookmarksSelect);
|
|
438
|
+
root.add(bookmarksBox);
|
|
439
|
+
|
|
440
|
+
function refreshBookmarks() {
|
|
441
|
+
const bookmarks = getBookmarks();
|
|
442
|
+
if (bookmarks.length === 0) {
|
|
443
|
+
bookmarksSelect.options = [{ name: "(no bookmarks)", description: "", value: null }];
|
|
444
|
+
} else {
|
|
445
|
+
bookmarksSelect.options = bookmarks.map((b) => ({
|
|
446
|
+
name: `${b.repo.fullName} ★ ${b.repo.stars.toLocaleString()}`,
|
|
447
|
+
description: b.repo.description ?? "",
|
|
448
|
+
value: b,
|
|
449
|
+
}));
|
|
450
|
+
}
|
|
451
|
+
bookmarksSelect.setSelectedIndex(0);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── Saved searches overlay ─────────────────────────────────
|
|
455
|
+
const savedBox = new BoxRenderable(renderer, {
|
|
456
|
+
visible: false,
|
|
457
|
+
flexGrow: 1,
|
|
458
|
+
backgroundColor: colors.bg,
|
|
459
|
+
border: true,
|
|
460
|
+
borderColor: colors.border,
|
|
461
|
+
title: " Saved Searches ",
|
|
462
|
+
titleColor: colors.yellow,
|
|
463
|
+
});
|
|
464
|
+
const savedSelect = new SelectRenderable(renderer, {
|
|
465
|
+
options: [{ name: "(no saved searches)", description: "", value: null }],
|
|
466
|
+
showDescription: false,
|
|
467
|
+
showSelectionIndicator: true,
|
|
468
|
+
flexGrow: 1,
|
|
469
|
+
backgroundColor: colors.bg,
|
|
470
|
+
textColor: colors.text,
|
|
471
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
472
|
+
selectedTextColor: colors.selectionText,
|
|
473
|
+
itemSpacing: 0,
|
|
474
|
+
});
|
|
475
|
+
savedBox.add(savedSelect);
|
|
476
|
+
root.add(savedBox);
|
|
477
|
+
|
|
478
|
+
// ── Topic explorer overlay ───────────────────────────────────
|
|
479
|
+
const topicsBox = new BoxRenderable(renderer, {
|
|
480
|
+
visible: false,
|
|
481
|
+
flexGrow: 1,
|
|
482
|
+
backgroundColor: colors.bg,
|
|
483
|
+
border: true,
|
|
484
|
+
borderColor: colors.border,
|
|
485
|
+
title: " Topics ",
|
|
486
|
+
titleColor: colors.purple ?? colors.blue,
|
|
487
|
+
});
|
|
488
|
+
const topicsSelect = new SelectRenderable(renderer, {
|
|
489
|
+
options: [{ name: "(loading topics...)", description: "", value: null }],
|
|
490
|
+
showDescription: true,
|
|
491
|
+
showSelectionIndicator: true,
|
|
492
|
+
flexGrow: 1,
|
|
493
|
+
backgroundColor: colors.bg,
|
|
494
|
+
textColor: colors.text,
|
|
495
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
496
|
+
selectedTextColor: colors.selectionText,
|
|
497
|
+
itemSpacing: 0,
|
|
498
|
+
});
|
|
499
|
+
topicsBox.add(topicsSelect);
|
|
500
|
+
root.add(topicsBox);
|
|
501
|
+
|
|
502
|
+
async function refreshTopics() {
|
|
503
|
+
try {
|
|
504
|
+
const topics = await fetchTopics();
|
|
505
|
+
topicsSelect.options = topics.map((t) => ({
|
|
506
|
+
name: `${t.name} (${t.repoCount.toLocaleString()} repos)`,
|
|
507
|
+
description: t.description.slice(0, 80),
|
|
508
|
+
value: t,
|
|
509
|
+
}));
|
|
510
|
+
} catch {
|
|
511
|
+
topicsSelect.options = [{ name: "(failed to load topics)", description: "", value: null }];
|
|
512
|
+
}
|
|
513
|
+
topicsSelect.setSelectedIndex(0);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── Export overlay ───────────────────────────────────────────
|
|
517
|
+
const exportBox = new BoxRenderable(renderer, {
|
|
518
|
+
visible: false,
|
|
519
|
+
flexGrow: 1,
|
|
520
|
+
backgroundColor: colors.bg,
|
|
521
|
+
border: true,
|
|
522
|
+
borderColor: colors.border,
|
|
523
|
+
title: " Export ",
|
|
524
|
+
titleColor: colors.green,
|
|
525
|
+
});
|
|
526
|
+
const exportSelect = new SelectRenderable(renderer, {
|
|
527
|
+
options: [
|
|
528
|
+
{ name: " JSON", description: "Full repo data as JSON array", value: "json" },
|
|
529
|
+
{ name: " CSV", description: "Rank, name, stars, forks, language, URL", value: "csv" },
|
|
530
|
+
{ name: " Markdown", description: "Pretty table with links (paste into README)", value: "markdown" },
|
|
531
|
+
{ name: " Plain text", description: "One repo per line", value: "text" },
|
|
532
|
+
],
|
|
533
|
+
showDescription: true,
|
|
534
|
+
showSelectionIndicator: true,
|
|
535
|
+
flexGrow: 1,
|
|
536
|
+
backgroundColor: colors.bg,
|
|
537
|
+
textColor: colors.text,
|
|
538
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
539
|
+
selectedTextColor: colors.selectionText,
|
|
540
|
+
itemSpacing: 0,
|
|
541
|
+
});
|
|
542
|
+
exportBox.add(exportSelect);
|
|
543
|
+
root.add(exportBox);
|
|
544
|
+
|
|
545
|
+
// ── Compare overlay ──────────────────────────────────────────
|
|
546
|
+
const compareBox = new BoxRenderable(renderer, {
|
|
547
|
+
visible: false,
|
|
548
|
+
flexGrow: 1,
|
|
549
|
+
backgroundColor: colors.bg,
|
|
550
|
+
border: true,
|
|
551
|
+
borderColor: colors.border,
|
|
552
|
+
title: " Comparison ",
|
|
553
|
+
titleColor: colors.yellow,
|
|
554
|
+
});
|
|
555
|
+
const compareText = new TextRenderable(renderer, {
|
|
556
|
+
content: "",
|
|
557
|
+
color: colors.text,
|
|
558
|
+
backgroundColor: colors.bg,
|
|
559
|
+
});
|
|
560
|
+
compareBox.add(compareText);
|
|
561
|
+
root.add(compareBox);
|
|
562
|
+
|
|
563
|
+
// ── Notifications overlay ────────────────────────────────────
|
|
564
|
+
const notifsBox = new BoxRenderable(renderer, {
|
|
565
|
+
visible: false,
|
|
566
|
+
flexGrow: 1,
|
|
567
|
+
backgroundColor: colors.bg,
|
|
568
|
+
border: true,
|
|
569
|
+
borderColor: colors.border,
|
|
570
|
+
title: " Notifications ",
|
|
571
|
+
titleColor: colors.yellow,
|
|
572
|
+
});
|
|
573
|
+
const notifsText = new TextRenderable(renderer, {
|
|
574
|
+
content: "(no notifications)",
|
|
575
|
+
color: colors.text,
|
|
576
|
+
backgroundColor: colors.bg,
|
|
577
|
+
});
|
|
578
|
+
notifsBox.add(notifsText);
|
|
579
|
+
root.add(notifsBox);
|
|
580
|
+
|
|
581
|
+
function refreshNotifications() {
|
|
582
|
+
const notifs = getNotifications();
|
|
583
|
+
if (notifs.length === 0) {
|
|
584
|
+
notifsText.content = " (no notifications)";
|
|
585
|
+
} else {
|
|
586
|
+
notifsText.content = notifs
|
|
587
|
+
.map((n) => ` ${n.icon} ${n.message}`)
|
|
588
|
+
.join("\n");
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ── Share overlay ────────────────────────────────────────────
|
|
593
|
+
const shareBox = new BoxRenderable(renderer, {
|
|
594
|
+
visible: false,
|
|
595
|
+
flexGrow: 1,
|
|
596
|
+
backgroundColor: colors.bg,
|
|
597
|
+
border: true,
|
|
598
|
+
borderColor: colors.border,
|
|
599
|
+
title: " Share Repo ",
|
|
600
|
+
titleColor: colors.blue,
|
|
601
|
+
});
|
|
602
|
+
const shareSelect = new SelectRenderable(renderer, {
|
|
603
|
+
options: [
|
|
604
|
+
{ name: " Markdown", description: "[owner/repo](url) — description", value: "markdown" },
|
|
605
|
+
{ name: " Plain", description: "owner/repo — description — url", value: "plain" },
|
|
606
|
+
{ name: " GitHub CLI", description: "gh repo view owner/repo", value: "gh-cli" },
|
|
607
|
+
{ name: " Short", description: "owner/repo", value: "short" },
|
|
608
|
+
],
|
|
609
|
+
showDescription: true,
|
|
610
|
+
showSelectionIndicator: true,
|
|
611
|
+
flexGrow: 1,
|
|
612
|
+
backgroundColor: colors.bg,
|
|
613
|
+
textColor: colors.text,
|
|
614
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
615
|
+
selectedTextColor: colors.selectionText,
|
|
616
|
+
itemSpacing: 0,
|
|
617
|
+
});
|
|
618
|
+
shareBox.add(shareSelect);
|
|
619
|
+
root.add(shareBox);
|
|
620
|
+
|
|
621
|
+
function refreshCompare() {
|
|
622
|
+
if (compareList.length < 2) {
|
|
623
|
+
compareText.content = "Select at least 2 repos to compare (press c to add).";
|
|
624
|
+
} else {
|
|
625
|
+
compareText.content = buildComparisonTable(compareList);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function refreshSavedSearches() {
|
|
630
|
+
const saved = getSavedSearches();
|
|
631
|
+
if (saved.length === 0) {
|
|
632
|
+
savedSelect.options = [{ name: "(no saved searches)", description: "", value: null }];
|
|
633
|
+
} else {
|
|
634
|
+
savedSelect.options = saved.map((s) => ({
|
|
635
|
+
name: `${s.name}`,
|
|
636
|
+
description: `${s.query} (${s.mode})`,
|
|
637
|
+
value: s,
|
|
638
|
+
}));
|
|
639
|
+
}
|
|
640
|
+
savedSelect.setSelectedIndex(0);
|
|
641
|
+
}
|
|
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
|
+
|
|
889
|
+
// ── Status bar ──────────────────────────────────────────────────────
|
|
890
|
+
const statusBar = new TextRenderable(renderer, {
|
|
891
|
+
content: " Ready. Press Enter to search.",
|
|
892
|
+
backgroundColor: colors.bg,
|
|
893
|
+
color: colors.muted,
|
|
894
|
+
height: 1,
|
|
895
|
+
paddingX: 1,
|
|
896
|
+
});
|
|
897
|
+
root.add(statusBar);
|
|
898
|
+
|
|
899
|
+
const statusMgr = new StatusManager((text) => {
|
|
900
|
+
statusBar.content = ` ${text}`;
|
|
901
|
+
renderer.requestRender();
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
// ── Startup tip ──
|
|
905
|
+
if (config.theme !== undefined || true) {
|
|
906
|
+
// Show a tip after 2s idle
|
|
907
|
+
setTimeout(() => {
|
|
908
|
+
if (currentOverlay === "none") {
|
|
909
|
+
setStatus(nextTip());
|
|
910
|
+
}
|
|
911
|
+
}, 2000);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// ── Helper functions ────────────────────────────────────────────────
|
|
915
|
+
|
|
916
|
+
function setStatus(msg: string) {
|
|
917
|
+
statusMgr.set("idle", msg);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function setToolbar() {
|
|
921
|
+
toolbarText.content = formatToolbar(currentSort, currentLimit);
|
|
922
|
+
renderer.requestRender();
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function updateDetail(repo: Repo | null) {
|
|
926
|
+
if (!repo) {
|
|
927
|
+
detailText.content = "";
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
const topics = repo.topics.length ? repo.topics.slice(0, 8).join(", ") : "—";
|
|
931
|
+
const desc = repo.description ?? "(no description)";
|
|
932
|
+
const lines = [
|
|
933
|
+
`Name ${repo.fullName}`,
|
|
934
|
+
`Stars ${repo.stars.toLocaleString()}`,
|
|
935
|
+
`Forks ${repo.forks.toLocaleString()}`,
|
|
936
|
+
`Lang ${repo.language ?? "—"}`,
|
|
937
|
+
`Updated ${repo.updatedAt.slice(0, 10)}`,
|
|
938
|
+
repo.archived ? "Status archived" : "",
|
|
939
|
+
repo.isFork ? "Status fork" : "",
|
|
940
|
+
`Topics ${topics}`,
|
|
941
|
+
"",
|
|
942
|
+
desc,
|
|
943
|
+
"",
|
|
944
|
+
repo.url,
|
|
945
|
+
].filter((l) => l !== "");
|
|
946
|
+
detailText.content = lines.join("\n");
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// ── Graph / Chart ──────────────────────────────────────────────────
|
|
950
|
+
|
|
951
|
+
/** Unicode blocks for 8 vertical levels within one character cell. */
|
|
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
|
+
}
|
|
958
|
+
|
|
959
|
+
function buildChartString(values: number[], termW: number, termH: number): string[] {
|
|
960
|
+
if (values.length === 0) return ["(no data)"];
|
|
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.
|
|
967
|
+
const sampled: number[] = [];
|
|
968
|
+
for (let i = 0; i < dotW; i++) {
|
|
969
|
+
sampled.push(values[Math.floor((i / dotW) * values.length)]);
|
|
970
|
+
}
|
|
971
|
+
const max = Math.max(...sampled, 1);
|
|
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;
|
|
981
|
+
}
|
|
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);
|
|
988
|
+
for (let r = lo; r <= hi; r++) {
|
|
989
|
+
dotGrid[dotH - 1 - r][c] = true;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Convert 4-row × 2-col blocks of dots into braille characters.
|
|
994
|
+
const lines: string[] = [];
|
|
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);
|
|
1016
|
+
let label = " ";
|
|
1017
|
+
if (idx >= 0) label = String(labelVals[idx]).padStart(4) + " ";
|
|
1018
|
+
lines.push(`${label}\u2524${chars.join("")}`);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// X-axis
|
|
1022
|
+
lines.push(` \u2514${String.fromCharCode(0x2500).repeat(termW)}`);
|
|
1023
|
+
return lines;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function loadChart(repo: Repo) {
|
|
1027
|
+
detailText.content = " Loading chart...";
|
|
1028
|
+
renderer.requestRender();
|
|
1029
|
+
try {
|
|
1030
|
+
const [chartRes, repoRes] = await Promise.all([
|
|
1031
|
+
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/stats/participation`,
|
|
1032
|
+
{ headers: { "User-Agent": "ghfind/1.0" } }),
|
|
1033
|
+
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}`,
|
|
1034
|
+
{ headers: { "User-Agent": "ghfind/1.0" } }).then(r => r.ok ? r.json() : null),
|
|
1035
|
+
]);
|
|
1036
|
+
|
|
1037
|
+
let chartSection = "";
|
|
1038
|
+
if (chartRes.ok) {
|
|
1039
|
+
const data = await chartRes.json();
|
|
1040
|
+
if (data?.all && data.all.length >= 2) {
|
|
1041
|
+
chartCommitData = data.all;
|
|
1042
|
+
const chartW = 60;
|
|
1043
|
+
const chartH = 10;
|
|
1044
|
+
const chartLines = buildChartString(chartCommitData, chartW, chartH);
|
|
1045
|
+
chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join("\n");
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const desc = repo.description ?? "";
|
|
1050
|
+
const lang = repo.language ?? "\u2014";
|
|
1051
|
+
const updated = repo.updatedAt ? repo.updatedAt.slice(0, 10) : "";
|
|
1052
|
+
const topics = repoRes?.topics?.length ? repoRes.topics.slice(0, 8).join(", ") : "";
|
|
1053
|
+
|
|
1054
|
+
const growthLine = currentMode === "trending" && repo.score > 0
|
|
1055
|
+
? ` Growth \u25b2 ${fmtStars(repo.score)} ${trendingPeriod}`
|
|
1056
|
+
: "";
|
|
1057
|
+
|
|
1058
|
+
const detailLines = [
|
|
1059
|
+
` ${repo.fullName}`,
|
|
1060
|
+
` \u2606 ${repo.stars.toLocaleString()} \u2666 ${repo.forks.toLocaleString()} ${lang}`,
|
|
1061
|
+
growthLine,
|
|
1062
|
+
updated ? ` Updated ${updated}` : "",
|
|
1063
|
+
topics ? ` Topics ${topics}` : "",
|
|
1064
|
+
"",
|
|
1065
|
+
desc,
|
|
1066
|
+
"",
|
|
1067
|
+
` ${repo.url}`,
|
|
1068
|
+
].filter(l => l !== "");
|
|
1069
|
+
|
|
1070
|
+
detailText.content = [
|
|
1071
|
+
...detailLines,
|
|
1072
|
+
chartSection,
|
|
1073
|
+
].join("\n");
|
|
1074
|
+
} catch {
|
|
1075
|
+
detailText.content = " Failed to load chart";
|
|
1076
|
+
}
|
|
1077
|
+
renderer.requestRender();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function toggleGraph() {
|
|
1081
|
+
graphFullscreen = !graphFullscreen;
|
|
1082
|
+
if (graphFullscreen) {
|
|
1083
|
+
resultsBox.visible = false;
|
|
1084
|
+
resultsBox.width = "0%";
|
|
1085
|
+
detailBox.width = "100%";
|
|
1086
|
+
const opt = resultsSelect.getSelectedOption();
|
|
1087
|
+
const repo = opt?.value as Repo | undefined;
|
|
1088
|
+
if (repo) loadChart(repo);
|
|
1089
|
+
else detailText.content = " Select a repo to view activity";
|
|
1090
|
+
} else {
|
|
1091
|
+
resultsBox.visible = true;
|
|
1092
|
+
resultsBox.width = "50%";
|
|
1093
|
+
detailBox.width = "50%";
|
|
1094
|
+
const opt = resultsSelect.getSelectedOption();
|
|
1095
|
+
if (opt?.value) updateDetail(opt.value as Repo);
|
|
1096
|
+
else detailText.content = "";
|
|
1097
|
+
}
|
|
1098
|
+
renderer.requestRender();
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// ── Trending helpers ────────────────────────────────────────────────
|
|
1102
|
+
|
|
1103
|
+
function formatTrendingLine(r: { rank: number; owner: string; name: string; stars: number; starsToday: number; language: string }): string {
|
|
1104
|
+
const rank = String(r.rank).padStart(2, "0");
|
|
1105
|
+
const name = `${r.owner}/${r.name}`.padEnd(30).slice(0, 30);
|
|
1106
|
+
const lang = (r.language || "?").padEnd(12).slice(0, 12);
|
|
1107
|
+
const stars = `★ ${fmtStars(r.stars)}`.padEnd(9).slice(0, 9);
|
|
1108
|
+
const arrow = r.starsToday > 0 ? "▲" : r.starsToday < 0 ? "▼" : "—";
|
|
1109
|
+
const growth = `${arrow} ${fmtStars(r.starsToday)} ${trendingPeriod}`;
|
|
1110
|
+
return `[${rank}] ${name} ${lang} ${stars} ${growth}`;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function trendingRepoToSelectValue(r: TrendingRepo): Repo {
|
|
1114
|
+
return {
|
|
1115
|
+
id: 0,
|
|
1116
|
+
fullName: `${r.owner}/${r.name}`,
|
|
1117
|
+
name: r.name,
|
|
1118
|
+
owner: r.owner,
|
|
1119
|
+
description: r.description,
|
|
1120
|
+
url: `https://github.com/${r.owner}/${r.name}`,
|
|
1121
|
+
stars: r.stars,
|
|
1122
|
+
forks: 0,
|
|
1123
|
+
watchers: 0,
|
|
1124
|
+
language: r.language,
|
|
1125
|
+
topics: [],
|
|
1126
|
+
archived: false,
|
|
1127
|
+
isFork: false,
|
|
1128
|
+
private: false,
|
|
1129
|
+
createdAt: "",
|
|
1130
|
+
updatedAt: "",
|
|
1131
|
+
pushedAt: "",
|
|
1132
|
+
score: r.starsToday,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
async function loadTrending() {
|
|
1137
|
+
currentMode = "trending";
|
|
1138
|
+
isLoading = true;
|
|
1139
|
+
searchBox.visible = false;
|
|
1140
|
+
toolbarText.visible = false;
|
|
1141
|
+
trendingTabBox.visible = true;
|
|
1142
|
+
renderTrendingTabs();
|
|
1143
|
+
resultsSelect.options = [{ name: " Loading trending...", description: "", value: null }];
|
|
1144
|
+
detailText.content = "";
|
|
1145
|
+
statusMgr.set("loading", "Loading trending repos...");
|
|
1146
|
+
renderer.requestRender();
|
|
1147
|
+
|
|
1148
|
+
try {
|
|
1149
|
+
const fetched = await fetchTrendingRepos(tabSince(trendingTab));
|
|
1150
|
+
const since = tabSince(trendingTab);
|
|
1151
|
+
trendingPeriod = since === "daily" ? "today" : since === "weekly" ? "this week" : "this month";
|
|
1152
|
+
resultsSelect.options = fetched.map(r => ({
|
|
1153
|
+
name: formatTrendingLine(r),
|
|
1154
|
+
description: r.description,
|
|
1155
|
+
value: trendingRepoToSelectValue(r),
|
|
1156
|
+
}));
|
|
1157
|
+
if (fetched.length > 0) {
|
|
1158
|
+
resultsSelect.setSelectedIndex(0);
|
|
1159
|
+
updateDetail(trendingRepoToSelectValue(fetched[0]));
|
|
1160
|
+
} else {
|
|
1161
|
+
throw new NoResultsError(trendingTab);
|
|
1162
|
+
}
|
|
1163
|
+
statusMgr.set("success", `${fetched.length} trending repos — ${trendingTab}`);
|
|
1164
|
+
appendHistory({ query: `trending:${trendingTab}`, mode: "trending", tab: trendingTab, timestamp: Date.now(), resultCount: fetched.length });
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
const msg = err instanceof SearchCliError ? err.userMessage : (err instanceof Error ? err.message : String(err));
|
|
1167
|
+
resultsSelect.options = [{ name: " (error)", description: "", value: null }];
|
|
1168
|
+
statusMgr.set("error", msg.slice(0, 60));
|
|
1169
|
+
}
|
|
1170
|
+
isLoading = false;
|
|
1171
|
+
renderer.requestRender();
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function showSearchMode() {
|
|
1175
|
+
currentMode = "search";
|
|
1176
|
+
trendingTabBox.visible = false;
|
|
1177
|
+
searchBox.visible = true;
|
|
1178
|
+
toolbarText.visible = true;
|
|
1179
|
+
searchInput.placeholder = "Search GitHub repos (e.g. rust cli, or language:Rust stars:>100)";
|
|
1180
|
+
searchInput.focus();
|
|
1181
|
+
renderer.requestRender();
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
async function doSearch(queryText: string, append = false) {
|
|
1185
|
+
const q = queryText.trim();
|
|
1186
|
+
if (q === "" || isLoading) return;
|
|
1187
|
+
|
|
1188
|
+
if (!append) currentPage = 1;
|
|
1189
|
+
deepDiveActive = false;
|
|
1190
|
+
|
|
1191
|
+
isLoading = true;
|
|
1192
|
+
currentQueryInput = q;
|
|
1193
|
+
if (!append) {
|
|
1194
|
+
resultsSelect.options = [{ name: " Searching...", description: "", value: null }];
|
|
1195
|
+
detailText.content = "";
|
|
1196
|
+
}
|
|
1197
|
+
statusMgr.set("searching", `Searching for "${q}"`);
|
|
1198
|
+
|
|
1199
|
+
try {
|
|
1200
|
+
const parsed = applyFlagFilters(parseQuery(q), {});
|
|
1201
|
+
validateQuery(parsed);
|
|
1202
|
+
const provider = createGitHubSearch(logger);
|
|
1203
|
+
const options: SearchOptions = {
|
|
1204
|
+
limit: currentLimit,
|
|
1205
|
+
sort: currentSort,
|
|
1206
|
+
json: false,
|
|
1207
|
+
verbose: false,
|
|
1208
|
+
token: githubToken,
|
|
1209
|
+
page: currentPage,
|
|
1210
|
+
};
|
|
1211
|
+
const response = await provider.search(parsed, options);
|
|
1212
|
+
totalCount = response.totalCount;
|
|
1213
|
+
const newRepos = rankRepos(response.repos, options.sort);
|
|
1214
|
+
|
|
1215
|
+
if (append) {
|
|
1216
|
+
currentRepos.push(...newRepos);
|
|
1217
|
+
} else {
|
|
1218
|
+
currentRepos = newRepos;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (currentRepos.length === 0) {
|
|
1222
|
+
throw new NoResultsError(q);
|
|
1223
|
+
} else {
|
|
1224
|
+
resultsSelect.options = currentRepos.map((r) => ({
|
|
1225
|
+
name: formatResultLine(r),
|
|
1226
|
+
description: "",
|
|
1227
|
+
value: r,
|
|
1228
|
+
}));
|
|
1229
|
+
const idx = append ? resultsSelect.options.length - newRepos.length : 0;
|
|
1230
|
+
updateDetail(currentRepos[idx]);
|
|
1231
|
+
resultsSelect.setSelectedIndex(idx);
|
|
1232
|
+
statusMgr.set("success", `Page ${currentPage} — ${currentRepos.length} of ${totalCount.toLocaleString()} results`);
|
|
1233
|
+
if (!append) {
|
|
1234
|
+
appendHistory({ query: q, mode: "search", timestamp: Date.now(), resultCount: currentRepos.length });
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
currentRepos = [];
|
|
1239
|
+
resultsSelect.options = [{ name: " (error)", description: "", value: null }];
|
|
1240
|
+
const msg = err instanceof SearchCliError ? err.userMessage : (err instanceof Error ? err.message : String(err));
|
|
1241
|
+
statusMgr.set("error", msg.slice(0, 60));
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
isLoading = false;
|
|
1245
|
+
renderer.requestRender();
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function changeSort() {
|
|
1249
|
+
const idx = SORT_MODES.findIndex((m) => m.key === currentSort);
|
|
1250
|
+
const next = SORT_MODES[(idx + 1) % SORT_MODES.length];
|
|
1251
|
+
currentSort = next.key;
|
|
1252
|
+
setToolbar();
|
|
1253
|
+
if (currentRepos.length > 0) {
|
|
1254
|
+
currentRepos = rankRepos(currentRepos, currentSort);
|
|
1255
|
+
resultsSelect.options = currentRepos.map((r) => ({
|
|
1256
|
+
name: formatResultLine(r),
|
|
1257
|
+
description: "",
|
|
1258
|
+
value: r,
|
|
1259
|
+
}));
|
|
1260
|
+
resultsSelect.setSelectedIndex(0);
|
|
1261
|
+
updateDetail(currentRepos[0]);
|
|
1262
|
+
renderer.requestRender();
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function changeLimit() {
|
|
1267
|
+
const limits = [10, 25, 50, 100];
|
|
1268
|
+
const idx = limits.indexOf(currentLimit);
|
|
1269
|
+
currentLimit = limits[(idx + 1) % limits.length];
|
|
1270
|
+
setToolbar();
|
|
1271
|
+
setStatus(`Limit set to ${currentLimit}. Press Enter to re-search.`);
|
|
1272
|
+
renderer.requestRender();
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// ── Wire events ────────────────────────────────────────────────────
|
|
1276
|
+
|
|
1277
|
+
// Enter in search input → show results
|
|
1278
|
+
searchInput.on("enter", () => {
|
|
1279
|
+
if (searchInput.value.trim() === "" || isLoading) return;
|
|
1280
|
+
showSearchMode();
|
|
1281
|
+
doSearch(searchInput.value);
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
// Navigate results → update detail pane
|
|
1285
|
+
resultsSelect.on("selectionChanged", () => {
|
|
1286
|
+
const opt = resultsSelect.getSelectedOption();
|
|
1287
|
+
const repo = opt?.value as Repo | undefined;
|
|
1288
|
+
if (repo) {
|
|
1289
|
+
if (graphFullscreen) loadChart(repo);
|
|
1290
|
+
else updateDetail(repo);
|
|
1291
|
+
}
|
|
1292
|
+
renderer.requestRender();
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
// Enter on a result → open URL
|
|
1296
|
+
resultsSelect.on("itemSelected", () => {
|
|
1297
|
+
const opt = resultsSelect.getSelectedOption();
|
|
1298
|
+
const repo = opt?.value as Repo | undefined;
|
|
1299
|
+
if (repo) openUrl(repo.url);
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
// ── Global keyboard shortcuts ──────────────────────────────────────
|
|
1303
|
+
renderer.keyInput.on("keypress", (key) => {
|
|
1304
|
+
// ── Overlay-mode handling ────────────────────────────────────────
|
|
1305
|
+
if (currentOverlay !== "none") {
|
|
1306
|
+
// Escape or q closes any overlay
|
|
1307
|
+
if (key.name === "escape" || key.name === "q") {
|
|
1308
|
+
showOverlay("none");
|
|
1309
|
+
renderer.requestRender();
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
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
|
+
|
|
1333
|
+
// History overlay
|
|
1334
|
+
if (currentOverlay === "history") {
|
|
1335
|
+
if (key.name === "d") {
|
|
1336
|
+
const sel = historySelect.getSelectedOption();
|
|
1337
|
+
if (sel?.value) {
|
|
1338
|
+
deleteHistoryEntry((sel.value as any).index);
|
|
1339
|
+
refreshHistory();
|
|
1340
|
+
setStatus("Deleted history entry");
|
|
1341
|
+
}
|
|
1342
|
+
renderer.requestRender();
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (key.ctrl && key.name === "x") {
|
|
1346
|
+
clearHistory();
|
|
1347
|
+
refreshHistory();
|
|
1348
|
+
setStatus("History cleared");
|
|
1349
|
+
renderer.requestRender();
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (key.name === "enter" || key.name === "return") {
|
|
1353
|
+
const sel = historySelect.getSelectedOption();
|
|
1354
|
+
if (sel?.value) {
|
|
1355
|
+
const entry = (sel.value as any).entry;
|
|
1356
|
+
showOverlay("none");
|
|
1357
|
+
if (entry.mode === "trending") {
|
|
1358
|
+
if (entry.tab) trendingTab = entry.tab as typeof TAB_NAMES[number];
|
|
1359
|
+
loadTrending();
|
|
1360
|
+
} else {
|
|
1361
|
+
searchInput.value = entry.query;
|
|
1362
|
+
showSearchMode();
|
|
1363
|
+
doSearch(entry.query);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// Bookmarks overlay
|
|
1372
|
+
if (currentOverlay === "bookmarks") {
|
|
1373
|
+
if (key.name === "d") {
|
|
1374
|
+
const sel = bookmarksSelect.getSelectedOption();
|
|
1375
|
+
if (sel?.value) {
|
|
1376
|
+
removeBookmark((sel.value as any).repo.fullName);
|
|
1377
|
+
refreshBookmarks();
|
|
1378
|
+
setStatus("Bookmark removed");
|
|
1379
|
+
}
|
|
1380
|
+
renderer.requestRender();
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
if (key.name === "enter" || key.name === "return") {
|
|
1384
|
+
const sel = bookmarksSelect.getSelectedOption();
|
|
1385
|
+
if (sel?.value) openUrl((sel.value as any).repo.url);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Saved searches overlay
|
|
1392
|
+
if (currentOverlay === "saved") {
|
|
1393
|
+
if (key.name === "d") {
|
|
1394
|
+
const sel = savedSelect.getSelectedOption();
|
|
1395
|
+
if (sel?.value) {
|
|
1396
|
+
deleteSavedSearch((sel.value as any).name);
|
|
1397
|
+
refreshSavedSearches();
|
|
1398
|
+
setStatus("Saved search deleted");
|
|
1399
|
+
}
|
|
1400
|
+
renderer.requestRender();
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (key.name === "enter" || key.name === "return") {
|
|
1404
|
+
const sel = savedSelect.getSelectedOption();
|
|
1405
|
+
if (sel?.value) {
|
|
1406
|
+
const s = sel.value as any;
|
|
1407
|
+
showOverlay("none");
|
|
1408
|
+
touchSavedSearch(s.name);
|
|
1409
|
+
if (s.mode === "trending") {
|
|
1410
|
+
if (s.tab) trendingTab = s.tab as typeof TAB_NAMES[number];
|
|
1411
|
+
loadTrending();
|
|
1412
|
+
} else {
|
|
1413
|
+
searchInput.value = s.query;
|
|
1414
|
+
showSearchMode();
|
|
1415
|
+
currentSort = s.sort;
|
|
1416
|
+
currentLimit = s.limit;
|
|
1417
|
+
setToolbar();
|
|
1418
|
+
doSearch(s.query);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// Topics explorer overlay
|
|
1427
|
+
if (currentOverlay === "topics") {
|
|
1428
|
+
if (key.name === "enter" || key.name === "return") {
|
|
1429
|
+
const sel = topicsSelect.getSelectedOption();
|
|
1430
|
+
if (sel?.value) {
|
|
1431
|
+
const topic = sel.value as any;
|
|
1432
|
+
showOverlay("none");
|
|
1433
|
+
searchInput.value = `topic:${topic.name}`;
|
|
1434
|
+
showSearchMode();
|
|
1435
|
+
doSearch(`topic:${topic.name}`);
|
|
1436
|
+
}
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// Export overlay
|
|
1443
|
+
if (currentOverlay === "export") {
|
|
1444
|
+
if (key.name === "enter" || key.name === "return") {
|
|
1445
|
+
const sel = exportSelect.getSelectedOption();
|
|
1446
|
+
if (sel?.value && currentRepos.length > 0) {
|
|
1447
|
+
const format = sel.value as ExportFormat;
|
|
1448
|
+
const path = exportToFile(currentRepos, format);
|
|
1449
|
+
showOverlay("none");
|
|
1450
|
+
setStatus(`✓ Exported to ${path}`);
|
|
1451
|
+
} else {
|
|
1452
|
+
setStatus("No results to export");
|
|
1453
|
+
showOverlay("none");
|
|
1454
|
+
}
|
|
1455
|
+
renderer.requestRender();
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Compare overlay: Esc/q closes
|
|
1462
|
+
if (currentOverlay === "compare") {
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// Notifications overlay
|
|
1467
|
+
if (currentOverlay === "notifications") {
|
|
1468
|
+
if (key.name === "d") {
|
|
1469
|
+
const notifs = getNotifications();
|
|
1470
|
+
if (notifs.length > 0) {
|
|
1471
|
+
dismissNotification(notifs[0].id);
|
|
1472
|
+
refreshNotifications();
|
|
1473
|
+
setStatus("Notification dismissed");
|
|
1474
|
+
}
|
|
1475
|
+
renderer.requestRender();
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (key.ctrl && key.name === "c") {
|
|
1479
|
+
dismissAll();
|
|
1480
|
+
refreshNotifications();
|
|
1481
|
+
renderer.requestRender();
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Share overlay
|
|
1488
|
+
if (currentOverlay === "share") {
|
|
1489
|
+
if (key.name === "enter" || key.name === "return") {
|
|
1490
|
+
const sel = shareSelect.getSelectedOption();
|
|
1491
|
+
if (sel?.value) {
|
|
1492
|
+
const opt = resultsSelect.getSelectedOption();
|
|
1493
|
+
const repo = opt?.value as Repo | undefined;
|
|
1494
|
+
if (repo) {
|
|
1495
|
+
const text = formatShare(repo, sel.value as ShareFormat);
|
|
1496
|
+
copyToClipboard(text).then((ok) => {
|
|
1497
|
+
showOverlay("none");
|
|
1498
|
+
setStatus(ok ? "✓ Copied!" : "Clipboard not available");
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
renderer.requestRender();
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Help overlay: any key closes
|
|
1509
|
+
if (currentOverlay === "help") {
|
|
1510
|
+
showOverlay("none");
|
|
1511
|
+
renderer.requestRender();
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// ── Main view handling (no overlay active) ────────────────────────
|
|
1519
|
+
|
|
1520
|
+
// q quits
|
|
1521
|
+
if (key.name === "q") {
|
|
1522
|
+
if (graphFullscreen) {
|
|
1523
|
+
toggleGraph();
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
saveSession({ mode: currentMode, query: currentQueryInput, sort: currentSort, limit: currentLimit, trendingTab });
|
|
1527
|
+
cleanup();
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// Esc exits graph mode
|
|
1532
|
+
if (key.name === "escape" && graphFullscreen) {
|
|
1533
|
+
toggleGraph();
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// Space opens leader menu
|
|
1538
|
+
if (key.name === "space") {
|
|
1539
|
+
showLeaderMenu();
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// '?' / Ctrl+H toggle help overlay (fast path)
|
|
1544
|
+
if (key.name === "?" || (key.name === "h" && key.ctrl)) {
|
|
1545
|
+
showOverlay("help");
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Tab auto-completes qualifiers in search input
|
|
1550
|
+
if (key.name === "tab") {
|
|
1551
|
+
if (searchBox.visible) {
|
|
1552
|
+
const val = searchInput.value;
|
|
1553
|
+
const lastSpace = val.lastIndexOf(" ");
|
|
1554
|
+
const currentWord = lastSpace >= 0 ? val.slice(lastSpace + 1) : val;
|
|
1555
|
+
const suggestions = suggestFor(currentWord);
|
|
1556
|
+
if (suggestions.length > 0) {
|
|
1557
|
+
const newVal = lastSpace >= 0
|
|
1558
|
+
? val.slice(0, lastSpace + 1) + suggestions[0]
|
|
1559
|
+
: suggestions[0];
|
|
1560
|
+
searchInput.value = newVal;
|
|
1561
|
+
renderer.requestRender();
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// '/' focuses search
|
|
1568
|
+
if (key.name === "/") {
|
|
1569
|
+
searchInput.focus();
|
|
1570
|
+
showSearchMode();
|
|
1571
|
+
renderer.requestRender();
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Number keys 1-5 switch trending tabs (only in trending mode)
|
|
1576
|
+
if (/^[1-5]$/.test(key.name)) {
|
|
1577
|
+
if (currentMode === "trending") {
|
|
1578
|
+
const idx = parseInt(key.name) - 1;
|
|
1579
|
+
if (idx >= 0 && idx < TAB_NAMES.length) {
|
|
1580
|
+
trendingTab = TAB_NAMES[idx];
|
|
1581
|
+
loadTrending();
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// Left/right arrows (and h/l) switch trending tabs
|
|
1588
|
+
if (key.name === "left" || key.name === "h") {
|
|
1589
|
+
if (currentMode === "trending") {
|
|
1590
|
+
const idx = TAB_NAMES.indexOf(trendingTab);
|
|
1591
|
+
if (idx > 0) { trendingTab = TAB_NAMES[idx - 1]; loadTrending(); }
|
|
1592
|
+
}
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
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(); }
|
|
1599
|
+
}
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// PageDown — next page (search mode)
|
|
1604
|
+
if (key.name === "pagedown") {
|
|
1605
|
+
if (currentMode === "search" && currentQueryInput) {
|
|
1606
|
+
currentPage++;
|
|
1607
|
+
doSearch(currentQueryInput, true);
|
|
1608
|
+
}
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// PageUp — scroll to top of results
|
|
1613
|
+
if (key.name === "pageup") {
|
|
1614
|
+
if (currentRepos.length > 0) {
|
|
1615
|
+
resultsSelect.setSelectedIndex(0);
|
|
1616
|
+
updateDetail(currentRepos[0]);
|
|
1617
|
+
renderer.requestRender();
|
|
1618
|
+
}
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
// ── Start ──────────────────────────────────────────────────────────
|
|
1624
|
+
renderer.start();
|
|
1625
|
+
if (currentMode === "trending") {
|
|
1626
|
+
loadTrending();
|
|
1627
|
+
} else {
|
|
1628
|
+
searchInput.value = currentQueryInput;
|
|
1629
|
+
if (currentQueryInput) {
|
|
1630
|
+
doSearch(currentQueryInput);
|
|
1631
|
+
} else {
|
|
1632
|
+
showSearchMode();
|
|
1633
|
+
}
|
|
1634
|
+
searchInput.focus();
|
|
1635
|
+
}
|
|
1636
|
+
renderer.requestRender();
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// ── Utilities ─────────────────────────────────────────────────────────
|
|
1640
|
+
|
|
1641
|
+
function formatResultLine(repo: Repo): string {
|
|
1642
|
+
const stars = formatStars(repo.stars);
|
|
1643
|
+
const lang = (repo.language ?? "?").padEnd(12).slice(0, 12);
|
|
1644
|
+
return `${repo.fullName} ${stars}`;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function formatStars(n: number): string {
|
|
1648
|
+
if (n >= 1000) return `★ ${(n / 1000).toFixed(1).replace(/\.0$/, "")}k`;
|
|
1649
|
+
return `★ ${n}`;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function formatToolbar(sort: SortStrategy, limit: number): string {
|
|
1653
|
+
const sortLabel = SORT_MODES.find((m) => m.key === sort)?.label ?? sort;
|
|
1654
|
+
return ` sort: ${sortLabel} limit: ${limit}`;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function cleanup(): void {
|
|
1658
|
+
try { rotateHistory(); } catch { /* non-critical */ }
|
|
1659
|
+
process.exit(0);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// ── Auto-run ──────────────────────────────────────────────────────────
|
|
1663
|
+
if (import.meta.main) {
|
|
1664
|
+
launchBrowser();
|
|
1665
|
+
}
|