github-search-cli 9.7.1 → 9.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +10 -11
- package/dist/cli.js +829 -248
- package/package.json +7 -2
- package/release-notes/9.8.0.md +55 -0
- package/src/help.ts +2 -1
- package/src/image-loader.ts +248 -0
- package/src/image-render.ts +209 -0
- package/src/markdown-images.ts +123 -0
- package/src/markdown-view.ts +300 -0
- package/src/png.ts +260 -0
- package/src/tui.ts +77 -41
- package/src/markdown-render.ts +0 -211
package/src/tui.ts
CHANGED
|
@@ -74,7 +74,8 @@ import {
|
|
|
74
74
|
import { saveSession, restoreSession } from "./session";
|
|
75
75
|
import { fetchDeepDive, buildDeepDiveText } from "./deepdive";
|
|
76
76
|
import { buildComparisonTable } from "./compare";
|
|
77
|
-
import {
|
|
77
|
+
import { createMarkdownView } from "./markdown-view";
|
|
78
|
+
import { fetchImageBytes, readmeBaseUrl } from "./image-loader";
|
|
78
79
|
import { fetchTopics } from "./explore";
|
|
79
80
|
import { exportToFile, type ExportFormat } from "./output";
|
|
80
81
|
import {
|
|
@@ -1079,12 +1080,22 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1079
1080
|
paddingLeft: 2,
|
|
1080
1081
|
paddingRight: 2,
|
|
1081
1082
|
});
|
|
1082
|
-
|
|
1083
|
+
// Version header only — the release notes below it are rendered markdown.
|
|
1084
|
+
const updateHeader = new TextRenderable(renderer, {
|
|
1083
1085
|
content: "",
|
|
1084
1086
|
fg: colors.accent,
|
|
1085
1087
|
bg: colors.surfaceDim,
|
|
1086
1088
|
});
|
|
1087
|
-
|
|
1089
|
+
const updateNotes = createMarkdownView({
|
|
1090
|
+
renderer,
|
|
1091
|
+
colors,
|
|
1092
|
+
// Notes ship with the release and are mostly prose/lists; skip the
|
|
1093
|
+
// image pipeline in this narrow panel.
|
|
1094
|
+
images: false,
|
|
1095
|
+
background: colors.surfaceDim,
|
|
1096
|
+
});
|
|
1097
|
+
// ponytail: the notes are markdown now; keep the content a column so
|
|
1098
|
+
// blocks stack and the scroll box can measure them.
|
|
1088
1099
|
const updateScroll = new ScrollBoxRenderable(renderer, {
|
|
1089
1100
|
flexGrow: 1,
|
|
1090
1101
|
backgroundColor: colors.surfaceDim,
|
|
@@ -1187,7 +1198,8 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1187
1198
|
paddingX: 1,
|
|
1188
1199
|
});
|
|
1189
1200
|
updateBox.add(updateScroll);
|
|
1190
|
-
updateScroll.add(
|
|
1201
|
+
updateScroll.add(updateHeader);
|
|
1202
|
+
updateScroll.add(updateNotes.renderable);
|
|
1191
1203
|
updateBox.add(updateOptionsRow);
|
|
1192
1204
|
updateBox.add(updateFooter);
|
|
1193
1205
|
root.add(updateDim);
|
|
@@ -1597,17 +1609,11 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1597
1609
|
if (state.lastInstalledVersion === currentVersion) return;
|
|
1598
1610
|
// We just upgraded — show notes for the new version
|
|
1599
1611
|
const notes = fetchReleaseNotes(currentVersion);
|
|
1600
|
-
// Box is 55% of the terminal; subtract border + padding so wrapped
|
|
1601
|
-
// lines never clip horizontally.
|
|
1602
|
-
const notesWidth = Math.max(
|
|
1603
|
-
30,
|
|
1604
|
-
Math.floor((process.stdout.columns || 80) * 0.55) - 8,
|
|
1605
|
-
);
|
|
1606
|
-
const notesBlock = notes
|
|
1607
|
-
? renderMarkdown(notes, { width: notesWidth })
|
|
1608
|
-
: " No release notes found for this version.";
|
|
1609
1612
|
updateSelectedOption = 0;
|
|
1610
|
-
|
|
1613
|
+
updateHeader.content = ` Updated ghfind ${state.lastInstalledVersion} → ${currentVersion}\n`;
|
|
1614
|
+
updateNotes.setContent(
|
|
1615
|
+
notes ?? "_No release notes found for this version._",
|
|
1616
|
+
);
|
|
1611
1617
|
renderUpdateOptions();
|
|
1612
1618
|
showOverlay("update");
|
|
1613
1619
|
}
|
|
@@ -1625,14 +1631,8 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1625
1631
|
if (latest) {
|
|
1626
1632
|
updateSelectedOption = 0;
|
|
1627
1633
|
const notes = fetchReleaseNotes(latest);
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
Math.floor((process.stdout.columns || 80) * 0.55) - 8,
|
|
1631
|
-
);
|
|
1632
|
-
const notesBlock = notes
|
|
1633
|
-
? renderMarkdown(notes, { width: notesWidth })
|
|
1634
|
-
: ` A new version is available\n\n Current: ${currentVersion}\n Latest: ${latest}\n\n Run "npm install -g ghfind"`;
|
|
1635
|
-
updateText.content = ` A new version is available\n\n Current: ${currentVersion}\n Latest: ${latest}\n\n${notesBlock}`;
|
|
1634
|
+
updateHeader.content = ` A new version is available\n\n Current: ${currentVersion}\n Latest: ${latest}\n`;
|
|
1635
|
+
updateNotes.setContent(notes ?? "Run `npm install -g ghfind` to update.");
|
|
1636
1636
|
renderUpdateOptions();
|
|
1637
1637
|
showOverlay("update");
|
|
1638
1638
|
} else {
|
|
@@ -1852,15 +1852,22 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1852
1852
|
},
|
|
1853
1853
|
},
|
|
1854
1854
|
});
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1855
|
+
// Full markdown rendering (headings, tables, code, inline images).
|
|
1856
|
+
const readmeView = createMarkdownView({
|
|
1857
|
+
renderer,
|
|
1858
|
+
colors,
|
|
1859
|
+
loadImage: (url) => fetchImageBytes(url, { token: githubToken }),
|
|
1860
|
+
getImageWidth: () => Math.max(24, (process.stdout.columns || 80) - 12),
|
|
1859
1861
|
});
|
|
1860
|
-
readmeScroll.add(
|
|
1862
|
+
readmeScroll.add(readmeView.renderable);
|
|
1861
1863
|
readmeBox.add(readmeScroll);
|
|
1864
|
+
|
|
1865
|
+
const readmeFooterText = () =>
|
|
1866
|
+
` ↑↓/jk scroll PgUp/PgDn page i images: ${
|
|
1867
|
+
readmeView.imagesEnabled ? "on" : "off"
|
|
1868
|
+
} Esc/q close`;
|
|
1862
1869
|
const readmeFooter = new TextRenderable(renderer, {
|
|
1863
|
-
content:
|
|
1870
|
+
content: readmeFooterText(),
|
|
1864
1871
|
fg: colors.muted,
|
|
1865
1872
|
bg: colors.bg,
|
|
1866
1873
|
height: 1,
|
|
@@ -1876,33 +1883,36 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1876
1883
|
setStatus("No repo selected");
|
|
1877
1884
|
return;
|
|
1878
1885
|
}
|
|
1879
|
-
|
|
1886
|
+
readmeView.setContent(`Loading README for ${repo.fullName}…`);
|
|
1887
|
+
readmeFooter.content = readmeFooterText();
|
|
1888
|
+
readmeScroll.scrollTop = 0;
|
|
1880
1889
|
showOverlay("readme");
|
|
1881
1890
|
try {
|
|
1882
1891
|
const headers: Record<string, string> = { "User-Agent": "ghfind/1.0" };
|
|
1883
1892
|
if (githubToken) headers.Authorization = `Bearer ${githubToken}`;
|
|
1884
1893
|
let text = "";
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1894
|
+
let sourceUrl = "";
|
|
1895
|
+
for (const [branch, file] of [
|
|
1896
|
+
["main", "README.md"],
|
|
1897
|
+
["master", "README.md"],
|
|
1898
|
+
["main", "README.rst"],
|
|
1899
|
+
] as const) {
|
|
1900
|
+
// The README's own URL doubles as the base for relative image paths.
|
|
1901
|
+
const url = readmeBaseUrl(repo.owner, repo.name, branch, file);
|
|
1902
|
+
const r = await fetch(url, { headers });
|
|
1893
1903
|
if (r.ok) {
|
|
1894
1904
|
text = await r.text();
|
|
1905
|
+
sourceUrl = url;
|
|
1895
1906
|
break;
|
|
1896
1907
|
}
|
|
1897
1908
|
}
|
|
1898
1909
|
if (!text) {
|
|
1899
|
-
|
|
1910
|
+
readmeView.setContent(`_(no README found for ${repo.fullName})_`);
|
|
1900
1911
|
} else {
|
|
1901
|
-
|
|
1902
|
-
readmeText.content = renderMarkdown(text, { width: termW });
|
|
1912
|
+
readmeView.setContent(text, sourceUrl);
|
|
1903
1913
|
}
|
|
1904
1914
|
} catch {
|
|
1905
|
-
|
|
1915
|
+
readmeView.setContent("_Failed to load README._");
|
|
1906
1916
|
}
|
|
1907
1917
|
renderer.requestRender();
|
|
1908
1918
|
}
|
|
@@ -2505,6 +2515,24 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
2505
2515
|
}
|
|
2506
2516
|
});
|
|
2507
2517
|
|
|
2518
|
+
// Pasted content flows through "input"; purge JSON-shaped blobs that
|
|
2519
|
+
// slipped into the query (a search line is never raw JSON: objects/arrays
|
|
2520
|
+
// always arrive as paste payloads from API responses / deep-dive output).
|
|
2521
|
+
searchInput.on("input", () => {
|
|
2522
|
+
const v = searchInput.value;
|
|
2523
|
+
if (v.length > 0) {
|
|
2524
|
+
const t = v.trim();
|
|
2525
|
+
if (
|
|
2526
|
+
(t.startsWith("{") && t.endsWith("}")) ||
|
|
2527
|
+
(t.startsWith("[") && t.endsWith("]"))
|
|
2528
|
+
) {
|
|
2529
|
+
searchInput.value = "";
|
|
2530
|
+
setStatus("Ignored JSON paste — search queries accept plain text");
|
|
2531
|
+
renderer.requestRender();
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2508
2536
|
// Navigate results → update detail pane
|
|
2509
2537
|
resultsSelect.on("selectionChanged", () => {
|
|
2510
2538
|
const opt = resultsSelect.getSelectedOption();
|
|
@@ -2606,6 +2634,14 @@ ${pack.description ?? ""}`;
|
|
|
2606
2634
|
|
|
2607
2635
|
// README viewer: scrollable, Esc/q already handled above
|
|
2608
2636
|
if (currentOverlay === "readme") {
|
|
2637
|
+
if (key.name === "i") {
|
|
2638
|
+
readmeView.setImagesEnabled(!readmeView.imagesEnabled);
|
|
2639
|
+
readmeFooter.content = readmeFooterText();
|
|
2640
|
+
setStatus(
|
|
2641
|
+
readmeView.imagesEnabled ? "README images on" : "README images off",
|
|
2642
|
+
);
|
|
2643
|
+
renderer.requestRender();
|
|
2644
|
+
}
|
|
2609
2645
|
return;
|
|
2610
2646
|
}
|
|
2611
2647
|
|
package/src/markdown-render.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal markdown-to-terminal-text renderer.
|
|
3
|
-
*
|
|
4
|
-
* Renders headings, code blocks, lists, tables, blockquotes, horizontal rules,
|
|
5
|
-
* bold/italic, and links into a plain-text representation suitable for a
|
|
6
|
-
* TextRenderable in the TUI.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
interface RenderOptions {
|
|
10
|
-
/** Terminal width for wrapping and table layout. Default: 80. */
|
|
11
|
-
width?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Render markdown text to terminal-friendly plain text. */
|
|
15
|
-
export function renderMarkdown(md: string, opts: RenderOptions = {}): string {
|
|
16
|
-
const width = opts.width ?? 80;
|
|
17
|
-
const lines = md.split("\n");
|
|
18
|
-
const out: string[] = [];
|
|
19
|
-
let inCodeBlock = false;
|
|
20
|
-
let codeLang = "";
|
|
21
|
-
let codeLines: string[] = [];
|
|
22
|
-
let inTable = false;
|
|
23
|
-
let tableRows: string[][] = [];
|
|
24
|
-
|
|
25
|
-
function flushCode() {
|
|
26
|
-
if (codeLines.length === 0) return;
|
|
27
|
-
const maxLen = Math.max(...codeLines.map((l) => l.length), codeLang.length);
|
|
28
|
-
const barW = Math.min(maxLen + 4, width - 2);
|
|
29
|
-
out.push(`┌${"─".repeat(barW)}┐`);
|
|
30
|
-
if (codeLang) out.push(`│ ${codeLang.padEnd(barW - 1)}│`);
|
|
31
|
-
for (const cl of codeLines) {
|
|
32
|
-
out.push(`│ ${cl.padEnd(barW - 1)}│`);
|
|
33
|
-
}
|
|
34
|
-
out.push(`└${"─".repeat(barW)}┘`);
|
|
35
|
-
out.push("");
|
|
36
|
-
codeLines = [];
|
|
37
|
-
codeLang = "";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function flushTable() {
|
|
41
|
-
if (tableRows.length === 0) return;
|
|
42
|
-
const dataRows = tableRows.filter(
|
|
43
|
-
(r) => !r.every((c) => /^[\s\-:|]+$/.test(c)),
|
|
44
|
-
);
|
|
45
|
-
if (dataRows.length === 0) {
|
|
46
|
-
tableRows = [];
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const colCount = Math.max(...dataRows.map((r) => r.length));
|
|
50
|
-
const colW: number[] = [];
|
|
51
|
-
for (let ci = 0; ci < colCount; ci++) {
|
|
52
|
-
colW.push(Math.max(3, ...dataRows.map((r) => (r[ci] ?? "").length)));
|
|
53
|
-
}
|
|
54
|
-
const totalW = colW.reduce((a, b) => a + b, 0) + colCount * 3 - 1;
|
|
55
|
-
if (totalW > width) {
|
|
56
|
-
for (const row of dataRows) {
|
|
57
|
-
out.push(` ${row.map((c) => c.trim()).join(" │ ")}`);
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
const sep = `┌${colW.map((w) => "─".repeat(w + 2)).join("┬")}┐`;
|
|
61
|
-
const mid = `├${colW.map((w) => "─".repeat(w + 2)).join("┼")}┤`;
|
|
62
|
-
const end = `└${colW.map((w) => "─".repeat(w + 2)).join("┴")}┘`;
|
|
63
|
-
out.push(sep);
|
|
64
|
-
dataRows.forEach((row, i) => {
|
|
65
|
-
const cells = colW.map((w, ci) => (row[ci] ?? "").trim().padEnd(w));
|
|
66
|
-
out.push(`│ ${cells.map((c) => ` ${c} `).join("│")}│`);
|
|
67
|
-
if (i === 0) out.push(mid);
|
|
68
|
-
});
|
|
69
|
-
out.push(end);
|
|
70
|
-
}
|
|
71
|
-
out.push("");
|
|
72
|
-
tableRows = [];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function renderInline(line: string): string {
|
|
76
|
-
// Bold
|
|
77
|
-
line = line.replace(/\*\*(.+?)\*\*/g, "$1");
|
|
78
|
-
// Italic (no asterisks)
|
|
79
|
-
line = line.replace(/\*([^*]+?)\*/g, "$1");
|
|
80
|
-
// Inline code
|
|
81
|
-
line = line.replace(/`([^`]+)`/g, "⟨$1⟩");
|
|
82
|
-
// Links [text](url)
|
|
83
|
-
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
|
|
84
|
-
// Images
|
|
85
|
-
line = line.replace(/!\[([^\]]*)\]\([^)]+\)/g, "🖼 $1");
|
|
86
|
-
return line;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function wrapLine(line: string, maxW: number): string[] {
|
|
90
|
-
if (line.length <= maxW) return [line];
|
|
91
|
-
const parts: string[] = [];
|
|
92
|
-
let remaining = line;
|
|
93
|
-
while (remaining.length > maxW) {
|
|
94
|
-
let breakAt = maxW;
|
|
95
|
-
const spaceIdx = remaining.lastIndexOf(" ", maxW);
|
|
96
|
-
if (spaceIdx > maxW / 2) breakAt = spaceIdx;
|
|
97
|
-
parts.push(remaining.slice(0, breakAt));
|
|
98
|
-
remaining = remaining.slice(breakAt).replace(/^ /, "");
|
|
99
|
-
}
|
|
100
|
-
if (remaining) parts.push(remaining);
|
|
101
|
-
return parts;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
for (let i = 0; i < lines.length; i++) {
|
|
105
|
-
const line = lines[i];
|
|
106
|
-
|
|
107
|
-
// Code block boundaries
|
|
108
|
-
if (/^```/.test(line)) {
|
|
109
|
-
if (inCodeBlock) {
|
|
110
|
-
flushCode();
|
|
111
|
-
inCodeBlock = false;
|
|
112
|
-
} else {
|
|
113
|
-
inCodeBlock = true;
|
|
114
|
-
codeLang = line.slice(3).trim();
|
|
115
|
-
}
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (inCodeBlock) {
|
|
119
|
-
codeLines.push(line);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Table row
|
|
124
|
-
if (line.includes("|")) {
|
|
125
|
-
const cells = line
|
|
126
|
-
.split("|")
|
|
127
|
-
.map((c) => c.trim())
|
|
128
|
-
.filter((_, idx, arr) => idx > 0 && idx < arr.length - 1);
|
|
129
|
-
if (cells.length >= 2) {
|
|
130
|
-
inTable = true;
|
|
131
|
-
tableRows.push(cells);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
} else if (inTable) {
|
|
135
|
-
flushTable();
|
|
136
|
-
inTable = false;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Horizontal rule
|
|
140
|
-
if (/^(\s*[-*_]){3,}\s*$/.test(line)) {
|
|
141
|
-
out.push("─".repeat(Math.min(width, 40)));
|
|
142
|
-
out.push("");
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Headings
|
|
147
|
-
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
148
|
-
if (headingMatch) {
|
|
149
|
-
const level = headingMatch[1].length;
|
|
150
|
-
const text = renderInline(headingMatch[2]);
|
|
151
|
-
const prefix = "█ ".repeat(level);
|
|
152
|
-
out.push("");
|
|
153
|
-
out.push(prefix + text);
|
|
154
|
-
out.push("");
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Blockquote
|
|
159
|
-
const bqMatch = line.match(/^>\s?(.*)/);
|
|
160
|
-
if (bqMatch) {
|
|
161
|
-
const inner = renderInline(bqMatch[1]);
|
|
162
|
-
for (const wl of wrapLine(inner, width - 4)) {
|
|
163
|
-
out.push(`│ ${wl}`);
|
|
164
|
-
}
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Unordered list
|
|
169
|
-
const ulMatch = line.match(/^(\s*)[-*+]\s+(.*)/);
|
|
170
|
-
if (ulMatch) {
|
|
171
|
-
const indent = ulMatch[1].length;
|
|
172
|
-
const depth = Math.floor(indent / 2);
|
|
173
|
-
const prefix = `${" ".repeat(depth)}• `;
|
|
174
|
-
const inner = renderInline(ulMatch[2]);
|
|
175
|
-
for (const wl of wrapLine(inner, width - prefix.length)) {
|
|
176
|
-
out.push(prefix + wl);
|
|
177
|
-
}
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Ordered list
|
|
182
|
-
const olMatch = line.match(/^(\s*)\d+\.\s+(.*)/);
|
|
183
|
-
if (olMatch) {
|
|
184
|
-
const indent = olMatch[1].length;
|
|
185
|
-
const depth = Math.floor(indent / 2);
|
|
186
|
-
const prefix = `${" ".repeat(depth)}◦ `;
|
|
187
|
-
const inner = renderInline(olMatch[2]);
|
|
188
|
-
for (const wl of wrapLine(inner, width - prefix.length)) {
|
|
189
|
-
out.push(prefix + wl);
|
|
190
|
-
}
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Empty line
|
|
195
|
-
if (line.trim() === "") {
|
|
196
|
-
out.push("");
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Paragraph
|
|
201
|
-
const rendered = renderInline(line);
|
|
202
|
-
for (const wl of wrapLine(rendered, width)) {
|
|
203
|
-
out.push(wl);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (inCodeBlock) flushCode();
|
|
208
|
-
if (inTable) flushTable();
|
|
209
|
-
|
|
210
|
-
return out.join("\n");
|
|
211
|
-
}
|