@tuidom/core 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/dist/backend/iTerminalBackend.d.ts +41 -0
- package/dist/backend/iTerminalBackend.js +1 -0
- package/dist/common/colorUtils.d.ts +19 -0
- package/dist/common/colorUtils.js +30 -0
- package/dist/common/displayLine.d.ts +72 -0
- package/dist/common/displayLine.js +190 -0
- package/dist/common/disposable.d.ts +9 -0
- package/dist/common/disposable.js +18 -0
- package/dist/common/geometryPromitives.d.ts +47 -0
- package/dist/common/geometryPromitives.js +115 -0
- package/dist/common/iTerminalSurface.d.ts +68 -0
- package/dist/common/iTerminalSurface.js +8 -0
- package/dist/common/measureTextWidth.d.ts +21 -0
- package/dist/common/measureTextWidth.js +43 -0
- package/dist/common/styleFlags.d.ts +17 -0
- package/dist/common/styleFlags.js +16 -0
- package/dist/common/textTruncation.d.ts +30 -0
- package/dist/common/textTruncation.js +124 -0
- package/dist/common/typingUtils.d.ts +1 -0
- package/dist/common/typingUtils.js +3 -0
- package/dist/common/unicodeWidth.d.ts +18 -0
- package/dist/common/unicodeWidth.js +339 -0
- package/dist/dom/borderStyle.d.ts +31 -0
- package/dist/dom/borderStyle.js +40 -0
- package/dist/dom/compositeElement.d.ts +20 -0
- package/dist/dom/compositeElement.js +44 -0
- package/dist/dom/events/contextMenuEventSource.d.ts +17 -0
- package/dist/dom/events/contextMenuEventSource.js +44 -0
- package/dist/dom/events/focusManager.d.ts +13 -0
- package/dist/dom/events/focusManager.js +56 -0
- package/dist/dom/events/mouseEventDispatcher.d.ts +24 -0
- package/dist/dom/events/mouseEventDispatcher.js +177 -0
- package/dist/dom/events/tuiEventBase.d.ts +25 -0
- package/dist/dom/events/tuiEventBase.js +39 -0
- package/dist/dom/events/tuiFocusEvent.d.ts +6 -0
- package/dist/dom/events/tuiFocusEvent.js +8 -0
- package/dist/dom/events/tuiKeyboardEvent.d.ts +21 -0
- package/dist/dom/events/tuiKeyboardEvent.js +20 -0
- package/dist/dom/events/tuiMouseEvent.d.ts +40 -0
- package/dist/dom/events/tuiMouseEvent.js +32 -0
- package/dist/dom/events/tuiPasteEvent.d.ts +10 -0
- package/dist/dom/events/tuiPasteEvent.js +13 -0
- package/dist/dom/overlayLayer.d.ts +92 -0
- package/dist/dom/overlayLayer.js +343 -0
- package/dist/dom/styles/index.d.ts +4 -0
- package/dist/dom/styles/index.js +2 -0
- package/dist/dom/styles/styleTokens.d.ts +114 -0
- package/dist/dom/styles/styleTokens.js +122 -0
- package/dist/dom/styles/tuiStyle.d.ts +75 -0
- package/dist/dom/styles/tuiStyle.js +121 -0
- package/dist/dom/tuiApplication.d.ts +57 -0
- package/dist/dom/tuiApplication.js +231 -0
- package/dist/dom/tuiElement.d.ts +528 -0
- package/dist/dom/tuiElement.js +1168 -0
- package/dist/dom/tuiSelector.d.ts +9 -0
- package/dist/dom/tuiSelector.js +82 -0
- package/dist/dom/validateTree.d.ts +42 -0
- package/dist/dom/validateTree.js +125 -0
- package/dist/input/convertToken.d.ts +3 -0
- package/dist/input/convertToken.js +114 -0
- package/dist/input/keyEvent.d.ts +46 -0
- package/dist/input/keyEvent.js +26 -0
- package/dist/input/keyInputParser.d.ts +72 -0
- package/dist/input/keyInputParser.js +249 -0
- package/dist/input/mouseTracking.d.ts +18 -0
- package/dist/input/mouseTracking.js +18 -0
- package/dist/input/parseInput.d.ts +13 -0
- package/dist/input/parseInput.js +17 -0
- package/dist/input/rawTerminalToken.d.ts +142 -0
- package/dist/input/rawTerminalToken.js +2 -0
- package/dist/input/serializeKey.d.ts +14 -0
- package/dist/input/serializeKey.js +179 -0
- package/dist/input/serializeMouse.d.ts +26 -0
- package/dist/input/serializeMouse.js +38 -0
- package/dist/input/tokenize.d.ts +59 -0
- package/dist/input/tokenize.js +681 -0
- package/dist/rendering/cell.d.ts +24 -0
- package/dist/rendering/cell.js +46 -0
- package/dist/rendering/damage.d.ts +35 -0
- package/dist/rendering/damage.js +99 -0
- package/dist/rendering/grid.d.ts +49 -0
- package/dist/rendering/grid.js +216 -0
- package/dist/rendering/gridSnapshot.d.ts +33 -0
- package/dist/rendering/gridSnapshot.js +32 -0
- package/dist/rendering/gridToSvg.d.ts +29 -0
- package/dist/rendering/gridToSvg.js +145 -0
- package/dist/rendering/terminalRenderer.d.ts +28 -0
- package/dist/rendering/terminalRenderer.js +161 -0
- package/dist/rendering/terminalScreen.d.ts +25 -0
- package/dist/rendering/terminalScreen.js +50 -0
- package/package.json +29 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { DEFAULT_COLOR, unpackB, unpackG, unpackR } from "../common/colorUtils.js";
|
|
2
|
+
import { StyleFlags } from "../common/styleFlags.js";
|
|
3
|
+
// VS Code Dark+ editor foreground / background.
|
|
4
|
+
const DEFAULT_FG = 0xd4d4d4;
|
|
5
|
+
const DEFAULT_BG = 0x1e1e1e;
|
|
6
|
+
function resolveOptions(options) {
|
|
7
|
+
const cellHeight = options.cellHeight ?? 18;
|
|
8
|
+
return {
|
|
9
|
+
cellWidth: options.cellWidth ?? 9,
|
|
10
|
+
cellHeight,
|
|
11
|
+
fontSize: options.fontSize ?? 15,
|
|
12
|
+
baseline: options.baseline ?? Math.round(cellHeight * 0.76),
|
|
13
|
+
fontFamily: options.fontFamily ?? "monospace",
|
|
14
|
+
defaultFg: options.defaultFg ?? DEFAULT_FG,
|
|
15
|
+
defaultBg: options.defaultBg ?? DEFAULT_BG,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function toHex(color) {
|
|
19
|
+
const r = unpackR(color).toString(16).padStart(2, "0");
|
|
20
|
+
const g = unpackG(color).toString(16).padStart(2, "0");
|
|
21
|
+
const b = unpackB(color).toString(16).padStart(2, "0");
|
|
22
|
+
return `#${r}${g}${b}`;
|
|
23
|
+
}
|
|
24
|
+
function resolveColor(color, fallback) {
|
|
25
|
+
return color === DEFAULT_COLOR ? fallback : color;
|
|
26
|
+
}
|
|
27
|
+
function escapeXml(text) {
|
|
28
|
+
return text.replace(/[&<>]/gu, (ch) => (ch === "&" ? "&" : ch === "<" ? "<" : ">"));
|
|
29
|
+
}
|
|
30
|
+
function styleAttrs(style) {
|
|
31
|
+
const attrs = [];
|
|
32
|
+
if ((style & StyleFlags.Bold) !== 0)
|
|
33
|
+
attrs.push(`font-weight="bold"`);
|
|
34
|
+
if ((style & StyleFlags.Italic) !== 0)
|
|
35
|
+
attrs.push(`font-style="italic"`);
|
|
36
|
+
const decorations = [];
|
|
37
|
+
if ((style & (StyleFlags.Underline | StyleFlags.Undercurl)) !== 0)
|
|
38
|
+
decorations.push("underline");
|
|
39
|
+
if ((style & StyleFlags.Strikethrough) !== 0)
|
|
40
|
+
decorations.push("line-through");
|
|
41
|
+
if (decorations.length > 0)
|
|
42
|
+
attrs.push(`text-decoration="${decorations.join(" ")}"`);
|
|
43
|
+
if ((style & StyleFlags.Dim) !== 0)
|
|
44
|
+
attrs.push(`opacity="0.6"`);
|
|
45
|
+
return attrs.length > 0 ? " " + attrs.join(" ") : "";
|
|
46
|
+
}
|
|
47
|
+
const TEXT_STYLE_MASK = ~StyleFlags.Inverse;
|
|
48
|
+
/**
|
|
49
|
+
* Render a {@link GridSnapshot} into a standalone, self-contained SVG string.
|
|
50
|
+
*
|
|
51
|
+
* Pure — no fonts, no libraries, no I/O. Background is drawn as merged per-row
|
|
52
|
+
* rects; glyphs as per-run `<text>` with `textLength`/`lengthAdjust` so the run
|
|
53
|
+
* fills exactly its column span (grid stays aligned under any font). `Inverse`
|
|
54
|
+
* swaps fg/bg per cell; wide cells (`width === 2`) span two columns and their
|
|
55
|
+
* continuation (`width === 0`) is skipped.
|
|
56
|
+
*/
|
|
57
|
+
export function gridToSvg(snapshot, options = {}) {
|
|
58
|
+
const opts = resolveOptions(options);
|
|
59
|
+
const { cols, rows, cells } = snapshot;
|
|
60
|
+
const width = cols * opts.cellWidth;
|
|
61
|
+
const height = rows * opts.cellHeight;
|
|
62
|
+
const parts = [];
|
|
63
|
+
parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" ` +
|
|
64
|
+
`viewBox="0 0 ${width} ${height}" font-family="${escapeXml(opts.fontFamily)}" ` +
|
|
65
|
+
`font-size="${opts.fontSize}">`);
|
|
66
|
+
parts.push(`<rect width="${width}" height="${height}" fill="${toHex(opts.defaultBg)}"/>`);
|
|
67
|
+
// Effective per-cell colours with Inverse applied and defaults resolved.
|
|
68
|
+
const effFg = new Array(cells.length);
|
|
69
|
+
const effBg = new Array(cells.length);
|
|
70
|
+
for (let i = 0; i < cells.length; i++) {
|
|
71
|
+
const cell = cells[i];
|
|
72
|
+
let fg = resolveColor(cell.fg, opts.defaultFg);
|
|
73
|
+
let bg = resolveColor(cell.bg, opts.defaultBg);
|
|
74
|
+
if ((cell.style & StyleFlags.Inverse) !== 0) {
|
|
75
|
+
const tmp = fg;
|
|
76
|
+
fg = bg;
|
|
77
|
+
bg = tmp;
|
|
78
|
+
}
|
|
79
|
+
effFg[i] = fg;
|
|
80
|
+
effBg[i] = bg;
|
|
81
|
+
}
|
|
82
|
+
// Background: merge horizontal runs of identical bg per row; skip page default.
|
|
83
|
+
for (let y = 0; y < rows; y++) {
|
|
84
|
+
let x = 0;
|
|
85
|
+
while (x < cols) {
|
|
86
|
+
const bg = effBg[y * cols + x];
|
|
87
|
+
let end = x;
|
|
88
|
+
while (end + 1 < cols && effBg[y * cols + end + 1] === bg)
|
|
89
|
+
end++;
|
|
90
|
+
if (bg !== opts.defaultBg) {
|
|
91
|
+
const rx = x * opts.cellWidth;
|
|
92
|
+
const rw = (end - x + 1) * opts.cellWidth;
|
|
93
|
+
parts.push(`<rect x="${rx}" y="${y * opts.cellHeight}" width="${rw}" height="${opts.cellHeight}" fill="${toHex(bg)}"/>`);
|
|
94
|
+
}
|
|
95
|
+
x = end + 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Text: merge contiguous glyph cells sharing fg + text-style into one <text>.
|
|
99
|
+
for (let y = 0; y < rows; y++) {
|
|
100
|
+
let x = 0;
|
|
101
|
+
let runStartCol = -1;
|
|
102
|
+
let runCols = 0;
|
|
103
|
+
let runText = "";
|
|
104
|
+
let runFg = 0;
|
|
105
|
+
let runStyle = 0;
|
|
106
|
+
const flush = () => {
|
|
107
|
+
if (runStartCol >= 0 && runText.length > 0) {
|
|
108
|
+
const tx = runStartCol * opts.cellWidth;
|
|
109
|
+
const ty = y * opts.cellHeight + opts.baseline;
|
|
110
|
+
const tl = runCols * opts.cellWidth;
|
|
111
|
+
parts.push(`<text x="${tx}" y="${ty}" textLength="${tl}" lengthAdjust="spacingAndGlyphs" ` +
|
|
112
|
+
`fill="${toHex(runFg)}"${styleAttrs(runStyle)}>${escapeXml(runText)}</text>`);
|
|
113
|
+
}
|
|
114
|
+
runStartCol = -1;
|
|
115
|
+
runCols = 0;
|
|
116
|
+
runText = "";
|
|
117
|
+
};
|
|
118
|
+
while (x < cols) {
|
|
119
|
+
const cell = cells[y * cols + x];
|
|
120
|
+
const advance = cell.width === 0 ? 1 : cell.width;
|
|
121
|
+
const isGlyph = cell.width !== 0 && cell.char !== " " && cell.char !== "";
|
|
122
|
+
if (!isGlyph) {
|
|
123
|
+
flush();
|
|
124
|
+
x += advance;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const fg = effFg[y * cols + x];
|
|
128
|
+
const style = cell.style & TEXT_STYLE_MASK;
|
|
129
|
+
if (runStartCol >= 0 && (fg !== runFg || style !== runStyle || runStartCol + runCols !== x)) {
|
|
130
|
+
flush();
|
|
131
|
+
}
|
|
132
|
+
if (runStartCol < 0) {
|
|
133
|
+
runStartCol = x;
|
|
134
|
+
runFg = fg;
|
|
135
|
+
runStyle = style;
|
|
136
|
+
}
|
|
137
|
+
runText += cell.char;
|
|
138
|
+
runCols += cell.width;
|
|
139
|
+
x += cell.width;
|
|
140
|
+
}
|
|
141
|
+
flush();
|
|
142
|
+
}
|
|
143
|
+
parts.push(`</svg>`);
|
|
144
|
+
return parts.join("");
|
|
145
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Grid } from "./grid.js";
|
|
2
|
+
/** Minimal writable interface so we can inject a test double instead of real stdout. */
|
|
3
|
+
export interface WritableOutput {
|
|
4
|
+
write(data: string): void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* High-performance terminal renderer.
|
|
8
|
+
*
|
|
9
|
+
* - On `setup()` switches to Alternate Screen Buffer and hides the cursor.
|
|
10
|
+
* - On `destroy()` restores the original screen and shows the cursor.
|
|
11
|
+
* - `render(current, previous)` diffs two grids, emits minimal ANSI escape
|
|
12
|
+
* sequences via an internal state machine, and flushes exactly one
|
|
13
|
+
* `stdout.write()` per frame.
|
|
14
|
+
*/
|
|
15
|
+
export declare class TerminalRenderer {
|
|
16
|
+
private readonly out;
|
|
17
|
+
constructor(out?: WritableOutput);
|
|
18
|
+
setup(): void;
|
|
19
|
+
destroy(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Diff `currentGrid` against `previousGrid`, emit only changed cells,
|
|
22
|
+
* then update `previousGrid` in-place so it mirrors `currentGrid` for the
|
|
23
|
+
* next frame.
|
|
24
|
+
*
|
|
25
|
+
* Exactly one `stdout.write()` at the end.
|
|
26
|
+
*/
|
|
27
|
+
render(currentGrid: Grid, previousGrid: Grid): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { DEFAULT_COLOR, unpackB, unpackG, unpackR } from "../common/colorUtils.js";
|
|
2
|
+
import { StyleFlags } from "../common/styleFlags.js";
|
|
3
|
+
/**
|
|
4
|
+
* High-performance terminal renderer.
|
|
5
|
+
*
|
|
6
|
+
* - On `setup()` switches to Alternate Screen Buffer and hides the cursor.
|
|
7
|
+
* - On `destroy()` restores the original screen and shows the cursor.
|
|
8
|
+
* - `render(current, previous)` diffs two grids, emits minimal ANSI escape
|
|
9
|
+
* sequences via an internal state machine, and flushes exactly one
|
|
10
|
+
* `stdout.write()` per frame.
|
|
11
|
+
*/
|
|
12
|
+
export class TerminalRenderer {
|
|
13
|
+
out;
|
|
14
|
+
constructor(out = process.stdout) {
|
|
15
|
+
this.out = out;
|
|
16
|
+
}
|
|
17
|
+
// ── Lifecycle ────────────────────────────────────────────────
|
|
18
|
+
setup() {
|
|
19
|
+
this.out.write("\x1b[?1049h" + // alternate screen buffer
|
|
20
|
+
"\x1b[?25l");
|
|
21
|
+
}
|
|
22
|
+
destroy() {
|
|
23
|
+
this.out.write("\x1b[?25h" + // show cursor
|
|
24
|
+
"\x1b[?1049l");
|
|
25
|
+
}
|
|
26
|
+
// ── Render ───────────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Diff `currentGrid` against `previousGrid`, emit only changed cells,
|
|
29
|
+
* then update `previousGrid` in-place so it mirrors `currentGrid` for the
|
|
30
|
+
* next frame.
|
|
31
|
+
*
|
|
32
|
+
* Exactly one `stdout.write()` at the end.
|
|
33
|
+
*/
|
|
34
|
+
render(currentGrid, previousGrid) {
|
|
35
|
+
const width = currentGrid.width;
|
|
36
|
+
const height = currentGrid.height;
|
|
37
|
+
let buf = "";
|
|
38
|
+
// State machine: track what the terminal "thinks" is active so we
|
|
39
|
+
// only emit SGR codes when something actually changes.
|
|
40
|
+
let activeFg = -2; // impossible sentinel — forces first emission
|
|
41
|
+
let activeBg = -2;
|
|
42
|
+
let activeStyle = -1; // impossible sentinel
|
|
43
|
+
let cursorX = -1; // current cursor column (0-based, -1 = unknown)
|
|
44
|
+
let cursorY = -1;
|
|
45
|
+
for (let y = 0; y < height; y++) {
|
|
46
|
+
for (let x = 0; x < width; x++) {
|
|
47
|
+
// Skip bottom-right corner: writing here triggers hardware scroll.
|
|
48
|
+
if (x === width - 1 && y === height - 1)
|
|
49
|
+
continue;
|
|
50
|
+
if (currentGrid.cellEqualsAt(x, y, previousGrid))
|
|
51
|
+
continue;
|
|
52
|
+
const cur = currentGrid.getCellAt(x, y);
|
|
53
|
+
// Skip continuation cells (second column of a wide character).
|
|
54
|
+
// The head cell already wrote the character, and the terminal
|
|
55
|
+
// auto-advanced the cursor past the continuation column.
|
|
56
|
+
if (cur.width === 0) {
|
|
57
|
+
previousGrid.copyCellFrom(x, y, currentGrid);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// ── Cursor positioning ──────────────────────────
|
|
61
|
+
// If the cursor is already at (x, y) we can skip the CUP sequence.
|
|
62
|
+
// After writing a character the cursor auto-advances by one column.
|
|
63
|
+
if (cursorY !== y || cursorX !== x) {
|
|
64
|
+
buf += `\x1b[${(y + 1).toString()};${(x + 1).toString()}H`;
|
|
65
|
+
}
|
|
66
|
+
// ── SGR (Select Graphic Rendition) ──────────────
|
|
67
|
+
const needFg = cur.fg;
|
|
68
|
+
const needBg = cur.bg;
|
|
69
|
+
const needStyle = cur.style;
|
|
70
|
+
if (needStyle !== activeStyle) {
|
|
71
|
+
// Style changed — cheapest strategy: full reset + re-apply everything.
|
|
72
|
+
buf += "\x1b[0m";
|
|
73
|
+
// activeFg = DEFAULT_COLOR;
|
|
74
|
+
// activeBg = DEFAULT_COLOR;
|
|
75
|
+
activeStyle = StyleFlags.None;
|
|
76
|
+
if (needStyle !== StyleFlags.None) {
|
|
77
|
+
buf += styleToSgr(needStyle);
|
|
78
|
+
activeStyle = needStyle;
|
|
79
|
+
}
|
|
80
|
+
// After reset we must re-emit colors even if they haven't changed,
|
|
81
|
+
// because the reset wiped them.
|
|
82
|
+
if (needFg !== DEFAULT_COLOR) {
|
|
83
|
+
buf += fgSgr(needFg);
|
|
84
|
+
}
|
|
85
|
+
activeFg = needFg;
|
|
86
|
+
if (needBg !== DEFAULT_COLOR) {
|
|
87
|
+
buf += bgSgr(needBg);
|
|
88
|
+
}
|
|
89
|
+
activeBg = needBg;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Style unchanged — update colors individually.
|
|
93
|
+
if (needFg !== activeFg) {
|
|
94
|
+
buf += needFg === DEFAULT_COLOR ? "\x1b[39m" : fgSgr(needFg);
|
|
95
|
+
activeFg = needFg;
|
|
96
|
+
}
|
|
97
|
+
if (needBg !== activeBg) {
|
|
98
|
+
buf += needBg === DEFAULT_COLOR ? "\x1b[49m" : bgSgr(needBg);
|
|
99
|
+
activeBg = needBg;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ── Character ───────────────────────────────────
|
|
103
|
+
buf += cur.char;
|
|
104
|
+
// Update cursor tracking (character auto-advances).
|
|
105
|
+
// Wide characters (width=2) advance the cursor by 2 columns.
|
|
106
|
+
const charWidth = cur.width;
|
|
107
|
+
if (x + charWidth >= width) {
|
|
108
|
+
// Right margin: terminal behaviour is unpredictable after
|
|
109
|
+
// writing the last column (may or may not auto-wrap).
|
|
110
|
+
// Invalidate cursor so the next cell gets an absolute CUP.
|
|
111
|
+
cursorX = -1;
|
|
112
|
+
cursorY = -1;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
cursorX = x + charWidth;
|
|
116
|
+
cursorY = y;
|
|
117
|
+
}
|
|
118
|
+
// Update previous-frame buffer so the next render sees no diff.
|
|
119
|
+
previousGrid.copyCellFrom(x, y, currentGrid);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Reset SGR at the end of the frame so we don't leave the terminal
|
|
123
|
+
// in a weird state between frames. Only if we actually emitted any cells.
|
|
124
|
+
if (buf.length > 0 &&
|
|
125
|
+
(activeStyle !== StyleFlags.None || activeFg !== DEFAULT_COLOR || activeBg !== DEFAULT_COLOR)) {
|
|
126
|
+
buf += "\x1b[0m";
|
|
127
|
+
}
|
|
128
|
+
if (buf.length > 0) {
|
|
129
|
+
this.out.write(buf);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// ── SGR helpers ─────────────────────────────────────────────────
|
|
134
|
+
function fgSgr(packedRgb) {
|
|
135
|
+
return `\x1b[38;2;${unpackR(packedRgb).toString()};${unpackG(packedRgb).toString()};${unpackB(packedRgb).toString()}m`;
|
|
136
|
+
}
|
|
137
|
+
function bgSgr(packedRgb) {
|
|
138
|
+
return `\x1b[48;2;${unpackR(packedRgb).toString()};${unpackG(packedRgb).toString()};${unpackB(packedRgb).toString()}m`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Convert a StyleFlags bitmask to the corresponding SGR escape sequence(s).
|
|
142
|
+
* Returns a single concatenated string (e.g. `"\x1b[1m\x1b[3m"` for Bold+Italic).
|
|
143
|
+
*/
|
|
144
|
+
function styleToSgr(flags) {
|
|
145
|
+
let s = "";
|
|
146
|
+
if (flags & StyleFlags.Bold)
|
|
147
|
+
s += "\x1b[1m";
|
|
148
|
+
if (flags & StyleFlags.Dim)
|
|
149
|
+
s += "\x1b[2m";
|
|
150
|
+
if (flags & StyleFlags.Italic)
|
|
151
|
+
s += "\x1b[3m";
|
|
152
|
+
if (flags & StyleFlags.Underline)
|
|
153
|
+
s += "\x1b[4m";
|
|
154
|
+
if (flags & StyleFlags.Undercurl)
|
|
155
|
+
s += "\x1b[4:3m";
|
|
156
|
+
if (flags & StyleFlags.Inverse)
|
|
157
|
+
s += "\x1b[7m";
|
|
158
|
+
if (flags & StyleFlags.Strikethrough)
|
|
159
|
+
s += "\x1b[9m";
|
|
160
|
+
return s;
|
|
161
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ITerminalBackend } from "../backend/iTerminalBackend.js";
|
|
2
|
+
import { Point, type Rect, Size } from "../common/geometryPromitives.js";
|
|
3
|
+
import type { CellPatch, ReadonlyCellData } from "./grid.js";
|
|
4
|
+
export declare class TerminalScreen {
|
|
5
|
+
private grid;
|
|
6
|
+
size: Size;
|
|
7
|
+
cursorPosition: Point | null;
|
|
8
|
+
get width(): number;
|
|
9
|
+
get height(): number;
|
|
10
|
+
constructor(size?: Size);
|
|
11
|
+
setCursorPosition(position: Point): void;
|
|
12
|
+
clearCursorPosition(): void;
|
|
13
|
+
setCell(position: Point, cell: CellPatch): void;
|
|
14
|
+
getCell(position: Point): ReadonlyCellData;
|
|
15
|
+
flush(backend: ITerminalBackend): void;
|
|
16
|
+
clear(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Семантика {@link clear} в границах rect'а — очистка damage-области перед
|
|
19
|
+
* частичным проходом отрисовки. Курсор НЕ трогает: им управляет кадр
|
|
20
|
+
* (renderFrame гасит позицию, только если она попала в damage).
|
|
21
|
+
*/
|
|
22
|
+
clearRect(rect: Rect): void;
|
|
23
|
+
/** См. {@link Grid.snapToWideChars} — границы области не рассекают wide-пары. */
|
|
24
|
+
snapToWideChars(rect: Rect): Rect;
|
|
25
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { DEFAULT_COLOR } from "../common/colorUtils.js";
|
|
2
|
+
import { Point, Size } from "../common/geometryPromitives.js";
|
|
3
|
+
import { StyleFlags } from "../common/styleFlags.js";
|
|
4
|
+
import { Grid } from "./grid.js";
|
|
5
|
+
export class TerminalScreen {
|
|
6
|
+
grid;
|
|
7
|
+
size;
|
|
8
|
+
cursorPosition = null;
|
|
9
|
+
get width() {
|
|
10
|
+
return this.size.width;
|
|
11
|
+
}
|
|
12
|
+
get height() {
|
|
13
|
+
return this.size.height;
|
|
14
|
+
}
|
|
15
|
+
constructor(size = new Size(80, 24)) {
|
|
16
|
+
this.size = size;
|
|
17
|
+
this.grid = new Grid(size);
|
|
18
|
+
}
|
|
19
|
+
setCursorPosition(position) {
|
|
20
|
+
this.cursorPosition = position;
|
|
21
|
+
}
|
|
22
|
+
clearCursorPosition() {
|
|
23
|
+
this.cursorPosition = null;
|
|
24
|
+
}
|
|
25
|
+
setCell(position, cell) {
|
|
26
|
+
this.grid.updateCell(position, cell);
|
|
27
|
+
}
|
|
28
|
+
getCell(position) {
|
|
29
|
+
return this.grid.getCell(position);
|
|
30
|
+
}
|
|
31
|
+
flush(backend) {
|
|
32
|
+
backend.renderFrame(this.grid, this.cursorPosition);
|
|
33
|
+
}
|
|
34
|
+
clear() {
|
|
35
|
+
this.grid.fill(" ", DEFAULT_COLOR, DEFAULT_COLOR, StyleFlags.None);
|
|
36
|
+
this.cursorPosition = null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Семантика {@link clear} в границах rect'а — очистка damage-области перед
|
|
40
|
+
* частичным проходом отрисовки. Курсор НЕ трогает: им управляет кадр
|
|
41
|
+
* (renderFrame гасит позицию, только если она попала в damage).
|
|
42
|
+
*/
|
|
43
|
+
clearRect(rect) {
|
|
44
|
+
this.grid.clearRect(rect);
|
|
45
|
+
}
|
|
46
|
+
/** См. {@link Grid.snapToWideChars} — границы области не рассекают wide-пары. */
|
|
47
|
+
snapToWideChars(rect) {
|
|
48
|
+
return this.grid.snapToWideChars(rect);
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tuidom/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Experimental terminal UI DOM — core: element tree, flex layout, grid rendering, input parsing. API is unstable.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/tuidom/tuidom.git",
|
|
10
|
+
"directory": "packages/core"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=24.0.0"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"tui",
|
|
17
|
+
"terminal",
|
|
18
|
+
"ui",
|
|
19
|
+
"dom"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
"./package.json": "./package.json",
|
|
26
|
+
"./*.js": "./dist/*.js",
|
|
27
|
+
"./*": "./dist/*.js"
|
|
28
|
+
}
|
|
29
|
+
}
|