github-search-cli 0.8.2 → 9.1.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 +109 -50
- package/bin/ghfind.js +3 -13
- package/dist/chunk-TUMYR2IC.js +57 -0
- package/dist/cli.js +3761 -0
- package/dist/config-DFL44OVL.js +14 -0
- package/package.json +3 -1
- package/scripts/postinstall.js +106 -106
- package/src/bookmarks.ts +4 -17
- package/src/cli.ts +90 -61
- package/src/config.ts +2 -2
- package/src/history.ts +16 -52
- package/src/notifications.ts +5 -12
- package/src/output.ts +124 -55
- package/src/saved-searches.ts +4 -12
- package/src/search-state.ts +229 -0
- package/src/search.ts +99 -2
- package/src/session.ts +6 -18
- package/src/storage.ts +106 -0
- package/src/themes.ts +47 -7
- package/src/trending.ts +27 -150
- package/src/tui.ts +1087 -270
- package/src/types.ts +2 -0
- package/src/update-check.ts +162 -0
- package/src/watch.ts +23 -11
- package/vendor/bun.exe +0 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3761 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadConfig,
|
|
4
|
+
saveConfig,
|
|
5
|
+
stateDir
|
|
6
|
+
} from "./chunk-TUMYR2IC.js";
|
|
7
|
+
|
|
8
|
+
// src/errors.ts
|
|
9
|
+
var SearchCliError = class extends Error {
|
|
10
|
+
constructor(message, userMessage, recoverable = true) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.userMessage = userMessage;
|
|
13
|
+
this.recoverable = recoverable;
|
|
14
|
+
this.name = "SearchCliError";
|
|
15
|
+
}
|
|
16
|
+
userMessage;
|
|
17
|
+
recoverable;
|
|
18
|
+
};
|
|
19
|
+
var NetworkError = class extends SearchCliError {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(
|
|
22
|
+
"Network request failed",
|
|
23
|
+
"\u26A0 Network error \u2014 check your connection. Retrying in 5s... [r]etry now"
|
|
24
|
+
);
|
|
25
|
+
this.name = "NetworkError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var RateLimitError = class extends SearchCliError {
|
|
29
|
+
constructor(hasToken, resetSeconds) {
|
|
30
|
+
const msg = hasToken ? `\u26A0 API rate limit exceeded (5,000/hr). Resets in ${resetSeconds ?? "?"}s. [r]etry [c]hange token` : "\u26A0 Rate limited (60/hr). Set GITHUB_TOKEN for 5,000/hr. [r]etry";
|
|
31
|
+
super(msg, msg);
|
|
32
|
+
this.hasToken = hasToken;
|
|
33
|
+
this.name = "RateLimitError";
|
|
34
|
+
}
|
|
35
|
+
hasToken;
|
|
36
|
+
};
|
|
37
|
+
var NoResultsError = class extends SearchCliError {
|
|
38
|
+
constructor(query) {
|
|
39
|
+
super(
|
|
40
|
+
`No results for "${query}"`,
|
|
41
|
+
`\u{1F50D} No results for "${query}". Tips: check spelling, try fewer qualifiers, or [b]rowse trending`
|
|
42
|
+
);
|
|
43
|
+
this.query = query;
|
|
44
|
+
this.name = "NoResultsError";
|
|
45
|
+
}
|
|
46
|
+
query;
|
|
47
|
+
};
|
|
48
|
+
var ParseError = class extends SearchCliError {
|
|
49
|
+
constructor(source, detail) {
|
|
50
|
+
super(
|
|
51
|
+
`Parse error from ${source}: ${detail}`,
|
|
52
|
+
`\u26A0 Could not parse ${source}. GitHub may have changed the layout. [r]etry`
|
|
53
|
+
);
|
|
54
|
+
this.name = "ParseError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/search.ts
|
|
59
|
+
var noopLogger = {
|
|
60
|
+
debug: () => {
|
|
61
|
+
},
|
|
62
|
+
info: () => {
|
|
63
|
+
},
|
|
64
|
+
warn: () => {
|
|
65
|
+
},
|
|
66
|
+
error: () => {
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var MemoryCache = class {
|
|
70
|
+
constructor(defaultTtlMs = 3e5) {
|
|
71
|
+
this.defaultTtlMs = defaultTtlMs;
|
|
72
|
+
}
|
|
73
|
+
defaultTtlMs;
|
|
74
|
+
cache = /* @__PURE__ */ new Map();
|
|
75
|
+
static key(...parts) {
|
|
76
|
+
return parts.join("::").toLowerCase().replace(/\s+/g, " ");
|
|
77
|
+
}
|
|
78
|
+
get(key) {
|
|
79
|
+
const entry = this.cache.get(key);
|
|
80
|
+
if (!entry) return null;
|
|
81
|
+
if (Date.now() - entry.cachedAt > entry.ttlMs) {
|
|
82
|
+
this.cache.delete(key);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
this.cache.delete(key);
|
|
86
|
+
this.cache.set(key, entry);
|
|
87
|
+
return entry.data;
|
|
88
|
+
}
|
|
89
|
+
set(key, data, ttlMs) {
|
|
90
|
+
if (this.cache.size >= MAX_CACHE_SIZE) {
|
|
91
|
+
const firstKey = this.cache.keys().next().value;
|
|
92
|
+
if (firstKey !== void 0) this.cache.delete(firstKey);
|
|
93
|
+
}
|
|
94
|
+
this.cache.set(key, {
|
|
95
|
+
data,
|
|
96
|
+
cachedAt: Date.now(),
|
|
97
|
+
ttlMs: ttlMs ?? this.defaultTtlMs
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
delete(key) {
|
|
101
|
+
this.cache.delete(key);
|
|
102
|
+
}
|
|
103
|
+
clear() {
|
|
104
|
+
this.cache.clear();
|
|
105
|
+
}
|
|
106
|
+
get size() {
|
|
107
|
+
return this.cache.size;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var MAX_CACHE_SIZE = 50;
|
|
111
|
+
function normalizeRepo(item) {
|
|
112
|
+
const owner = item.owner?.login ?? item.full_name?.split("/")[0] ?? "";
|
|
113
|
+
return {
|
|
114
|
+
id: item.id ?? 0,
|
|
115
|
+
fullName: item.full_name ?? "",
|
|
116
|
+
name: item.name ?? "",
|
|
117
|
+
owner,
|
|
118
|
+
description: item.description ?? null,
|
|
119
|
+
url: item.html_url ?? "",
|
|
120
|
+
stars: item.stargazers_count ?? 0,
|
|
121
|
+
forks: item.forks_count ?? 0,
|
|
122
|
+
watchers: item.watchers_count ?? 0,
|
|
123
|
+
language: item.language ?? null,
|
|
124
|
+
topics: item.topics ?? [],
|
|
125
|
+
archived: item.archived ?? false,
|
|
126
|
+
isFork: item.fork ?? false,
|
|
127
|
+
private: item.private ?? false,
|
|
128
|
+
createdAt: item.created_at ?? "",
|
|
129
|
+
updatedAt: item.updated_at ?? "",
|
|
130
|
+
pushedAt: item.pushed_at ?? "",
|
|
131
|
+
score: item.score ?? 0
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function normalizeEnvelope(env) {
|
|
135
|
+
return (env.items ?? []).map(normalizeRepo);
|
|
136
|
+
}
|
|
137
|
+
var KNOWN_QUALIFIERS = [
|
|
138
|
+
"language",
|
|
139
|
+
"stars",
|
|
140
|
+
"fork",
|
|
141
|
+
"archived",
|
|
142
|
+
"topic",
|
|
143
|
+
"user",
|
|
144
|
+
"org",
|
|
145
|
+
"repo",
|
|
146
|
+
"updated",
|
|
147
|
+
"pushed",
|
|
148
|
+
"visibility",
|
|
149
|
+
"in",
|
|
150
|
+
"size",
|
|
151
|
+
"license",
|
|
152
|
+
"created"
|
|
153
|
+
];
|
|
154
|
+
var QUALIFIER_SUGGESTIONS = {
|
|
155
|
+
l: ["language:"],
|
|
156
|
+
s: ["stars:", "size:"],
|
|
157
|
+
t: ["topic:"],
|
|
158
|
+
u: ["user:"],
|
|
159
|
+
o: ["org:"],
|
|
160
|
+
r: ["repo:"],
|
|
161
|
+
c: ["created:"],
|
|
162
|
+
p: ["pushed:"],
|
|
163
|
+
v: ["visibility:"],
|
|
164
|
+
i: ["in:"],
|
|
165
|
+
f: ["fork:"],
|
|
166
|
+
a: ["archived:"]
|
|
167
|
+
};
|
|
168
|
+
function suggestFor(input) {
|
|
169
|
+
const trimmed = input.trim().toLowerCase();
|
|
170
|
+
if (trimmed.length === 0) return [];
|
|
171
|
+
if (trimmed.length === 1 && QUALIFIER_SUGGESTIONS[trimmed]) {
|
|
172
|
+
return QUALIFIER_SUGGESTIONS[trimmed];
|
|
173
|
+
}
|
|
174
|
+
const matches = KNOWN_QUALIFIERS.filter((k) => k.startsWith(trimmed));
|
|
175
|
+
return matches.map((k) => k + ":");
|
|
176
|
+
}
|
|
177
|
+
function tokenize(input) {
|
|
178
|
+
const tokens = [];
|
|
179
|
+
let current = "";
|
|
180
|
+
let inQuotes = false;
|
|
181
|
+
for (let i = 0; i < input.length; i++) {
|
|
182
|
+
const ch = input[i];
|
|
183
|
+
if (ch === '"') {
|
|
184
|
+
inQuotes = !inQuotes;
|
|
185
|
+
current += ch;
|
|
186
|
+
} else if (/\s/.test(ch) && !inQuotes) {
|
|
187
|
+
if (current) {
|
|
188
|
+
tokens.push(current);
|
|
189
|
+
current = "";
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
current += ch;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (current) {
|
|
196
|
+
if (current.startsWith('"') && current.endsWith('"') && current.length >= 2) {
|
|
197
|
+
current = current.slice(1, -1);
|
|
198
|
+
}
|
|
199
|
+
tokens.push(current);
|
|
200
|
+
}
|
|
201
|
+
return tokens;
|
|
202
|
+
}
|
|
203
|
+
function parseQuery(raw) {
|
|
204
|
+
const trimmed = raw.trim();
|
|
205
|
+
if (trimmed === "") {
|
|
206
|
+
return { keywords: [], qualifiers: [], raw };
|
|
207
|
+
}
|
|
208
|
+
const keywords = [];
|
|
209
|
+
const qualifiers = [];
|
|
210
|
+
for (const token of tokenize(trimmed)) {
|
|
211
|
+
const qualifier = matchQualifier(token);
|
|
212
|
+
if (qualifier) {
|
|
213
|
+
qualifiers.push(qualifier);
|
|
214
|
+
} else {
|
|
215
|
+
keywords.push(token);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { keywords, qualifiers, raw: trimmed };
|
|
219
|
+
}
|
|
220
|
+
function matchQualifier(token) {
|
|
221
|
+
const negated = token.startsWith("-");
|
|
222
|
+
const body = negated ? token.slice(1) : token;
|
|
223
|
+
const idx = body.indexOf(":");
|
|
224
|
+
if (idx <= 0) return null;
|
|
225
|
+
const key = body.slice(0, idx).toLowerCase();
|
|
226
|
+
let value = body.slice(idx + 1);
|
|
227
|
+
if (value === "") return null;
|
|
228
|
+
if (!/^[a-z]+$/i.test(key)) return null;
|
|
229
|
+
if (value.startsWith("//")) return null;
|
|
230
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
231
|
+
value = value.slice(1, -1);
|
|
232
|
+
}
|
|
233
|
+
return { key, value, negated };
|
|
234
|
+
}
|
|
235
|
+
function applyFlagFilters(query, flags) {
|
|
236
|
+
const qualifiers = [...query.qualifiers];
|
|
237
|
+
if (flags.language) qualifiers.push({ key: "language", value: flags.language, negated: false });
|
|
238
|
+
if (flags.stars) qualifiers.push({ key: "stars", value: flags.stars, negated: false });
|
|
239
|
+
if (flags.org) qualifiers.push({ key: "org", value: flags.org, negated: false });
|
|
240
|
+
if (flags.user) qualifiers.push({ key: "user", value: flags.user, negated: false });
|
|
241
|
+
if (flags.topic) qualifiers.push({ key: "topic", value: flags.topic, negated: false });
|
|
242
|
+
if (flags.archived !== void 0)
|
|
243
|
+
qualifiers.push({ key: "archived", value: flags.archived ? "true" : "false", negated: false });
|
|
244
|
+
if (flags.fork !== void 0)
|
|
245
|
+
qualifiers.push({ key: "fork", value: flags.fork ? "true" : "false", negated: false });
|
|
246
|
+
return { ...query, qualifiers };
|
|
247
|
+
}
|
|
248
|
+
function validateQuery(query) {
|
|
249
|
+
const has = (key) => query.qualifiers.some((q) => q.key === key && !q.negated);
|
|
250
|
+
if (has("visibility") && has("private")) {
|
|
251
|
+
throw new Error("Cannot combine `visibility:` and `private:` filters.");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function buildGitHubQuery(query) {
|
|
255
|
+
const parts = [];
|
|
256
|
+
for (const kw of query.keywords) {
|
|
257
|
+
const alreadyQuoted = kw.startsWith('"') && kw.endsWith('"');
|
|
258
|
+
parts.push(kw.includes(" ") && !alreadyQuoted ? `"${kw}"` : kw);
|
|
259
|
+
}
|
|
260
|
+
for (const q of query.qualifiers) {
|
|
261
|
+
const needsQuote = q.value.includes(" ") && !q.value.startsWith('"');
|
|
262
|
+
const value = needsQuote ? `"${q.value}"` : q.value;
|
|
263
|
+
parts.push(`${q.negated ? "-" : ""}${q.key}:${value}`);
|
|
264
|
+
}
|
|
265
|
+
return parts.join(" ");
|
|
266
|
+
}
|
|
267
|
+
function githubSortParam(sort) {
|
|
268
|
+
switch (sort) {
|
|
269
|
+
case "stars":
|
|
270
|
+
return { sort: "stars", order: "desc" };
|
|
271
|
+
case "forks":
|
|
272
|
+
return { sort: "forks", order: "desc" };
|
|
273
|
+
case "updated":
|
|
274
|
+
return { sort: "updated", order: "desc" };
|
|
275
|
+
case "best-match":
|
|
276
|
+
default:
|
|
277
|
+
return {};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function buildSearchUrl(query, options) {
|
|
281
|
+
const q = buildGitHubQuery(query);
|
|
282
|
+
const { sort, order } = githubSortParam(options.sort);
|
|
283
|
+
const params = new URLSearchParams({ q: q || " " });
|
|
284
|
+
if (sort) params.set("sort", sort);
|
|
285
|
+
if (order) params.set("order", order);
|
|
286
|
+
return `https://api.github.com/search/repositories?${params.toString()}`;
|
|
287
|
+
}
|
|
288
|
+
function rankRepos(repos, strategy) {
|
|
289
|
+
const copy = [...repos];
|
|
290
|
+
switch (strategy) {
|
|
291
|
+
case "stars":
|
|
292
|
+
copy.sort(byKey((r) => r.stars));
|
|
293
|
+
break;
|
|
294
|
+
case "forks":
|
|
295
|
+
copy.sort(byKey((r) => r.forks));
|
|
296
|
+
break;
|
|
297
|
+
case "updated":
|
|
298
|
+
copy.sort(byKey((r) => Date.parse(r.updatedAt)));
|
|
299
|
+
break;
|
|
300
|
+
case "best-match":
|
|
301
|
+
default:
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
return copy;
|
|
305
|
+
}
|
|
306
|
+
function byKey(key) {
|
|
307
|
+
return (a, b) => key(b) - key(a) || b.stars - a.stars || a.id - b.id;
|
|
308
|
+
}
|
|
309
|
+
var GitHubSearchAdapter = class {
|
|
310
|
+
name = "github";
|
|
311
|
+
logger;
|
|
312
|
+
tokens;
|
|
313
|
+
tokenIndex = 0;
|
|
314
|
+
constructor(logger2 = noopLogger, tokens = []) {
|
|
315
|
+
this.logger = logger2;
|
|
316
|
+
this.tokens = tokens.filter(Boolean);
|
|
317
|
+
}
|
|
318
|
+
nextToken() {
|
|
319
|
+
if (this.tokens.length === 0) return void 0;
|
|
320
|
+
const token = this.tokens[this.tokenIndex % this.tokens.length];
|
|
321
|
+
this.tokenIndex++;
|
|
322
|
+
return token;
|
|
323
|
+
}
|
|
324
|
+
async search(query, options) {
|
|
325
|
+
const url = buildSearchUrl(query, options);
|
|
326
|
+
this.logger.debug(`[github] outgoing query: ${query.raw}`);
|
|
327
|
+
this.logger.debug(`[github] request url: ${url}`);
|
|
328
|
+
const all = [];
|
|
329
|
+
let totalCount = 0;
|
|
330
|
+
let rateLimited = false;
|
|
331
|
+
let rateLimitRemaining;
|
|
332
|
+
const perPage = Math.min(Math.max(options.limit, 1), 100);
|
|
333
|
+
const startPage = options.page ?? 1;
|
|
334
|
+
const maxPages = startPage > 1 ? startPage : Math.ceil(options.limit / perPage);
|
|
335
|
+
for (let page = startPage > 1 ? startPage : 1; page <= maxPages; page++) {
|
|
336
|
+
const pagedUrl = appendPage(url, page, perPage);
|
|
337
|
+
let lastErr;
|
|
338
|
+
for (let attempt = 0; attempt <= this.tokens.length; attempt++) {
|
|
339
|
+
const token = this.tokens.length > 0 ? this.nextToken() : options.token;
|
|
340
|
+
try {
|
|
341
|
+
const res = await fetch(pagedUrl, {
|
|
342
|
+
headers: {
|
|
343
|
+
Accept: "application/vnd.github+json",
|
|
344
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
345
|
+
"User-Agent": "ghfind",
|
|
346
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
const remainingStr = res.headers.get("x-ratelimit-remaining");
|
|
350
|
+
rateLimitRemaining = remainingStr !== null ? Number(remainingStr) : void 0;
|
|
351
|
+
if (res.status === 403) {
|
|
352
|
+
if (remainingStr === "0" || remainingStr === null) {
|
|
353
|
+
if (this.tokens.length > 0 && attempt < this.tokens.length) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
this.logger.warn("[github] rate limit exceeded");
|
|
357
|
+
rateLimited = true;
|
|
358
|
+
throw new RateLimitError(!!token);
|
|
359
|
+
}
|
|
360
|
+
const body = await res.text().catch(() => "");
|
|
361
|
+
this.logger.error(`[github] non-rate-limit 403: ${body.slice(0, 200)}`);
|
|
362
|
+
throw new NetworkError();
|
|
363
|
+
}
|
|
364
|
+
if (!res.ok) {
|
|
365
|
+
const body = await res.text().catch(() => "");
|
|
366
|
+
this.logger.error(`[github] API error ${res.status}: ${body.slice(0, 200)}`);
|
|
367
|
+
throw new NetworkError();
|
|
368
|
+
}
|
|
369
|
+
const raw = await res.text();
|
|
370
|
+
let parsed;
|
|
371
|
+
try {
|
|
372
|
+
parsed = JSON.parse(raw);
|
|
373
|
+
} catch {
|
|
374
|
+
const snippet = raw.slice(0, 200);
|
|
375
|
+
this.logger.error(`[github] invalid JSON response: ${snippet}`);
|
|
376
|
+
throw new ParseError("GitHub API", snippet.includes("<!DOCTYPE") ? "GitHub returned an HTML page (maintenance or CAPTCHA?)" : "Invalid JSON response");
|
|
377
|
+
}
|
|
378
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.items)) {
|
|
379
|
+
this.logger.error(`[github] unexpected response shape: ${JSON.stringify(parsed).slice(0, 200)}`);
|
|
380
|
+
throw new ParseError("GitHub API", "Unexpected response shape \u2014 missing items array");
|
|
381
|
+
}
|
|
382
|
+
const env = parsed;
|
|
383
|
+
totalCount = env.total_count;
|
|
384
|
+
const repos2 = normalizeEnvelope(env);
|
|
385
|
+
all.push(...repos2);
|
|
386
|
+
lastErr = void 0;
|
|
387
|
+
break;
|
|
388
|
+
} catch (err) {
|
|
389
|
+
lastErr = err;
|
|
390
|
+
if (rateLimited) break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (lastErr && !rateLimited) throw lastErr;
|
|
394
|
+
if (rateLimited) break;
|
|
395
|
+
if (all.length >= options.limit) break;
|
|
396
|
+
}
|
|
397
|
+
const repos = all.slice(0, options.limit);
|
|
398
|
+
if (repos.length === 0) {
|
|
399
|
+
this.logger.debug("[github] empty results");
|
|
400
|
+
}
|
|
401
|
+
return { totalCount, repos, rateLimited, rateLimitRemaining };
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
function appendPage(url, page, perPage) {
|
|
405
|
+
const u = new URL(url);
|
|
406
|
+
u.searchParams.set("page", String(page));
|
|
407
|
+
u.searchParams.set("per_page", String(perPage));
|
|
408
|
+
return u.toString();
|
|
409
|
+
}
|
|
410
|
+
var TRENDING_URL = "https://github.com/trending";
|
|
411
|
+
function parseTrendingHtml(html) {
|
|
412
|
+
const flat = html.replace(/>\s+</g, "><").replace(/\s+/g, " ");
|
|
413
|
+
const repos = [];
|
|
414
|
+
const articleRe = /<article class="Box-row">(.*?)<\/article>/gi;
|
|
415
|
+
let match;
|
|
416
|
+
let rank = 0;
|
|
417
|
+
while ((match = articleRe.exec(flat)) !== null) {
|
|
418
|
+
rank++;
|
|
419
|
+
const block = match[1];
|
|
420
|
+
const repoMatch = block.match(/href="\/([^\/\"]+)\/([^\"?#]+)"[^>]*class="Link"/);
|
|
421
|
+
if (!repoMatch) continue;
|
|
422
|
+
const owner = repoMatch[1];
|
|
423
|
+
const name = repoMatch[2];
|
|
424
|
+
const starsMatch = block.match(/href="\/[^\/\"]+\/[^\/\"]+\/stargazers"[^>]*>.*?<\/svg>\s*(\d[\d,]*)/);
|
|
425
|
+
const stars = starsMatch ? parseInt(starsMatch[1].replace(/,/g, ""), 10) : 0;
|
|
426
|
+
const growthMatch = block.match(/(\d[\d,]*)\s+stars\s+(today|this\s+\w+)/);
|
|
427
|
+
const starsToday = growthMatch ? parseInt(growthMatch[1].replace(/,/g, ""), 10) : 0;
|
|
428
|
+
const langMatch = block.match(/itemprop="programmingLanguage">([^<]+)</);
|
|
429
|
+
const language = langMatch ? langMatch[1].trim() : "";
|
|
430
|
+
const descMatch = block.match(/<p[^>]*class="[^"]*color-fg-muted[^"]*"[^>]*>([^<]+)</);
|
|
431
|
+
const description = descMatch ? descMatch[1].trim() : "";
|
|
432
|
+
repos.push({ rank, owner, name, stars, starsToday, language, description });
|
|
433
|
+
}
|
|
434
|
+
return repos;
|
|
435
|
+
}
|
|
436
|
+
function trendingRepoToRepo(r) {
|
|
437
|
+
return {
|
|
438
|
+
id: 0,
|
|
439
|
+
fullName: `${r.owner}/${r.name}`,
|
|
440
|
+
name: r.name,
|
|
441
|
+
owner: r.owner,
|
|
442
|
+
description: r.description,
|
|
443
|
+
url: `https://github.com/${r.owner}/${r.name}`,
|
|
444
|
+
stars: r.stars,
|
|
445
|
+
forks: 0,
|
|
446
|
+
watchers: 0,
|
|
447
|
+
language: r.language,
|
|
448
|
+
topics: [],
|
|
449
|
+
archived: false,
|
|
450
|
+
isFork: false,
|
|
451
|
+
private: false,
|
|
452
|
+
createdAt: "",
|
|
453
|
+
updatedAt: "",
|
|
454
|
+
pushedAt: "",
|
|
455
|
+
score: r.starsToday
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
var TrendingAdapter = class {
|
|
459
|
+
name = "trending";
|
|
460
|
+
logger;
|
|
461
|
+
constructor(logger2 = noopLogger) {
|
|
462
|
+
this.logger = logger2;
|
|
463
|
+
}
|
|
464
|
+
async search(_query, options) {
|
|
465
|
+
const since = options.trendingSince ?? "daily";
|
|
466
|
+
const url = since === "daily" ? TRENDING_URL : `${TRENDING_URL}?since=${since}`;
|
|
467
|
+
let res;
|
|
468
|
+
try {
|
|
469
|
+
res = await fetch(url, { headers: { "User-Agent": "ghfind" } });
|
|
470
|
+
} catch {
|
|
471
|
+
throw new NetworkError();
|
|
472
|
+
}
|
|
473
|
+
if (!res.ok) throw new NetworkError();
|
|
474
|
+
const html = await res.text();
|
|
475
|
+
const repos = parseTrendingHtml(html).map(trendingRepoToRepo);
|
|
476
|
+
return { totalCount: repos.length, repos, rateLimited: false };
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
var SearchModule = class _SearchModule {
|
|
480
|
+
name = "search";
|
|
481
|
+
adapter;
|
|
482
|
+
logger;
|
|
483
|
+
/** Shared cache across instances. */
|
|
484
|
+
static cache = new MemoryCache(3e5);
|
|
485
|
+
constructor(adapter, logger2 = noopLogger) {
|
|
486
|
+
this.adapter = adapter;
|
|
487
|
+
this.logger = logger2;
|
|
488
|
+
}
|
|
489
|
+
async search(query, options) {
|
|
490
|
+
const cacheKey = MemoryCache.key(
|
|
491
|
+
query.raw,
|
|
492
|
+
options.sort,
|
|
493
|
+
String(options.limit),
|
|
494
|
+
String(options.page ?? 1)
|
|
495
|
+
);
|
|
496
|
+
const cached = _SearchModule.cache.get(cacheKey);
|
|
497
|
+
if (cached) {
|
|
498
|
+
this.logger.debug("[search] cache hit");
|
|
499
|
+
return cached;
|
|
500
|
+
}
|
|
501
|
+
this.logger.debug("[search] cache miss");
|
|
502
|
+
const response = await this.adapter.search(query, options);
|
|
503
|
+
if (!response.rateLimited) {
|
|
504
|
+
_SearchModule.cache.set(cacheKey, response);
|
|
505
|
+
}
|
|
506
|
+
return response;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
function createGitHubSearch(logger2 = noopLogger, tokens = []) {
|
|
510
|
+
return new SearchModule(new GitHubSearchAdapter(logger2, tokens), logger2);
|
|
511
|
+
}
|
|
512
|
+
function createTrendingSearch(logger2 = noopLogger) {
|
|
513
|
+
return new SearchModule(new TrendingAdapter(logger2), logger2);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/output.ts
|
|
517
|
+
import { writeFileSync } from "fs";
|
|
518
|
+
|
|
519
|
+
// src/open-url.ts
|
|
520
|
+
function openUrl(url) {
|
|
521
|
+
try {
|
|
522
|
+
const p = process.platform;
|
|
523
|
+
if (p === "win32") Bun.spawn(["cmd", "/c", "start", "", url]);
|
|
524
|
+
else if (p === "darwin") Bun.spawn(["open", url]);
|
|
525
|
+
else Bun.spawn(["xdg-open", url]);
|
|
526
|
+
} catch {
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/output.ts
|
|
531
|
+
var FORMATS = /* @__PURE__ */ new Map([
|
|
532
|
+
// Export formats
|
|
533
|
+
["json", formatJson],
|
|
534
|
+
["csv", formatCsv],
|
|
535
|
+
["markdown", formatMarkdown],
|
|
536
|
+
["text", formatText],
|
|
537
|
+
// Pipe formats
|
|
538
|
+
["urls", formatUrls],
|
|
539
|
+
["names", formatNames],
|
|
540
|
+
["ssh-urls", formatSshUrls],
|
|
541
|
+
["clone-commands", formatCloneCommands],
|
|
542
|
+
["ids", formatIds]
|
|
543
|
+
]);
|
|
544
|
+
function formatJson(repos) {
|
|
545
|
+
return JSON.stringify(repos, null, 2);
|
|
546
|
+
}
|
|
547
|
+
function csvEscape(val) {
|
|
548
|
+
const str = String(val ?? "");
|
|
549
|
+
if (str.includes('"') || str.includes(",") || str.includes("\n")) {
|
|
550
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
551
|
+
}
|
|
552
|
+
return str;
|
|
553
|
+
}
|
|
554
|
+
function formatCsv(repos) {
|
|
555
|
+
const header = "rank,full_name,stars,forks,language,url";
|
|
556
|
+
const rows = repos.map(
|
|
557
|
+
(r, i) => `${i + 1},${csvEscape(r.fullName)},${r.stars},${r.forks},${csvEscape(r.language ?? "")},${csvEscape(r.url)}`
|
|
558
|
+
);
|
|
559
|
+
return [header, ...rows].join("\n");
|
|
560
|
+
}
|
|
561
|
+
function formatMarkdown(repos) {
|
|
562
|
+
const header = "| # | Repo | Stars | Forks | Language |";
|
|
563
|
+
const sep = "|---|------|-------|-------|----------|";
|
|
564
|
+
const rows = repos.map(
|
|
565
|
+
(r, i) => `| ${i + 1} | [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.forks.toLocaleString()} | ${r.language ?? "\u2014"} |`
|
|
566
|
+
);
|
|
567
|
+
return [header, sep, ...rows].join("\n");
|
|
568
|
+
}
|
|
569
|
+
function formatText(repos) {
|
|
570
|
+
return repos.map((r, i) => `${i + 1}. ${r.fullName} \u2605 ${r.stars.toLocaleString()} ${r.url}`).join("\n");
|
|
571
|
+
}
|
|
572
|
+
function formatUrls(repos) {
|
|
573
|
+
return repos.map((r) => r.url).join("\n");
|
|
574
|
+
}
|
|
575
|
+
function formatNames(repos) {
|
|
576
|
+
return repos.map((r) => r.fullName).join("\n");
|
|
577
|
+
}
|
|
578
|
+
function formatSshUrls(repos) {
|
|
579
|
+
return repos.map((r) => `git@github.com:${r.fullName}.git`).join("\n");
|
|
580
|
+
}
|
|
581
|
+
function formatCloneCommands(repos) {
|
|
582
|
+
return repos.map((r) => `git clone git@github.com:${r.fullName}.git`).join("\n");
|
|
583
|
+
}
|
|
584
|
+
function formatIds(repos) {
|
|
585
|
+
return repos.map((r) => String(r.id)).join("\n");
|
|
586
|
+
}
|
|
587
|
+
function format(repos, format2) {
|
|
588
|
+
const fn = FORMATS.get(format2);
|
|
589
|
+
if (!fn) throw new Error(`Unknown format: ${format2}`);
|
|
590
|
+
return fn(repos);
|
|
591
|
+
}
|
|
592
|
+
function exportToFile(repos, fmt) {
|
|
593
|
+
const ext = fmt === "markdown" ? "md" : fmt;
|
|
594
|
+
const ts = Date.now();
|
|
595
|
+
const path = `./ghfind-export-${ts}.${ext}`;
|
|
596
|
+
writeFileSync(path, format(repos, fmt), "utf-8");
|
|
597
|
+
return path;
|
|
598
|
+
}
|
|
599
|
+
async function pipeExec(repos, target) {
|
|
600
|
+
for (const repo of repos) {
|
|
601
|
+
const url = repo.url;
|
|
602
|
+
switch (target) {
|
|
603
|
+
case "open":
|
|
604
|
+
await openUrl(url);
|
|
605
|
+
break;
|
|
606
|
+
case "clone":
|
|
607
|
+
console.log(`git clone git@github.com:${repo.fullName}.git`);
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/watch.ts
|
|
614
|
+
var MAX_FAILS = 5;
|
|
615
|
+
async function runWatch(opts, onChange, onError) {
|
|
616
|
+
let previous = [];
|
|
617
|
+
let failCount = 0;
|
|
618
|
+
let intervalId = null;
|
|
619
|
+
const tick = async () => {
|
|
620
|
+
try {
|
|
621
|
+
let repos;
|
|
622
|
+
if (opts.trending) {
|
|
623
|
+
const since = opts.query === "weekly" ? "weekly" : opts.query === "monthly" ? "monthly" : "daily";
|
|
624
|
+
const trendingSearch = createTrendingSearch();
|
|
625
|
+
const response = await trendingSearch.search(
|
|
626
|
+
{ keywords: [], qualifiers: [], raw: "trending" },
|
|
627
|
+
{ limit: opts.limit, sort: opts.sort, json: false, verbose: false, trendingSince: since }
|
|
628
|
+
);
|
|
629
|
+
repos = response.repos;
|
|
630
|
+
} else {
|
|
631
|
+
const parsed = applyFlagFilters(parseQuery(opts.query), {});
|
|
632
|
+
const provider = createGitHubSearch(void 0, opts.token ? [opts.token] : []);
|
|
633
|
+
const options = {
|
|
634
|
+
limit: opts.limit,
|
|
635
|
+
sort: opts.sort,
|
|
636
|
+
json: false,
|
|
637
|
+
verbose: false,
|
|
638
|
+
token: opts.token
|
|
639
|
+
};
|
|
640
|
+
const response = await provider.search(parsed, options);
|
|
641
|
+
repos = rankRepos(response.repos, options.sort);
|
|
642
|
+
}
|
|
643
|
+
failCount = 0;
|
|
644
|
+
const delta = previous.length > 0 ? repos.length - previous.length : 0;
|
|
645
|
+
previous = repos;
|
|
646
|
+
onChange(repos, delta);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
failCount++;
|
|
649
|
+
onError(err instanceof Error ? err : new Error(String(err)));
|
|
650
|
+
if (failCount >= MAX_FAILS) {
|
|
651
|
+
onError(new Error(`Watch stopped after ${MAX_FAILS} consecutive failures`));
|
|
652
|
+
if (intervalId) clearInterval(intervalId);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
await tick();
|
|
657
|
+
if (failCount < MAX_FAILS) {
|
|
658
|
+
intervalId = setInterval(tick, opts.intervalMs);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/init.ts
|
|
663
|
+
import { createInterface } from "readline";
|
|
664
|
+
|
|
665
|
+
// src/themes.ts
|
|
666
|
+
var THEMES = {
|
|
667
|
+
"premium-dark": {
|
|
668
|
+
name: "premium-dark",
|
|
669
|
+
bg: "#3D3B3B",
|
|
670
|
+
surface: "#4a4848",
|
|
671
|
+
surfaceAlt: "#525050",
|
|
672
|
+
text: "#e0e4f0",
|
|
673
|
+
muted: "#a8a6a6",
|
|
674
|
+
blue: "#89b4fa",
|
|
675
|
+
green: "#a6e3a1",
|
|
676
|
+
yellow: "#f9e2af",
|
|
677
|
+
red: "#f38ba8",
|
|
678
|
+
teal: "#94e2d5",
|
|
679
|
+
purple: "#cba6f7",
|
|
680
|
+
orange: "#fab387",
|
|
681
|
+
border: "#1C1C1C",
|
|
682
|
+
borderAlt: "#2a2a2a",
|
|
683
|
+
separator: "#1C1C1C",
|
|
684
|
+
selectionBg: "#2A2A9C",
|
|
685
|
+
selectionText: "#ffffff",
|
|
686
|
+
success: "#a6e3a1",
|
|
687
|
+
warning: "#f9e2af",
|
|
688
|
+
error: "#f38ba8"
|
|
689
|
+
},
|
|
690
|
+
"tokyo-night": {
|
|
691
|
+
name: "tokyo-night",
|
|
692
|
+
bg: "#3D3B3B",
|
|
693
|
+
surface: "#4a4848",
|
|
694
|
+
surfaceAlt: "#525050",
|
|
695
|
+
text: "#e0e4f0",
|
|
696
|
+
muted: "#a8a6a6",
|
|
697
|
+
blue: "#89b4fa",
|
|
698
|
+
green: "#a6e3a1",
|
|
699
|
+
yellow: "#f9e2af",
|
|
700
|
+
red: "#f38ba8",
|
|
701
|
+
teal: "#94e2d5",
|
|
702
|
+
purple: "#cba6f7",
|
|
703
|
+
orange: "#fab387",
|
|
704
|
+
border: "#1C1C1C",
|
|
705
|
+
borderAlt: "#2a2a2a",
|
|
706
|
+
separator: "#1C1C1C",
|
|
707
|
+
selectionBg: "#2A2A9C",
|
|
708
|
+
selectionText: "#ffffff",
|
|
709
|
+
success: "#a6e3a1",
|
|
710
|
+
warning: "#f9e2af",
|
|
711
|
+
error: "#f38ba8"
|
|
712
|
+
},
|
|
713
|
+
dracula: {
|
|
714
|
+
name: "dracula",
|
|
715
|
+
bg: "#3D3B3B",
|
|
716
|
+
surface: "#4a4848",
|
|
717
|
+
surfaceAlt: "#525050",
|
|
718
|
+
text: "#f8f8f2",
|
|
719
|
+
muted: "#a8a6a6",
|
|
720
|
+
blue: "#8be9fd",
|
|
721
|
+
green: "#50fa7b",
|
|
722
|
+
yellow: "#f1fa8c",
|
|
723
|
+
red: "#ff5555",
|
|
724
|
+
teal: "#50fa7b",
|
|
725
|
+
purple: "#bd93f9",
|
|
726
|
+
orange: "#ffb86c",
|
|
727
|
+
border: "#1C1C1C",
|
|
728
|
+
borderAlt: "#2a2a2a",
|
|
729
|
+
separator: "#1C1C1C",
|
|
730
|
+
selectionBg: "#2A2A9C",
|
|
731
|
+
selectionText: "#ffffff",
|
|
732
|
+
success: "#50fa7b",
|
|
733
|
+
warning: "#f1fa8c",
|
|
734
|
+
error: "#ff5555"
|
|
735
|
+
},
|
|
736
|
+
monokai: {
|
|
737
|
+
name: "monokai",
|
|
738
|
+
bg: "#3D3B3B",
|
|
739
|
+
surface: "#4a4848",
|
|
740
|
+
surfaceAlt: "#525050",
|
|
741
|
+
text: "#f8f8f2",
|
|
742
|
+
muted: "#a8a6a6",
|
|
743
|
+
blue: "#66d9ef",
|
|
744
|
+
green: "#a6e22e",
|
|
745
|
+
yellow: "#e6db74",
|
|
746
|
+
red: "#f92672",
|
|
747
|
+
teal: "#a6e22e",
|
|
748
|
+
purple: "#ae81ff",
|
|
749
|
+
orange: "#fd971f",
|
|
750
|
+
border: "#1C1C1C",
|
|
751
|
+
borderAlt: "#2a2a2a",
|
|
752
|
+
separator: "#1C1C1C",
|
|
753
|
+
selectionBg: "#2A2A9C",
|
|
754
|
+
selectionText: "#f8f8f2",
|
|
755
|
+
success: "#a6e22e",
|
|
756
|
+
warning: "#e6db74",
|
|
757
|
+
error: "#f92672"
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
function loadTheme(name) {
|
|
761
|
+
return THEMES[name] ?? THEMES["tokyo-night"];
|
|
762
|
+
}
|
|
763
|
+
function listThemes() {
|
|
764
|
+
return Object.keys(THEMES);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/init.ts
|
|
768
|
+
function ask(query) {
|
|
769
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
770
|
+
return new Promise((resolve) => {
|
|
771
|
+
rl.question(query, (answer) => {
|
|
772
|
+
rl.close();
|
|
773
|
+
resolve(answer.trim());
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
async function runInitWizard() {
|
|
778
|
+
console.log("");
|
|
779
|
+
console.log(" ghfind setup wizard");
|
|
780
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
781
|
+
console.log("");
|
|
782
|
+
const token = await ask(" GitHub token (optional, press Enter to skip): ");
|
|
783
|
+
if (token && !token.startsWith("ghp_") && !token.startsWith("github_pat_") && !token.startsWith("gho_")) {
|
|
784
|
+
console.log(" \u26A0 Token doesn't look like a GitHub token (should start with ghp_, gho_, or github_pat_)");
|
|
785
|
+
}
|
|
786
|
+
const sortStr = await ask(" Default sort [best-match/stars/updated/forks] (best-match): ");
|
|
787
|
+
const limitStr = await ask(" Default result limit [10-100] (50): ");
|
|
788
|
+
const themes = listThemes();
|
|
789
|
+
const themeStr = await ask(` Theme [${themes.join("/")}] (tokyo-night): `);
|
|
790
|
+
const config = {
|
|
791
|
+
defaultSort: ["best-match", "stars", "updated", "forks"].includes(sortStr) ? sortStr : "best-match",
|
|
792
|
+
defaultLimit: Math.min(100, Math.max(10, parseInt(limitStr) || 50)),
|
|
793
|
+
theme: themes.includes(themeStr) ? themeStr : "tokyo-night",
|
|
794
|
+
cacheTtlSeconds: 300,
|
|
795
|
+
defaultTab: "search"
|
|
796
|
+
};
|
|
797
|
+
if (token) config.githubToken = token;
|
|
798
|
+
saveConfig(config);
|
|
799
|
+
const { configPath } = await import("./config-DFL44OVL.js");
|
|
800
|
+
console.log(` \u2713 Config saved to ${configPath()}`);
|
|
801
|
+
console.log(" Run 'ghfind' to start browsing!");
|
|
802
|
+
console.log("");
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/cli.ts
|
|
806
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
807
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
808
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
809
|
+
|
|
810
|
+
// src/tui.ts
|
|
811
|
+
import {
|
|
812
|
+
createCliRenderer as createCliRenderer2,
|
|
813
|
+
BoxRenderable as BoxRenderable2,
|
|
814
|
+
TextRenderable as TextRenderable2,
|
|
815
|
+
InputRenderable,
|
|
816
|
+
SelectRenderable,
|
|
817
|
+
ScrollBoxRenderable as ScrollBoxRenderable2
|
|
818
|
+
} from "@opentui/core";
|
|
819
|
+
|
|
820
|
+
// src/trending.ts
|
|
821
|
+
import {
|
|
822
|
+
createCliRenderer,
|
|
823
|
+
BoxRenderable,
|
|
824
|
+
TextRenderable,
|
|
825
|
+
ScrollBoxRenderable,
|
|
826
|
+
StyledText,
|
|
827
|
+
bold,
|
|
828
|
+
fg,
|
|
829
|
+
dim,
|
|
830
|
+
t
|
|
831
|
+
} from "@opentui/core";
|
|
832
|
+
var TAB_NAMES = ["Today", "This Week", "This Month"];
|
|
833
|
+
function tabSince(tab) {
|
|
834
|
+
switch (tab) {
|
|
835
|
+
case "Today":
|
|
836
|
+
return "daily";
|
|
837
|
+
case "This Week":
|
|
838
|
+
return "weekly";
|
|
839
|
+
default:
|
|
840
|
+
return "monthly";
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function fmtStars(n) {
|
|
844
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
|
|
845
|
+
if (n >= 999500) return "1M";
|
|
846
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}k`;
|
|
847
|
+
return String(n);
|
|
848
|
+
}
|
|
849
|
+
if (import.meta.main && false) {
|
|
850
|
+
launchTrending();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/help.ts
|
|
854
|
+
function buildHelpSections() {
|
|
855
|
+
return [
|
|
856
|
+
{
|
|
857
|
+
title: " Navigation ",
|
|
858
|
+
titleColor: "blue",
|
|
859
|
+
rows: [
|
|
860
|
+
{ keys: "\u2191 \u2193 / j k", action: "Move selection" },
|
|
861
|
+
{ keys: "Enter", action: "Open repo in browser" },
|
|
862
|
+
{ keys: "/", action: "Focus search input" },
|
|
863
|
+
{ keys: "Space", action: "Open command menu" },
|
|
864
|
+
{ keys: "? / Ctrl+H", action: "Toggle this help screen" },
|
|
865
|
+
{ keys: "q", action: "Quit" }
|
|
866
|
+
]
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
title: " Main View ",
|
|
870
|
+
titleColor: "green",
|
|
871
|
+
rows: [
|
|
872
|
+
{ keys: "Tab", action: "Auto-complete qualifier" },
|
|
873
|
+
{ keys: "PageDown", action: "Next page of results" },
|
|
874
|
+
{ keys: "PageUp", action: "Scroll to top of results" }
|
|
875
|
+
]
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
title: " Trending Mode ",
|
|
879
|
+
titleColor: "yellow",
|
|
880
|
+
rows: [
|
|
881
|
+
{ keys: "1-5", action: "Switch tab: Today \u2192 Week \u2192 Month \u2192 \u2026" },
|
|
882
|
+
{ keys: "\u2190 \u2192 / h l", action: "Navigate tabs" }
|
|
883
|
+
]
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
title: " Command Menu (Space)",
|
|
887
|
+
titleColor: "purple",
|
|
888
|
+
rows: [
|
|
889
|
+
{ keys: "Sort", action: "Cycle sort: best-match \u2192 stars \u2192 \u2026" },
|
|
890
|
+
{ keys: "Limit", action: "Cycle limit: 10 \u2192 25 \u2192 50 \u2192 100" },
|
|
891
|
+
{ keys: "Refresh", action: "Re-run current search" },
|
|
892
|
+
{ keys: "Graph", action: "Toggle commit activity chart" },
|
|
893
|
+
{ keys: "Readme", action: "Full README viewer" },
|
|
894
|
+
{ keys: "Deep-dive", action: "Languages + contributors + excerpt" },
|
|
895
|
+
{ keys: "Bookmark", action: "Save / unsave selected repo" },
|
|
896
|
+
{ keys: "Compare", action: "Add to / remove from comparison" },
|
|
897
|
+
{ keys: "Trending", action: "Switch to trending browser" }
|
|
898
|
+
],
|
|
899
|
+
note: "Also: Open, History, Export, Share, Topics, Saved, Notifs, Help"
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
title: " Overlay Keys ",
|
|
903
|
+
titleColor: "teal",
|
|
904
|
+
rows: [
|
|
905
|
+
{ keys: "Esc / q", action: "Close any overlay" },
|
|
906
|
+
{ keys: "Enter", action: "Confirm selection / run action" },
|
|
907
|
+
{ keys: "d", action: "Delete entry (history, bookmarks, saved)" },
|
|
908
|
+
{ keys: "\u2191 \u2193 / j k", action: "Navigate overlay list" }
|
|
909
|
+
]
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
title: " Search Syntax ",
|
|
913
|
+
titleColor: "teal",
|
|
914
|
+
rows: [
|
|
915
|
+
{ keys: "keywords", action: "Free-text search" },
|
|
916
|
+
{ keys: "key:value", action: "Qualifier filter" },
|
|
917
|
+
{ keys: "-key:value", action: "Negated qualifier" },
|
|
918
|
+
{ keys: '"phrase"', action: "Exact phrase match" }
|
|
919
|
+
]
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
title: " Qualifiers ",
|
|
923
|
+
titleColor: "muted",
|
|
924
|
+
rows: [
|
|
925
|
+
{ keys: "language:", action: "Filter by language" },
|
|
926
|
+
{ keys: "stars:>N", action: "Minimum stars" },
|
|
927
|
+
{ keys: "fork:true", action: "Include forks" },
|
|
928
|
+
{ keys: "user:", action: "User's repos" },
|
|
929
|
+
{ keys: "org:", action: "Organization repos" },
|
|
930
|
+
{ keys: "repo:", action: "Specific repo" },
|
|
931
|
+
{ keys: "topic:", action: "Filter by topic" },
|
|
932
|
+
{ keys: "license:", action: "License type" },
|
|
933
|
+
{ keys: "pushed:>YYYY-MM-DD", action: "Last push date" },
|
|
934
|
+
{ keys: "archived:false", action: "Exclude archived" }
|
|
935
|
+
]
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
title: " Rate Limits ",
|
|
939
|
+
titleColor: "orange",
|
|
940
|
+
rows: [
|
|
941
|
+
{ keys: "No token", action: "10 req / min" },
|
|
942
|
+
{ keys: "GITHUB_TOKEN", action: "30 req / min (or your plan limit)" }
|
|
943
|
+
],
|
|
944
|
+
note: "Set GITHUB_TOKEN in your environment or add to config."
|
|
945
|
+
}
|
|
946
|
+
];
|
|
947
|
+
}
|
|
948
|
+
var HELP_KEYS_COLUMN = 24;
|
|
949
|
+
|
|
950
|
+
// src/storage.ts
|
|
951
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync as writeFileSync2, appendFileSync, unlinkSync } from "fs";
|
|
952
|
+
import { join } from "path";
|
|
953
|
+
function ensureDir() {
|
|
954
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
955
|
+
}
|
|
956
|
+
function readJSON(filename, fallback) {
|
|
957
|
+
ensureDir();
|
|
958
|
+
try {
|
|
959
|
+
return JSON.parse(readFileSync(join(stateDir(), filename), "utf-8"));
|
|
960
|
+
} catch {
|
|
961
|
+
return fallback;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function writeJSON(filename, data) {
|
|
965
|
+
ensureDir();
|
|
966
|
+
writeFileSync2(join(stateDir(), filename), JSON.stringify(data, null, 2));
|
|
967
|
+
}
|
|
968
|
+
function appendJSONL(filename, obj) {
|
|
969
|
+
ensureDir();
|
|
970
|
+
try {
|
|
971
|
+
appendFileSync(join(stateDir(), filename), JSON.stringify(obj) + "\n", "utf-8");
|
|
972
|
+
} catch {
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function readJSONL(filename) {
|
|
976
|
+
ensureDir();
|
|
977
|
+
try {
|
|
978
|
+
const raw = readFileSync(join(stateDir(), filename), "utf-8").trim();
|
|
979
|
+
if (!raw) return [];
|
|
980
|
+
return raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
981
|
+
} catch {
|
|
982
|
+
return [];
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function writeRaw(filename, data) {
|
|
986
|
+
ensureDir();
|
|
987
|
+
writeFileSync2(join(stateDir(), filename), data);
|
|
988
|
+
}
|
|
989
|
+
function debugLog(msg) {
|
|
990
|
+
if (!process.env.DEBUG) return;
|
|
991
|
+
try {
|
|
992
|
+
appendFileSync(join(stateDir(), "ghfind-debug.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
993
|
+
`, "utf-8");
|
|
994
|
+
} catch {
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/history.ts
|
|
999
|
+
var HISTORY_FILE = "history.jsonl";
|
|
1000
|
+
var MAX_ENTRIES = 500;
|
|
1001
|
+
function appendHistory(entry) {
|
|
1002
|
+
appendJSONL(HISTORY_FILE, entry);
|
|
1003
|
+
}
|
|
1004
|
+
function readHistory() {
|
|
1005
|
+
const entries = readJSONL(HISTORY_FILE);
|
|
1006
|
+
entries.reverse();
|
|
1007
|
+
const deduped = [];
|
|
1008
|
+
let lastQuery;
|
|
1009
|
+
for (const e of entries) {
|
|
1010
|
+
if (e.query !== lastQuery) {
|
|
1011
|
+
deduped.push(e);
|
|
1012
|
+
lastQuery = e.query;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return deduped;
|
|
1016
|
+
}
|
|
1017
|
+
function deleteHistoryEntry(index) {
|
|
1018
|
+
const entries = readHistory();
|
|
1019
|
+
if (index < 0 || index >= entries.length) return;
|
|
1020
|
+
const removed = entries[index];
|
|
1021
|
+
const remaining = entries.filter((e) => e.timestamp !== removed.timestamp || e.query !== removed.query);
|
|
1022
|
+
writeRaw(HISTORY_FILE, remaining.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
1023
|
+
}
|
|
1024
|
+
function clearHistory() {
|
|
1025
|
+
writeRaw(HISTORY_FILE, "");
|
|
1026
|
+
}
|
|
1027
|
+
function rotateHistory() {
|
|
1028
|
+
const entries = readHistory();
|
|
1029
|
+
if (entries.length <= MAX_ENTRIES) return;
|
|
1030
|
+
const trimmed = entries.slice(0, MAX_ENTRIES);
|
|
1031
|
+
writeRaw(HISTORY_FILE, trimmed.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/bookmarks.ts
|
|
1035
|
+
var BOOKMARK_FILE = "bookmarks.json";
|
|
1036
|
+
function loadRaw() {
|
|
1037
|
+
return readJSON(BOOKMARK_FILE, []);
|
|
1038
|
+
}
|
|
1039
|
+
function saveRaw(bookmarks) {
|
|
1040
|
+
writeJSON(BOOKMARK_FILE, bookmarks);
|
|
1041
|
+
}
|
|
1042
|
+
function getBookmarks() {
|
|
1043
|
+
return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
|
|
1044
|
+
}
|
|
1045
|
+
function toggleBookmark(repo, tags = []) {
|
|
1046
|
+
const bookmarks = loadRaw();
|
|
1047
|
+
const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
|
|
1048
|
+
if (idx >= 0) {
|
|
1049
|
+
bookmarks.splice(idx, 1);
|
|
1050
|
+
saveRaw(bookmarks);
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
bookmarks.push({ repo, savedAt: Date.now(), tags });
|
|
1054
|
+
saveRaw(bookmarks);
|
|
1055
|
+
return true;
|
|
1056
|
+
}
|
|
1057
|
+
function removeBookmark(fullName) {
|
|
1058
|
+
const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
|
|
1059
|
+
saveRaw(bookmarks);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/saved-searches.ts
|
|
1063
|
+
var SAVED_FILE = "saved-searches.json";
|
|
1064
|
+
function loadRaw2() {
|
|
1065
|
+
return readJSON(SAVED_FILE, []);
|
|
1066
|
+
}
|
|
1067
|
+
function saveRaw2(searches) {
|
|
1068
|
+
writeJSON(SAVED_FILE, searches);
|
|
1069
|
+
}
|
|
1070
|
+
function getSavedSearches() {
|
|
1071
|
+
return loadRaw2().sort((a, b) => (b.lastRunAt ?? b.createdAt) - (a.lastRunAt ?? a.createdAt));
|
|
1072
|
+
}
|
|
1073
|
+
function saveSearch(name, query, mode, sort, limit, tab) {
|
|
1074
|
+
const searches = loadRaw2();
|
|
1075
|
+
const now = Date.now();
|
|
1076
|
+
const existing = searches.findIndex((s) => s.name === name);
|
|
1077
|
+
const entry = { name, query, mode, sort, limit, tab, createdAt: now, lastRunAt: now };
|
|
1078
|
+
if (existing >= 0) {
|
|
1079
|
+
searches[existing] = { ...searches[existing], ...entry, createdAt: searches[existing].createdAt };
|
|
1080
|
+
} else {
|
|
1081
|
+
searches.push(entry);
|
|
1082
|
+
}
|
|
1083
|
+
saveRaw2(searches);
|
|
1084
|
+
return entry;
|
|
1085
|
+
}
|
|
1086
|
+
function deleteSavedSearch(name) {
|
|
1087
|
+
saveRaw2(loadRaw2().filter((s) => s.name !== name));
|
|
1088
|
+
}
|
|
1089
|
+
function touchSavedSearch(name) {
|
|
1090
|
+
const searches = loadRaw2();
|
|
1091
|
+
const found = searches.find((s) => s.name === name);
|
|
1092
|
+
if (found) {
|
|
1093
|
+
found.lastRunAt = Date.now();
|
|
1094
|
+
saveRaw2(searches);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/session.ts
|
|
1099
|
+
var SESSION_FILE = "session.json";
|
|
1100
|
+
function saveSession(state) {
|
|
1101
|
+
try {
|
|
1102
|
+
writeJSON(SESSION_FILE, state);
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
if (process.env.DEBUG)
|
|
1105
|
+
console.error(`[ghfind] Failed to save session: ${err instanceof Error ? err.message : String(err)}`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function restoreSession() {
|
|
1109
|
+
return readJSON(SESSION_FILE, null);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/deepdive.ts
|
|
1113
|
+
var USER_AGENT = "ghfind/1.0";
|
|
1114
|
+
async function fetchDeepDive(repo, token) {
|
|
1115
|
+
const auth = token ? { Authorization: `Bearer ${token}` } : {};
|
|
1116
|
+
const headers = { "User-Agent": USER_AGENT, ...auth };
|
|
1117
|
+
const [languagesRes, contributorsRes] = await Promise.all([
|
|
1118
|
+
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, { headers }),
|
|
1119
|
+
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`, { headers })
|
|
1120
|
+
]);
|
|
1121
|
+
let readmeText = "";
|
|
1122
|
+
for (const path of [`${repo.owner}/${repo.name}/main/README.md`, `${repo.owner}/${repo.name}/master/README.md`, `${repo.owner}/${repo.name}/main/README.rst`]) {
|
|
1123
|
+
const r = await fetch(`https://raw.githubusercontent.com/${path}`, { headers });
|
|
1124
|
+
if (r.ok) {
|
|
1125
|
+
readmeText = await r.text();
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
summary: formatSummary(repo),
|
|
1131
|
+
languages: formatLanguages(languagesRes.ok ? await languagesRes.json().catch(() => ({})) : {}),
|
|
1132
|
+
contributors: formatContributors(contributorsRes.ok ? await contributorsRes.json().catch(() => []) : []),
|
|
1133
|
+
readme: formatReadme(readmeText)
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
function formatSummary(repo) {
|
|
1137
|
+
const lines = [
|
|
1138
|
+
` ${repo.fullName}`,
|
|
1139
|
+
` \u2605 ${repo.stars.toLocaleString()} \u25C6 ${repo.forks.toLocaleString()} ${repo.language ?? "\u2014"}`,
|
|
1140
|
+
repo.description ? ` ${repo.description}` : "",
|
|
1141
|
+
repo.topics.length ? ` topics: ${repo.topics.slice(0, 8).join(", ")}` : "",
|
|
1142
|
+
` ${repo.url}`
|
|
1143
|
+
];
|
|
1144
|
+
return lines.filter(Boolean).join("\n");
|
|
1145
|
+
}
|
|
1146
|
+
function formatLanguages(langs) {
|
|
1147
|
+
const entries = Object.entries(langs);
|
|
1148
|
+
if (entries.length === 0) return " (no language data)";
|
|
1149
|
+
const total = entries.reduce((s, [, v]) => s + v, 0);
|
|
1150
|
+
const barW = 20;
|
|
1151
|
+
return entries.sort(([, a], [, b]) => b - a).slice(0, 8).map(([name, bytes]) => {
|
|
1152
|
+
const pct = (bytes / total * 100).toFixed(1);
|
|
1153
|
+
const filled = Math.round(bytes / total * barW);
|
|
1154
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barW - filled);
|
|
1155
|
+
return ` ${name.padEnd(16)} ${bar} ${pct}%`;
|
|
1156
|
+
}).join("\n");
|
|
1157
|
+
}
|
|
1158
|
+
function formatContributors(contributors) {
|
|
1159
|
+
if (contributors.length === 0) return " (no contributor data)";
|
|
1160
|
+
return contributors.slice(0, 5).map((c) => ` \u25B2 ${c.login.padEnd(20)} ${c.contributions} commits`).join("\n");
|
|
1161
|
+
}
|
|
1162
|
+
function formatReadme(readme) {
|
|
1163
|
+
if (!readme) return " (no README)";
|
|
1164
|
+
const stripped = readme.replace(/```[\s\S]*?```/g, "[code block]").replace(/#{1,6}\s/g, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1").replace(/`([^`]+)`/g, "$1").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1165
|
+
const lines = stripped.slice(0, 30);
|
|
1166
|
+
if (stripped.length > 30) lines.push("... (more lines, open in browser to read full)");
|
|
1167
|
+
return lines.map((l) => ` ${l}`).join("\n");
|
|
1168
|
+
}
|
|
1169
|
+
function buildDeepDiveText(data) {
|
|
1170
|
+
return [
|
|
1171
|
+
"\u2500\u2500 Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
1172
|
+
data.summary,
|
|
1173
|
+
"",
|
|
1174
|
+
"\u2500\u2500 Languages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
1175
|
+
data.languages,
|
|
1176
|
+
"",
|
|
1177
|
+
"\u2500\u2500 Top Contributors \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
1178
|
+
data.contributors,
|
|
1179
|
+
"",
|
|
1180
|
+
"\u2500\u2500 README \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
1181
|
+
data.readme
|
|
1182
|
+
].join("\n");
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/compare.ts
|
|
1186
|
+
function buildComparisonTable(repos) {
|
|
1187
|
+
if (repos.length < 2) return "Select at least 2 repos to compare (press c to add).";
|
|
1188
|
+
const rows = [];
|
|
1189
|
+
const names = repos.map((r) => r.fullName);
|
|
1190
|
+
rows.push({ label: "Stars", values: repos.map((r) => `\u2605 ${r.stars.toLocaleString()}`) });
|
|
1191
|
+
rows.push({ label: "Forks", values: repos.map((r) => r.forks.toLocaleString()) });
|
|
1192
|
+
rows.push({ label: "Language", values: repos.map((r) => r.language ?? "\u2014") });
|
|
1193
|
+
rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "\u2014") });
|
|
1194
|
+
rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "\u2014") });
|
|
1195
|
+
rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 3).join(", ") ?? "\u2014") || "\u2014") });
|
|
1196
|
+
const colW = Math.max(15, ...names.map((n) => n.length));
|
|
1197
|
+
const valW = Math.max(8, ...rows.flatMap((r) => r.values.map((v) => v.length)));
|
|
1198
|
+
const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
|
|
1199
|
+
const sep = ["".padEnd(14, "\u2500"), ...names.map(() => "".padEnd(colW, "\u2500"))];
|
|
1200
|
+
const lines = [
|
|
1201
|
+
"\u250C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u252C") + "\u2510",
|
|
1202
|
+
"\u2502" + header.map((h) => ` ${h} `).join("\u2502") + "\u2502",
|
|
1203
|
+
"\u251C" + sep.map((s) => "\u2500" + s + "\u2500").join("\u253C") + "\u2524"
|
|
1204
|
+
];
|
|
1205
|
+
for (const row of rows) {
|
|
1206
|
+
const cells = [row.label.padEnd(14), ...row.values.map((v) => v.padEnd(colW))];
|
|
1207
|
+
lines.push("\u2502" + cells.map((c) => ` ${c} `).join("\u2502") + "\u2502");
|
|
1208
|
+
}
|
|
1209
|
+
lines.push("\u2514" + sep.map((s) => "\u2500" + s + "\u2500").join("\u2534") + "\u2518");
|
|
1210
|
+
return lines.join("\n");
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/explore.ts
|
|
1214
|
+
async function fetchTopics() {
|
|
1215
|
+
const res = await fetch("https://api.github.com/search/topics?q=popular&per_page=30", {
|
|
1216
|
+
headers: {
|
|
1217
|
+
Accept: "application/vnd.github.mercy-preview+json",
|
|
1218
|
+
"User-Agent": "search-cli"
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
if (!res.ok) throw new Error(`Topics API: HTTP ${res.status}`);
|
|
1222
|
+
const data = await res.json();
|
|
1223
|
+
return (data.items ?? []).map((item) => ({
|
|
1224
|
+
name: item.name,
|
|
1225
|
+
description: item.description || "",
|
|
1226
|
+
repoCount: item.score || 0
|
|
1227
|
+
}));
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// src/status.ts
|
|
1231
|
+
var SEARCH_SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1232
|
+
var LOADING_SPINNER = ["\u25B0\u25B1\u25B1\u25B1\u25B1", "\u25B0\u25B0\u25B1\u25B1\u25B1", "\u25B0\u25B0\u25B0\u25B1\u25B1", "\u25B0\u25B0\u25B0\u25B0\u25B1", "\u25B0\u25B0\u25B0\u25B0\u25B0"];
|
|
1233
|
+
var StatusManager = class {
|
|
1234
|
+
spinnerInterval = null;
|
|
1235
|
+
spinnerIndex = 0;
|
|
1236
|
+
type = "idle";
|
|
1237
|
+
message = "";
|
|
1238
|
+
onUpdate;
|
|
1239
|
+
constructor(onUpdate) {
|
|
1240
|
+
this.onUpdate = onUpdate;
|
|
1241
|
+
}
|
|
1242
|
+
set(type, message) {
|
|
1243
|
+
this.stopSpinner();
|
|
1244
|
+
this.type = type;
|
|
1245
|
+
this.message = message;
|
|
1246
|
+
if (type === "searching" || type === "loading") {
|
|
1247
|
+
this.startSpinner(type === "searching" ? SEARCH_SPINNER : LOADING_SPINNER);
|
|
1248
|
+
} else {
|
|
1249
|
+
this.emit();
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
startSpinner(frames) {
|
|
1253
|
+
this.spinnerIndex = 0;
|
|
1254
|
+
this.emit(frames[0]);
|
|
1255
|
+
this.spinnerInterval = setInterval(() => {
|
|
1256
|
+
this.spinnerIndex = (this.spinnerIndex + 1) % frames.length;
|
|
1257
|
+
this.emit(frames[this.spinnerIndex]);
|
|
1258
|
+
}, 120);
|
|
1259
|
+
}
|
|
1260
|
+
stopSpinner() {
|
|
1261
|
+
if (this.spinnerInterval) {
|
|
1262
|
+
clearInterval(this.spinnerInterval);
|
|
1263
|
+
this.spinnerInterval = null;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
emit(spinner) {
|
|
1267
|
+
const icon = this.type === "searching" ? "\u{1F50D}" : this.type === "loading" ? "\u{1F4E6}" : this.type === "success" ? "\u2713" : this.type === "error" ? "\u26A0" : " ";
|
|
1268
|
+
const prefix = spinner ?? "";
|
|
1269
|
+
this.onUpdate(`${icon} ${this.message} ${prefix}`.trim());
|
|
1270
|
+
}
|
|
1271
|
+
dispose() {
|
|
1272
|
+
this.stopSpinner();
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
// src/notifications.ts
|
|
1277
|
+
var nextId = 1;
|
|
1278
|
+
var notifs = [];
|
|
1279
|
+
var FILE = "notifications.json";
|
|
1280
|
+
function load() {
|
|
1281
|
+
notifs = readJSON(FILE, []);
|
|
1282
|
+
nextId = Math.max(...notifs.map((n) => n.id), 0) + 1;
|
|
1283
|
+
}
|
|
1284
|
+
function save() {
|
|
1285
|
+
writeJSON(FILE, notifs);
|
|
1286
|
+
}
|
|
1287
|
+
function getNotifications() {
|
|
1288
|
+
if (notifs.length === 0) load();
|
|
1289
|
+
return [...notifs];
|
|
1290
|
+
}
|
|
1291
|
+
function dismissNotification(id) {
|
|
1292
|
+
notifs = notifs.filter((n) => n.id !== id);
|
|
1293
|
+
save();
|
|
1294
|
+
}
|
|
1295
|
+
function dismissAll() {
|
|
1296
|
+
notifs = [];
|
|
1297
|
+
save();
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// src/share.ts
|
|
1301
|
+
import { spawn } from "child_process";
|
|
1302
|
+
function formatShare(repo, format2) {
|
|
1303
|
+
switch (format2) {
|
|
1304
|
+
case "markdown":
|
|
1305
|
+
return `[${repo.fullName}](${repo.url}) \u2014 ${repo.description ?? ""}`.trim();
|
|
1306
|
+
case "plain":
|
|
1307
|
+
return `${repo.fullName} \u2014 ${repo.description ?? ""} \u2014 ${repo.url}`.trim();
|
|
1308
|
+
case "gh-cli":
|
|
1309
|
+
return `gh repo view ${repo.fullName}`;
|
|
1310
|
+
case "short":
|
|
1311
|
+
return repo.fullName;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
async function copyToClipboard(text) {
|
|
1315
|
+
try {
|
|
1316
|
+
const p = process.platform;
|
|
1317
|
+
const cmd = p === "win32" ? ["clip"] : p === "darwin" ? ["pbcopy"] : await findLinuxClipCmd();
|
|
1318
|
+
if (!cmd) return false;
|
|
1319
|
+
const proc = spawn(cmd[0], cmd.slice(1), { stdio: ["pipe", "ignore", "ignore"] });
|
|
1320
|
+
proc.stdin.write(text);
|
|
1321
|
+
proc.stdin.end();
|
|
1322
|
+
return new Promise((resolve) => {
|
|
1323
|
+
proc.on("close", (code) => resolve(code === 0));
|
|
1324
|
+
proc.on("error", () => resolve(false));
|
|
1325
|
+
});
|
|
1326
|
+
} catch {
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/tips.ts
|
|
1332
|
+
var TIPS = [
|
|
1333
|
+
"\u{1F4A1} Tip: Press ? to see all keyboard shortcuts",
|
|
1334
|
+
"\u{1F4A1} Tip: Use Ctrl+R to recall past searches",
|
|
1335
|
+
"\u{1F4A1} Tip: Press b to bookmark a repository",
|
|
1336
|
+
"\u{1F4A1} Tip: Tab auto-completes qualifiers like language: and stars:",
|
|
1337
|
+
"\u{1F4A1} Tip: Press Ctrl+E to export results as CSV or Markdown",
|
|
1338
|
+
"\u{1F4A1} Tip: Set GITHUB_TOKEN for higher API rate limits",
|
|
1339
|
+
"\u{1F4A1} Tip: Press c to compare up to 5 repos side-by-side",
|
|
1340
|
+
"\u{1F4A1} Tip: Press d for a deep-dive into any repo",
|
|
1341
|
+
"\u{1F4A1} Tip: Press E to browse popular GitHub topics",
|
|
1342
|
+
"\u{1F4A1} Tip: Press Ctrl+P to copy a repo link to clipboard"
|
|
1343
|
+
];
|
|
1344
|
+
var lastIndex = -1;
|
|
1345
|
+
function nextTip() {
|
|
1346
|
+
let idx;
|
|
1347
|
+
do {
|
|
1348
|
+
idx = Math.floor(Math.random() * TIPS.length);
|
|
1349
|
+
} while (idx === lastIndex && TIPS.length > 1);
|
|
1350
|
+
lastIndex = idx;
|
|
1351
|
+
return TIPS[idx];
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// src/tui.ts
|
|
1355
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1356
|
+
import { join as join2, dirname } from "path";
|
|
1357
|
+
import { fileURLToPath } from "url";
|
|
1358
|
+
|
|
1359
|
+
// src/update-check.ts
|
|
1360
|
+
var REGISTRY_URL = "https://registry.npmjs.org/github-search-cli/latest";
|
|
1361
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1362
|
+
var REQUEST_TIMEOUT_MS = 5e3;
|
|
1363
|
+
var STATE_FILE = "update-state.json";
|
|
1364
|
+
var DEFAULTS = {
|
|
1365
|
+
lastCheck: 0,
|
|
1366
|
+
snoozeUntil: 0,
|
|
1367
|
+
suppressed: false
|
|
1368
|
+
};
|
|
1369
|
+
function readUpdateState() {
|
|
1370
|
+
return { ...DEFAULTS, ...readJSON(STATE_FILE, {}) };
|
|
1371
|
+
}
|
|
1372
|
+
function writeUpdateState(state) {
|
|
1373
|
+
writeJSON(STATE_FILE, state);
|
|
1374
|
+
}
|
|
1375
|
+
function shouldCheckUpdate() {
|
|
1376
|
+
const state = readUpdateState();
|
|
1377
|
+
if (state.suppressed) return false;
|
|
1378
|
+
if (Date.now() < state.snoozeUntil) return false;
|
|
1379
|
+
if (Date.now() - state.lastCheck < CHECK_INTERVAL_MS) return false;
|
|
1380
|
+
return true;
|
|
1381
|
+
}
|
|
1382
|
+
function isNewerVersion(current, latest) {
|
|
1383
|
+
if (typeof Bun !== "undefined" && Bun.semver) {
|
|
1384
|
+
return Bun.semver.order(current, latest) < 0;
|
|
1385
|
+
}
|
|
1386
|
+
const curParts = current.split(".").map((s) => Number(s.split("-")[0]));
|
|
1387
|
+
const newParts = latest.split(".").map((s) => Number(s.split("-")[0]));
|
|
1388
|
+
for (let i = 0; i < Math.max(curParts.length, newParts.length); i++) {
|
|
1389
|
+
const c = curParts[i] ?? 0;
|
|
1390
|
+
const n = newParts[i] ?? 0;
|
|
1391
|
+
if (n > c) return true;
|
|
1392
|
+
if (n < c) return false;
|
|
1393
|
+
}
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
async function checkForUpdate(currentVersion) {
|
|
1397
|
+
if (process.env.DEBUG) debugLog(`Checking for update (current: ${currentVersion})`);
|
|
1398
|
+
try {
|
|
1399
|
+
const res = await fetch(REGISTRY_URL, {
|
|
1400
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1401
|
+
});
|
|
1402
|
+
if (!res.ok) {
|
|
1403
|
+
if (process.env.DEBUG) debugLog(`Registry returned ${res.status}`);
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
const data = await res.json();
|
|
1407
|
+
const latest = data.version;
|
|
1408
|
+
if (!latest) {
|
|
1409
|
+
if (process.env.DEBUG) debugLog("No version in registry response");
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1412
|
+
if (process.env.DEBUG) debugLog(`Registry latest: ${latest}`);
|
|
1413
|
+
if (!isNewerVersion(currentVersion, latest)) return null;
|
|
1414
|
+
return latest;
|
|
1415
|
+
} catch (e) {
|
|
1416
|
+
if (process.env.DEBUG) debugLog(`Update check failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1417
|
+
return null;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
function markUpdateChecked(latestVersion) {
|
|
1421
|
+
writeUpdateState({
|
|
1422
|
+
...readUpdateState(),
|
|
1423
|
+
lastCheck: Date.now(),
|
|
1424
|
+
latestVersion
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
function snoozeUpdateNotices(days) {
|
|
1428
|
+
writeUpdateState({
|
|
1429
|
+
...readUpdateState(),
|
|
1430
|
+
snoozeUntil: Date.now() + days * 24 * 60 * 60 * 1e3
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
function suppressUpdateNotices() {
|
|
1434
|
+
writeUpdateState({
|
|
1435
|
+
...readUpdateState(),
|
|
1436
|
+
suppressed: true
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
async function performUpdate() {
|
|
1440
|
+
const isBun = typeof process.versions !== "undefined" && "bun" in process.versions;
|
|
1441
|
+
const cmd = isBun ? ["bun", "install", "-g", "ghfind"] : ["npm", "install", "-g", "ghfind"];
|
|
1442
|
+
try {
|
|
1443
|
+
const proc = Bun.spawn(cmd, {
|
|
1444
|
+
stdout: "pipe",
|
|
1445
|
+
stderr: "pipe"
|
|
1446
|
+
});
|
|
1447
|
+
await proc.exited;
|
|
1448
|
+
if (proc.exitCode !== 0) {
|
|
1449
|
+
const err = await proc.stderr.text();
|
|
1450
|
+
debugLog(`Install failed (code ${proc.exitCode}): ${err}`);
|
|
1451
|
+
}
|
|
1452
|
+
return proc.exitCode === 0;
|
|
1453
|
+
} catch {
|
|
1454
|
+
return false;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/tui.ts
|
|
1459
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1460
|
+
var PKG_DIR = join2(__dirname, "..");
|
|
1461
|
+
var cachedVersion = null;
|
|
1462
|
+
var colors = {
|
|
1463
|
+
bg: "#3D3B3B",
|
|
1464
|
+
surface: "#4a4848",
|
|
1465
|
+
surfaceAlt: "#525050",
|
|
1466
|
+
text: "#e0e4f0",
|
|
1467
|
+
muted: "#a8a6a6",
|
|
1468
|
+
blue: "#89b4fa",
|
|
1469
|
+
green: "#a6e3a1",
|
|
1470
|
+
yellow: "#f9e2af",
|
|
1471
|
+
red: "#f38ba8",
|
|
1472
|
+
teal: "#94e2d5",
|
|
1473
|
+
purple: "#cba6f7",
|
|
1474
|
+
orange: "#fab387",
|
|
1475
|
+
border: "#1C1C1C",
|
|
1476
|
+
borderAlt: "#2a2a2a",
|
|
1477
|
+
separator: "#1C1C1C",
|
|
1478
|
+
// Dark thin colorblock - thicker than border, visible separation
|
|
1479
|
+
selectionBg: "#2A2A9C",
|
|
1480
|
+
selectionText: "#ffffff",
|
|
1481
|
+
// Premium accents for the command menu
|
|
1482
|
+
accent: "#89b4fa",
|
|
1483
|
+
accentDim: "#587cf5",
|
|
1484
|
+
surfaceDim: "#1a1919",
|
|
1485
|
+
borderAccent: "#1a1919"
|
|
1486
|
+
};
|
|
1487
|
+
var logger = {
|
|
1488
|
+
debug: () => {
|
|
1489
|
+
},
|
|
1490
|
+
info: () => {
|
|
1491
|
+
},
|
|
1492
|
+
warn: () => {
|
|
1493
|
+
},
|
|
1494
|
+
error: () => {
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
var SORT_MODES = [
|
|
1498
|
+
{ key: "best-match", label: "best-match" },
|
|
1499
|
+
{ key: "stars", label: "stars" },
|
|
1500
|
+
{ key: "updated", label: "updated" },
|
|
1501
|
+
{ key: "forks", label: "forks" }
|
|
1502
|
+
];
|
|
1503
|
+
async function launchBrowser() {
|
|
1504
|
+
let renderer;
|
|
1505
|
+
try {
|
|
1506
|
+
renderer = await createCliRenderer2();
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
const isNode = process.versions.bun === void 0;
|
|
1509
|
+
if (isNode) {
|
|
1510
|
+
console.error("\n\u274C The interactive TUI requires Bun runtime.\n");
|
|
1511
|
+
console.error(" Install Bun: curl -fsSL https://bun.sh/install | bash");
|
|
1512
|
+
console.error(" Then run: bunx ghfind");
|
|
1513
|
+
console.error("\n Or use non-interactive mode (works on Node.js):");
|
|
1514
|
+
console.error(` ghfind "language:Rust" --json | jq '.[].fullName'`);
|
|
1515
|
+
console.error(" ghfind --trending --count");
|
|
1516
|
+
} else {
|
|
1517
|
+
console.error(
|
|
1518
|
+
"Failed to start the interactive browser. OpenTUI native backend unavailable.\n",
|
|
1519
|
+
`Reason: ${err instanceof Error ? err.message : String(err)}
|
|
1520
|
+
`,
|
|
1521
|
+
"Install Zig or use a platform with native OpenTUI support."
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
process.exit(1);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const root = renderer.root;
|
|
1528
|
+
root.flexDirection = "column";
|
|
1529
|
+
root.backgroundColor = colors.bg;
|
|
1530
|
+
const config = loadConfig();
|
|
1531
|
+
const githubToken = config.githubToken || process.env.GITHUB_TOKEN;
|
|
1532
|
+
const theme = loadTheme(config.theme);
|
|
1533
|
+
Object.assign(colors, theme);
|
|
1534
|
+
const session = restoreSession();
|
|
1535
|
+
let currentRepos = [];
|
|
1536
|
+
let currentSort = session?.sort ?? config.defaultSort;
|
|
1537
|
+
let currentLimit = session?.limit ?? config.defaultLimit;
|
|
1538
|
+
let currentQueryInput = session?.query ?? "";
|
|
1539
|
+
let isLoading = false;
|
|
1540
|
+
let currentMode = session?.mode ?? "search";
|
|
1541
|
+
let trendingTab = session?.trendingTab ?? "This Week";
|
|
1542
|
+
let trendingPeriod = "this week";
|
|
1543
|
+
let graphFullscreen = false;
|
|
1544
|
+
let chartCommitData = [];
|
|
1545
|
+
let currentOverlay = "none";
|
|
1546
|
+
let currentPage = 1;
|
|
1547
|
+
let totalCount = 0;
|
|
1548
|
+
let deepDiveActive = false;
|
|
1549
|
+
let compareList = [];
|
|
1550
|
+
let quitArmed = false;
|
|
1551
|
+
const header = new TextRenderable2(renderer, {
|
|
1552
|
+
content: " ghfind \u2014 GitHub repo browser [/]search [Space]menu [\u2192]open [?]help [q]uit",
|
|
1553
|
+
backgroundColor: colors.bg,
|
|
1554
|
+
color: colors.muted,
|
|
1555
|
+
height: 1,
|
|
1556
|
+
borderBottom: true,
|
|
1557
|
+
borderBottomColor: colors.border
|
|
1558
|
+
});
|
|
1559
|
+
const trendingTabBox = new BoxRenderable2(renderer, {
|
|
1560
|
+
flexDirection: "row",
|
|
1561
|
+
height: 1,
|
|
1562
|
+
backgroundColor: colors.surface,
|
|
1563
|
+
// No border — pure contract-edge separation via text contrast
|
|
1564
|
+
paddingX: 1,
|
|
1565
|
+
visible: false
|
|
1566
|
+
});
|
|
1567
|
+
const trendingTabTexts = [];
|
|
1568
|
+
function renderTrendingTabs() {
|
|
1569
|
+
for (const t2 of trendingTabTexts) trendingTabBox.remove(t2);
|
|
1570
|
+
trendingTabTexts.length = 0;
|
|
1571
|
+
TAB_NAMES.forEach((name, i) => {
|
|
1572
|
+
const isActive = name === trendingTab;
|
|
1573
|
+
const label = ` ${name} `;
|
|
1574
|
+
const tt = new TextRenderable2(renderer, {
|
|
1575
|
+
content: label,
|
|
1576
|
+
color: isActive ? colors.bg : colors.muted,
|
|
1577
|
+
backgroundColor: isActive ? colors.blue : colors.bg,
|
|
1578
|
+
height: 1
|
|
1579
|
+
});
|
|
1580
|
+
trendingTabTexts.push(tt);
|
|
1581
|
+
trendingTabBox.add(tt);
|
|
1582
|
+
if (i < TAB_NAMES.length - 1) {
|
|
1583
|
+
const sp = new TextRenderable2(renderer, {
|
|
1584
|
+
content: " ",
|
|
1585
|
+
color: colors.muted,
|
|
1586
|
+
backgroundColor: colors.bg,
|
|
1587
|
+
height: 1
|
|
1588
|
+
});
|
|
1589
|
+
trendingTabTexts.push(sp);
|
|
1590
|
+
trendingTabBox.add(sp);
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
root.add(header);
|
|
1595
|
+
root.add(trendingTabBox);
|
|
1596
|
+
const searchBox = new BoxRenderable2(renderer, {
|
|
1597
|
+
visible: true,
|
|
1598
|
+
borderBottom: true,
|
|
1599
|
+
borderBottomColor: colors.border,
|
|
1600
|
+
title: " SEARCH ",
|
|
1601
|
+
titleColor: colors.blue,
|
|
1602
|
+
width: "100%",
|
|
1603
|
+
flexDirection: "row",
|
|
1604
|
+
paddingTop: 0,
|
|
1605
|
+
paddingBottom: 0,
|
|
1606
|
+
backgroundColor: colors.surface
|
|
1607
|
+
});
|
|
1608
|
+
const searchInput = new InputRenderable(renderer, {
|
|
1609
|
+
placeholder: "Search GitHub repos (e.g. rust cli, or language:Rust stars:>100)",
|
|
1610
|
+
value: "",
|
|
1611
|
+
backgroundColor: colors.surface,
|
|
1612
|
+
textColor: colors.text,
|
|
1613
|
+
borderColor: colors.blue,
|
|
1614
|
+
selectedBorderColor: colors.blue,
|
|
1615
|
+
paddingX: 0,
|
|
1616
|
+
flexGrow: 1
|
|
1617
|
+
});
|
|
1618
|
+
searchBox.add(searchInput);
|
|
1619
|
+
root.add(searchBox);
|
|
1620
|
+
const toolbarText = new TextRenderable2(renderer, {
|
|
1621
|
+
content: formatToolbar(currentSort, currentLimit, totalCount),
|
|
1622
|
+
visible: true,
|
|
1623
|
+
// hidden in trending mode
|
|
1624
|
+
backgroundColor: colors.surface,
|
|
1625
|
+
// Surface panel
|
|
1626
|
+
color: colors.muted,
|
|
1627
|
+
height: 1,
|
|
1628
|
+
borderBottom: true,
|
|
1629
|
+
borderBottomColor: colors.border,
|
|
1630
|
+
paddingX: 1
|
|
1631
|
+
});
|
|
1632
|
+
root.add(toolbarText);
|
|
1633
|
+
const body = new BoxRenderable2(renderer, {
|
|
1634
|
+
flexDirection: "row",
|
|
1635
|
+
flexGrow: 1,
|
|
1636
|
+
gap: 1,
|
|
1637
|
+
paddingY: 1,
|
|
1638
|
+
paddingX: 1
|
|
1639
|
+
});
|
|
1640
|
+
const resultsBox = new BoxRenderable2(renderer, {
|
|
1641
|
+
borderBottom: true,
|
|
1642
|
+
borderBottomColor: colors.border,
|
|
1643
|
+
title: " Results ",
|
|
1644
|
+
titleColor: colors.blue,
|
|
1645
|
+
width: "50%",
|
|
1646
|
+
flexDirection: "column",
|
|
1647
|
+
paddingLeft: 0,
|
|
1648
|
+
focusable: false
|
|
1649
|
+
});
|
|
1650
|
+
const resultsSelect = new SelectRenderable(renderer, {
|
|
1651
|
+
options: [
|
|
1652
|
+
{
|
|
1653
|
+
name: "",
|
|
1654
|
+
description: "Type a query and press Enter to search",
|
|
1655
|
+
value: null
|
|
1656
|
+
}
|
|
1657
|
+
],
|
|
1658
|
+
showDescription: false,
|
|
1659
|
+
showSelectionIndicator: false,
|
|
1660
|
+
flexGrow: 1,
|
|
1661
|
+
textColor: colors.text,
|
|
1662
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
1663
|
+
selectedTextColor: colors.selectionText,
|
|
1664
|
+
focusedBackgroundColor: colors.bg,
|
|
1665
|
+
focusedTextColor: colors.text,
|
|
1666
|
+
itemSpacing: 0
|
|
1667
|
+
});
|
|
1668
|
+
resultsBox.add(resultsSelect);
|
|
1669
|
+
const detailBox = new BoxRenderable2(renderer, {
|
|
1670
|
+
borderBottom: true,
|
|
1671
|
+
borderBottomColor: colors.border,
|
|
1672
|
+
title: " Details ",
|
|
1673
|
+
titleColor: colors.green,
|
|
1674
|
+
width: "50%",
|
|
1675
|
+
flexDirection: "column",
|
|
1676
|
+
paddingX: 1,
|
|
1677
|
+
paddingY: 1,
|
|
1678
|
+
focusable: false,
|
|
1679
|
+
backgroundColor: colors.surface
|
|
1680
|
+
});
|
|
1681
|
+
const detailText = new TextRenderable2(renderer, {
|
|
1682
|
+
content: "",
|
|
1683
|
+
color: colors.text
|
|
1684
|
+
});
|
|
1685
|
+
detailBox.add(detailText);
|
|
1686
|
+
body.add(resultsBox);
|
|
1687
|
+
body.add(detailBox);
|
|
1688
|
+
root.add(body);
|
|
1689
|
+
function hideMainContent() {
|
|
1690
|
+
trendingTabBox.visible = false;
|
|
1691
|
+
searchBox.visible = false;
|
|
1692
|
+
toolbarText.visible = false;
|
|
1693
|
+
body.visible = false;
|
|
1694
|
+
}
|
|
1695
|
+
function showMainContent() {
|
|
1696
|
+
if (currentMode === "trending") {
|
|
1697
|
+
trendingTabBox.visible = true;
|
|
1698
|
+
} else {
|
|
1699
|
+
searchBox.visible = true;
|
|
1700
|
+
toolbarText.visible = true;
|
|
1701
|
+
}
|
|
1702
|
+
body.visible = true;
|
|
1703
|
+
}
|
|
1704
|
+
function showOverlay(type) {
|
|
1705
|
+
helpBox.visible = false;
|
|
1706
|
+
historyBox.visible = false;
|
|
1707
|
+
bookmarksBox.visible = false;
|
|
1708
|
+
savedBox.visible = false;
|
|
1709
|
+
topicsBox.visible = false;
|
|
1710
|
+
exportBox.visible = false;
|
|
1711
|
+
compareBox.visible = false;
|
|
1712
|
+
notifsBox.visible = false;
|
|
1713
|
+
shareBox.visible = false;
|
|
1714
|
+
leaderBox.visible = false;
|
|
1715
|
+
leaderDim.visible = false;
|
|
1716
|
+
readmeBox.visible = false;
|
|
1717
|
+
updateBox.visible = false;
|
|
1718
|
+
updateDim.visible = false;
|
|
1719
|
+
if (type === "none") {
|
|
1720
|
+
currentOverlay = "none";
|
|
1721
|
+
showMainContent();
|
|
1722
|
+
renderer.requestRender();
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
currentOverlay = type;
|
|
1726
|
+
if (type !== "leader") {
|
|
1727
|
+
hideMainContent();
|
|
1728
|
+
}
|
|
1729
|
+
if (type === "help") {
|
|
1730
|
+
helpBox.visible = true;
|
|
1731
|
+
} else if (type === "history") {
|
|
1732
|
+
historyBox.visible = true;
|
|
1733
|
+
} else if (type === "bookmarks") {
|
|
1734
|
+
bookmarksBox.visible = true;
|
|
1735
|
+
} else if (type === "saved") {
|
|
1736
|
+
savedBox.visible = true;
|
|
1737
|
+
} else if (type === "topics") {
|
|
1738
|
+
topicsBox.visible = true;
|
|
1739
|
+
} else if (type === "export") {
|
|
1740
|
+
exportBox.visible = true;
|
|
1741
|
+
} else if (type === "compare") {
|
|
1742
|
+
compareBox.visible = true;
|
|
1743
|
+
} else if (type === "notifications") {
|
|
1744
|
+
notifsBox.visible = true;
|
|
1745
|
+
} else if (type === "share") {
|
|
1746
|
+
shareBox.visible = true;
|
|
1747
|
+
} else if (type === "leader") {
|
|
1748
|
+
leaderDim.visible = true;
|
|
1749
|
+
leaderBox.visible = true;
|
|
1750
|
+
} else if (type === "readme") {
|
|
1751
|
+
readmeBox.visible = true;
|
|
1752
|
+
} else if (type === "update") {
|
|
1753
|
+
updateDim.visible = true;
|
|
1754
|
+
updateBox.visible = true;
|
|
1755
|
+
updateSelect.focus();
|
|
1756
|
+
}
|
|
1757
|
+
renderer.requestRender();
|
|
1758
|
+
}
|
|
1759
|
+
const helpSections = buildHelpSections();
|
|
1760
|
+
const helpBox = new BoxRenderable2(renderer, {
|
|
1761
|
+
visible: false,
|
|
1762
|
+
flexGrow: 1,
|
|
1763
|
+
backgroundColor: colors.bg,
|
|
1764
|
+
borderBottom: true,
|
|
1765
|
+
borderBottomColor: colors.border
|
|
1766
|
+
});
|
|
1767
|
+
const helpScroll = new ScrollBoxRenderable2(renderer, {
|
|
1768
|
+
flexGrow: 1,
|
|
1769
|
+
backgroundColor: colors.bg,
|
|
1770
|
+
scrollY: true,
|
|
1771
|
+
scrollX: false,
|
|
1772
|
+
paddingX: 0,
|
|
1773
|
+
viewportOptions: { backgroundColor: colors.bg },
|
|
1774
|
+
contentOptions: { backgroundColor: colors.bg, flexDirection: "column" },
|
|
1775
|
+
scrollbarOptions: {
|
|
1776
|
+
backgroundColor: colors.bg,
|
|
1777
|
+
foregroundColor: colors.muted,
|
|
1778
|
+
width: 1
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
for (const section of helpSections) {
|
|
1782
|
+
const sectionBox = new BoxRenderable2(renderer, {
|
|
1783
|
+
borderBottom: true,
|
|
1784
|
+
borderBottomColor: colors.border,
|
|
1785
|
+
title: section.title,
|
|
1786
|
+
titleColor: colors[section.titleColor] ?? section.titleColor,
|
|
1787
|
+
flexDirection: "column",
|
|
1788
|
+
paddingY: 0,
|
|
1789
|
+
marginLeft: 1,
|
|
1790
|
+
marginRight: 1,
|
|
1791
|
+
marginTop: 1
|
|
1792
|
+
});
|
|
1793
|
+
for (const row of section.rows) {
|
|
1794
|
+
const keys = row.keys.padEnd(HELP_KEYS_COLUMN);
|
|
1795
|
+
sectionBox.add(
|
|
1796
|
+
new TextRenderable2(renderer, {
|
|
1797
|
+
content: ` ${keys} ${row.action}`,
|
|
1798
|
+
color: colors.text
|
|
1799
|
+
})
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
if (section.note) {
|
|
1803
|
+
sectionBox.add(
|
|
1804
|
+
new TextRenderable2(renderer, {
|
|
1805
|
+
content: ` ${section.note}`,
|
|
1806
|
+
color: colors.muted
|
|
1807
|
+
})
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
helpScroll.add(sectionBox);
|
|
1811
|
+
}
|
|
1812
|
+
helpBox.add(helpScroll);
|
|
1813
|
+
root.add(helpBox);
|
|
1814
|
+
const historyBox = new BoxRenderable2(renderer, {
|
|
1815
|
+
visible: false,
|
|
1816
|
+
flexGrow: 1,
|
|
1817
|
+
backgroundColor: colors.bg,
|
|
1818
|
+
borderBottom: true,
|
|
1819
|
+
borderBottomColor: colors.border,
|
|
1820
|
+
title: " Search History ",
|
|
1821
|
+
titleColor: colors.blue
|
|
1822
|
+
});
|
|
1823
|
+
const historySelect = new SelectRenderable(renderer, {
|
|
1824
|
+
options: [{ name: "(no history)", description: "", value: null }],
|
|
1825
|
+
showDescription: false,
|
|
1826
|
+
showSelectionIndicator: true,
|
|
1827
|
+
flexGrow: 1,
|
|
1828
|
+
backgroundColor: colors.bg,
|
|
1829
|
+
textColor: colors.text,
|
|
1830
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
1831
|
+
selectedTextColor: colors.selectionText,
|
|
1832
|
+
itemSpacing: 0
|
|
1833
|
+
});
|
|
1834
|
+
historyBox.add(historySelect);
|
|
1835
|
+
root.add(historyBox);
|
|
1836
|
+
function refreshHistory() {
|
|
1837
|
+
const entries = readHistory();
|
|
1838
|
+
if (entries.length === 0) {
|
|
1839
|
+
historySelect.options = [
|
|
1840
|
+
{ name: "(no history)", description: "", value: null }
|
|
1841
|
+
];
|
|
1842
|
+
} else {
|
|
1843
|
+
historySelect.options = entries.map((e, i) => ({
|
|
1844
|
+
name: `${e.query} \u2014 ${e.mode}${e.tab ? ` (${e.tab})` : ""}`,
|
|
1845
|
+
description: `${e.resultCount} results`,
|
|
1846
|
+
value: { index: i, entry: e }
|
|
1847
|
+
}));
|
|
1848
|
+
}
|
|
1849
|
+
historySelect.setSelectedIndex(0);
|
|
1850
|
+
}
|
|
1851
|
+
const bookmarksBox = new BoxRenderable2(renderer, {
|
|
1852
|
+
visible: false,
|
|
1853
|
+
flexGrow: 1,
|
|
1854
|
+
backgroundColor: colors.bg,
|
|
1855
|
+
borderBottom: true,
|
|
1856
|
+
borderBottomColor: colors.border,
|
|
1857
|
+
title: " Bookmarks ",
|
|
1858
|
+
titleColor: colors.green
|
|
1859
|
+
});
|
|
1860
|
+
const bookmarksSelect = new SelectRenderable(renderer, {
|
|
1861
|
+
options: [{ name: "(no bookmarks)", description: "", value: null }],
|
|
1862
|
+
showDescription: false,
|
|
1863
|
+
showSelectionIndicator: true,
|
|
1864
|
+
flexGrow: 1,
|
|
1865
|
+
backgroundColor: colors.bg,
|
|
1866
|
+
textColor: colors.text,
|
|
1867
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
1868
|
+
selectedTextColor: colors.selectionText,
|
|
1869
|
+
itemSpacing: 0
|
|
1870
|
+
});
|
|
1871
|
+
bookmarksBox.add(bookmarksSelect);
|
|
1872
|
+
root.add(bookmarksBox);
|
|
1873
|
+
function refreshBookmarks() {
|
|
1874
|
+
const bookmarks = getBookmarks();
|
|
1875
|
+
if (bookmarks.length === 0) {
|
|
1876
|
+
bookmarksSelect.options = [
|
|
1877
|
+
{ name: "(no bookmarks)", description: "", value: null }
|
|
1878
|
+
];
|
|
1879
|
+
} else {
|
|
1880
|
+
bookmarksSelect.options = bookmarks.map((b) => ({
|
|
1881
|
+
name: `${b.repo.fullName} \u2605 ${b.repo.stars.toLocaleString()}`,
|
|
1882
|
+
description: b.repo.description ?? "",
|
|
1883
|
+
value: b
|
|
1884
|
+
}));
|
|
1885
|
+
}
|
|
1886
|
+
bookmarksSelect.setSelectedIndex(0);
|
|
1887
|
+
}
|
|
1888
|
+
const savedBox = new BoxRenderable2(renderer, {
|
|
1889
|
+
visible: false,
|
|
1890
|
+
flexGrow: 1,
|
|
1891
|
+
backgroundColor: colors.bg,
|
|
1892
|
+
borderBottom: true,
|
|
1893
|
+
borderBottomColor: colors.border,
|
|
1894
|
+
title: " Saved Searches ",
|
|
1895
|
+
titleColor: colors.yellow
|
|
1896
|
+
});
|
|
1897
|
+
const savedSelect = new SelectRenderable(renderer, {
|
|
1898
|
+
options: [{ name: "(no saved searches)", description: "", value: null }],
|
|
1899
|
+
showDescription: false,
|
|
1900
|
+
showSelectionIndicator: true,
|
|
1901
|
+
flexGrow: 1,
|
|
1902
|
+
backgroundColor: colors.bg,
|
|
1903
|
+
textColor: colors.text,
|
|
1904
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
1905
|
+
selectedTextColor: colors.selectionText,
|
|
1906
|
+
itemSpacing: 0
|
|
1907
|
+
});
|
|
1908
|
+
savedBox.add(savedSelect);
|
|
1909
|
+
root.add(savedBox);
|
|
1910
|
+
const topicsBox = new BoxRenderable2(renderer, {
|
|
1911
|
+
visible: false,
|
|
1912
|
+
flexGrow: 1,
|
|
1913
|
+
backgroundColor: colors.bg,
|
|
1914
|
+
borderBottom: true,
|
|
1915
|
+
borderBottomColor: colors.border,
|
|
1916
|
+
title: " Topics ",
|
|
1917
|
+
titleColor: colors.purple ?? colors.blue
|
|
1918
|
+
});
|
|
1919
|
+
const topicsSelect = new SelectRenderable(renderer, {
|
|
1920
|
+
options: [{ name: "(loading topics...)", description: "", value: null }],
|
|
1921
|
+
showDescription: true,
|
|
1922
|
+
showSelectionIndicator: true,
|
|
1923
|
+
flexGrow: 1,
|
|
1924
|
+
backgroundColor: colors.bg,
|
|
1925
|
+
textColor: colors.text,
|
|
1926
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
1927
|
+
selectedTextColor: colors.selectionText,
|
|
1928
|
+
itemSpacing: 0
|
|
1929
|
+
});
|
|
1930
|
+
topicsBox.add(topicsSelect);
|
|
1931
|
+
root.add(topicsBox);
|
|
1932
|
+
async function refreshTopics() {
|
|
1933
|
+
try {
|
|
1934
|
+
const topics = await fetchTopics();
|
|
1935
|
+
topicsSelect.options = topics.map((t2) => ({
|
|
1936
|
+
name: `${t2.name} (${t2.repoCount.toLocaleString()} repos)`,
|
|
1937
|
+
description: t2.description.slice(0, 80),
|
|
1938
|
+
value: t2
|
|
1939
|
+
}));
|
|
1940
|
+
} catch {
|
|
1941
|
+
topicsSelect.options = [
|
|
1942
|
+
{ name: "(failed to load topics)", description: "", value: null }
|
|
1943
|
+
];
|
|
1944
|
+
}
|
|
1945
|
+
topicsSelect.setSelectedIndex(0);
|
|
1946
|
+
}
|
|
1947
|
+
const exportBox = new BoxRenderable2(renderer, {
|
|
1948
|
+
visible: false,
|
|
1949
|
+
flexGrow: 1,
|
|
1950
|
+
backgroundColor: colors.bg,
|
|
1951
|
+
borderBottom: true,
|
|
1952
|
+
borderBottomColor: colors.border,
|
|
1953
|
+
title: " Export ",
|
|
1954
|
+
titleColor: colors.green
|
|
1955
|
+
});
|
|
1956
|
+
const exportSelect = new SelectRenderable(renderer, {
|
|
1957
|
+
options: [
|
|
1958
|
+
{
|
|
1959
|
+
name: " JSON",
|
|
1960
|
+
description: "Full repo data as JSON array",
|
|
1961
|
+
value: "json"
|
|
1962
|
+
},
|
|
1963
|
+
{
|
|
1964
|
+
name: " CSV",
|
|
1965
|
+
description: "Rank, name, stars, forks, language, URL",
|
|
1966
|
+
value: "csv"
|
|
1967
|
+
},
|
|
1968
|
+
{
|
|
1969
|
+
name: " Markdown",
|
|
1970
|
+
description: "Pretty table with links (paste into README)",
|
|
1971
|
+
value: "markdown"
|
|
1972
|
+
},
|
|
1973
|
+
{ name: " Plain text", description: "One repo per line", value: "text" }
|
|
1974
|
+
],
|
|
1975
|
+
showDescription: true,
|
|
1976
|
+
showSelectionIndicator: true,
|
|
1977
|
+
flexGrow: 1,
|
|
1978
|
+
backgroundColor: colors.bg,
|
|
1979
|
+
textColor: colors.text,
|
|
1980
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
1981
|
+
selectedTextColor: colors.selectionText,
|
|
1982
|
+
itemSpacing: 0
|
|
1983
|
+
});
|
|
1984
|
+
exportBox.add(exportSelect);
|
|
1985
|
+
root.add(exportBox);
|
|
1986
|
+
const compareBox = new BoxRenderable2(renderer, {
|
|
1987
|
+
visible: false,
|
|
1988
|
+
flexGrow: 1,
|
|
1989
|
+
backgroundColor: colors.bg,
|
|
1990
|
+
borderBottom: true,
|
|
1991
|
+
borderBottomColor: colors.border,
|
|
1992
|
+
title: " Comparison ",
|
|
1993
|
+
titleColor: colors.yellow
|
|
1994
|
+
});
|
|
1995
|
+
const compareText = new TextRenderable2(renderer, {
|
|
1996
|
+
content: "",
|
|
1997
|
+
color: colors.text,
|
|
1998
|
+
backgroundColor: colors.bg
|
|
1999
|
+
});
|
|
2000
|
+
compareBox.add(compareText);
|
|
2001
|
+
root.add(compareBox);
|
|
2002
|
+
const notifsBox = new BoxRenderable2(renderer, {
|
|
2003
|
+
visible: false,
|
|
2004
|
+
flexGrow: 1,
|
|
2005
|
+
backgroundColor: colors.bg,
|
|
2006
|
+
borderBottom: true,
|
|
2007
|
+
borderBottomColor: colors.border,
|
|
2008
|
+
title: " Notifications ",
|
|
2009
|
+
titleColor: colors.yellow
|
|
2010
|
+
});
|
|
2011
|
+
const notifsText = new TextRenderable2(renderer, {
|
|
2012
|
+
content: "(no notifications)",
|
|
2013
|
+
color: colors.text,
|
|
2014
|
+
backgroundColor: colors.bg
|
|
2015
|
+
});
|
|
2016
|
+
notifsBox.add(notifsText);
|
|
2017
|
+
root.add(notifsBox);
|
|
2018
|
+
function refreshNotifications() {
|
|
2019
|
+
const notifs2 = getNotifications();
|
|
2020
|
+
if (notifs2.length === 0) {
|
|
2021
|
+
notifsText.content = " (no notifications)";
|
|
2022
|
+
} else {
|
|
2023
|
+
notifsText.content = notifs2.map((n) => ` ${n.icon} ${n.message}`).join("\n");
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
const shareBox = new BoxRenderable2(renderer, {
|
|
2027
|
+
visible: false,
|
|
2028
|
+
flexGrow: 1,
|
|
2029
|
+
backgroundColor: colors.bg,
|
|
2030
|
+
borderBottom: true,
|
|
2031
|
+
borderBottomColor: colors.border,
|
|
2032
|
+
title: " Share Repo ",
|
|
2033
|
+
titleColor: colors.blue
|
|
2034
|
+
});
|
|
2035
|
+
const shareSelect = new SelectRenderable(renderer, {
|
|
2036
|
+
options: [
|
|
2037
|
+
{
|
|
2038
|
+
name: " Markdown",
|
|
2039
|
+
description: "[owner/repo](url) \u2014 description",
|
|
2040
|
+
value: "markdown"
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
name: " Plain",
|
|
2044
|
+
description: "owner/repo \u2014 description \u2014 url",
|
|
2045
|
+
value: "plain"
|
|
2046
|
+
},
|
|
2047
|
+
{
|
|
2048
|
+
name: " GitHub CLI",
|
|
2049
|
+
description: "gh repo view owner/repo",
|
|
2050
|
+
value: "gh-cli"
|
|
2051
|
+
},
|
|
2052
|
+
{ name: " Short", description: "owner/repo", value: "short" }
|
|
2053
|
+
],
|
|
2054
|
+
showDescription: true,
|
|
2055
|
+
showSelectionIndicator: true,
|
|
2056
|
+
flexGrow: 1,
|
|
2057
|
+
backgroundColor: colors.bg,
|
|
2058
|
+
textColor: colors.text,
|
|
2059
|
+
selectedBackgroundColor: colors.selectionBg,
|
|
2060
|
+
selectedTextColor: colors.selectionText,
|
|
2061
|
+
itemSpacing: 0
|
|
2062
|
+
});
|
|
2063
|
+
shareBox.add(shareSelect);
|
|
2064
|
+
root.add(shareBox);
|
|
2065
|
+
function refreshCompare() {
|
|
2066
|
+
if (compareList.length < 2) {
|
|
2067
|
+
compareText.content = "Select at least 2 repos to compare (press c to add).";
|
|
2068
|
+
} else {
|
|
2069
|
+
compareText.content = buildComparisonTable(compareList);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
function refreshSavedSearches() {
|
|
2073
|
+
const saved = getSavedSearches();
|
|
2074
|
+
if (saved.length === 0) {
|
|
2075
|
+
savedSelect.options = [
|
|
2076
|
+
{ name: "(no saved searches)", description: "", value: null }
|
|
2077
|
+
];
|
|
2078
|
+
} else {
|
|
2079
|
+
savedSelect.options = saved.map((s) => ({
|
|
2080
|
+
name: `${s.name}`,
|
|
2081
|
+
description: `${s.query} (${s.mode})`,
|
|
2082
|
+
value: s
|
|
2083
|
+
}));
|
|
2084
|
+
}
|
|
2085
|
+
savedSelect.setSelectedIndex(0);
|
|
2086
|
+
}
|
|
2087
|
+
const leaderDim = new BoxRenderable2(renderer, {
|
|
2088
|
+
visible: false,
|
|
2089
|
+
position: "absolute",
|
|
2090
|
+
top: 0,
|
|
2091
|
+
left: 0,
|
|
2092
|
+
width: "100%",
|
|
2093
|
+
height: "100%",
|
|
2094
|
+
backgroundColor: "#00000099"
|
|
2095
|
+
// 60% opacity black (hex alpha: 0x99 ≈ 153/255)
|
|
2096
|
+
});
|
|
2097
|
+
root.add(leaderDim);
|
|
2098
|
+
const leaderBox = new BoxRenderable2(renderer, {
|
|
2099
|
+
visible: false,
|
|
2100
|
+
position: "absolute",
|
|
2101
|
+
width: "80%",
|
|
2102
|
+
height: 18,
|
|
2103
|
+
left: "10%",
|
|
2104
|
+
top: "30%",
|
|
2105
|
+
backgroundColor: colors.surfaceDim,
|
|
2106
|
+
border: true,
|
|
2107
|
+
borderStyle: "rounded",
|
|
2108
|
+
borderColor: colors.borderAccent,
|
|
2109
|
+
title: " \u2699 Menu ",
|
|
2110
|
+
titleColor: colors.accent,
|
|
2111
|
+
titleAlignment: "left",
|
|
2112
|
+
flexDirection: "column",
|
|
2113
|
+
paddingX: 1
|
|
2114
|
+
});
|
|
2115
|
+
const leaderSelect = new SelectRenderable(renderer, {
|
|
2116
|
+
options: [{ name: "(empty)", description: "", value: null }],
|
|
2117
|
+
showDescription: true,
|
|
2118
|
+
showSelectionIndicator: true,
|
|
2119
|
+
flexGrow: 1,
|
|
2120
|
+
backgroundColor: colors.surfaceDim,
|
|
2121
|
+
textColor: colors.text,
|
|
2122
|
+
focusedBackgroundColor: colors.surfaceDim,
|
|
2123
|
+
focusedTextColor: colors.text,
|
|
2124
|
+
selectedBackgroundColor: colors.accent,
|
|
2125
|
+
selectedTextColor: colors.selectionText,
|
|
2126
|
+
descriptionColor: colors.muted,
|
|
2127
|
+
selectedDescriptionColor: colors.selectionText,
|
|
2128
|
+
itemSpacing: 0
|
|
2129
|
+
});
|
|
2130
|
+
const leaderFooter = new TextRenderable2(renderer, {
|
|
2131
|
+
content: " \u2191\u2193 select Enter run Esc/q close ",
|
|
2132
|
+
color: colors.muted,
|
|
2133
|
+
backgroundColor: colors.surfaceDim,
|
|
2134
|
+
height: 1,
|
|
2135
|
+
borderTop: true,
|
|
2136
|
+
borderTopColor: colors.border,
|
|
2137
|
+
paddingX: 2
|
|
2138
|
+
});
|
|
2139
|
+
leaderBox.add(leaderSelect);
|
|
2140
|
+
leaderBox.add(leaderFooter);
|
|
2141
|
+
root.add(leaderDim);
|
|
2142
|
+
root.add(leaderBox);
|
|
2143
|
+
const updateDim = new BoxRenderable2(renderer, {
|
|
2144
|
+
visible: false,
|
|
2145
|
+
position: "absolute",
|
|
2146
|
+
top: 0,
|
|
2147
|
+
left: 0,
|
|
2148
|
+
width: "100%",
|
|
2149
|
+
height: "100%",
|
|
2150
|
+
backgroundColor: "#00000088"
|
|
2151
|
+
});
|
|
2152
|
+
root.add(updateDim);
|
|
2153
|
+
const updateBox = new BoxRenderable2(renderer, {
|
|
2154
|
+
visible: false,
|
|
2155
|
+
position: "absolute",
|
|
2156
|
+
width: "55%",
|
|
2157
|
+
height: 16,
|
|
2158
|
+
left: "22.5%",
|
|
2159
|
+
top: "25%",
|
|
2160
|
+
backgroundColor: colors.surfaceDim,
|
|
2161
|
+
border: true,
|
|
2162
|
+
borderStyle: "rounded",
|
|
2163
|
+
borderColor: colors.border,
|
|
2164
|
+
title: " ghfind ",
|
|
2165
|
+
titleColor: colors.accent,
|
|
2166
|
+
titleAlignment: "left",
|
|
2167
|
+
flexDirection: "column",
|
|
2168
|
+
paddingX: 2,
|
|
2169
|
+
paddingLeft: 2,
|
|
2170
|
+
paddingRight: 2
|
|
2171
|
+
});
|
|
2172
|
+
const updateText = new TextRenderable2(renderer, {
|
|
2173
|
+
content: "",
|
|
2174
|
+
color: colors.accent,
|
|
2175
|
+
backgroundColor: colors.surfaceDim
|
|
2176
|
+
});
|
|
2177
|
+
let updateSelectedOption = 0;
|
|
2178
|
+
const updateOptionsRow = new BoxRenderable2(renderer, {
|
|
2179
|
+
flexDirection: "row",
|
|
2180
|
+
gap: 1,
|
|
2181
|
+
backgroundColor: colors.surfaceDim,
|
|
2182
|
+
height: 3,
|
|
2183
|
+
width: "100%"
|
|
2184
|
+
});
|
|
2185
|
+
const updateNowBox = new BoxRenderable2(renderer, {
|
|
2186
|
+
border: true,
|
|
2187
|
+
borderColor: updateSelectedOption === 0 ? colors.yellow : colors.border,
|
|
2188
|
+
backgroundColor: colors.surfaceDim,
|
|
2189
|
+
height: 3,
|
|
2190
|
+
width: "50%",
|
|
2191
|
+
paddingLeft: 1,
|
|
2192
|
+
paddingRight: 1
|
|
2193
|
+
});
|
|
2194
|
+
const updateNowText = new TextRenderable2(renderer, {
|
|
2195
|
+
content: " Update Now ",
|
|
2196
|
+
color: updateSelectedOption === 0 ? colors.yellow : colors.text,
|
|
2197
|
+
backgroundColor: colors.surfaceDim
|
|
2198
|
+
});
|
|
2199
|
+
updateNowBox.add(updateNowText);
|
|
2200
|
+
const laterBox = new BoxRenderable2(renderer, {
|
|
2201
|
+
border: true,
|
|
2202
|
+
borderColor: updateSelectedOption === 1 ? colors.yellow : colors.border,
|
|
2203
|
+
backgroundColor: colors.surfaceDim,
|
|
2204
|
+
height: 3,
|
|
2205
|
+
width: "50%",
|
|
2206
|
+
paddingLeft: 1,
|
|
2207
|
+
paddingRight: 1
|
|
2208
|
+
});
|
|
2209
|
+
const laterText = new TextRenderable2(renderer, {
|
|
2210
|
+
content: " Later",
|
|
2211
|
+
color: updateSelectedOption === 1 ? colors.yellow : colors.text,
|
|
2212
|
+
backgroundColor: colors.surfaceDim
|
|
2213
|
+
});
|
|
2214
|
+
laterBox.add(laterText);
|
|
2215
|
+
const neverBox = new BoxRenderable2(renderer, {
|
|
2216
|
+
border: true,
|
|
2217
|
+
borderColor: updateSelectedOption === 2 ? colors.yellow : colors.border,
|
|
2218
|
+
backgroundColor: colors.surfaceDim,
|
|
2219
|
+
height: 3,
|
|
2220
|
+
width: "50%",
|
|
2221
|
+
paddingLeft: 1,
|
|
2222
|
+
paddingRight: 1
|
|
2223
|
+
});
|
|
2224
|
+
const neverText = new TextRenderable2(renderer, {
|
|
2225
|
+
content: " Don't show again",
|
|
2226
|
+
color: updateSelectedOption === 2 ? colors.yellow : colors.text,
|
|
2227
|
+
backgroundColor: colors.surfaceDim
|
|
2228
|
+
});
|
|
2229
|
+
neverBox.add(neverText);
|
|
2230
|
+
function renderUpdateOptions() {
|
|
2231
|
+
updateNowBox.borderColor = updateSelectedOption === 0 ? colors.yellow : colors.border;
|
|
2232
|
+
laterBox.borderColor = updateSelectedOption === 1 ? colors.yellow : colors.border;
|
|
2233
|
+
neverBox.borderColor = updateSelectedOption === 2 ? colors.yellow : colors.border;
|
|
2234
|
+
updateNowText.color = updateSelectedOption === 0 ? colors.yellow : colors.text;
|
|
2235
|
+
laterText.color = updateSelectedOption === 1 ? colors.yellow : colors.text;
|
|
2236
|
+
neverText.color = updateSelectedOption === 2 ? colors.yellow : colors.text;
|
|
2237
|
+
renderer.requestRender();
|
|
2238
|
+
}
|
|
2239
|
+
updateOptionsRow.add(updateNowBox);
|
|
2240
|
+
updateOptionsRow.add(laterBox);
|
|
2241
|
+
const updateFooter = new TextRenderable2(renderer, {
|
|
2242
|
+
content: " \u2190\u2192 select Enter Esc/q close",
|
|
2243
|
+
color: colors.muted,
|
|
2244
|
+
backgroundColor: colors.surfaceDim,
|
|
2245
|
+
height: 1,
|
|
2246
|
+
paddingX: 1
|
|
2247
|
+
});
|
|
2248
|
+
updateBox.add(updateText);
|
|
2249
|
+
updateBox.add(updateOptionsRow);
|
|
2250
|
+
updateBox.add(updateFooter);
|
|
2251
|
+
root.add(updateDim);
|
|
2252
|
+
root.add(updateBox);
|
|
2253
|
+
const menuStack = [];
|
|
2254
|
+
function pushMenuLevel(title, parentTitle, entries, selectIndex = 0) {
|
|
2255
|
+
menuStack.push({
|
|
2256
|
+
title,
|
|
2257
|
+
parentTitle,
|
|
2258
|
+
entries,
|
|
2259
|
+
selectedIndex: leaderSelect.getSelectedIndex?.()
|
|
2260
|
+
});
|
|
2261
|
+
leaderSelect.options = entries.map((e) => ({
|
|
2262
|
+
name: e.type === "category" ? ` ${e.icon} ${e.name} \u2192` : ` ${e.name}`,
|
|
2263
|
+
description: e.description,
|
|
2264
|
+
value: e
|
|
2265
|
+
}));
|
|
2266
|
+
leaderSelect.setSelectedIndex(selectIndex);
|
|
2267
|
+
leaderBox.title = title;
|
|
2268
|
+
const isTop = menuStack.length === 1;
|
|
2269
|
+
leaderFooter.content = isTop ? " \u2191\u2193 select \u2192 open category Enter run Esc/q close" : " Esc back \u2191\u2193 select Enter run";
|
|
2270
|
+
}
|
|
2271
|
+
function popMenuLevel() {
|
|
2272
|
+
if (menuStack.length <= 1) return false;
|
|
2273
|
+
const prev = menuStack[menuStack.length - 2];
|
|
2274
|
+
const current = menuStack.pop();
|
|
2275
|
+
leaderSelect.options = prev.entries.map((e) => ({
|
|
2276
|
+
name: e.type === "category" ? ` ${e.icon} ${e.name} \u2192` : ` ${e.name}`,
|
|
2277
|
+
description: e.description,
|
|
2278
|
+
value: e
|
|
2279
|
+
}));
|
|
2280
|
+
leaderSelect.setSelectedIndex(current.selectedIndex);
|
|
2281
|
+
leaderBox.title = prev.title;
|
|
2282
|
+
const isTop = menuStack.length === 1;
|
|
2283
|
+
leaderFooter.content = isTop ? " \u2191\u2193 select \u2192 open category Enter run Esc/q close" : " Esc back \u2191\u2193 select Enter run";
|
|
2284
|
+
return true;
|
|
2285
|
+
}
|
|
2286
|
+
function resetMenu() {
|
|
2287
|
+
menuStack.length = 0;
|
|
2288
|
+
}
|
|
2289
|
+
function buildMenuHierarchy() {
|
|
2290
|
+
const hasRepo = (() => {
|
|
2291
|
+
const opt = resultsSelect.getSelectedOption();
|
|
2292
|
+
return opt?.value && typeof opt.value?.fullName === "string";
|
|
2293
|
+
})();
|
|
2294
|
+
const groups = [];
|
|
2295
|
+
const searchItems = [];
|
|
2296
|
+
if (currentMode === "search") {
|
|
2297
|
+
searchItems.push(
|
|
2298
|
+
{
|
|
2299
|
+
type: "action",
|
|
2300
|
+
name: "Sort",
|
|
2301
|
+
description: `Cycle sort (current: ${currentSort})`,
|
|
2302
|
+
action: () => changeSort()
|
|
2303
|
+
},
|
|
2304
|
+
{
|
|
2305
|
+
type: "action",
|
|
2306
|
+
name: "Limit",
|
|
2307
|
+
description: `Cycle result limit (current: ${currentLimit})`,
|
|
2308
|
+
action: () => changeLimit()
|
|
2309
|
+
},
|
|
2310
|
+
{
|
|
2311
|
+
type: "action",
|
|
2312
|
+
name: "Refresh",
|
|
2313
|
+
description: "Re-run current search",
|
|
2314
|
+
action: () => {
|
|
2315
|
+
if (currentQueryInput) doSearch(currentQueryInput);
|
|
2316
|
+
}
|
|
2317
|
+
},
|
|
2318
|
+
{
|
|
2319
|
+
type: "action",
|
|
2320
|
+
name: "Save search",
|
|
2321
|
+
description: "Save current query as a named search",
|
|
2322
|
+
action: () => saveCurrentSearch()
|
|
2323
|
+
},
|
|
2324
|
+
{
|
|
2325
|
+
type: "action",
|
|
2326
|
+
name: "Saved searches",
|
|
2327
|
+
description: "Load a saved search",
|
|
2328
|
+
action: () => {
|
|
2329
|
+
refreshSavedSearches();
|
|
2330
|
+
showOverlay("saved");
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
if (searchItems.length > 0) {
|
|
2336
|
+
groups.push({
|
|
2337
|
+
type: "category",
|
|
2338
|
+
name: "Search",
|
|
2339
|
+
description: "Sort, limit, refresh, save searches",
|
|
2340
|
+
icon: "\u{1F50D}",
|
|
2341
|
+
children: searchItems
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
const repoItems = [];
|
|
2345
|
+
if (hasRepo) {
|
|
2346
|
+
repoItems.push(
|
|
2347
|
+
{
|
|
2348
|
+
type: "action",
|
|
2349
|
+
name: "Open in browser",
|
|
2350
|
+
description: "Open selected repo in browser",
|
|
2351
|
+
action: () => openUrlIfSelected()
|
|
2352
|
+
},
|
|
2353
|
+
{
|
|
2354
|
+
type: "action",
|
|
2355
|
+
name: "Readme",
|
|
2356
|
+
description: "Full README viewer",
|
|
2357
|
+
action: () => {
|
|
2358
|
+
showReadme();
|
|
2359
|
+
}
|
|
2360
|
+
},
|
|
2361
|
+
{
|
|
2362
|
+
type: "action",
|
|
2363
|
+
name: "Activity graph",
|
|
2364
|
+
description: "Toggle commit chart fullscreen",
|
|
2365
|
+
action: () => toggleGraph()
|
|
2366
|
+
}
|
|
2367
|
+
);
|
|
2368
|
+
if (currentMode === "search") {
|
|
2369
|
+
repoItems.push({
|
|
2370
|
+
type: "action",
|
|
2371
|
+
name: "Deep-dive",
|
|
2372
|
+
description: "Languages, contributors, README excerpt",
|
|
2373
|
+
action: () => {
|
|
2374
|
+
showOverlay("none");
|
|
2375
|
+
triggerDeepDive();
|
|
2376
|
+
}
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
repoItems.push(
|
|
2380
|
+
{
|
|
2381
|
+
type: "action",
|
|
2382
|
+
name: "Bookmark",
|
|
2383
|
+
description: "Save / unsave selected repo",
|
|
2384
|
+
action: () => toggleBookmarkOnSelected()
|
|
2385
|
+
},
|
|
2386
|
+
{
|
|
2387
|
+
type: "action",
|
|
2388
|
+
name: "Compare",
|
|
2389
|
+
description: "Add/remove repo to comparison",
|
|
2390
|
+
action: () => toggleCompareOnSelected()
|
|
2391
|
+
},
|
|
2392
|
+
{
|
|
2393
|
+
type: "action",
|
|
2394
|
+
name: "Share",
|
|
2395
|
+
description: "Copy repo link to clipboard",
|
|
2396
|
+
action: () => showShareIfRepo()
|
|
2397
|
+
}
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
if (repoItems.length > 0) {
|
|
2401
|
+
groups.push({
|
|
2402
|
+
type: "category",
|
|
2403
|
+
name: "Repo",
|
|
2404
|
+
description: "Open, readme, bookmark, share, compare",
|
|
2405
|
+
icon: "\u{1F4CB}",
|
|
2406
|
+
children: repoItems
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
const navItems = [];
|
|
2410
|
+
if (currentMode === "search") {
|
|
2411
|
+
navItems.push({
|
|
2412
|
+
type: "action",
|
|
2413
|
+
name: "Trending",
|
|
2414
|
+
description: "Browse GitHub trending repos",
|
|
2415
|
+
action: () => loadTrending()
|
|
2416
|
+
});
|
|
2417
|
+
} else {
|
|
2418
|
+
navItems.push({
|
|
2419
|
+
type: "action",
|
|
2420
|
+
name: "Search mode",
|
|
2421
|
+
description: "Switch to query search",
|
|
2422
|
+
action: () => showSearchMode()
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
navItems.push(
|
|
2426
|
+
{
|
|
2427
|
+
type: "action",
|
|
2428
|
+
name: "History",
|
|
2429
|
+
description: "Search history",
|
|
2430
|
+
action: () => {
|
|
2431
|
+
refreshHistory();
|
|
2432
|
+
showOverlay("history");
|
|
2433
|
+
}
|
|
2434
|
+
},
|
|
2435
|
+
{
|
|
2436
|
+
type: "action",
|
|
2437
|
+
name: "Bookmarks",
|
|
2438
|
+
description: "Browse saved repos",
|
|
2439
|
+
action: () => {
|
|
2440
|
+
refreshBookmarks();
|
|
2441
|
+
showOverlay("bookmarks");
|
|
2442
|
+
}
|
|
2443
|
+
},
|
|
2444
|
+
{
|
|
2445
|
+
type: "action",
|
|
2446
|
+
name: "Topics",
|
|
2447
|
+
description: "Browse popular GitHub topics",
|
|
2448
|
+
action: () => {
|
|
2449
|
+
refreshTopics();
|
|
2450
|
+
showOverlay("topics");
|
|
2451
|
+
}
|
|
2452
|
+
},
|
|
2453
|
+
{
|
|
2454
|
+
type: "action",
|
|
2455
|
+
name: "Export",
|
|
2456
|
+
description: "Export results to JSON/CSV/Markdown",
|
|
2457
|
+
action: () => showOverlay("export")
|
|
2458
|
+
}
|
|
2459
|
+
);
|
|
2460
|
+
groups.push({
|
|
2461
|
+
type: "category",
|
|
2462
|
+
name: "Navigate",
|
|
2463
|
+
description: "Trending, history, bookmarks, topics, export",
|
|
2464
|
+
icon: "\u{1F9ED}",
|
|
2465
|
+
children: navItems
|
|
2466
|
+
});
|
|
2467
|
+
const sysItems = [
|
|
2468
|
+
{
|
|
2469
|
+
type: "action",
|
|
2470
|
+
name: "Check for updates",
|
|
2471
|
+
description: "Check npm for a newer version (debug)",
|
|
2472
|
+
action: () => {
|
|
2473
|
+
showOverlay("none");
|
|
2474
|
+
checkForUpdateAndShow(true);
|
|
2475
|
+
}
|
|
2476
|
+
},
|
|
2477
|
+
{
|
|
2478
|
+
type: "action",
|
|
2479
|
+
name: "Help",
|
|
2480
|
+
description: "Keybindings reference",
|
|
2481
|
+
action: () => showOverlay("help")
|
|
2482
|
+
},
|
|
2483
|
+
{
|
|
2484
|
+
type: "action",
|
|
2485
|
+
name: "Notifications",
|
|
2486
|
+
description: "View alerts",
|
|
2487
|
+
action: () => {
|
|
2488
|
+
refreshNotifications();
|
|
2489
|
+
showOverlay("notifications");
|
|
2490
|
+
}
|
|
2491
|
+
},
|
|
2492
|
+
{
|
|
2493
|
+
type: "action",
|
|
2494
|
+
name: "Compare view",
|
|
2495
|
+
description: "Show side-by-side comparison",
|
|
2496
|
+
action: () => {
|
|
2497
|
+
refreshCompare();
|
|
2498
|
+
showOverlay("compare");
|
|
2499
|
+
}
|
|
2500
|
+
},
|
|
2501
|
+
{
|
|
2502
|
+
type: "action",
|
|
2503
|
+
name: "Quit",
|
|
2504
|
+
description: "Exit ghfind",
|
|
2505
|
+
action: () => {
|
|
2506
|
+
saveSession({
|
|
2507
|
+
mode: currentMode,
|
|
2508
|
+
query: currentQueryInput,
|
|
2509
|
+
sort: currentSort,
|
|
2510
|
+
limit: currentLimit,
|
|
2511
|
+
trendingTab
|
|
2512
|
+
});
|
|
2513
|
+
cleanup();
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
];
|
|
2517
|
+
groups.push({
|
|
2518
|
+
type: "category",
|
|
2519
|
+
name: "System",
|
|
2520
|
+
description: "Help, notifications, compare, quit",
|
|
2521
|
+
icon: "\u2699\uFE0F",
|
|
2522
|
+
children: sysItems
|
|
2523
|
+
});
|
|
2524
|
+
return groups;
|
|
2525
|
+
}
|
|
2526
|
+
function showLeaderMenu() {
|
|
2527
|
+
resetMenu();
|
|
2528
|
+
const topLevel = buildMenuHierarchy();
|
|
2529
|
+
pushMenuLevel(" \u2699 Menu ", "", topLevel, 0);
|
|
2530
|
+
leaderSelect.focus();
|
|
2531
|
+
showOverlay("leader");
|
|
2532
|
+
}
|
|
2533
|
+
async function checkForUpdateAndShow(force = false) {
|
|
2534
|
+
if (!force && !shouldCheckUpdate()) return;
|
|
2535
|
+
if (cachedVersion === null) {
|
|
2536
|
+
try {
|
|
2537
|
+
const pkg = JSON.parse(readFileSync2(join2(PKG_DIR, "package.json"), "utf-8"));
|
|
2538
|
+
cachedVersion = pkg.version;
|
|
2539
|
+
} catch {
|
|
2540
|
+
debugLog("Failed to read package.json");
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
const currentVersion2 = cachedVersion;
|
|
2545
|
+
const latest = process.env.DEBUG_FORCE_UPDATE || await checkForUpdate(currentVersion2);
|
|
2546
|
+
markUpdateChecked(latest ?? void 0);
|
|
2547
|
+
if (latest) {
|
|
2548
|
+
updateSelectedOption = 0;
|
|
2549
|
+
updateText.content = ` A new version is available
|
|
2550
|
+
|
|
2551
|
+
Current: ${currentVersion2}
|
|
2552
|
+
Latest: ${latest}
|
|
2553
|
+
|
|
2554
|
+
Run "npm install -g ghfind"`;
|
|
2555
|
+
renderUpdateOptions();
|
|
2556
|
+
showOverlay("update");
|
|
2557
|
+
} else {
|
|
2558
|
+
debugLog(`No update available (current: ${currentVersion2})`);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
function openUrlIfSelected() {
|
|
2562
|
+
const opt = resultsSelect.getSelectedOption();
|
|
2563
|
+
const repo = opt?.value;
|
|
2564
|
+
if (repo) openUrl(repo.url);
|
|
2565
|
+
else setStatus("No repo selected");
|
|
2566
|
+
}
|
|
2567
|
+
function toggleBookmarkOnSelected() {
|
|
2568
|
+
const opt = resultsSelect.getSelectedOption();
|
|
2569
|
+
const repo = opt?.value;
|
|
2570
|
+
if (repo) {
|
|
2571
|
+
const added = toggleBookmark(repo);
|
|
2572
|
+
setStatus(
|
|
2573
|
+
added ? `Bookmarked ${repo.fullName}` : `Unbookmarked ${repo.fullName}`
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
function toggleCompareOnSelected() {
|
|
2578
|
+
const opt = resultsSelect.getSelectedOption();
|
|
2579
|
+
const repo = opt?.value;
|
|
2580
|
+
if (!repo) return;
|
|
2581
|
+
const idx = compareList.findIndex((r) => r.fullName === repo.fullName);
|
|
2582
|
+
if (idx >= 0) {
|
|
2583
|
+
compareList.splice(idx, 1);
|
|
2584
|
+
setStatus(`Removed ${repo.fullName} from comparison`);
|
|
2585
|
+
} else {
|
|
2586
|
+
compareList.push(repo);
|
|
2587
|
+
setStatus(
|
|
2588
|
+
`${repo.fullName} added to comparison (${compareList.length} selected)`
|
|
2589
|
+
);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
function showShareIfRepo() {
|
|
2593
|
+
const opt = resultsSelect.getSelectedOption();
|
|
2594
|
+
const repo = opt?.value;
|
|
2595
|
+
if (!repo) {
|
|
2596
|
+
setStatus("No repo selected to share");
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
showOverlay("share");
|
|
2600
|
+
}
|
|
2601
|
+
function saveCurrentSearch() {
|
|
2602
|
+
if (currentQueryInput) {
|
|
2603
|
+
const name = currentQueryInput.length > 40 ? currentQueryInput.slice(0, 37) + "..." : currentQueryInput;
|
|
2604
|
+
saveSearch(
|
|
2605
|
+
name,
|
|
2606
|
+
currentQueryInput,
|
|
2607
|
+
currentMode,
|
|
2608
|
+
currentSort,
|
|
2609
|
+
currentLimit,
|
|
2610
|
+
currentMode === "trending" ? trendingTab : void 0
|
|
2611
|
+
);
|
|
2612
|
+
setStatus(`Saved as "${name}"`);
|
|
2613
|
+
} else {
|
|
2614
|
+
setStatus("No search to save");
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
function triggerDeepDive() {
|
|
2618
|
+
const opt = resultsSelect.getSelectedOption();
|
|
2619
|
+
const repo = opt?.value;
|
|
2620
|
+
if (!repo) return;
|
|
2621
|
+
deepDiveActive = !deepDiveActive;
|
|
2622
|
+
if (deepDiveActive) {
|
|
2623
|
+
detailText.content = " Loading deep-dive...";
|
|
2624
|
+
renderer.requestRender();
|
|
2625
|
+
fetchDeepDive(repo, githubToken).then((data) => {
|
|
2626
|
+
detailText.content = buildDeepDiveText(data);
|
|
2627
|
+
renderer.requestRender();
|
|
2628
|
+
}).catch(() => {
|
|
2629
|
+
detailText.content = " Failed to load deep-dive";
|
|
2630
|
+
renderer.requestRender();
|
|
2631
|
+
});
|
|
2632
|
+
} else {
|
|
2633
|
+
updateDetail(repo);
|
|
2634
|
+
renderer.requestRender();
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
const readmeBox = new BoxRenderable2(renderer, {
|
|
2638
|
+
visible: false,
|
|
2639
|
+
flexGrow: 1,
|
|
2640
|
+
backgroundColor: colors.surface,
|
|
2641
|
+
borderBottom: true,
|
|
2642
|
+
borderBottomColor: colors.border,
|
|
2643
|
+
title: " README ",
|
|
2644
|
+
titleColor: colors.green
|
|
2645
|
+
});
|
|
2646
|
+
const readmeScroll = new ScrollBoxRenderable2(renderer, {
|
|
2647
|
+
flexGrow: 1,
|
|
2648
|
+
backgroundColor: colors.bg,
|
|
2649
|
+
scrollY: true,
|
|
2650
|
+
scrollX: false,
|
|
2651
|
+
paddingX: 1,
|
|
2652
|
+
viewportOptions: { backgroundColor: colors.bg },
|
|
2653
|
+
contentOptions: { backgroundColor: colors.bg, flexDirection: "column" },
|
|
2654
|
+
scrollbarOptions: {
|
|
2655
|
+
backgroundColor: colors.bg,
|
|
2656
|
+
foregroundColor: colors.muted,
|
|
2657
|
+
width: 1
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
const readmeText = new TextRenderable2(renderer, {
|
|
2661
|
+
content: "",
|
|
2662
|
+
color: colors.text,
|
|
2663
|
+
backgroundColor: colors.bg
|
|
2664
|
+
});
|
|
2665
|
+
readmeScroll.add(readmeText);
|
|
2666
|
+
readmeBox.add(readmeScroll);
|
|
2667
|
+
const readmeFooter = new TextRenderable2(renderer, {
|
|
2668
|
+
content: " \u2191\u2193/jk scroll Esc/q close",
|
|
2669
|
+
color: colors.muted,
|
|
2670
|
+
backgroundColor: colors.bg,
|
|
2671
|
+
height: 1,
|
|
2672
|
+
paddingX: 1
|
|
2673
|
+
});
|
|
2674
|
+
readmeBox.add(readmeFooter);
|
|
2675
|
+
root.add(readmeBox);
|
|
2676
|
+
async function showReadme() {
|
|
2677
|
+
const opt = resultsSelect.getSelectedOption();
|
|
2678
|
+
const repo = opt?.value;
|
|
2679
|
+
if (!repo) {
|
|
2680
|
+
setStatus("No repo selected");
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
readmeText.content = ` Loading README for ${repo.fullName}...`;
|
|
2684
|
+
showOverlay("readme");
|
|
2685
|
+
try {
|
|
2686
|
+
const headers = { "User-Agent": "ghfind/1.0" };
|
|
2687
|
+
if (githubToken) headers.Authorization = `Bearer ${githubToken}`;
|
|
2688
|
+
let text = "";
|
|
2689
|
+
for (const path of [
|
|
2690
|
+
`${repo.owner}/${repo.name}/main/README.md`,
|
|
2691
|
+
`${repo.owner}/${repo.name}/master/README.md`,
|
|
2692
|
+
`${repo.owner}/${repo.name}/main/README.rst`
|
|
2693
|
+
]) {
|
|
2694
|
+
const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
|
|
2695
|
+
headers
|
|
2696
|
+
});
|
|
2697
|
+
if (r.ok) {
|
|
2698
|
+
text = await r.text();
|
|
2699
|
+
break;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
if (!text) {
|
|
2703
|
+
readmeText.content = ` (no README found for ${repo.fullName})`;
|
|
2704
|
+
} else {
|
|
2705
|
+
const stripped = text.replace(/```[\s\S]*?```/g, "[code block]").replace(/#{1,6}\s/g, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1").replace(/`([^`]+)`/g, "$1").split("\n").map((l) => l.trimEnd()).join("\n");
|
|
2706
|
+
readmeText.content = stripped;
|
|
2707
|
+
}
|
|
2708
|
+
} catch {
|
|
2709
|
+
readmeText.content = " Failed to load README";
|
|
2710
|
+
}
|
|
2711
|
+
renderer.requestRender();
|
|
2712
|
+
}
|
|
2713
|
+
const statusBar = new TextRenderable2(renderer, {
|
|
2714
|
+
content: " Ready. Press Enter to search.",
|
|
2715
|
+
backgroundColor: colors.surface,
|
|
2716
|
+
color: colors.muted,
|
|
2717
|
+
height: 1,
|
|
2718
|
+
paddingX: 1,
|
|
2719
|
+
borderTop: true,
|
|
2720
|
+
borderTopColor: colors.border
|
|
2721
|
+
});
|
|
2722
|
+
root.add(statusBar);
|
|
2723
|
+
const statusMgr = new StatusManager((text) => {
|
|
2724
|
+
statusBar.content = ` ${text}`;
|
|
2725
|
+
renderer.requestRender();
|
|
2726
|
+
});
|
|
2727
|
+
if (config.theme !== void 0 || true) {
|
|
2728
|
+
setTimeout(() => {
|
|
2729
|
+
if (currentOverlay === "none") {
|
|
2730
|
+
setStatus(nextTip());
|
|
2731
|
+
}
|
|
2732
|
+
}, 2e3);
|
|
2733
|
+
}
|
|
2734
|
+
function setStatus(msg) {
|
|
2735
|
+
statusMgr.set("idle", msg);
|
|
2736
|
+
}
|
|
2737
|
+
function setToolbar() {
|
|
2738
|
+
toolbarText.content = formatToolbar(currentSort, currentLimit, totalCount);
|
|
2739
|
+
renderer.requestRender();
|
|
2740
|
+
}
|
|
2741
|
+
function updateDetail(repo) {
|
|
2742
|
+
if (!repo) {
|
|
2743
|
+
detailText.content = "";
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
const topics = repo.topics.length ? repo.topics.slice(0, 8).join(", ") : "\u2014";
|
|
2747
|
+
const desc = repo.description ?? "(no description)";
|
|
2748
|
+
const stars = repo.stars.toLocaleString();
|
|
2749
|
+
const forks = repo.forks.toLocaleString();
|
|
2750
|
+
const lang = repo.language ?? "\u2014";
|
|
2751
|
+
const updated = repo.updatedAt.slice(0, 10);
|
|
2752
|
+
const statusTags = [];
|
|
2753
|
+
if (repo.archived) statusTags.push("archived");
|
|
2754
|
+
if (repo.isFork) statusTags.push("fork");
|
|
2755
|
+
const tagStr = statusTags.length ? ` [${statusTags.join(", ")}]` : "";
|
|
2756
|
+
const lines = [
|
|
2757
|
+
` ${repo.fullName}${tagStr}`,
|
|
2758
|
+
``,
|
|
2759
|
+
` \u2605 ${stars} \u2661 ${forks} ${lang}`,
|
|
2760
|
+
` Updated ${updated}`,
|
|
2761
|
+
` Topics ${topics}`,
|
|
2762
|
+
``,
|
|
2763
|
+
` ${desc}`,
|
|
2764
|
+
``,
|
|
2765
|
+
` ${repo.url}`
|
|
2766
|
+
];
|
|
2767
|
+
detailText.content = lines.join("\n");
|
|
2768
|
+
}
|
|
2769
|
+
function brailleChar(bits) {
|
|
2770
|
+
return bits === 0 ? " " : String.fromCodePoint(10240 + bits);
|
|
2771
|
+
}
|
|
2772
|
+
function buildChartString(values, termW, termH) {
|
|
2773
|
+
if (values.length === 0) return ["(no data)"];
|
|
2774
|
+
const dotW = termW * 2;
|
|
2775
|
+
const dotH = termH * 4;
|
|
2776
|
+
const sampled = [];
|
|
2777
|
+
for (let i = 0; i < dotW; i++) {
|
|
2778
|
+
sampled.push(values[Math.floor(i / dotW * values.length)]);
|
|
2779
|
+
}
|
|
2780
|
+
const max = Math.max(...sampled, 1);
|
|
2781
|
+
const dotRows = sampled.map((v) => Math.round(v / max * (dotH - 1)));
|
|
2782
|
+
const dotGrid = Array.from(
|
|
2783
|
+
{ length: dotH },
|
|
2784
|
+
() => Array(dotW).fill(false)
|
|
2785
|
+
);
|
|
2786
|
+
for (let c = 0; c < dotW; c++) {
|
|
2787
|
+
dotGrid[dotH - 1 - dotRows[c]][c] = true;
|
|
2788
|
+
}
|
|
2789
|
+
for (let c = 0; c < dotW - 1; c++) {
|
|
2790
|
+
const r1 = dotRows[c];
|
|
2791
|
+
const r2 = dotRows[c + 1];
|
|
2792
|
+
const lo = Math.min(r1, r2);
|
|
2793
|
+
const hi = Math.max(r1, r2);
|
|
2794
|
+
for (let r = lo; r <= hi; r++) {
|
|
2795
|
+
dotGrid[dotH - 1 - r][c] = true;
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
const lines = [];
|
|
2799
|
+
for (let tr = 0; tr < termH; tr++) {
|
|
2800
|
+
const chars = [];
|
|
2801
|
+
for (let tc = 0; tc < termW; tc++) {
|
|
2802
|
+
const r0 = tr * 4;
|
|
2803
|
+
const c0 = tc * 2;
|
|
2804
|
+
let bits = 0;
|
|
2805
|
+
if (r0 + 0 < dotH && c0 + 0 < dotW && dotGrid[r0 + 0][c0 + 0])
|
|
2806
|
+
bits |= 1;
|
|
2807
|
+
if (r0 + 1 < dotH && c0 + 0 < dotW && dotGrid[r0 + 1][c0 + 0])
|
|
2808
|
+
bits |= 2;
|
|
2809
|
+
if (r0 + 2 < dotH && c0 + 0 < dotW && dotGrid[r0 + 2][c0 + 0])
|
|
2810
|
+
bits |= 4;
|
|
2811
|
+
if (r0 + 0 < dotH && c0 + 1 < dotW && dotGrid[r0 + 0][c0 + 1])
|
|
2812
|
+
bits |= 8;
|
|
2813
|
+
if (r0 + 1 < dotH && c0 + 1 < dotW && dotGrid[r0 + 1][c0 + 1])
|
|
2814
|
+
bits |= 16;
|
|
2815
|
+
if (r0 + 2 < dotH && c0 + 1 < dotW && dotGrid[r0 + 2][c0 + 1])
|
|
2816
|
+
bits |= 32;
|
|
2817
|
+
if (r0 + 3 < dotH && c0 + 0 < dotW && dotGrid[r0 + 3][c0 + 0])
|
|
2818
|
+
bits |= 64;
|
|
2819
|
+
if (r0 + 3 < dotH && c0 + 1 < dotW && dotGrid[r0 + 3][c0 + 1])
|
|
2820
|
+
bits |= 128;
|
|
2821
|
+
chars.push(brailleChar(bits));
|
|
2822
|
+
}
|
|
2823
|
+
const labelRows = [
|
|
2824
|
+
0,
|
|
2825
|
+
Math.floor(termH / 4),
|
|
2826
|
+
Math.floor(termH / 2),
|
|
2827
|
+
Math.floor(3 * termH / 4),
|
|
2828
|
+
termH - 1
|
|
2829
|
+
];
|
|
2830
|
+
const labelVals = [
|
|
2831
|
+
max,
|
|
2832
|
+
Math.round(max * 0.75),
|
|
2833
|
+
Math.round(max / 2),
|
|
2834
|
+
Math.round(max / 4),
|
|
2835
|
+
0
|
|
2836
|
+
];
|
|
2837
|
+
const idx = labelRows.indexOf(tr);
|
|
2838
|
+
let label = " ";
|
|
2839
|
+
if (idx >= 0) {
|
|
2840
|
+
const val = labelVals[idx];
|
|
2841
|
+
label = val >= 1e3 ? `${(val / 1e3).toFixed(1).replace(/\.0$/, "")}k` : String(val);
|
|
2842
|
+
label = label.padStart(5) + " ";
|
|
2843
|
+
}
|
|
2844
|
+
lines.push(`${label}\u2506${chars.join("")}`);
|
|
2845
|
+
}
|
|
2846
|
+
const xAxis = ` \u2506${"\u2500".repeat(termW)}`;
|
|
2847
|
+
const weekLabels = ` \u2506 ${"1w".padEnd(Math.floor(termW / 4))} ${"13w".padEnd(Math.floor(termW / 4))} ${"26w".padEnd(Math.floor(termW / 4))} ${"52w"}`;
|
|
2848
|
+
return [xAxis, weekLabels];
|
|
2849
|
+
}
|
|
2850
|
+
async function loadChart(repo) {
|
|
2851
|
+
detailText.content = " Loading chart...";
|
|
2852
|
+
renderer.requestRender();
|
|
2853
|
+
try {
|
|
2854
|
+
const [chartRes, repoRes] = await Promise.all([
|
|
2855
|
+
fetch(
|
|
2856
|
+
`https://api.github.com/repos/${repo.owner}/${repo.name}/stats/participation`,
|
|
2857
|
+
{ headers: { "User-Agent": "ghfind/1.0" } }
|
|
2858
|
+
),
|
|
2859
|
+
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}`, {
|
|
2860
|
+
headers: { "User-Agent": "ghfind/1.0" }
|
|
2861
|
+
}).then((r) => r.ok ? r.json() : null)
|
|
2862
|
+
]);
|
|
2863
|
+
let chartSection = "";
|
|
2864
|
+
if (chartRes.ok) {
|
|
2865
|
+
const data = await chartRes.json();
|
|
2866
|
+
if (data?.all && data.all.length >= 2) {
|
|
2867
|
+
chartCommitData = data.all;
|
|
2868
|
+
const chartW = 60;
|
|
2869
|
+
const chartH = 10;
|
|
2870
|
+
const chartLines = buildChartString(chartCommitData, chartW, chartH);
|
|
2871
|
+
chartSection = ["", "Weekly commits (52 weeks)", ...chartLines].join(
|
|
2872
|
+
"\n"
|
|
2873
|
+
);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
const desc = repo.description ?? "";
|
|
2877
|
+
const lang = repo.language ?? "\u2014";
|
|
2878
|
+
const updated = repo.updatedAt ? repo.updatedAt.slice(0, 10) : "";
|
|
2879
|
+
const topics = repoRes?.topics?.length ? repoRes.topics.slice(0, 8).join(", ") : "";
|
|
2880
|
+
const growthLine = currentMode === "trending" && repo.score > 0 ? ` Growth \u25B2 ${fmtStars(repo.score)} ${trendingPeriod}` : "";
|
|
2881
|
+
const detailLines = [
|
|
2882
|
+
` ${repo.fullName}`,
|
|
2883
|
+
``,
|
|
2884
|
+
` \u2605 ${repo.stars.toLocaleString()} \u2661 ${repo.forks.toLocaleString()} ${lang}`,
|
|
2885
|
+
growthLine,
|
|
2886
|
+
updated ? ` Updated ${updated}` : "",
|
|
2887
|
+
topics ? ` Topics ${topics}` : "",
|
|
2888
|
+
"",
|
|
2889
|
+
desc,
|
|
2890
|
+
"",
|
|
2891
|
+
` ${repo.url}`
|
|
2892
|
+
].filter((l) => l !== "");
|
|
2893
|
+
detailText.content = [...detailLines, chartSection].join("\n");
|
|
2894
|
+
} catch {
|
|
2895
|
+
detailText.content = " Failed to load chart";
|
|
2896
|
+
}
|
|
2897
|
+
renderer.requestRender();
|
|
2898
|
+
}
|
|
2899
|
+
function toggleGraph() {
|
|
2900
|
+
graphFullscreen = !graphFullscreen;
|
|
2901
|
+
if (graphFullscreen) {
|
|
2902
|
+
resultsBox.visible = false;
|
|
2903
|
+
resultsBox.width = "0%";
|
|
2904
|
+
detailBox.width = "100%";
|
|
2905
|
+
const opt = resultsSelect.getSelectedOption();
|
|
2906
|
+
const repo = opt?.value;
|
|
2907
|
+
if (repo) loadChart(repo);
|
|
2908
|
+
else detailText.content = " Select a repo to view activity";
|
|
2909
|
+
} else {
|
|
2910
|
+
resultsBox.visible = true;
|
|
2911
|
+
resultsBox.width = "50%";
|
|
2912
|
+
detailBox.width = "50%";
|
|
2913
|
+
const opt = resultsSelect.getSelectedOption();
|
|
2914
|
+
if (opt?.value) updateDetail(opt.value);
|
|
2915
|
+
else detailText.content = "";
|
|
2916
|
+
}
|
|
2917
|
+
renderer.requestRender();
|
|
2918
|
+
}
|
|
2919
|
+
function formatTrendingLine(r, rank) {
|
|
2920
|
+
const rankStr = String(rank).padStart(2, "0");
|
|
2921
|
+
const name = `${r.owner}/${r.name}`.padEnd(30).slice(0, 30);
|
|
2922
|
+
const lang = (r.language || "?").padEnd(12).slice(0, 12);
|
|
2923
|
+
const stars = `\u2605 ${fmtStars(r.stars)}`.padEnd(9).slice(0, 9);
|
|
2924
|
+
const arrow = r.score > 0 ? "\u25B2" : r.score < 0 ? "\u25BC" : "\u2014";
|
|
2925
|
+
const growth = `${arrow} ${fmtStars(r.score)} ${trendingPeriod}`;
|
|
2926
|
+
return `[${rankStr}] ${name} ${lang} ${stars} ${growth}`;
|
|
2927
|
+
}
|
|
2928
|
+
async function loadTrending() {
|
|
2929
|
+
currentMode = "trending";
|
|
2930
|
+
isLoading = true;
|
|
2931
|
+
searchBox.visible = false;
|
|
2932
|
+
toolbarText.visible = false;
|
|
2933
|
+
trendingTabBox.visible = true;
|
|
2934
|
+
renderTrendingTabs();
|
|
2935
|
+
resultsSelect.options = [
|
|
2936
|
+
{ name: " Loading trending...", description: "", value: null }
|
|
2937
|
+
];
|
|
2938
|
+
detailText.content = "";
|
|
2939
|
+
statusMgr.set("loading", "Loading trending repos...");
|
|
2940
|
+
renderer.requestRender();
|
|
2941
|
+
try {
|
|
2942
|
+
const searchModule = new SearchModule(new TrendingAdapter());
|
|
2943
|
+
const response = await searchModule.search(
|
|
2944
|
+
{ keywords: [], qualifiers: [], raw: "trending" },
|
|
2945
|
+
{ limit: 25, sort: "stars", json: false, verbose: false, trendingSince: tabSince(trendingTab) }
|
|
2946
|
+
);
|
|
2947
|
+
const fetched = response.repos;
|
|
2948
|
+
const since = tabSince(trendingTab);
|
|
2949
|
+
trendingPeriod = since === "daily" ? "today" : since === "weekly" ? "this week" : "this month";
|
|
2950
|
+
resultsSelect.options = fetched.map((r, i) => ({
|
|
2951
|
+
name: formatTrendingLine(r, i + 1),
|
|
2952
|
+
description: r.description ?? "",
|
|
2953
|
+
value: r
|
|
2954
|
+
}));
|
|
2955
|
+
if (fetched.length > 0) {
|
|
2956
|
+
resultsSelect.setSelectedIndex(0);
|
|
2957
|
+
updateDetail(fetched[0]);
|
|
2958
|
+
} else {
|
|
2959
|
+
throw new NoResultsError(trendingTab);
|
|
2960
|
+
}
|
|
2961
|
+
statusMgr.set(
|
|
2962
|
+
"success",
|
|
2963
|
+
`${fetched.length} trending repos \u2014 ${trendingTab}`
|
|
2964
|
+
);
|
|
2965
|
+
appendHistory({
|
|
2966
|
+
query: `trending:${trendingTab}`,
|
|
2967
|
+
mode: "trending",
|
|
2968
|
+
tab: trendingTab,
|
|
2969
|
+
timestamp: Date.now(),
|
|
2970
|
+
resultCount: fetched.length
|
|
2971
|
+
});
|
|
2972
|
+
} catch (err) {
|
|
2973
|
+
const msg = err instanceof SearchCliError ? err.userMessage : err instanceof Error ? err.message : String(err);
|
|
2974
|
+
resultsSelect.options = [
|
|
2975
|
+
{ name: " (error)", description: "", value: null }
|
|
2976
|
+
];
|
|
2977
|
+
statusMgr.set("error", msg.slice(0, 60));
|
|
2978
|
+
}
|
|
2979
|
+
isLoading = false;
|
|
2980
|
+
renderer.requestRender();
|
|
2981
|
+
}
|
|
2982
|
+
function showSearchMode() {
|
|
2983
|
+
currentMode = "search";
|
|
2984
|
+
trendingTabBox.visible = false;
|
|
2985
|
+
searchBox.visible = true;
|
|
2986
|
+
toolbarText.visible = true;
|
|
2987
|
+
searchInput.placeholder = "Search GitHub repos (e.g. rust cli, or language:Rust stars:>100)";
|
|
2988
|
+
searchInput.focus();
|
|
2989
|
+
renderer.requestRender();
|
|
2990
|
+
}
|
|
2991
|
+
async function doSearch(queryText, append = false) {
|
|
2992
|
+
const q = queryText.trim();
|
|
2993
|
+
if (q === "" || isLoading) return;
|
|
2994
|
+
if (!append) currentPage = 1;
|
|
2995
|
+
deepDiveActive = false;
|
|
2996
|
+
isLoading = true;
|
|
2997
|
+
currentQueryInput = q;
|
|
2998
|
+
if (!append) {
|
|
2999
|
+
resultsSelect.options = [
|
|
3000
|
+
{ name: " Searching...", description: "", value: null }
|
|
3001
|
+
];
|
|
3002
|
+
detailText.content = "";
|
|
3003
|
+
}
|
|
3004
|
+
statusMgr.set("searching", `Searching for "${q}"`);
|
|
3005
|
+
try {
|
|
3006
|
+
const parsed = applyFlagFilters(parseQuery(q), {});
|
|
3007
|
+
validateQuery(parsed);
|
|
3008
|
+
const provider = createGitHubSearch(logger);
|
|
3009
|
+
const options = {
|
|
3010
|
+
limit: currentLimit,
|
|
3011
|
+
sort: currentSort,
|
|
3012
|
+
json: false,
|
|
3013
|
+
verbose: false,
|
|
3014
|
+
token: githubToken,
|
|
3015
|
+
page: currentPage
|
|
3016
|
+
};
|
|
3017
|
+
const response = await provider.search(parsed, options);
|
|
3018
|
+
totalCount = response.totalCount;
|
|
3019
|
+
const newRepos = rankRepos(response.repos, options.sort);
|
|
3020
|
+
if (append) {
|
|
3021
|
+
currentRepos.push(...newRepos);
|
|
3022
|
+
} else {
|
|
3023
|
+
currentRepos = newRepos;
|
|
3024
|
+
}
|
|
3025
|
+
if (currentRepos.length === 0) {
|
|
3026
|
+
throw new NoResultsError(q);
|
|
3027
|
+
} else {
|
|
3028
|
+
resultsSelect.options = currentRepos.map((r) => ({
|
|
3029
|
+
name: formatResultLine(r),
|
|
3030
|
+
description: "",
|
|
3031
|
+
value: r
|
|
3032
|
+
}));
|
|
3033
|
+
const idx = append ? resultsSelect.options.length - newRepos.length : 0;
|
|
3034
|
+
updateDetail(currentRepos[idx]);
|
|
3035
|
+
resultsSelect.setSelectedIndex(idx);
|
|
3036
|
+
statusMgr.set(
|
|
3037
|
+
"success",
|
|
3038
|
+
`Page ${currentPage} \u2014 ${currentRepos.length} of ${totalCount.toLocaleString()} results`
|
|
3039
|
+
);
|
|
3040
|
+
if (!append) {
|
|
3041
|
+
appendHistory({
|
|
3042
|
+
query: q,
|
|
3043
|
+
mode: "search",
|
|
3044
|
+
timestamp: Date.now(),
|
|
3045
|
+
resultCount: currentRepos.length
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
} catch (err) {
|
|
3050
|
+
currentRepos = [];
|
|
3051
|
+
resultsSelect.options = [
|
|
3052
|
+
{ name: " (error)", description: "", value: null }
|
|
3053
|
+
];
|
|
3054
|
+
const msg = err instanceof SearchCliError ? err.userMessage : err instanceof Error ? err.message : String(err);
|
|
3055
|
+
statusMgr.set("error", msg.slice(0, 60));
|
|
3056
|
+
}
|
|
3057
|
+
isLoading = false;
|
|
3058
|
+
renderer.requestRender();
|
|
3059
|
+
}
|
|
3060
|
+
function changeSort() {
|
|
3061
|
+
const idx = SORT_MODES.findIndex((m) => m.key === currentSort);
|
|
3062
|
+
const next = SORT_MODES[(idx + 1) % SORT_MODES.length];
|
|
3063
|
+
currentSort = next.key;
|
|
3064
|
+
setToolbar();
|
|
3065
|
+
if (currentRepos.length > 0) {
|
|
3066
|
+
currentRepos = rankRepos(currentRepos, currentSort);
|
|
3067
|
+
resultsSelect.options = currentRepos.map((r) => ({
|
|
3068
|
+
name: formatResultLine(r),
|
|
3069
|
+
description: "",
|
|
3070
|
+
value: r
|
|
3071
|
+
}));
|
|
3072
|
+
resultsSelect.setSelectedIndex(0);
|
|
3073
|
+
updateDetail(currentRepos[0]);
|
|
3074
|
+
renderer.requestRender();
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
function changeLimit() {
|
|
3078
|
+
const limits = [10, 25, 50, 100];
|
|
3079
|
+
const idx = limits.indexOf(currentLimit);
|
|
3080
|
+
currentLimit = limits[(idx + 1) % limits.length];
|
|
3081
|
+
setToolbar();
|
|
3082
|
+
setStatus(`Limit set to ${currentLimit}. Press Enter to re-search.`);
|
|
3083
|
+
renderer.requestRender();
|
|
3084
|
+
}
|
|
3085
|
+
searchInput.on("enter", () => {
|
|
3086
|
+
if (searchInput.value.trim() === "" || isLoading) return;
|
|
3087
|
+
showSearchMode();
|
|
3088
|
+
doSearch(searchInput.value);
|
|
3089
|
+
});
|
|
3090
|
+
resultsSelect.on("selectionChanged", () => {
|
|
3091
|
+
const opt = resultsSelect.getSelectedOption();
|
|
3092
|
+
const repo = opt?.value;
|
|
3093
|
+
if (repo) {
|
|
3094
|
+
if (graphFullscreen) loadChart(repo);
|
|
3095
|
+
else updateDetail(repo);
|
|
3096
|
+
}
|
|
3097
|
+
renderer.requestRender();
|
|
3098
|
+
});
|
|
3099
|
+
resultsSelect.on("itemSelected", () => {
|
|
3100
|
+
const opt = resultsSelect.getSelectedOption();
|
|
3101
|
+
const repo = opt?.value;
|
|
3102
|
+
if (repo) openUrl(repo.url);
|
|
3103
|
+
});
|
|
3104
|
+
renderer.keyInput.on("keypress", (key) => {
|
|
3105
|
+
if (currentOverlay !== "none") {
|
|
3106
|
+
if (key.name === "escape") {
|
|
3107
|
+
if (currentOverlay === "leader") {
|
|
3108
|
+
if (!popMenuLevel()) {
|
|
3109
|
+
showOverlay("none");
|
|
3110
|
+
}
|
|
3111
|
+
setToolbar();
|
|
3112
|
+
renderer.requestRender();
|
|
3113
|
+
} else {
|
|
3114
|
+
showOverlay("none");
|
|
3115
|
+
renderer.requestRender();
|
|
3116
|
+
}
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
if (key.name === "q") {
|
|
3120
|
+
showOverlay("none");
|
|
3121
|
+
renderer.requestRender();
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
if (currentOverlay === "leader") {
|
|
3125
|
+
if (key.name === "enter" || key.name === "return" || key.name === "right" || key.name === "l") {
|
|
3126
|
+
const sel = leaderSelect.getSelectedOption();
|
|
3127
|
+
const entry = sel?.value;
|
|
3128
|
+
if (!entry) return;
|
|
3129
|
+
if (entry.type === "category") {
|
|
3130
|
+
pushMenuLevel(
|
|
3131
|
+
` Menu > ${entry.name}`,
|
|
3132
|
+
entry.name,
|
|
3133
|
+
entry.children,
|
|
3134
|
+
0
|
|
3135
|
+
);
|
|
3136
|
+
renderer.requestRender();
|
|
3137
|
+
} else {
|
|
3138
|
+
resetMenu();
|
|
3139
|
+
showOverlay("none");
|
|
3140
|
+
entry.action();
|
|
3141
|
+
renderer.requestRender();
|
|
3142
|
+
}
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
if (key.name === "left" || key.name === "h") {
|
|
3146
|
+
if (popMenuLevel()) {
|
|
3147
|
+
setToolbar();
|
|
3148
|
+
renderer.requestRender();
|
|
3149
|
+
}
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
if (currentOverlay === "readme") {
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
if (currentOverlay === "history") {
|
|
3157
|
+
if (key.name === "d") {
|
|
3158
|
+
const sel = historySelect.getSelectedOption();
|
|
3159
|
+
if (sel?.value) {
|
|
3160
|
+
deleteHistoryEntry(sel.value.index);
|
|
3161
|
+
refreshHistory();
|
|
3162
|
+
setStatus("Deleted history entry");
|
|
3163
|
+
}
|
|
3164
|
+
renderer.requestRender();
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
if (key.ctrl && key.name === "x") {
|
|
3168
|
+
clearHistory();
|
|
3169
|
+
refreshHistory();
|
|
3170
|
+
setStatus("History cleared");
|
|
3171
|
+
renderer.requestRender();
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
if (key.name === "enter" || key.name === "return") {
|
|
3175
|
+
const sel = historySelect.getSelectedOption();
|
|
3176
|
+
if (sel?.value) {
|
|
3177
|
+
const entry = sel.value.entry;
|
|
3178
|
+
showOverlay("none");
|
|
3179
|
+
if (entry.mode === "trending") {
|
|
3180
|
+
if (entry.tab)
|
|
3181
|
+
trendingTab = entry.tab;
|
|
3182
|
+
loadTrending();
|
|
3183
|
+
} else {
|
|
3184
|
+
searchInput.value = entry.query;
|
|
3185
|
+
showSearchMode();
|
|
3186
|
+
doSearch(entry.query);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
if (currentOverlay === "bookmarks") {
|
|
3194
|
+
if (key.name === "d") {
|
|
3195
|
+
const sel = bookmarksSelect.getSelectedOption();
|
|
3196
|
+
if (sel?.value) {
|
|
3197
|
+
removeBookmark(sel.value.repo.fullName);
|
|
3198
|
+
refreshBookmarks();
|
|
3199
|
+
setStatus("Bookmark removed");
|
|
3200
|
+
}
|
|
3201
|
+
renderer.requestRender();
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
if (key.name === "enter" || key.name === "return") {
|
|
3205
|
+
const sel = bookmarksSelect.getSelectedOption();
|
|
3206
|
+
if (sel?.value) openUrl(sel.value.repo.url);
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
return;
|
|
3210
|
+
}
|
|
3211
|
+
if (currentOverlay === "saved") {
|
|
3212
|
+
if (key.name === "d") {
|
|
3213
|
+
const sel = savedSelect.getSelectedOption();
|
|
3214
|
+
if (sel?.value) {
|
|
3215
|
+
deleteSavedSearch(sel.value.name);
|
|
3216
|
+
refreshSavedSearches();
|
|
3217
|
+
setStatus("Saved search deleted");
|
|
3218
|
+
}
|
|
3219
|
+
renderer.requestRender();
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
if (key.name === "enter" || key.name === "return") {
|
|
3223
|
+
const sel = savedSelect.getSelectedOption();
|
|
3224
|
+
if (sel?.value) {
|
|
3225
|
+
const s = sel.value;
|
|
3226
|
+
showOverlay("none");
|
|
3227
|
+
touchSavedSearch(s.name);
|
|
3228
|
+
if (s.mode === "trending") {
|
|
3229
|
+
if (s.tab) trendingTab = s.tab;
|
|
3230
|
+
loadTrending();
|
|
3231
|
+
} else {
|
|
3232
|
+
searchInput.value = s.query;
|
|
3233
|
+
showSearchMode();
|
|
3234
|
+
currentSort = s.sort;
|
|
3235
|
+
currentLimit = s.limit;
|
|
3236
|
+
setToolbar();
|
|
3237
|
+
doSearch(s.query);
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
if (currentOverlay === "topics") {
|
|
3245
|
+
if (key.name === "enter" || key.name === "return") {
|
|
3246
|
+
const sel = topicsSelect.getSelectedOption();
|
|
3247
|
+
if (sel?.value) {
|
|
3248
|
+
const topic = sel.value;
|
|
3249
|
+
showOverlay("none");
|
|
3250
|
+
searchInput.value = `topic:${topic.name}`;
|
|
3251
|
+
showSearchMode();
|
|
3252
|
+
doSearch(`topic:${topic.name}`);
|
|
3253
|
+
}
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
return;
|
|
3257
|
+
}
|
|
3258
|
+
if (currentOverlay === "export") {
|
|
3259
|
+
if (key.name === "enter" || key.name === "return") {
|
|
3260
|
+
const sel = exportSelect.getSelectedOption();
|
|
3261
|
+
if (sel?.value && currentRepos.length > 0) {
|
|
3262
|
+
const format2 = sel.value;
|
|
3263
|
+
const path = exportToFile(currentRepos, format2);
|
|
3264
|
+
showOverlay("none");
|
|
3265
|
+
setStatus(`\u2713 Exported to ${path}`);
|
|
3266
|
+
} else {
|
|
3267
|
+
setStatus("No results to export");
|
|
3268
|
+
showOverlay("none");
|
|
3269
|
+
}
|
|
3270
|
+
renderer.requestRender();
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
if (currentOverlay === "compare") {
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
if (currentOverlay === "notifications") {
|
|
3279
|
+
if (key.name === "d") {
|
|
3280
|
+
const notifs2 = getNotifications();
|
|
3281
|
+
if (notifs2.length > 0) {
|
|
3282
|
+
dismissNotification(notifs2[0].id);
|
|
3283
|
+
refreshNotifications();
|
|
3284
|
+
setStatus("Notification dismissed");
|
|
3285
|
+
}
|
|
3286
|
+
renderer.requestRender();
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
if (key.ctrl && key.name === "c") {
|
|
3290
|
+
dismissAll();
|
|
3291
|
+
refreshNotifications();
|
|
3292
|
+
renderer.requestRender();
|
|
3293
|
+
return;
|
|
3294
|
+
}
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
if (currentOverlay === "share") {
|
|
3298
|
+
if (key.name === "enter" || key.name === "return") {
|
|
3299
|
+
const sel = shareSelect.getSelectedOption();
|
|
3300
|
+
if (sel?.value) {
|
|
3301
|
+
const opt = resultsSelect.getSelectedOption();
|
|
3302
|
+
const repo = opt?.value;
|
|
3303
|
+
if (repo) {
|
|
3304
|
+
const text = formatShare(repo, sel.value);
|
|
3305
|
+
copyToClipboard(text).then((ok) => {
|
|
3306
|
+
showOverlay("none");
|
|
3307
|
+
setStatus(ok ? "\u2713 Copied!" : "Clipboard not available");
|
|
3308
|
+
});
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
renderer.requestRender();
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
if (currentOverlay === "help") {
|
|
3317
|
+
showOverlay("none");
|
|
3318
|
+
renderer.requestRender();
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
if (currentOverlay === "update") {
|
|
3322
|
+
if (key.name === "escape" || key.name === "q") {
|
|
3323
|
+
showOverlay("none");
|
|
3324
|
+
renderer.requestRender();
|
|
3325
|
+
return;
|
|
3326
|
+
}
|
|
3327
|
+
if (key.name === "left" || key.name === "h") {
|
|
3328
|
+
updateSelectedOption = Math.max(0, updateSelectedOption - 1);
|
|
3329
|
+
renderUpdateOptions();
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
if (key.name === "right" || key.name === "l") {
|
|
3333
|
+
updateSelectedOption = Math.min(2, updateSelectedOption + 1);
|
|
3334
|
+
renderUpdateOptions();
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
if (key.name === "enter" || key.name === "return") {
|
|
3338
|
+
const action = ["now", "later", "never"][updateSelectedOption];
|
|
3339
|
+
if (action === "now") {
|
|
3340
|
+
showOverlay("none");
|
|
3341
|
+
performUpdate().then((ok) => {
|
|
3342
|
+
if (ok) {
|
|
3343
|
+
setStatus("\u2713 Update complete \u2014 restart ghfind");
|
|
3344
|
+
} else {
|
|
3345
|
+
setStatus("\u2717 Update failed \u2014 check terminal output");
|
|
3346
|
+
}
|
|
3347
|
+
}).catch(() => {
|
|
3348
|
+
setStatus("\u2717 Update failed \u2014 check terminal output");
|
|
3349
|
+
});
|
|
3350
|
+
} else if (action === "never") {
|
|
3351
|
+
suppressUpdateNotices();
|
|
3352
|
+
showOverlay("none");
|
|
3353
|
+
setStatus("Update notices suppressed");
|
|
3354
|
+
} else {
|
|
3355
|
+
snoozeUpdateNotices(3);
|
|
3356
|
+
showOverlay("none");
|
|
3357
|
+
setStatus("Update reminder snoozed for 3 days");
|
|
3358
|
+
}
|
|
3359
|
+
renderer.requestRender();
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
if (key.name === "q") {
|
|
3367
|
+
if (graphFullscreen) {
|
|
3368
|
+
toggleGraph();
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
saveSession({
|
|
3372
|
+
mode: currentMode,
|
|
3373
|
+
query: currentQueryInput,
|
|
3374
|
+
sort: currentSort,
|
|
3375
|
+
limit: currentLimit,
|
|
3376
|
+
trendingTab
|
|
3377
|
+
});
|
|
3378
|
+
cleanup();
|
|
3379
|
+
return;
|
|
3380
|
+
}
|
|
3381
|
+
if (key.name === "escape" && graphFullscreen) {
|
|
3382
|
+
toggleGraph();
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
if (key.name === "space" && !searchInput.focused) {
|
|
3386
|
+
if (currentOverlay === "leader") {
|
|
3387
|
+
showOverlay("none");
|
|
3388
|
+
} else {
|
|
3389
|
+
showLeaderMenu();
|
|
3390
|
+
}
|
|
3391
|
+
return;
|
|
3392
|
+
}
|
|
3393
|
+
if (key.name === "?" || key.name === "h" && key.ctrl) {
|
|
3394
|
+
showOverlay("help");
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3397
|
+
if (key.name === "tab") {
|
|
3398
|
+
if (searchBox.visible) {
|
|
3399
|
+
const val = searchInput.value;
|
|
3400
|
+
const lastSpace = val.lastIndexOf(" ");
|
|
3401
|
+
const currentWord = lastSpace >= 0 ? val.slice(lastSpace + 1) : val;
|
|
3402
|
+
const suggestions = suggestFor(currentWord);
|
|
3403
|
+
if (suggestions.length > 0) {
|
|
3404
|
+
const newVal = lastSpace >= 0 ? val.slice(0, lastSpace + 1) + suggestions[0] : suggestions[0];
|
|
3405
|
+
searchInput.value = newVal;
|
|
3406
|
+
renderer.requestRender();
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3411
|
+
if (key.name === "/") {
|
|
3412
|
+
searchInput.focus();
|
|
3413
|
+
showSearchMode();
|
|
3414
|
+
renderer.requestRender();
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
if (/^[1-5]$/.test(key.name)) {
|
|
3418
|
+
if (currentMode === "trending") {
|
|
3419
|
+
const idx = parseInt(key.name) - 1;
|
|
3420
|
+
if (idx >= 0 && idx < TAB_NAMES.length) {
|
|
3421
|
+
trendingTab = TAB_NAMES[idx];
|
|
3422
|
+
loadTrending();
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
if (key.name === "left" || key.name === "h") {
|
|
3428
|
+
if (currentMode === "trending") {
|
|
3429
|
+
const idx = TAB_NAMES.indexOf(trendingTab);
|
|
3430
|
+
if (idx > 0) {
|
|
3431
|
+
trendingTab = TAB_NAMES[idx - 1];
|
|
3432
|
+
loadTrending();
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
return;
|
|
3436
|
+
}
|
|
3437
|
+
if (key.name === "right" || key.name === "l") {
|
|
3438
|
+
if (currentMode === "trending") {
|
|
3439
|
+
const idx = TAB_NAMES.indexOf(trendingTab);
|
|
3440
|
+
if (idx < TAB_NAMES.length - 1) {
|
|
3441
|
+
trendingTab = TAB_NAMES[idx + 1];
|
|
3442
|
+
loadTrending();
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
if (key.name === "pagedown") {
|
|
3448
|
+
if (currentMode === "search" && currentQueryInput) {
|
|
3449
|
+
currentPage++;
|
|
3450
|
+
doSearch(currentQueryInput, true);
|
|
3451
|
+
}
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
if (key.name === "pageup") {
|
|
3455
|
+
if (currentRepos.length > 0) {
|
|
3456
|
+
resultsSelect.setSelectedIndex(0);
|
|
3457
|
+
updateDetail(currentRepos[0]);
|
|
3458
|
+
renderer.requestRender();
|
|
3459
|
+
}
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
});
|
|
3463
|
+
const pkgPath = join2(
|
|
3464
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
3465
|
+
"..",
|
|
3466
|
+
"package.json"
|
|
3467
|
+
);
|
|
3468
|
+
let currentVersion = "0.0.0";
|
|
3469
|
+
try {
|
|
3470
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
3471
|
+
currentVersion = pkg.version;
|
|
3472
|
+
} catch {
|
|
3473
|
+
}
|
|
3474
|
+
checkForUpdateAndShow();
|
|
3475
|
+
renderer.start();
|
|
3476
|
+
if (currentMode === "trending") {
|
|
3477
|
+
loadTrending();
|
|
3478
|
+
} else {
|
|
3479
|
+
searchInput.value = currentQueryInput;
|
|
3480
|
+
if (currentQueryInput) {
|
|
3481
|
+
doSearch(currentQueryInput);
|
|
3482
|
+
} else {
|
|
3483
|
+
showSearchMode();
|
|
3484
|
+
}
|
|
3485
|
+
searchInput.focus();
|
|
3486
|
+
}
|
|
3487
|
+
renderer.requestRender();
|
|
3488
|
+
}
|
|
3489
|
+
function formatResultLine(repo) {
|
|
3490
|
+
const stars = formatStars(repo.stars);
|
|
3491
|
+
const lang = (repo.language ?? "?").padEnd(12).slice(0, 12);
|
|
3492
|
+
const name = repo.fullName.padEnd(40).slice(0, 40);
|
|
3493
|
+
return `${name} ${lang} ${stars}`;
|
|
3494
|
+
}
|
|
3495
|
+
function formatStars(n) {
|
|
3496
|
+
if (n >= 1e6) return `\u2605 ${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
|
|
3497
|
+
if (n >= 1e3) return `\u2605 ${(n / 1e3).toFixed(1).replace(/\.0$/, "")}k`;
|
|
3498
|
+
return `\u2605 ${n}`;
|
|
3499
|
+
}
|
|
3500
|
+
function formatToolbar(sort, limit, totalCount) {
|
|
3501
|
+
const sortLabel = SORT_MODES.find((m) => m.key === sort)?.label ?? sort;
|
|
3502
|
+
return ` sort: ${sortLabel} limit: ${limit} results: ${totalCount.toLocaleString()}`;
|
|
3503
|
+
}
|
|
3504
|
+
function cleanup() {
|
|
3505
|
+
try {
|
|
3506
|
+
rotateHistory();
|
|
3507
|
+
} catch {
|
|
3508
|
+
}
|
|
3509
|
+
process.exit(0);
|
|
3510
|
+
}
|
|
3511
|
+
if (import.meta.main && false) {
|
|
3512
|
+
launchBrowser();
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
// src/cli.ts
|
|
3516
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
3517
|
+
var PKG_DIR2 = join3(__dirname2, "..");
|
|
3518
|
+
function parseArgs(args) {
|
|
3519
|
+
const flags = {
|
|
3520
|
+
query: "",
|
|
3521
|
+
json: false,
|
|
3522
|
+
csv: false,
|
|
3523
|
+
markdown: false,
|
|
3524
|
+
count: false,
|
|
3525
|
+
limit: 50,
|
|
3526
|
+
sort: "best-match",
|
|
3527
|
+
token: process.env.GITHUB_TOKEN,
|
|
3528
|
+
trending: false,
|
|
3529
|
+
since: "daily",
|
|
3530
|
+
watch: false,
|
|
3531
|
+
interval: 300,
|
|
3532
|
+
init: false,
|
|
3533
|
+
version: false,
|
|
3534
|
+
help: false
|
|
3535
|
+
};
|
|
3536
|
+
let queryParts = [];
|
|
3537
|
+
for (let i = 0; i < args.length; i++) {
|
|
3538
|
+
const arg = args[i];
|
|
3539
|
+
switch (arg) {
|
|
3540
|
+
case "--json":
|
|
3541
|
+
flags.json = true;
|
|
3542
|
+
break;
|
|
3543
|
+
case "--csv":
|
|
3544
|
+
flags.csv = true;
|
|
3545
|
+
break;
|
|
3546
|
+
case "--markdown":
|
|
3547
|
+
flags.markdown = true;
|
|
3548
|
+
break;
|
|
3549
|
+
case "--count":
|
|
3550
|
+
flags.count = true;
|
|
3551
|
+
break;
|
|
3552
|
+
case "--trending":
|
|
3553
|
+
flags.trending = true;
|
|
3554
|
+
break;
|
|
3555
|
+
case "--watch":
|
|
3556
|
+
flags.watch = true;
|
|
3557
|
+
break;
|
|
3558
|
+
case "--version":
|
|
3559
|
+
case "-v":
|
|
3560
|
+
flags.version = true;
|
|
3561
|
+
break;
|
|
3562
|
+
case "--help":
|
|
3563
|
+
case "-h":
|
|
3564
|
+
flags.help = true;
|
|
3565
|
+
break;
|
|
3566
|
+
case "init":
|
|
3567
|
+
flags.init = true;
|
|
3568
|
+
break;
|
|
3569
|
+
case "--limit": {
|
|
3570
|
+
const parsed = parseInt(args[++i]);
|
|
3571
|
+
if (isNaN(parsed) || parsed < 1 || parsed > 100) {
|
|
3572
|
+
console.error(`Invalid limit: must be 1-100. Using default 50.`);
|
|
3573
|
+
flags.limit = 50;
|
|
3574
|
+
} else {
|
|
3575
|
+
flags.limit = parsed;
|
|
3576
|
+
}
|
|
3577
|
+
break;
|
|
3578
|
+
}
|
|
3579
|
+
case "--sort":
|
|
3580
|
+
flags.sort = args[++i];
|
|
3581
|
+
break;
|
|
3582
|
+
case "--token":
|
|
3583
|
+
flags.token = args[++i];
|
|
3584
|
+
break;
|
|
3585
|
+
case "--since":
|
|
3586
|
+
flags.since = args[++i];
|
|
3587
|
+
break;
|
|
3588
|
+
case "--pipe":
|
|
3589
|
+
flags.pipe = args[++i];
|
|
3590
|
+
break;
|
|
3591
|
+
case "--format":
|
|
3592
|
+
flags.format = args[++i];
|
|
3593
|
+
break;
|
|
3594
|
+
case "--interval":
|
|
3595
|
+
flags.interval = parseInt(args[++i]) || 300;
|
|
3596
|
+
break;
|
|
3597
|
+
case "--completion":
|
|
3598
|
+
flags.completion = args[++i];
|
|
3599
|
+
break;
|
|
3600
|
+
default:
|
|
3601
|
+
if (!arg.startsWith("-")) queryParts.push(arg);
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
flags.query = queryParts.join(" ");
|
|
3605
|
+
return flags;
|
|
3606
|
+
}
|
|
3607
|
+
async function main() {
|
|
3608
|
+
const flags = parseArgs(process.argv.slice(2));
|
|
3609
|
+
if (flags.version) {
|
|
3610
|
+
const pkg = JSON.parse(readFileSync3(join3(PKG_DIR2, "package.json"), "utf-8"));
|
|
3611
|
+
console.log(`ghfind v${pkg.version}`);
|
|
3612
|
+
return;
|
|
3613
|
+
}
|
|
3614
|
+
if (flags.help) {
|
|
3615
|
+
console.log(`
|
|
3616
|
+
ghfind \u2014 Interactive GitHub repository browser
|
|
3617
|
+
|
|
3618
|
+
Usage:
|
|
3619
|
+
ghfind Launch interactive TUI
|
|
3620
|
+
ghfind <query> --json Search, output JSON
|
|
3621
|
+
ghfind <query> --csv Search, output CSV
|
|
3622
|
+
ghfind <query> --markdown Search, output Markdown table
|
|
3623
|
+
ghfind <query> --count Just the result count
|
|
3624
|
+
ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
|
|
3625
|
+
ghfind <query> --pipe <target> Pipe to clone/open
|
|
3626
|
+
ghfind --trending --json Trending repos as JSON
|
|
3627
|
+
ghfind --watch <query> Watch mode (poll every Ns)
|
|
3628
|
+
ghfind init Run setup wizard
|
|
3629
|
+
ghfind --completion <shell> Print completion script (bash|zsh|fish)
|
|
3630
|
+
ghfind --version Print version
|
|
3631
|
+
ghfind --help Print this help
|
|
3632
|
+
|
|
3633
|
+
Options:
|
|
3634
|
+
--json, --csv, --markdown, --count Output format
|
|
3635
|
+
--limit <n> Max results (default: 50)
|
|
3636
|
+
--sort <strategy> Sort: best-match|stars|updated|forks
|
|
3637
|
+
--token <token> GitHub API token
|
|
3638
|
+
--trending Trending mode
|
|
3639
|
+
--since <period> Trending period: daily|weekly|monthly
|
|
3640
|
+
--pipe <target> Pipe target: clone|open
|
|
3641
|
+
--format <fmt> Line format: urls|names|ssh-urls|...
|
|
3642
|
+
--watch Watch mode (re-run periodically)
|
|
3643
|
+
--interval <s> Watch interval in seconds (default: 300)
|
|
3644
|
+
--completion <shell> Generate completions
|
|
3645
|
+
`);
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
if (flags.completion) {
|
|
3649
|
+
const shell = flags.completion;
|
|
3650
|
+
try {
|
|
3651
|
+
const content = readFileSync3(join3(PKG_DIR2, "completions", `ghfind.${shell}`), "utf-8");
|
|
3652
|
+
console.log(content);
|
|
3653
|
+
} catch {
|
|
3654
|
+
console.error(`Completions not available for shell: ${shell}`);
|
|
3655
|
+
process.exit(1);
|
|
3656
|
+
}
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
3659
|
+
if (flags.init) {
|
|
3660
|
+
await runInitWizard();
|
|
3661
|
+
return;
|
|
3662
|
+
}
|
|
3663
|
+
const outputFormat = flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : void 0;
|
|
3664
|
+
const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch;
|
|
3665
|
+
if (isNonInteractive) {
|
|
3666
|
+
await runNonInteractive(flags, outputFormat);
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
await launchBrowser();
|
|
3670
|
+
}
|
|
3671
|
+
function buildSearchContext(flags) {
|
|
3672
|
+
const parsed = applyFlagFilters(parseQuery(flags.query), {});
|
|
3673
|
+
return {
|
|
3674
|
+
query: flags.query,
|
|
3675
|
+
parsed,
|
|
3676
|
+
provider: createGitHubSearch(void 0, flags.token ? [flags.token] : [])
|
|
3677
|
+
};
|
|
3678
|
+
}
|
|
3679
|
+
async function runNonInteractive(flags, outputFormat) {
|
|
3680
|
+
const handlers = {
|
|
3681
|
+
trending: async (ctx2) => {
|
|
3682
|
+
const since = flags.since === "weekly" ? "weekly" : flags.since === "monthly" ? "monthly" : "daily";
|
|
3683
|
+
const trendingSearch = createTrendingSearch();
|
|
3684
|
+
const response = await trendingSearch.search(
|
|
3685
|
+
{ keywords: [], qualifiers: [], raw: "trending" },
|
|
3686
|
+
{ limit: 25, sort: "stars", json: false, verbose: false, trendingSince: since }
|
|
3687
|
+
);
|
|
3688
|
+
if (outputFormat) {
|
|
3689
|
+
console.log(format(response.repos, outputFormat));
|
|
3690
|
+
} else if (flags.count) {
|
|
3691
|
+
console.log(response.totalCount);
|
|
3692
|
+
} else {
|
|
3693
|
+
for (const r of response.repos) {
|
|
3694
|
+
console.log(r.fullName + " \u2605 " + r.stars + " \u25B2 +" + r.score + " " + since + " \u25CF " + (r.language ?? ""));
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
},
|
|
3698
|
+
watch: async (ctx2) => {
|
|
3699
|
+
let tick = 0;
|
|
3700
|
+
await runWatch(
|
|
3701
|
+
{ query: ctx2.query, sort: flags.sort, limit: flags.limit, token: flags.token, intervalMs: flags.interval * 1e3, trending: flags.trending },
|
|
3702
|
+
(repos, delta) => {
|
|
3703
|
+
tick++;
|
|
3704
|
+
if (outputFormat) {
|
|
3705
|
+
console.log(`[Watch #${tick}] ${repos.length} results (${delta >= 0 ? "+" : ""}${delta} since last check)`);
|
|
3706
|
+
console.log(format(repos, outputFormat));
|
|
3707
|
+
} else {
|
|
3708
|
+
console.log(`[Watch #${tick}] ${repos.length} results \u2014 ${delta >= 0 ? "+" : ""}${delta} since last check`);
|
|
3709
|
+
}
|
|
3710
|
+
},
|
|
3711
|
+
(err) => console.error(`[Watch error] ${err.message}`)
|
|
3712
|
+
);
|
|
3713
|
+
},
|
|
3714
|
+
pipe: async (ctx2) => {
|
|
3715
|
+
const response = await ctx2.provider.search(ctx2.parsed, {
|
|
3716
|
+
limit: flags.limit,
|
|
3717
|
+
sort: flags.sort,
|
|
3718
|
+
json: false,
|
|
3719
|
+
verbose: false,
|
|
3720
|
+
token: flags.token
|
|
3721
|
+
});
|
|
3722
|
+
const repos = rankRepos(response.repos, flags.sort);
|
|
3723
|
+
await pipeExec(repos, flags.pipe);
|
|
3724
|
+
},
|
|
3725
|
+
format: async (ctx2) => {
|
|
3726
|
+
const response = await ctx2.provider.search(ctx2.parsed, {
|
|
3727
|
+
limit: flags.limit,
|
|
3728
|
+
sort: flags.sort,
|
|
3729
|
+
json: false,
|
|
3730
|
+
verbose: false,
|
|
3731
|
+
token: flags.token
|
|
3732
|
+
});
|
|
3733
|
+
const repos = rankRepos(response.repos, flags.sort);
|
|
3734
|
+
console.log(format(repos, flags.format));
|
|
3735
|
+
},
|
|
3736
|
+
search: async (ctx2) => {
|
|
3737
|
+
const response = await ctx2.provider.search(ctx2.parsed, {
|
|
3738
|
+
limit: flags.limit,
|
|
3739
|
+
sort: flags.sort,
|
|
3740
|
+
json: false,
|
|
3741
|
+
verbose: false,
|
|
3742
|
+
token: flags.token
|
|
3743
|
+
});
|
|
3744
|
+
const repos = rankRepos(response.repos, flags.sort);
|
|
3745
|
+
if (flags.count) {
|
|
3746
|
+
console.log(response.totalCount);
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
if (outputFormat) {
|
|
3750
|
+
console.log(format(repos, outputFormat));
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
};
|
|
3754
|
+
const key = flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";
|
|
3755
|
+
const ctx = buildSearchContext(flags);
|
|
3756
|
+
await handlers[key](ctx);
|
|
3757
|
+
}
|
|
3758
|
+
main().catch((err) => {
|
|
3759
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
3760
|
+
process.exit(1);
|
|
3761
|
+
});
|