@nghyane/arcane-tui 0.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/CHANGELOG.md +3 -0
- package/README.md +704 -0
- package/package.json +72 -0
- package/src/autocomplete.ts +772 -0
- package/src/buffer/ansi-parser.ts +349 -0
- package/src/buffer/buffer.ts +120 -0
- package/src/buffer/cell.ts +103 -0
- package/src/buffer/index.ts +16 -0
- package/src/buffer/render.ts +149 -0
- package/src/components/box.ts +144 -0
- package/src/components/cancellable-loader.ts +39 -0
- package/src/components/editor.ts +2289 -0
- package/src/components/image.ts +86 -0
- package/src/components/input.ts +531 -0
- package/src/components/loader.ts +59 -0
- package/src/components/markdown.ts +858 -0
- package/src/components/select-list.ts +198 -0
- package/src/components/settings-list.ts +194 -0
- package/src/components/spacer.ts +28 -0
- package/src/components/tab-bar.ts +142 -0
- package/src/components/text.ts +110 -0
- package/src/components/truncated-text.ts +61 -0
- package/src/editor-component.ts +71 -0
- package/src/fuzzy.ts +143 -0
- package/src/index.ts +69 -0
- package/src/keybindings.ts +197 -0
- package/src/keys.ts +270 -0
- package/src/kill-ring.ts +46 -0
- package/src/mermaid.ts +140 -0
- package/src/stdin-buffer.ts +385 -0
- package/src/symbols.ts +24 -0
- package/src/terminal-capabilities.ts +393 -0
- package/src/terminal.ts +467 -0
- package/src/ttyid.ts +66 -0
- package/src/tui.ts +1134 -0
- package/src/utils.ts +149 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { Buffer } from "./buffer.js";
|
|
2
|
+
import { type Cell, DEFAULT_STYLE, emptyCell, Mod, NO_COLOR, packRgb, palette256ToRgb, type Style } from "./cell.js";
|
|
3
|
+
|
|
4
|
+
const STANDARD_COLORS: [number, number, number][] = [
|
|
5
|
+
[0, 0, 0], // 0 black
|
|
6
|
+
[187, 0, 0], // 1 red
|
|
7
|
+
[0, 187, 0], // 2 green
|
|
8
|
+
[187, 187, 0], // 3 yellow
|
|
9
|
+
[0, 0, 187], // 4 blue
|
|
10
|
+
[187, 0, 187], // 5 magenta
|
|
11
|
+
[0, 187, 187], // 6 cyan
|
|
12
|
+
[187, 187, 187], // 7 white
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const BRIGHT_COLORS: [number, number, number][] = [
|
|
16
|
+
[85, 85, 85], // 8
|
|
17
|
+
[255, 85, 85], // 9
|
|
18
|
+
[85, 255, 85], // 10
|
|
19
|
+
[255, 255, 85], // 11
|
|
20
|
+
[85, 85, 255], // 12
|
|
21
|
+
[255, 85, 255], // 13
|
|
22
|
+
[85, 255, 255], // 14
|
|
23
|
+
[255, 255, 255], // 15
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function standardColor(index: number): number {
|
|
27
|
+
const [r, g, b] = STANDARD_COLORS[index]!;
|
|
28
|
+
return packRgb(r, g, b);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function brightColor(index: number): number {
|
|
32
|
+
const [r, g, b] = BRIGHT_COLORS[index]!;
|
|
33
|
+
return packRgb(r, g, b);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const segmenter = new Intl.Segmenter();
|
|
37
|
+
|
|
38
|
+
function cloneStyle(s: Style): Style {
|
|
39
|
+
return { fg: s.fg, bg: s.bg, mods: s.mods, link: s.link };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseSgr(params: number[], style: Style): void {
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < params.length) {
|
|
45
|
+
const p = params[i]!;
|
|
46
|
+
switch (p) {
|
|
47
|
+
case 0:
|
|
48
|
+
style.fg = NO_COLOR;
|
|
49
|
+
style.bg = NO_COLOR;
|
|
50
|
+
style.mods = 0;
|
|
51
|
+
style.link = "";
|
|
52
|
+
break;
|
|
53
|
+
case 1:
|
|
54
|
+
style.mods |= Mod.Bold;
|
|
55
|
+
break;
|
|
56
|
+
case 2:
|
|
57
|
+
style.mods |= Mod.Dim;
|
|
58
|
+
break;
|
|
59
|
+
case 3:
|
|
60
|
+
style.mods |= Mod.Italic;
|
|
61
|
+
break;
|
|
62
|
+
case 4:
|
|
63
|
+
style.mods |= Mod.Underline;
|
|
64
|
+
break;
|
|
65
|
+
case 5:
|
|
66
|
+
style.mods |= Mod.Blink;
|
|
67
|
+
break;
|
|
68
|
+
case 7:
|
|
69
|
+
style.mods |= Mod.Reverse;
|
|
70
|
+
break;
|
|
71
|
+
case 8:
|
|
72
|
+
style.mods |= Mod.Hidden;
|
|
73
|
+
break;
|
|
74
|
+
case 9:
|
|
75
|
+
style.mods |= Mod.Strikethrough;
|
|
76
|
+
break;
|
|
77
|
+
case 22:
|
|
78
|
+
style.mods &= ~(Mod.Bold | Mod.Dim);
|
|
79
|
+
break;
|
|
80
|
+
case 23:
|
|
81
|
+
style.mods &= ~Mod.Italic;
|
|
82
|
+
break;
|
|
83
|
+
case 24:
|
|
84
|
+
style.mods &= ~Mod.Underline;
|
|
85
|
+
break;
|
|
86
|
+
case 25:
|
|
87
|
+
style.mods &= ~Mod.Blink;
|
|
88
|
+
break;
|
|
89
|
+
case 27:
|
|
90
|
+
style.mods &= ~Mod.Reverse;
|
|
91
|
+
break;
|
|
92
|
+
case 28:
|
|
93
|
+
style.mods &= ~Mod.Hidden;
|
|
94
|
+
break;
|
|
95
|
+
case 29:
|
|
96
|
+
style.mods &= ~Mod.Strikethrough;
|
|
97
|
+
break;
|
|
98
|
+
case 30:
|
|
99
|
+
case 31:
|
|
100
|
+
case 32:
|
|
101
|
+
case 33:
|
|
102
|
+
case 34:
|
|
103
|
+
case 35:
|
|
104
|
+
case 36:
|
|
105
|
+
case 37:
|
|
106
|
+
style.fg = standardColor(p - 30);
|
|
107
|
+
break;
|
|
108
|
+
case 38: {
|
|
109
|
+
const mode = params[i + 1];
|
|
110
|
+
if (mode === 5 && i + 2 < params.length) {
|
|
111
|
+
const idx = params[i + 2]!;
|
|
112
|
+
style.fg = palette256ToRgb(idx);
|
|
113
|
+
i += 2;
|
|
114
|
+
} else if (mode === 2 && i + 4 < params.length) {
|
|
115
|
+
style.fg = packRgb(params[i + 2]!, params[i + 3]!, params[i + 4]!);
|
|
116
|
+
i += 4;
|
|
117
|
+
} else {
|
|
118
|
+
i = params.length; // malformed, skip rest
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case 39:
|
|
123
|
+
style.fg = NO_COLOR;
|
|
124
|
+
break;
|
|
125
|
+
case 40:
|
|
126
|
+
case 41:
|
|
127
|
+
case 42:
|
|
128
|
+
case 43:
|
|
129
|
+
case 44:
|
|
130
|
+
case 45:
|
|
131
|
+
case 46:
|
|
132
|
+
case 47:
|
|
133
|
+
style.bg = standardColor(p - 40);
|
|
134
|
+
break;
|
|
135
|
+
case 48: {
|
|
136
|
+
const mode = params[i + 1];
|
|
137
|
+
if (mode === 5 && i + 2 < params.length) {
|
|
138
|
+
const idx = params[i + 2]!;
|
|
139
|
+
style.bg = palette256ToRgb(idx);
|
|
140
|
+
i += 2;
|
|
141
|
+
} else if (mode === 2 && i + 4 < params.length) {
|
|
142
|
+
style.bg = packRgb(params[i + 2]!, params[i + 3]!, params[i + 4]!);
|
|
143
|
+
i += 4;
|
|
144
|
+
} else {
|
|
145
|
+
i = params.length;
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 49:
|
|
150
|
+
style.bg = NO_COLOR;
|
|
151
|
+
break;
|
|
152
|
+
case 90:
|
|
153
|
+
case 91:
|
|
154
|
+
case 92:
|
|
155
|
+
case 93:
|
|
156
|
+
case 94:
|
|
157
|
+
case 95:
|
|
158
|
+
case 96:
|
|
159
|
+
case 97:
|
|
160
|
+
style.fg = brightColor(p - 90);
|
|
161
|
+
break;
|
|
162
|
+
case 100:
|
|
163
|
+
case 101:
|
|
164
|
+
case 102:
|
|
165
|
+
case 103:
|
|
166
|
+
case 104:
|
|
167
|
+
case 105:
|
|
168
|
+
case 106:
|
|
169
|
+
case 107:
|
|
170
|
+
style.bg = brightColor(p - 100);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
i++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function parseAnsiLine(line: string, width: number, style?: Style): Cell[] {
|
|
178
|
+
const cells: Cell[] = [];
|
|
179
|
+
const cur: Style = style ? cloneStyle(style) : cloneStyle(DEFAULT_STYLE);
|
|
180
|
+
let col = 0;
|
|
181
|
+
let cachedStyle: Style | null = null;
|
|
182
|
+
let i = 0;
|
|
183
|
+
const len = line.length;
|
|
184
|
+
|
|
185
|
+
// Pre-strip ANSI to get visible text, but we need to process inline
|
|
186
|
+
// Process character by character
|
|
187
|
+
while (i < len && col < width) {
|
|
188
|
+
const ch = line.charCodeAt(i);
|
|
189
|
+
|
|
190
|
+
// ESC sequence
|
|
191
|
+
if (ch === 0x1b) {
|
|
192
|
+
const next = line.charCodeAt(i + 1);
|
|
193
|
+
// CSI: \x1b[
|
|
194
|
+
if (next === 0x5b) {
|
|
195
|
+
// '['
|
|
196
|
+
i += 2;
|
|
197
|
+
// Parse parameters
|
|
198
|
+
const params: number[] = [];
|
|
199
|
+
let num = -1;
|
|
200
|
+
while (i < len) {
|
|
201
|
+
const c = line.charCodeAt(i);
|
|
202
|
+
if (c >= 0x30 && c <= 0x39) {
|
|
203
|
+
// digit
|
|
204
|
+
num = (num === -1 ? 0 : num) * 10 + (c - 0x30);
|
|
205
|
+
i++;
|
|
206
|
+
} else if (c === 0x3b) {
|
|
207
|
+
// ';'
|
|
208
|
+
params.push(num === -1 ? 0 : num);
|
|
209
|
+
num = -1;
|
|
210
|
+
i++;
|
|
211
|
+
} else if (c >= 0x40 && c <= 0x7e) {
|
|
212
|
+
// final byte
|
|
213
|
+
params.push(num === -1 ? 0 : num);
|
|
214
|
+
if (c === 0x6d) {
|
|
215
|
+
// 'm' = SGR
|
|
216
|
+
parseSgr(params, cur);
|
|
217
|
+
cachedStyle = null;
|
|
218
|
+
}
|
|
219
|
+
i++;
|
|
220
|
+
break;
|
|
221
|
+
} else {
|
|
222
|
+
// intermediate bytes (0x20-0x2f), skip
|
|
223
|
+
i++;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
// OSC: \x1b]
|
|
229
|
+
if (next === 0x5d) {
|
|
230
|
+
// ']'
|
|
231
|
+
i += 2;
|
|
232
|
+
// Read until BEL (0x07) or ST (\x1b\\)
|
|
233
|
+
let oscContent = "";
|
|
234
|
+
while (i < len) {
|
|
235
|
+
if (line.charCodeAt(i) === 0x07) {
|
|
236
|
+
i++;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
if (line.charCodeAt(i) === 0x1b && line.charCodeAt(i + 1) === 0x5c) {
|
|
240
|
+
i += 2;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
oscContent += line[i];
|
|
244
|
+
i++;
|
|
245
|
+
}
|
|
246
|
+
// Check for OSC 8 hyperlink
|
|
247
|
+
if (oscContent.startsWith("8;")) {
|
|
248
|
+
const semiIdx = oscContent.indexOf(";", 2);
|
|
249
|
+
if (semiIdx !== -1) {
|
|
250
|
+
cur.link = oscContent.slice(semiIdx + 1);
|
|
251
|
+
cachedStyle = null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
// Other ESC sequences - skip until we find a letter or run out
|
|
257
|
+
i += 2;
|
|
258
|
+
while (i < len) {
|
|
259
|
+
const c = line.charCodeAt(i);
|
|
260
|
+
if (c >= 0x40 && c <= 0x7e) {
|
|
261
|
+
i++;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
i++;
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Tab
|
|
270
|
+
if (ch === 0x09) {
|
|
271
|
+
const spaces = Math.min(3, width - col);
|
|
272
|
+
for (let s = 0; s < spaces; s++) {
|
|
273
|
+
if (!cachedStyle) cachedStyle = cloneStyle(cur);
|
|
274
|
+
cells.push({ char: " ", width: 1, style: cachedStyle });
|
|
275
|
+
col++;
|
|
276
|
+
}
|
|
277
|
+
i++;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Control characters
|
|
282
|
+
if (ch < 0x20) {
|
|
283
|
+
i++;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ASCII fast path: printable ASCII (0x20-0x7E) is always width 1, single char
|
|
288
|
+
if (ch >= 0x20 && ch <= 0x7e) {
|
|
289
|
+
if (col >= width) break;
|
|
290
|
+
if (!cachedStyle) cachedStyle = cloneStyle(cur);
|
|
291
|
+
cells.push({ char: line[i]!, width: 1, style: cachedStyle });
|
|
292
|
+
col++;
|
|
293
|
+
i++;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Non-ASCII: use segmenter for grapheme cluster detection
|
|
298
|
+
const remaining = line.slice(i);
|
|
299
|
+
const seg = segmenter.segment(remaining);
|
|
300
|
+
const first = seg[Symbol.iterator]().next();
|
|
301
|
+
if (first.done) {
|
|
302
|
+
i++;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const grapheme = first.value.segment;
|
|
307
|
+
const charWidth = Bun.stringWidth(grapheme);
|
|
308
|
+
|
|
309
|
+
if (charWidth === 0) {
|
|
310
|
+
i += grapheme.length;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (col + charWidth > width) {
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!cachedStyle) cachedStyle = cloneStyle(cur);
|
|
319
|
+
cells.push({ char: grapheme, width: charWidth, style: cachedStyle });
|
|
320
|
+
col++;
|
|
321
|
+
|
|
322
|
+
if (charWidth === 2) {
|
|
323
|
+
if (!cachedStyle) cachedStyle = cloneStyle(cur);
|
|
324
|
+
cells.push({ char: "", width: 0, style: cachedStyle });
|
|
325
|
+
col++;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
i += grapheme.length;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Pad to width
|
|
332
|
+
while (cells.length < width) {
|
|
333
|
+
cells.push(emptyCell());
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return cells;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function linesToBuffer(lines: string[], width: number, height: number): Buffer {
|
|
340
|
+
const buf = new Buffer(width, height);
|
|
341
|
+
const rowCount = Math.min(lines.length, height);
|
|
342
|
+
for (let row = 0; row < rowCount; row++) {
|
|
343
|
+
const cells = parseAnsiLine(lines[row]!, width);
|
|
344
|
+
for (let col = 0; col < width; col++) {
|
|
345
|
+
buf.set(col, row, cells[col]!);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return buf;
|
|
349
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type Cell, cellsEqual, emptyCell, type Style } from "./cell.js";
|
|
2
|
+
|
|
3
|
+
export interface Rect {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CellChange {
|
|
11
|
+
col: number;
|
|
12
|
+
row: number;
|
|
13
|
+
cell: Cell;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
17
|
+
|
|
18
|
+
export class Buffer {
|
|
19
|
+
readonly width: number;
|
|
20
|
+
readonly height: number;
|
|
21
|
+
#cells: Cell[];
|
|
22
|
+
|
|
23
|
+
constructor(width: number, height: number) {
|
|
24
|
+
this.width = width;
|
|
25
|
+
this.height = height;
|
|
26
|
+
this.#cells = new Array(width * height);
|
|
27
|
+
for (let i = 0; i < this.#cells.length; i++) {
|
|
28
|
+
this.#cells[i] = emptyCell();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get(col: number, row: number): Cell {
|
|
33
|
+
if (col < 0 || col >= this.width || row < 0 || row >= this.height) return emptyCell();
|
|
34
|
+
return this.#cells[row * this.width + col];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
set(col: number, row: number, cell: Cell): void {
|
|
38
|
+
if (col < 0 || col >= this.width || row < 0 || row >= this.height) return;
|
|
39
|
+
this.#cells[row * this.width + col] = cell;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setChar(col: number, row: number, char: string, charWidth: number, style: Style): void {
|
|
43
|
+
if (row < 0 || row >= this.height || col < 0 || col >= this.width) return;
|
|
44
|
+
if (charWidth === 2 && col + 1 >= this.width) {
|
|
45
|
+
// Wide char at last column — can't fit placeholder
|
|
46
|
+
this.#cells[row * this.width + col] = { char: " ", width: 1, style: { ...style } };
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.#cells[row * this.width + col] = { char, width: charWidth, style: { ...style } };
|
|
50
|
+
if (charWidth === 2) {
|
|
51
|
+
this.#cells[row * this.width + col + 1] = { char: "", width: 0, style: { ...style } };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
writeString(col: number, row: number, text: string, style: Style): number {
|
|
56
|
+
let c = col;
|
|
57
|
+
for (const { segment } of segmenter.segment(text)) {
|
|
58
|
+
const w = Bun.stringWidth(segment);
|
|
59
|
+
if (w === 0) continue;
|
|
60
|
+
if (c + w > this.width) break;
|
|
61
|
+
this.setChar(c, row, segment, w, style);
|
|
62
|
+
c += w;
|
|
63
|
+
}
|
|
64
|
+
return c - col;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fill(rect: Rect, cell: Cell): void {
|
|
68
|
+
const x0 = Math.max(0, rect.x);
|
|
69
|
+
const y0 = Math.max(0, rect.y);
|
|
70
|
+
const x1 = Math.min(this.width, rect.x + rect.width);
|
|
71
|
+
const y1 = Math.min(this.height, rect.y + rect.height);
|
|
72
|
+
for (let r = y0; r < y1; r++) {
|
|
73
|
+
for (let c = x0; c < x1; c++) {
|
|
74
|
+
this.#cells[r * this.width + c] = { char: cell.char, width: cell.width, style: { ...cell.style } };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clear(): void {
|
|
80
|
+
for (let i = 0; i < this.#cells.length; i++) {
|
|
81
|
+
this.#cells[i] = emptyCell();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
copyFrom(source: Buffer, srcRect: Rect, destCol: number, destRow: number): void {
|
|
86
|
+
const sx0 = Math.max(0, srcRect.x);
|
|
87
|
+
const sy0 = Math.max(0, srcRect.y);
|
|
88
|
+
const sx1 = Math.min(source.width, srcRect.x + srcRect.width);
|
|
89
|
+
const sy1 = Math.min(source.height, srcRect.y + srcRect.height);
|
|
90
|
+
|
|
91
|
+
for (let sr = sy0; sr < sy1; sr++) {
|
|
92
|
+
const dr = destRow + (sr - srcRect.y);
|
|
93
|
+
if (dr < 0 || dr >= this.height) continue;
|
|
94
|
+
for (let sc = sx0; sc < sx1; sc++) {
|
|
95
|
+
const dc = destCol + (sc - srcRect.x);
|
|
96
|
+
if (dc < 0 || dc >= this.width) continue;
|
|
97
|
+
const cell = source.get(sc, sr);
|
|
98
|
+
this.#cells[dr * this.width + dc] = { char: cell.char, width: cell.width, style: { ...cell.style } };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
diff(prev: Buffer): CellChange[] {
|
|
104
|
+
const changes: CellChange[] = [];
|
|
105
|
+
const maxW = Math.max(this.width, prev.width);
|
|
106
|
+
const maxH = Math.max(this.height, prev.height);
|
|
107
|
+
const empty = emptyCell();
|
|
108
|
+
|
|
109
|
+
for (let r = 0; r < maxH; r++) {
|
|
110
|
+
for (let c = 0; c < maxW; c++) {
|
|
111
|
+
const cur = r < this.height && c < this.width ? this.#cells[r * this.width + c] : empty;
|
|
112
|
+
const old = r < prev.height && c < prev.width ? prev.get(c, r) : empty;
|
|
113
|
+
if (!cellsEqual(cur, old)) {
|
|
114
|
+
changes.push({ col: c, row: r, cell: cur });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return changes;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export const enum Mod {
|
|
2
|
+
Bold = 1,
|
|
3
|
+
Dim = 2,
|
|
4
|
+
Italic = 4,
|
|
5
|
+
Underline = 8,
|
|
6
|
+
Blink = 16,
|
|
7
|
+
Reverse = 32,
|
|
8
|
+
Hidden = 64,
|
|
9
|
+
Strikethrough = 128,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const NO_COLOR = 0;
|
|
13
|
+
|
|
14
|
+
export function packRgb(r: number, g: number, b: number): number {
|
|
15
|
+
return (1 << 24) | (r << 16) | (g << 8) | b;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function unpackRgb(packed: number): [r: number, g: number, b: number] {
|
|
19
|
+
return [(packed >> 16) & 0xff, (packed >> 8) & 0xff, packed & 0xff];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hasColor(packed: number): boolean {
|
|
23
|
+
return (packed & (1 << 24)) !== 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Style {
|
|
27
|
+
fg: number;
|
|
28
|
+
bg: number;
|
|
29
|
+
mods: number;
|
|
30
|
+
link: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_STYLE: Style = { fg: NO_COLOR, bg: NO_COLOR, mods: 0, link: "" };
|
|
34
|
+
|
|
35
|
+
export interface Cell {
|
|
36
|
+
char: string;
|
|
37
|
+
width: number;
|
|
38
|
+
style: Style;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function emptyCell(): Cell {
|
|
42
|
+
return { char: " ", width: 1, style: { ...DEFAULT_STYLE } };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function cellsEqual(a: Cell, b: Cell): boolean {
|
|
46
|
+
if (a.char !== b.char || a.width !== b.width) return false;
|
|
47
|
+
if (a.style === b.style) return true;
|
|
48
|
+
return (
|
|
49
|
+
a.style.fg === b.style.fg &&
|
|
50
|
+
a.style.bg === b.style.bg &&
|
|
51
|
+
a.style.mods === b.style.mods &&
|
|
52
|
+
a.style.link === b.style.link
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 256-color palette → packed RGB
|
|
57
|
+
const PALETTE_256: number[] = (() => {
|
|
58
|
+
const p: number[] = new Array(256);
|
|
59
|
+
|
|
60
|
+
// Standard 16 colors (xterm defaults)
|
|
61
|
+
const base16: [number, number, number][] = [
|
|
62
|
+
[0, 0, 0],
|
|
63
|
+
[128, 0, 0],
|
|
64
|
+
[0, 128, 0],
|
|
65
|
+
[128, 128, 0],
|
|
66
|
+
[0, 0, 128],
|
|
67
|
+
[128, 0, 128],
|
|
68
|
+
[0, 128, 128],
|
|
69
|
+
[192, 192, 192],
|
|
70
|
+
[128, 128, 128],
|
|
71
|
+
[255, 0, 0],
|
|
72
|
+
[0, 255, 0],
|
|
73
|
+
[255, 255, 0],
|
|
74
|
+
[0, 0, 255],
|
|
75
|
+
[255, 0, 255],
|
|
76
|
+
[0, 255, 255],
|
|
77
|
+
[255, 255, 255],
|
|
78
|
+
];
|
|
79
|
+
for (let i = 0; i < 16; i++) {
|
|
80
|
+
p[i] = packRgb(base16[i][0], base16[i][1], base16[i][2]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 6x6x6 color cube (indices 16-231)
|
|
84
|
+
const vals = [0, 95, 135, 175, 215, 255];
|
|
85
|
+
for (let i = 0; i < 216; i++) {
|
|
86
|
+
const r = vals[Math.floor(i / 36)];
|
|
87
|
+
const g = vals[Math.floor(i / 6) % 6];
|
|
88
|
+
const b = vals[i % 6];
|
|
89
|
+
p[16 + i] = packRgb(r, g, b);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Grayscale ramp (indices 232-255)
|
|
93
|
+
for (let i = 0; i < 24; i++) {
|
|
94
|
+
const v = 8 + i * 10;
|
|
95
|
+
p[232 + i] = packRgb(v, v, v);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return p;
|
|
99
|
+
})();
|
|
100
|
+
|
|
101
|
+
export function palette256ToRgb(index: number): number {
|
|
102
|
+
return PALETTE_256[index] ?? NO_COLOR;
|
|
103
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { linesToBuffer, parseAnsiLine } from "./ansi-parser.js";
|
|
2
|
+
export { Buffer, type CellChange, type Rect } from "./buffer.js";
|
|
3
|
+
export {
|
|
4
|
+
type Cell,
|
|
5
|
+
cellsEqual,
|
|
6
|
+
DEFAULT_STYLE,
|
|
7
|
+
emptyCell,
|
|
8
|
+
hasColor,
|
|
9
|
+
Mod,
|
|
10
|
+
NO_COLOR,
|
|
11
|
+
packRgb,
|
|
12
|
+
palette256ToRgb,
|
|
13
|
+
type Style,
|
|
14
|
+
unpackRgb,
|
|
15
|
+
} from "./cell.js";
|
|
16
|
+
export { renderBuffer, renderDiff } from "./render.js";
|