@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,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single terminal cell. Mutable for performance — avoids allocations on every frame.
|
|
3
|
+
*/
|
|
4
|
+
export declare class Cell {
|
|
5
|
+
/** The displayed character (single grapheme) */
|
|
6
|
+
char: string;
|
|
7
|
+
/** Foreground color as packed 24-bit RGB, or DEFAULT_COLOR */
|
|
8
|
+
fg: number;
|
|
9
|
+
/** Background color as packed 24-bit RGB, or DEFAULT_COLOR */
|
|
10
|
+
bg: number;
|
|
11
|
+
/** Bitmask of StyleFlags */
|
|
12
|
+
style: number;
|
|
13
|
+
/**
|
|
14
|
+
* Display width of this cell in terminal columns.
|
|
15
|
+
* - 1 for normal characters
|
|
16
|
+
* - 2 for wide characters (CJK, emoji) — only on the "head" cell
|
|
17
|
+
* - 0 for continuation cells (second column of a wide character)
|
|
18
|
+
*/
|
|
19
|
+
width: number;
|
|
20
|
+
constructor(char?: string, fg?: number, bg?: number, style?: number, width?: number);
|
|
21
|
+
static empty(): Cell;
|
|
22
|
+
equals(other: Cell): boolean;
|
|
23
|
+
copyFrom(other: Cell): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DEFAULT_COLOR } from "../common/colorUtils.js";
|
|
2
|
+
import { StyleFlags } from "../common/styleFlags.js";
|
|
3
|
+
/**
|
|
4
|
+
* A single terminal cell. Mutable for performance — avoids allocations on every frame.
|
|
5
|
+
*/
|
|
6
|
+
export class Cell {
|
|
7
|
+
/** The displayed character (single grapheme) */
|
|
8
|
+
char;
|
|
9
|
+
/** Foreground color as packed 24-bit RGB, or DEFAULT_COLOR */
|
|
10
|
+
fg;
|
|
11
|
+
/** Background color as packed 24-bit RGB, or DEFAULT_COLOR */
|
|
12
|
+
bg;
|
|
13
|
+
/** Bitmask of StyleFlags */
|
|
14
|
+
style;
|
|
15
|
+
/**
|
|
16
|
+
* Display width of this cell in terminal columns.
|
|
17
|
+
* - 1 for normal characters
|
|
18
|
+
* - 2 for wide characters (CJK, emoji) — only on the "head" cell
|
|
19
|
+
* - 0 for continuation cells (second column of a wide character)
|
|
20
|
+
*/
|
|
21
|
+
width;
|
|
22
|
+
constructor(char = " ", fg = DEFAULT_COLOR, bg = DEFAULT_COLOR, style = StyleFlags.None, width = 1) {
|
|
23
|
+
this.char = char;
|
|
24
|
+
this.fg = fg;
|
|
25
|
+
this.bg = bg;
|
|
26
|
+
this.style = style;
|
|
27
|
+
this.width = width;
|
|
28
|
+
}
|
|
29
|
+
static empty() {
|
|
30
|
+
return new Cell();
|
|
31
|
+
}
|
|
32
|
+
equals(other) {
|
|
33
|
+
return (this.char === other.char &&
|
|
34
|
+
this.fg === other.fg &&
|
|
35
|
+
this.bg === other.bg &&
|
|
36
|
+
this.style === other.style &&
|
|
37
|
+
this.width === other.width);
|
|
38
|
+
}
|
|
39
|
+
copyFrom(other) {
|
|
40
|
+
this.char = other.char;
|
|
41
|
+
this.fg = other.fg;
|
|
42
|
+
this.bg = other.bg;
|
|
43
|
+
this.style = other.style;
|
|
44
|
+
this.width = other.width;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Rect } from "../common/geometryPromitives.js";
|
|
2
|
+
/**
|
|
3
|
+
* Кап списка damage-областей кадра. Больше — слишком дробный кадр (оверхед
|
|
4
|
+
* прохода на каждый rect), меньше — слияния накрывают чистые виджеты между
|
|
5
|
+
* повреждёнными. 8 хватает на типичный кадр воркбенча: виджет + статусбар +
|
|
6
|
+
* пара лейблов.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MAX_DAMAGE_RECTS = 8;
|
|
9
|
+
/**
|
|
10
|
+
* Накопитель повреждённых областей кадра (в экранных координатах).
|
|
11
|
+
*
|
|
12
|
+
* `add` поглощает вложенные rect'ы (damage-обход идёт pre-order, родитель
|
|
13
|
+
* раньше детей — вложенные добавления схлопываются дёшево). `finalize` даёт
|
|
14
|
+
* итоговые области прохода отрисовки: клип к экрану, слияние пересекающихся,
|
|
15
|
+
* кап {@link MAX_DAMAGE_RECTS} слиянием пары с минимальным приростом площади.
|
|
16
|
+
*
|
|
17
|
+
* Области НЕ дилатируются: расширение «на всякий случай» заставляло бы
|
|
18
|
+
* соседний виджет рендериться на каждый чужой кадр (клип срезает запись, но
|
|
19
|
+
* не работу — редактор сегментирует все видимые строки независимо от ширины
|
|
20
|
+
* клипа). Wide-char пары на кромке области защищает содержимо-зависимый снап
|
|
21
|
+
* Grid.snapToWideChars в renderFrame.
|
|
22
|
+
*
|
|
23
|
+
* Инвариант потребителя (renderFrame): множество очищаемых ячеек обязано
|
|
24
|
+
* совпадать с множеством, по которому идёт прунинг прохода, — поэтому и
|
|
25
|
+
* очистка, и клип каждого прохода берут ровно rect'ы из finalize() (после
|
|
26
|
+
* снапа).
|
|
27
|
+
*/
|
|
28
|
+
export declare class DamageList {
|
|
29
|
+
private rects;
|
|
30
|
+
add(rect: Rect): void;
|
|
31
|
+
get isEmpty(): boolean;
|
|
32
|
+
/** Сырые накопленные rect'ы (до дилатации/слияния) — для тестов и инспектора. */
|
|
33
|
+
snapshot(): readonly Rect[];
|
|
34
|
+
finalize(screenBounds: Rect): Rect[];
|
|
35
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Point, Rect, Size } from "../common/geometryPromitives.js";
|
|
2
|
+
/** Сливает пересекающиеся rect'ы на месте до неподвижной точки. */
|
|
3
|
+
function mergeIntersecting(out) {
|
|
4
|
+
let merged = true;
|
|
5
|
+
while (merged) {
|
|
6
|
+
merged = false;
|
|
7
|
+
outer: for (let i = 0; i < out.length; i++) {
|
|
8
|
+
for (let j = i + 1; j < out.length; j++) {
|
|
9
|
+
if (out[i].intersects(out[j])) {
|
|
10
|
+
out[i] = out[i].union(out[j]);
|
|
11
|
+
out.splice(j, 1);
|
|
12
|
+
merged = true;
|
|
13
|
+
break outer;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Кап списка damage-областей кадра. Больше — слишком дробный кадр (оверхед
|
|
21
|
+
* прохода на каждый rect), меньше — слияния накрывают чистые виджеты между
|
|
22
|
+
* повреждёнными. 8 хватает на типичный кадр воркбенча: виджет + статусбар +
|
|
23
|
+
* пара лейблов.
|
|
24
|
+
*/
|
|
25
|
+
export const MAX_DAMAGE_RECTS = 8;
|
|
26
|
+
/**
|
|
27
|
+
* Накопитель повреждённых областей кадра (в экранных координатах).
|
|
28
|
+
*
|
|
29
|
+
* `add` поглощает вложенные rect'ы (damage-обход идёт pre-order, родитель
|
|
30
|
+
* раньше детей — вложенные добавления схлопываются дёшево). `finalize` даёт
|
|
31
|
+
* итоговые области прохода отрисовки: клип к экрану, слияние пересекающихся,
|
|
32
|
+
* кап {@link MAX_DAMAGE_RECTS} слиянием пары с минимальным приростом площади.
|
|
33
|
+
*
|
|
34
|
+
* Области НЕ дилатируются: расширение «на всякий случай» заставляло бы
|
|
35
|
+
* соседний виджет рендериться на каждый чужой кадр (клип срезает запись, но
|
|
36
|
+
* не работу — редактор сегментирует все видимые строки независимо от ширины
|
|
37
|
+
* клипа). Wide-char пары на кромке области защищает содержимо-зависимый снап
|
|
38
|
+
* Grid.snapToWideChars в renderFrame.
|
|
39
|
+
*
|
|
40
|
+
* Инвариант потребителя (renderFrame): множество очищаемых ячеек обязано
|
|
41
|
+
* совпадать с множеством, по которому идёт прунинг прохода, — поэтому и
|
|
42
|
+
* очистка, и клип каждого прохода берут ровно rect'ы из finalize() (после
|
|
43
|
+
* снапа).
|
|
44
|
+
*/
|
|
45
|
+
export class DamageList {
|
|
46
|
+
rects = [];
|
|
47
|
+
add(rect) {
|
|
48
|
+
if (rect.isEmpty)
|
|
49
|
+
return;
|
|
50
|
+
for (let i = 0; i < this.rects.length; i++) {
|
|
51
|
+
const existing = this.rects[i];
|
|
52
|
+
if (existing.containsRect(rect))
|
|
53
|
+
return;
|
|
54
|
+
if (rect.containsRect(existing)) {
|
|
55
|
+
this.rects[i] = rect;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
this.rects.push(rect);
|
|
60
|
+
}
|
|
61
|
+
get isEmpty() {
|
|
62
|
+
return this.rects.length === 0;
|
|
63
|
+
}
|
|
64
|
+
/** Сырые накопленные rect'ы (до дилатации/слияния) — для тестов и инспектора. */
|
|
65
|
+
snapshot() {
|
|
66
|
+
return this.rects;
|
|
67
|
+
}
|
|
68
|
+
finalize(screenBounds) {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const rect of this.rects) {
|
|
71
|
+
const clipped = rect.intersect(screenBounds);
|
|
72
|
+
if (!clipped.isEmpty)
|
|
73
|
+
out.push(clipped);
|
|
74
|
+
}
|
|
75
|
+
mergeIntersecting(out);
|
|
76
|
+
while (out.length > MAX_DAMAGE_RECTS) {
|
|
77
|
+
// Слить пару с минимальным приростом площади объединения.
|
|
78
|
+
let bestI = 0;
|
|
79
|
+
let bestJ = 1;
|
|
80
|
+
let bestGrowth = Infinity;
|
|
81
|
+
for (let i = 0; i < out.length; i++) {
|
|
82
|
+
for (let j = i + 1; j < out.length; j++) {
|
|
83
|
+
const union = out[i].union(out[j]);
|
|
84
|
+
const growth = union.width * union.height - out[i].width * out[i].height - out[j].width * out[j].height;
|
|
85
|
+
if (growth < bestGrowth) {
|
|
86
|
+
bestGrowth = growth;
|
|
87
|
+
bestI = i;
|
|
88
|
+
bestJ = j;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
out[bestI] = out[bestI].union(out[bestJ]);
|
|
93
|
+
out.splice(bestJ, 1);
|
|
94
|
+
// Объединение могло пересечься с остальными.
|
|
95
|
+
mergeIntersecting(out);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Point, Rect, Size } from "../common/geometryPromitives.js";
|
|
2
|
+
export interface ReadonlyCellData {
|
|
3
|
+
readonly char: string;
|
|
4
|
+
readonly fg: number;
|
|
5
|
+
readonly bg: number;
|
|
6
|
+
readonly style: number;
|
|
7
|
+
readonly width: number;
|
|
8
|
+
}
|
|
9
|
+
export interface CellPatch {
|
|
10
|
+
char?: string;
|
|
11
|
+
fg?: number;
|
|
12
|
+
bg?: number;
|
|
13
|
+
style?: number;
|
|
14
|
+
width?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 2D grid of terminal cells backed by a flat array for cache-friendly access.
|
|
18
|
+
*/
|
|
19
|
+
export declare class Grid {
|
|
20
|
+
readonly size: Size;
|
|
21
|
+
private readonly cells;
|
|
22
|
+
get width(): number;
|
|
23
|
+
get height(): number;
|
|
24
|
+
get cellCount(): number;
|
|
25
|
+
constructor(size: Size);
|
|
26
|
+
getCell(position: Point): ReadonlyCellData;
|
|
27
|
+
getCellAt(x: number, y: number): ReadonlyCellData;
|
|
28
|
+
setCell(position: Point, char: string, fg?: number, bg?: number, style?: number, width?: number): void;
|
|
29
|
+
updateCell(position: Point, patch: CellPatch): void;
|
|
30
|
+
cellEqualsAt(x: number, y: number, other: Grid): boolean;
|
|
31
|
+
copyCellFrom(x: number, y: number, source: Grid): void;
|
|
32
|
+
copyAllCellsFrom(source: Grid): void;
|
|
33
|
+
/**
|
|
34
|
+
* Расширяет rect так, чтобы его вертикальные границы не рассекали wide-char
|
|
35
|
+
* пары ТЕКУЩЕГО содержимого: продолжение (width 0) на левой кромке тянет
|
|
36
|
+
* внутрь голову слева, голова (width 2) на правой — продолжение справа.
|
|
37
|
+
* Иначе частичная очистка damage-области лечила бы голову в пробел ВНЕ
|
|
38
|
+
* области перерисовки — видимая порча соседнего чистого виджета. Одного
|
|
39
|
+
* прохода достаточно: за расширенной кромкой пара уже целиком внутри.
|
|
40
|
+
*/
|
|
41
|
+
snapToWideChars(rect: Rect): Rect;
|
|
42
|
+
/**
|
|
43
|
+
* Сбрасывает ячейки rect'а к значениям clear() — через {@link updateCell},
|
|
44
|
+
* чтобы wide-char пара, пересекающая границу rect'а, чинилась штатной
|
|
45
|
+
* бухгалтерией головы/продолжения, а не оставляла осиротевшие половинки.
|
|
46
|
+
*/
|
|
47
|
+
clearRect(rect: Rect): void;
|
|
48
|
+
fill(char?: string, fg?: number, bg?: number, style?: number): void;
|
|
49
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { DEFAULT_COLOR } from "../common/colorUtils.js";
|
|
2
|
+
import { Point, Rect, Size } from "../common/geometryPromitives.js";
|
|
3
|
+
import { StyleFlags } from "../common/styleFlags.js";
|
|
4
|
+
import { Cell } from "./cell.js";
|
|
5
|
+
/**
|
|
6
|
+
* 2D grid of terminal cells backed by a flat array for cache-friendly access.
|
|
7
|
+
*/
|
|
8
|
+
export class Grid {
|
|
9
|
+
size;
|
|
10
|
+
cells;
|
|
11
|
+
get width() {
|
|
12
|
+
return this.size.width;
|
|
13
|
+
}
|
|
14
|
+
get height() {
|
|
15
|
+
return this.size.height;
|
|
16
|
+
}
|
|
17
|
+
get cellCount() {
|
|
18
|
+
return this.cells.length;
|
|
19
|
+
}
|
|
20
|
+
constructor(size) {
|
|
21
|
+
this.size = size;
|
|
22
|
+
const total = size.width * size.height;
|
|
23
|
+
this.cells = new Array(total);
|
|
24
|
+
for (let i = 0; i < total; i++) {
|
|
25
|
+
this.cells[i] = Cell.empty();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
getCell(position) {
|
|
29
|
+
return this.cells[position.y * this.size.width + position.x];
|
|
30
|
+
}
|
|
31
|
+
getCellAt(x, y) {
|
|
32
|
+
return this.cells[y * this.size.width + x];
|
|
33
|
+
}
|
|
34
|
+
setCell(position, char, fg = DEFAULT_COLOR, bg = DEFAULT_COLOR, style = StyleFlags.None, width = 1) {
|
|
35
|
+
const x = position.x;
|
|
36
|
+
const y = position.y;
|
|
37
|
+
const idx = y * this.size.width + x;
|
|
38
|
+
const cell = this.cells[idx];
|
|
39
|
+
// If overwriting a continuation cell, clear the head cell of that wide char
|
|
40
|
+
if (cell.width === 0 && x > 0) {
|
|
41
|
+
const head = this.cells[idx - 1];
|
|
42
|
+
if (head.width === 2) {
|
|
43
|
+
head.char = " ";
|
|
44
|
+
head.width = 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// If overwriting a head cell of a wide char, clear its continuation
|
|
48
|
+
if (cell.width === 2 && x + 1 < this.size.width) {
|
|
49
|
+
const cont = this.cells[idx + 1];
|
|
50
|
+
/* v8 ignore start -- a wide head is always followed by a width-0 continuation; the else is an unreachable invariant guard */
|
|
51
|
+
if (cont.width === 0) {
|
|
52
|
+
cont.char = " ";
|
|
53
|
+
cont.width = 1;
|
|
54
|
+
}
|
|
55
|
+
/* v8 ignore stop */
|
|
56
|
+
}
|
|
57
|
+
cell.char = char;
|
|
58
|
+
cell.fg = fg;
|
|
59
|
+
cell.bg = bg;
|
|
60
|
+
cell.style = style;
|
|
61
|
+
cell.width = width;
|
|
62
|
+
// For wide chars, set up the continuation cell
|
|
63
|
+
if (width === 2 && x + 1 < this.size.width) {
|
|
64
|
+
const cont = this.cells[idx + 1];
|
|
65
|
+
// If the continuation position holds a wide char head, clear its own continuation
|
|
66
|
+
if (cont.width === 2 && x + 2 < this.size.width) {
|
|
67
|
+
const nextCont = this.cells[idx + 2];
|
|
68
|
+
/* v8 ignore start -- a wide head is always followed by a width-0 continuation; the else is an unreachable invariant guard */
|
|
69
|
+
if (nextCont.width === 0) {
|
|
70
|
+
nextCont.char = " ";
|
|
71
|
+
nextCont.width = 1;
|
|
72
|
+
}
|
|
73
|
+
/* v8 ignore stop */
|
|
74
|
+
}
|
|
75
|
+
cont.char = "";
|
|
76
|
+
cont.fg = fg;
|
|
77
|
+
cont.bg = bg;
|
|
78
|
+
cont.style = style;
|
|
79
|
+
cont.width = 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
updateCell(position, patch) {
|
|
83
|
+
const x = position.x;
|
|
84
|
+
const y = position.y;
|
|
85
|
+
const w = this.size.width;
|
|
86
|
+
const idx = y * w + x;
|
|
87
|
+
const cell = this.cells[idx];
|
|
88
|
+
// Wide-char bookkeeping only when char or width are being set
|
|
89
|
+
if (patch.char !== undefined || patch.width !== undefined) {
|
|
90
|
+
// If overwriting a continuation cell, clear the head cell of that wide char
|
|
91
|
+
if (cell.width === 0 && x > 0) {
|
|
92
|
+
const head = this.cells[idx - 1];
|
|
93
|
+
if (head.width === 2) {
|
|
94
|
+
head.char = " ";
|
|
95
|
+
head.width = 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// If overwriting a head cell of a wide char, clear its continuation
|
|
99
|
+
if (cell.width === 2 && x + 1 < w) {
|
|
100
|
+
const cont = this.cells[idx + 1];
|
|
101
|
+
/* v8 ignore start -- a wide head is always followed by a width-0 continuation; the else is an unreachable invariant guard */
|
|
102
|
+
if (cont.width === 0) {
|
|
103
|
+
cont.char = " ";
|
|
104
|
+
cont.width = 1;
|
|
105
|
+
}
|
|
106
|
+
/* v8 ignore stop */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (patch.char !== undefined)
|
|
110
|
+
cell.char = patch.char;
|
|
111
|
+
if (patch.fg !== undefined)
|
|
112
|
+
cell.fg = patch.fg;
|
|
113
|
+
if (patch.bg !== undefined)
|
|
114
|
+
cell.bg = patch.bg;
|
|
115
|
+
if (patch.style !== undefined)
|
|
116
|
+
cell.style = patch.style;
|
|
117
|
+
if (patch.width !== undefined)
|
|
118
|
+
cell.width = patch.width;
|
|
119
|
+
// For wide chars, set up the continuation cell
|
|
120
|
+
const newWidth = patch.width ?? cell.width;
|
|
121
|
+
if (newWidth === 2 && x + 1 < w) {
|
|
122
|
+
const cont = this.cells[idx + 1];
|
|
123
|
+
// If the continuation position holds a wide char head, clear its own continuation
|
|
124
|
+
if (cont.width === 2 && x + 2 < w) {
|
|
125
|
+
const nextCont = this.cells[idx + 2];
|
|
126
|
+
/* v8 ignore start -- a wide head is always followed by a width-0 continuation; the else is an unreachable invariant guard */
|
|
127
|
+
if (nextCont.width === 0) {
|
|
128
|
+
nextCont.char = " ";
|
|
129
|
+
nextCont.width = 1;
|
|
130
|
+
}
|
|
131
|
+
/* v8 ignore stop */
|
|
132
|
+
}
|
|
133
|
+
cont.char = "";
|
|
134
|
+
cont.width = 0;
|
|
135
|
+
if (patch.fg !== undefined)
|
|
136
|
+
cont.fg = patch.fg;
|
|
137
|
+
if (patch.bg !== undefined)
|
|
138
|
+
cont.bg = patch.bg;
|
|
139
|
+
if (patch.style !== undefined)
|
|
140
|
+
cont.style = patch.style;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
cellEqualsAt(x, y, other) {
|
|
144
|
+
const idx = y * this.size.width + x;
|
|
145
|
+
return this.cells[idx].equals(other.cells[idx]);
|
|
146
|
+
}
|
|
147
|
+
copyCellFrom(x, y, source) {
|
|
148
|
+
const idx = y * this.size.width + x;
|
|
149
|
+
this.cells[idx].copyFrom(source.cells[idx]);
|
|
150
|
+
}
|
|
151
|
+
copyAllCellsFrom(source) {
|
|
152
|
+
for (let i = 0, len = this.cells.length; i < len; i++) {
|
|
153
|
+
this.cells[i].copyFrom(source.cells[i]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Расширяет rect так, чтобы его вертикальные границы не рассекали wide-char
|
|
158
|
+
* пары ТЕКУЩЕГО содержимого: продолжение (width 0) на левой кромке тянет
|
|
159
|
+
* внутрь голову слева, голова (width 2) на правой — продолжение справа.
|
|
160
|
+
* Иначе частичная очистка damage-области лечила бы голову в пробел ВНЕ
|
|
161
|
+
* области перерисовки — видимая порча соседнего чистого виджета. Одного
|
|
162
|
+
* прохода достаточно: за расширенной кромкой пара уже целиком внутри.
|
|
163
|
+
*/
|
|
164
|
+
snapToWideChars(rect) {
|
|
165
|
+
const w = this.size.width;
|
|
166
|
+
const x0 = Math.max(0, rect.x);
|
|
167
|
+
const y0 = Math.max(0, rect.y);
|
|
168
|
+
const x1 = Math.min(w, rect.right);
|
|
169
|
+
const y1 = Math.min(this.size.height, rect.bottom);
|
|
170
|
+
let extendLeft = false;
|
|
171
|
+
let extendRight = false;
|
|
172
|
+
for (let y = y0; y < y1; y++) {
|
|
173
|
+
if (x0 > 0 && this.cells[y * w + x0].width === 0)
|
|
174
|
+
extendLeft = true;
|
|
175
|
+
if (x1 < w && x1 > 0 && this.cells[y * w + x1 - 1].width === 2)
|
|
176
|
+
extendRight = true;
|
|
177
|
+
}
|
|
178
|
+
if (!extendLeft && !extendRight)
|
|
179
|
+
return rect;
|
|
180
|
+
const newX0 = extendLeft ? x0 - 1 : x0;
|
|
181
|
+
const newX1 = extendRight ? x1 + 1 : x1;
|
|
182
|
+
return new Rect(new Point(newX0, rect.y), new Size(newX1 - newX0, rect.height));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Сбрасывает ячейки rect'а к значениям clear() — через {@link updateCell},
|
|
186
|
+
* чтобы wide-char пара, пересекающая границу rect'а, чинилась штатной
|
|
187
|
+
* бухгалтерией головы/продолжения, а не оставляла осиротевшие половинки.
|
|
188
|
+
*/
|
|
189
|
+
clearRect(rect) {
|
|
190
|
+
const x0 = Math.max(0, rect.x);
|
|
191
|
+
const y0 = Math.max(0, rect.y);
|
|
192
|
+
const x1 = Math.min(this.size.width, rect.right);
|
|
193
|
+
const y1 = Math.min(this.size.height, rect.bottom);
|
|
194
|
+
for (let y = y0; y < y1; y++) {
|
|
195
|
+
for (let x = x0; x < x1; x++) {
|
|
196
|
+
this.updateCell(new Point(x, y), {
|
|
197
|
+
char: " ",
|
|
198
|
+
fg: DEFAULT_COLOR,
|
|
199
|
+
bg: DEFAULT_COLOR,
|
|
200
|
+
style: StyleFlags.None,
|
|
201
|
+
width: 1,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
fill(char = " ", fg = DEFAULT_COLOR, bg = DEFAULT_COLOR, style = StyleFlags.None) {
|
|
207
|
+
for (let i = 0, len = this.cells.length; i < len; i++) {
|
|
208
|
+
const cell = this.cells[i];
|
|
209
|
+
cell.char = char;
|
|
210
|
+
cell.fg = fg;
|
|
211
|
+
cell.bg = bg;
|
|
212
|
+
cell.style = style;
|
|
213
|
+
cell.width = 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Point } from "../common/geometryPromitives.js";
|
|
2
|
+
import type { Grid } from "./grid.js";
|
|
3
|
+
/**
|
|
4
|
+
* Plain-data snapshot of a single rendered {@link Grid} cell. Colours are packed
|
|
5
|
+
* 24-bit RGB or the `DEFAULT_COLOR` sentinel; `style` is a `StyleFlags` bitmask;
|
|
6
|
+
* `width` mirrors {@link Grid} cell width (1 normal, 2 wide head, 0 continuation).
|
|
7
|
+
*/
|
|
8
|
+
export interface CellSnapshot {
|
|
9
|
+
char: string;
|
|
10
|
+
fg: number;
|
|
11
|
+
bg: number;
|
|
12
|
+
style: number;
|
|
13
|
+
width: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Serialized terminal frame: everything needed to re-draw the screen elsewhere
|
|
17
|
+
* (an image rasterizer, a diff viewer) without a terminal. Pure data — safe to
|
|
18
|
+
* JSON-encode and send over the inspector protocol. `cells` is row-major with
|
|
19
|
+
* `cols * rows` entries.
|
|
20
|
+
*/
|
|
21
|
+
export interface GridSnapshot {
|
|
22
|
+
cols: number;
|
|
23
|
+
rows: number;
|
|
24
|
+
cursor: {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
} | null;
|
|
28
|
+
cells: CellSnapshot[];
|
|
29
|
+
}
|
|
30
|
+
/** Freeze the current state of `grid` (plus cursor) into a {@link GridSnapshot}. */
|
|
31
|
+
export declare function snapshotGrid(grid: Grid, cursor: Point | null): GridSnapshot;
|
|
32
|
+
/** An empty snapshot of the given size — used before the first frame is rendered. */
|
|
33
|
+
export declare function emptyGridSnapshot(cols: number, rows: number): GridSnapshot;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DEFAULT_COLOR } from "../common/colorUtils.js";
|
|
2
|
+
/** Freeze the current state of `grid` (plus cursor) into a {@link GridSnapshot}. */
|
|
3
|
+
export function snapshotGrid(grid, cursor) {
|
|
4
|
+
const { width: cols, height: rows } = grid;
|
|
5
|
+
const cells = new Array(cols * rows);
|
|
6
|
+
for (let y = 0; y < rows; y++) {
|
|
7
|
+
for (let x = 0; x < cols; x++) {
|
|
8
|
+
const cell = grid.getCellAt(x, y);
|
|
9
|
+
cells[y * cols + x] = {
|
|
10
|
+
char: cell.char,
|
|
11
|
+
fg: cell.fg,
|
|
12
|
+
bg: cell.bg,
|
|
13
|
+
style: cell.style,
|
|
14
|
+
width: cell.width,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
cols,
|
|
20
|
+
rows,
|
|
21
|
+
cursor: cursor === null ? null : { x: cursor.x, y: cursor.y },
|
|
22
|
+
cells,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** An empty snapshot of the given size — used before the first frame is rendered. */
|
|
26
|
+
export function emptyGridSnapshot(cols, rows) {
|
|
27
|
+
const cells = new Array(cols * rows);
|
|
28
|
+
for (let i = 0; i < cells.length; i++) {
|
|
29
|
+
cells[i] = { char: " ", fg: DEFAULT_COLOR, bg: DEFAULT_COLOR, style: 0, width: 1 };
|
|
30
|
+
}
|
|
31
|
+
return { cols, rows, cursor: null, cells };
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { GridSnapshot } from "./gridSnapshot.js";
|
|
2
|
+
/**
|
|
3
|
+
* Options for {@link gridToSvg}. Colours are packed 24-bit RGB. Metrics are in
|
|
4
|
+
* pixels; the cell grid is drawn on a fixed lattice so every run lands exactly on
|
|
5
|
+
* `col * cellWidth` regardless of the viewer's font advance.
|
|
6
|
+
*/
|
|
7
|
+
export interface GridToSvgOptions {
|
|
8
|
+
cellWidth?: number;
|
|
9
|
+
cellHeight?: number;
|
|
10
|
+
fontSize?: number;
|
|
11
|
+
/** Vertical baseline offset inside a cell (0 = top). */
|
|
12
|
+
baseline?: number;
|
|
13
|
+
/** Font family used for glyphs. For PNG rendering pass a concrete family. */
|
|
14
|
+
fontFamily?: string;
|
|
15
|
+
/** Resolves cells whose fg is `DEFAULT_COLOR`. */
|
|
16
|
+
defaultFg?: number;
|
|
17
|
+
/** Resolves cells whose bg is `DEFAULT_COLOR`, and paints the page background. */
|
|
18
|
+
defaultBg?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Render a {@link GridSnapshot} into a standalone, self-contained SVG string.
|
|
22
|
+
*
|
|
23
|
+
* Pure — no fonts, no libraries, no I/O. Background is drawn as merged per-row
|
|
24
|
+
* rects; glyphs as per-run `<text>` with `textLength`/`lengthAdjust` so the run
|
|
25
|
+
* fills exactly its column span (grid stays aligned under any font). `Inverse`
|
|
26
|
+
* swaps fg/bg per cell; wide cells (`width === 2`) span two columns and their
|
|
27
|
+
* continuation (`width === 0`) is skipped.
|
|
28
|
+
*/
|
|
29
|
+
export declare function gridToSvg(snapshot: GridSnapshot, options?: GridToSvgOptions): string;
|