dsh-neotui 0.0.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/LICENSE +21 -0
- package/README.md +133 -0
- package/bin/dsh-tui.js +130 -0
- package/package.json +35 -0
- package/src/api.js +140 -0
- package/src/index.js +54 -0
- package/src/md.js +374 -0
- package/src/panels.js +1464 -0
- package/src/screen.js +215 -0
- package/src/term.js +270 -0
- package/src/text.js +110 -0
- package/src/theme.js +90 -0
- package/src/views.js +2403 -0
- package/src/widgets.js +567 -0
package/src/md.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// md.js — Pragmatic Markdown → styled terminal lines.
|
|
2
|
+
// Segments: { t: string, fg?, bg?, bold, italic, underline, strike, code, link? }
|
|
3
|
+
// Links use OSC 8 (clickable in most terminals, emitted by Screen.render).
|
|
4
|
+
|
|
5
|
+
import { hexRgb, strWidth, truncate } from "./text.js";
|
|
6
|
+
|
|
7
|
+
import { T } from "./theme.js";
|
|
8
|
+
|
|
9
|
+
// Live theme accessor (keys are lowercase; mapped from the active palette).
|
|
10
|
+
export const C = new Proxy({}, {
|
|
11
|
+
get(_c, key) {
|
|
12
|
+
const map = {
|
|
13
|
+
text: "TXT", dim: "DIM", faint: "FAINT", heading: "HEADING", bold: "BOLD",
|
|
14
|
+
code: "CODE", codeBg: "CODEBG", link: "LINK", blockquote: "QUOTE",
|
|
15
|
+
blockquoteFg: "QUOTEFG", tableHead: "TABLEHEAD", tableSep: "TABLESEP",
|
|
16
|
+
listMarker: "ACCENT", hr: "BORDER2", img: "PURPLE",
|
|
17
|
+
keyword: "KEYWORD", string: "STRING", number: "NUMBER",
|
|
18
|
+
};
|
|
19
|
+
return T[map[key] ?? key];
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const SEG = (t, extra = {}) => ({ t, ...extra });
|
|
24
|
+
|
|
25
|
+
// ---- inline parser ----
|
|
26
|
+
function parseInline(text, base = {}) {
|
|
27
|
+
const segs = [];
|
|
28
|
+
const push = (t, extra = {}) => {
|
|
29
|
+
if (!t) return;
|
|
30
|
+
segs.push(SEG(t, { ...base, ...extra }));
|
|
31
|
+
};
|
|
32
|
+
let i = 0;
|
|
33
|
+
let plain = "";
|
|
34
|
+
const flush = () => { if (plain) { push(plain); plain = ""; } };
|
|
35
|
+
while (i < text.length) {
|
|
36
|
+
const ch = text[i];
|
|
37
|
+
if (ch === "`") {
|
|
38
|
+
let j = i + 1;
|
|
39
|
+
while (j < text.length && text[j] !== "`") j++;
|
|
40
|
+
if (j < text.length) {
|
|
41
|
+
flush();
|
|
42
|
+
push(text.slice(i + 1, j), { code: true, fg: C.code });
|
|
43
|
+
i = j + 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
} else if (ch === "*") {
|
|
47
|
+
if (text[i + 1] === "*") {
|
|
48
|
+
let j = text.indexOf("**", i + 2);
|
|
49
|
+
if (j > i) {
|
|
50
|
+
flush();
|
|
51
|
+
push(text.slice(i + 2, j), { bold: true, fg: C.bold });
|
|
52
|
+
i = j + 2;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
let j = text.indexOf("*", i + 1);
|
|
57
|
+
if (j > i) {
|
|
58
|
+
flush();
|
|
59
|
+
push(text.slice(i + 1, j), { italic: true });
|
|
60
|
+
i = j + 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else if (ch === "_" && text[i + 1] === "_") {
|
|
65
|
+
let j = text.indexOf("__", i + 2);
|
|
66
|
+
if (j > i) {
|
|
67
|
+
flush();
|
|
68
|
+
push(text.slice(i + 2, j), { bold: true, fg: C.bold });
|
|
69
|
+
i = j + 2;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
} else if (ch === "~" && text[i + 1] === "~") {
|
|
73
|
+
let j = text.indexOf("~~", i + 2);
|
|
74
|
+
if (j > i) {
|
|
75
|
+
flush();
|
|
76
|
+
push(text.slice(i + 2, j), { strike: true, fg: C.dim });
|
|
77
|
+
i = j + 2;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
} else if (ch === "!") {
|
|
81
|
+
const m = /^!\[([^\]]*)\]\(([^)\s]+)\)/.exec(text.slice(i));
|
|
82
|
+
if (m) {
|
|
83
|
+
flush();
|
|
84
|
+
push(`🖼 ${m[1] || "image"}`, { fg: C.img, link: m[2] });
|
|
85
|
+
i += m[0].length;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
} else if (ch === "[") {
|
|
89
|
+
const m = /^\[([^\]]+)\]\(([^)\s]+)\)/.exec(text.slice(i));
|
|
90
|
+
if (m) {
|
|
91
|
+
flush();
|
|
92
|
+
push(m[1], { fg: C.link, link: m[2] });
|
|
93
|
+
i += m[0].length;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
plain += ch;
|
|
98
|
+
i++;
|
|
99
|
+
}
|
|
100
|
+
flush();
|
|
101
|
+
return segs;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- lightweight syntax highlighting (regex-based, good enough for common langs) ----
|
|
105
|
+
const HL = {
|
|
106
|
+
js: [/\b(const|let|var|function|return|if|else|for|while|async|await|import|export|from|new|class|extends|try|catch|finally|throw|typeof|of|in|do|switch|case|break|default)\b/g, C.keyword],
|
|
107
|
+
ts: [/\b(const|let|var|function|return|if|else|for|while|async|await|import|export|from|new|class|extends|try|catch|finally|throw|typeof|of|in|interface|type|enum|implements|public|private|readonly)\b/g, C.keyword],
|
|
108
|
+
py: [/\b(def|return|if|elif|else|for|while|import|from|class|try|except|finally|with|as|lambda|yield|raise|pass|break|continue|async|await|None|True|False|self)\b/g, C.keyword],
|
|
109
|
+
sh: [/(^|\s)(cd|ls|cat|grep|sed|awk|node|npm|pnpm|git|curl|find|echo|export|mkdir|rm|mv|cp|chmod|sudo|docker|systemctl|python|pip|bash|zsh|tmux|ssh|scp|rsync|head|tail|sort|uniq|wc|diff|make|tar|zip|unzip)(?=\s|$)/g, C.keyword],
|
|
110
|
+
bash: [/(^|\s)(cd|ls|cat|grep|sed|awk|node|npm|pnpm|git|curl|find|echo|export|mkdir|rm|mv|cp|chmod|sudo|docker|systemctl|python|pip|bash|zsh|tmux|ssh|scp|rsync|head|tail|sort|uniq|wc|diff|make|tar|zip|unzip)(?=\s|$)/g, C.keyword],
|
|
111
|
+
json: [/"(\\"|[^"])*"(?=\s*:)/g, C.link],
|
|
112
|
+
yaml: [/^(\s*)([A-Za-z0-9_.-]+)(:)/g, C.link],
|
|
113
|
+
md: [/(^|\s)(#{1,6}\s[^\n]*)/g, C.link],
|
|
114
|
+
sql: [/\b(SELECT|FROM|WHERE|INSERT|INTO|VALUES|UPDATE|SET|DELETE|CREATE|TABLE|DROP|ALTER|JOIN|LEFT|RIGHT|INNER|ON|GROUP|BY|ORDER|LIMIT|AND|OR|NOT|NULL|AS|COUNT|SUM|AVG)\b/g, C.keyword],
|
|
115
|
+
};
|
|
116
|
+
const HL_STRINGS = /"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/g;
|
|
117
|
+
const HL_COMMENTS = /\/\/[^\n]*|#[^\n]*|--[^\n]*/g;
|
|
118
|
+
const HL_NUMBERS = /\b\d+(\.\d+)?\b/g;
|
|
119
|
+
|
|
120
|
+
function highlightLine(line, lang) {
|
|
121
|
+
const segs = [{ t: line, fg: C.text }];
|
|
122
|
+
const rules = HL[lang] ? [HL[lang]] : [];
|
|
123
|
+
for (const [re, color] of rules) {
|
|
124
|
+
const out = [];
|
|
125
|
+
for (const seg of segs) {
|
|
126
|
+
if (seg.fg !== C.text || seg.code) { out.push(seg); continue; }
|
|
127
|
+
let last = 0;
|
|
128
|
+
re.lastIndex = 0;
|
|
129
|
+
for (let m; (m = re.exec(seg.t)) !== null;) {
|
|
130
|
+
if (m.index > last) out.push(SEG(seg.t.slice(last, m.index), { fg: C.text }));
|
|
131
|
+
out.push(SEG(m[0], { fg: color, bold: true }));
|
|
132
|
+
last = m.index + m[0].length;
|
|
133
|
+
if (m[0].length === 0) re.lastIndex++;
|
|
134
|
+
}
|
|
135
|
+
if (last < seg.t.length) out.push(SEG(seg.t.slice(last), { fg: C.text }));
|
|
136
|
+
}
|
|
137
|
+
segs.splice(0, segs.length, ...out);
|
|
138
|
+
}
|
|
139
|
+
const apply = (re, color, style = {}) => {
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const seg of segs) {
|
|
142
|
+
if (seg.fg !== C.text || seg.code) { out.push(seg); continue; }
|
|
143
|
+
let last = 0;
|
|
144
|
+
re.lastIndex = 0;
|
|
145
|
+
for (let m; (m = re.exec(seg.t)) !== null;) {
|
|
146
|
+
if (m.index > last) out.push(SEG(seg.t.slice(last, m.index), { fg: C.text }));
|
|
147
|
+
out.push(SEG(m[0], { fg: color, ...style }));
|
|
148
|
+
last = m.index + m[0].length;
|
|
149
|
+
}
|
|
150
|
+
if (last < seg.t.length) out.push(SEG(seg.t.slice(last), { fg: C.text }));
|
|
151
|
+
}
|
|
152
|
+
segs.splice(0, segs.length, ...out);
|
|
153
|
+
};
|
|
154
|
+
apply(HL_STRINGS, C.string);
|
|
155
|
+
apply(HL_COMMENTS, C.faint, { italic: true });
|
|
156
|
+
apply(HL_NUMBERS, C.number);
|
|
157
|
+
return segs;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---- block renderer ----
|
|
161
|
+
// Returns array of lines; each line = array of segments.
|
|
162
|
+
|
|
163
|
+
export function renderMd(text, width) {
|
|
164
|
+
const lines = [];
|
|
165
|
+
const pushLine = (segs = []) => {
|
|
166
|
+
if (segs.length === 0) segs = [SEG(" ")];
|
|
167
|
+
lines.push(segs);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const src = text.replace(/\r\n/g, "\n").split("\n");
|
|
171
|
+
let i = 0;
|
|
172
|
+
let inCode = null;
|
|
173
|
+
let codeBuf = [];
|
|
174
|
+
let para = [];
|
|
175
|
+
let quote = [];
|
|
176
|
+
let listBuf = [];
|
|
177
|
+
let tableBuf = [];
|
|
178
|
+
|
|
179
|
+
const flushPara = () => {
|
|
180
|
+
if (para.length === 0) return;
|
|
181
|
+
const segs = parseInline(para.join(" "));
|
|
182
|
+
lines.push(...wrapSegs(segs, width));
|
|
183
|
+
para = [];
|
|
184
|
+
};
|
|
185
|
+
const flushQuote = () => {
|
|
186
|
+
if (quote.length === 0) return;
|
|
187
|
+
const segs = parseInline(quote.join(" "));
|
|
188
|
+
const wrapped = wrapSegs(segs, width - 3);
|
|
189
|
+
for (const ln of wrapped) lines.push([SEG("▎", { fg: C.blockquote, bold: true }), SEG(" "), ...ln]);
|
|
190
|
+
quote = [];
|
|
191
|
+
};
|
|
192
|
+
const flushList = () => {
|
|
193
|
+
if (listBuf.length === 0) return;
|
|
194
|
+
for (const item of listBuf) {
|
|
195
|
+
const segs = parseInline(item);
|
|
196
|
+
const wrapped = wrapSegs(segs, width - 3);
|
|
197
|
+
wrapped[0] = [SEG("•", { fg: C.listMarker, bold: true }), SEG(" "), ...wrapped[0]];
|
|
198
|
+
for (let k = 1; k < wrapped.length; k++) wrapped[k] = [SEG(" "), ...wrapped[k]];
|
|
199
|
+
lines.push(...wrapped);
|
|
200
|
+
}
|
|
201
|
+
listBuf = [];
|
|
202
|
+
};
|
|
203
|
+
const flushTable = () => {
|
|
204
|
+
if (tableBuf.length < 2) { tableBuf = []; return; }
|
|
205
|
+
const rows = tableBuf.filter((r) => !/^\s*\|?[\s:|-]+\|?\s*$/.test(r));
|
|
206
|
+
if (rows.length === 0) { tableBuf = []; return; }
|
|
207
|
+
const split = (r) => r.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim());
|
|
208
|
+
const header = split(rows[0]);
|
|
209
|
+
const body = rows.slice(1).map(split);
|
|
210
|
+
const n = Math.max(header.length, ...body.map((r) => r.length));
|
|
211
|
+
const maxw = [];
|
|
212
|
+
for (let c = 0; c < n; c++) {
|
|
213
|
+
let m = strWidth(header[c] ?? "");
|
|
214
|
+
for (const r of body) m = Math.max(m, strWidth(r[c] ?? ""));
|
|
215
|
+
maxw[c] = Math.min(m, Math.max(6, Math.floor(width / n) - 2));
|
|
216
|
+
}
|
|
217
|
+
const renderRow = (cells, headerRow) => {
|
|
218
|
+
const segs = [];
|
|
219
|
+
cells.forEach((cell, c) => {
|
|
220
|
+
const cellText = truncate(cell ?? "", maxw[c]);
|
|
221
|
+
const parts = headerRow
|
|
222
|
+
? parseInline(cellText, { bold: true, fg: C.tableHead })
|
|
223
|
+
: parseInline(cellText);
|
|
224
|
+
segs.push(...parts);
|
|
225
|
+
if (c < n - 1) segs.push(SEG(" │ ", { fg: C.tableSep }));
|
|
226
|
+
});
|
|
227
|
+
lines.push(segs);
|
|
228
|
+
};
|
|
229
|
+
renderRow(header, true);
|
|
230
|
+
lines.push([SEG(header.map((_, c) => "─".repeat(maxw[c]) + (c < n - 1 ? "─┼─" : "")).join(""), { fg: C.tableSep })]);
|
|
231
|
+
for (const r of body) renderRow(r, false);
|
|
232
|
+
tableBuf = [];
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
while (i < src.length) {
|
|
236
|
+
const line = src[i];
|
|
237
|
+
const fence = /^```(\S*)/.exec(line);
|
|
238
|
+
if (fence) {
|
|
239
|
+
flushPara(); flushQuote(); flushList(); flushTable();
|
|
240
|
+
if (inCode === null) {
|
|
241
|
+
inCode = fence[1] || "";
|
|
242
|
+
codeBuf = [];
|
|
243
|
+
} else {
|
|
244
|
+
const lang = inCode || "text";
|
|
245
|
+
const hw = Math.max(2, width - 4);
|
|
246
|
+
const codeLines = codeBuf.length === 0 ? [""] : codeBuf;
|
|
247
|
+
let maxLen = Math.max(...codeLines.map((l) => strWidth(l)), 1);
|
|
248
|
+
const barW = Math.min(hw, maxLen + 2);
|
|
249
|
+
lines.push([SEG("┌" + "─".repeat(Math.max(1, barW - 2)) + "┐", { fg: C.hr }), SEG(" " + lang, { fg: C.dim })]);
|
|
250
|
+
for (const cl of codeLines) {
|
|
251
|
+
const hls = highlightLine(cl, lang);
|
|
252
|
+
lines.push([SEG("│ ", { fg: C.hr }), ...wrapSegs(hls, hw, { pad: true }).map((l) => l).flatMap((l, k) => (k === 0 ? l : [SEG(" ", { fg: C.hr }), ...l])), SEG(" │", { fg: C.hr })]);
|
|
253
|
+
}
|
|
254
|
+
lines.push([SEG("└" + "─".repeat(Math.max(1, barW - 2)) + "┘", { fg: C.hr })]);
|
|
255
|
+
inCode = null;
|
|
256
|
+
}
|
|
257
|
+
i++;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (inCode !== null) {
|
|
261
|
+
codeBuf.push(line);
|
|
262
|
+
i++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (/^\s*$/.test(line)) {
|
|
266
|
+
flushPara(); flushQuote(); flushList(); flushTable();
|
|
267
|
+
i++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
271
|
+
if (heading) {
|
|
272
|
+
flushPara(); flushQuote(); flushList(); flushTable();
|
|
273
|
+
const level = heading[1].length;
|
|
274
|
+
const prefix = "#".repeat(level) + " ";
|
|
275
|
+
const segs = parseInline(heading[2], { bold: true, fg: C.heading });
|
|
276
|
+
lines.push([SEG(prefix, { fg: C.listMarker, bold: true }), ...wrapSegs(segs, width - level - 1).flatMap((l, k) => (k === 0 ? l : [SEG(" ".repeat(level), { fg: C.faint }), ...l]))]);
|
|
277
|
+
i++;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (/^\s*(---+|\*\*\*+|___+)\s*$/.test(line)) {
|
|
281
|
+
flushPara(); flushQuote(); flushList(); flushTable();
|
|
282
|
+
lines.push([SEG("─".repeat(Math.min(width, 40)), { fg: C.hr })]);
|
|
283
|
+
i++;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (/^\s*>/.test(line)) {
|
|
287
|
+
flushPara(); flushList(); flushTable();
|
|
288
|
+
quote.push(line.replace(/^\s*>\s?/, ""));
|
|
289
|
+
i++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (/^\s*[-*+]\s+/.test(line)) {
|
|
293
|
+
flushPara(); flushQuote(); flushTable();
|
|
294
|
+
listBuf.push(line.replace(/^\s*[-*+]\s+/, ""));
|
|
295
|
+
i++;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (/^\s*\d+[.)]\s+/.test(line)) {
|
|
299
|
+
flushPara(); flushQuote(); flushTable();
|
|
300
|
+
listBuf.push(line.replace(/^\s*\d+[.)]\s+/, ""));
|
|
301
|
+
i++;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (line.includes("|")) {
|
|
305
|
+
flushPara(); flushQuote(); flushList();
|
|
306
|
+
tableBuf.push(line);
|
|
307
|
+
i++;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
flushQuote(); flushList(); flushTable();
|
|
311
|
+
para.push(line.trim());
|
|
312
|
+
i++;
|
|
313
|
+
}
|
|
314
|
+
flushPara(); flushQuote(); flushList(); flushTable();
|
|
315
|
+
if (inCode !== null && codeBuf.length) {
|
|
316
|
+
const hw = Math.max(2, width - 4);
|
|
317
|
+
for (const cl of codeBuf) lines.push([SEG("│ ", { fg: C.hr }), ...wrapSegs(highlightLine(cl, inCode || "text"), hw), SEG(" │", { fg: C.hr })]);
|
|
318
|
+
}
|
|
319
|
+
return lines;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Word-wrap styled segments to width. Keeps segment styles. */
|
|
323
|
+
export function wrapSegs(segs, width, { pad: padLines = false } = {}) {
|
|
324
|
+
if (width < 2) width = 2;
|
|
325
|
+
const flat = [];
|
|
326
|
+
for (const seg of segs) {
|
|
327
|
+
for (const word of seg.t.split(/(\s+)/)) {
|
|
328
|
+
if (!word) continue;
|
|
329
|
+
flat.push({ ...seg, t: word });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const lines = [];
|
|
333
|
+
let cur = [];
|
|
334
|
+
let curW = 0;
|
|
335
|
+
for (const seg of flat) {
|
|
336
|
+
let w = strWidth(seg.t);
|
|
337
|
+
if (w > width) {
|
|
338
|
+
// hard-break overlong segment
|
|
339
|
+
let rest = seg.t;
|
|
340
|
+
while (strWidth(rest) > width) {
|
|
341
|
+
let cut = "";
|
|
342
|
+
let cw = 0;
|
|
343
|
+
for (const ch of rest) {
|
|
344
|
+
const c = ch.codePointAt(0);
|
|
345
|
+
const cwc = wcwidthFor(c);
|
|
346
|
+
if (cw + cwc > width) break;
|
|
347
|
+
cut += ch; cw += cwc;
|
|
348
|
+
}
|
|
349
|
+
if (!cut) cut = rest[0];
|
|
350
|
+
if (curW > 0) { lines.push(cur); cur = []; curW = 0; }
|
|
351
|
+
lines.push([{ ...seg, t: cut }]);
|
|
352
|
+
rest = rest.slice(cut.length);
|
|
353
|
+
}
|
|
354
|
+
if (rest) { cur.push({ ...seg, t: rest }); curW = strWidth(rest); }
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (curW > 0 && curW + w > width) {
|
|
358
|
+
lines.push(cur);
|
|
359
|
+
cur = [];
|
|
360
|
+
curW = 0;
|
|
361
|
+
}
|
|
362
|
+
if (curW === 0 && /^\s+$/.test(seg.t)) continue; // drop leading space at line start
|
|
363
|
+
cur.push(seg);
|
|
364
|
+
curW += w;
|
|
365
|
+
}
|
|
366
|
+
if (cur.length) lines.push(cur);
|
|
367
|
+
if (lines.length === 0) lines.push([SEG("")]);
|
|
368
|
+
return lines;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function wcwidthFor(cp) {
|
|
372
|
+
// local import to avoid cycles
|
|
373
|
+
return strWidth(String.fromCodePoint(cp));
|
|
374
|
+
}
|