@svelterm/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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/src/components/spinner.d.ts +11 -0
- package/dist/src/components/spinner.js +19 -0
- package/dist/src/components/text-buffer.d.ts +21 -0
- package/dist/src/components/text-buffer.js +87 -0
- package/dist/src/css/animation-runner.d.ts +17 -0
- package/dist/src/css/animation-runner.js +72 -0
- package/dist/src/css/animation.d.ts +5 -0
- package/dist/src/css/animation.js +6 -0
- package/dist/src/css/calc.d.ts +5 -0
- package/dist/src/css/calc.js +130 -0
- package/dist/src/css/color.d.ts +1 -0
- package/dist/src/css/color.js +157 -0
- package/dist/src/css/compute.d.ts +63 -0
- package/dist/src/css/compute.js +606 -0
- package/dist/src/css/defaults.d.ts +8 -0
- package/dist/src/css/defaults.js +44 -0
- package/dist/src/css/incremental.d.ts +9 -0
- package/dist/src/css/incremental.js +46 -0
- package/dist/src/css/index.d.ts +5 -0
- package/dist/src/css/index.js +3 -0
- package/dist/src/css/media.d.ts +11 -0
- package/dist/src/css/media.js +59 -0
- package/dist/src/css/parser.d.ts +20 -0
- package/dist/src/css/parser.js +241 -0
- package/dist/src/css/selector.d.ts +17 -0
- package/dist/src/css/selector.js +272 -0
- package/dist/src/css/specificity.d.ts +7 -0
- package/dist/src/css/specificity.js +89 -0
- package/dist/src/css/values.d.ts +17 -0
- package/dist/src/css/values.js +58 -0
- package/dist/src/css/variables.d.ts +6 -0
- package/dist/src/css/variables.js +42 -0
- package/dist/src/debug/console.d.ts +16 -0
- package/dist/src/debug/console.js +65 -0
- package/dist/src/debug/server.d.ts +22 -0
- package/dist/src/debug/server.js +90 -0
- package/dist/src/headless.d.ts +21 -0
- package/dist/src/headless.js +26 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +485 -0
- package/dist/src/input/dispatch.d.ts +18 -0
- package/dist/src/input/dispatch.js +70 -0
- package/dist/src/input/focus.d.ts +18 -0
- package/dist/src/input/focus.js +81 -0
- package/dist/src/input/hit.d.ts +3 -0
- package/dist/src/input/hit.js +29 -0
- package/dist/src/input/keyboard.d.ts +9 -0
- package/dist/src/input/keyboard.js +100 -0
- package/dist/src/input/mouse.d.ts +7 -0
- package/dist/src/input/mouse.js +35 -0
- package/dist/src/input/scroll.d.ts +2 -0
- package/dist/src/input/scroll.js +24 -0
- package/dist/src/layout/cache.d.ts +4 -0
- package/dist/src/layout/cache.js +8 -0
- package/dist/src/layout/engine.d.ts +9 -0
- package/dist/src/layout/engine.js +455 -0
- package/dist/src/layout/flex.d.ts +4 -0
- package/dist/src/layout/flex.js +30 -0
- package/dist/src/layout/incremental.d.ts +8 -0
- package/dist/src/layout/incremental.js +58 -0
- package/dist/src/layout/size.d.ts +2 -0
- package/dist/src/layout/size.js +25 -0
- package/dist/src/layout/text.d.ts +7 -0
- package/dist/src/layout/text.js +52 -0
- package/dist/src/render/ansi.d.ts +23 -0
- package/dist/src/render/ansi.js +108 -0
- package/dist/src/render/border.d.ts +4 -0
- package/dist/src/render/border.js +60 -0
- package/dist/src/render/buffer.d.ts +23 -0
- package/dist/src/render/buffer.js +70 -0
- package/dist/src/render/context.d.ts +19 -0
- package/dist/src/render/context.js +98 -0
- package/dist/src/render/diff.d.ts +2 -0
- package/dist/src/render/diff.js +53 -0
- package/dist/src/render/incremental-paint.d.ts +10 -0
- package/dist/src/render/incremental-paint.js +94 -0
- package/dist/src/render/paint-text.d.ts +29 -0
- package/dist/src/render/paint-text.js +120 -0
- package/dist/src/render/paint.d.ts +5 -0
- package/dist/src/render/paint.js +220 -0
- package/dist/src/render/queue.d.ts +24 -0
- package/dist/src/render/queue.js +54 -0
- package/dist/src/render/scrollbar.d.ts +3 -0
- package/dist/src/render/scrollbar.js +19 -0
- package/dist/src/render/snapshot.d.ts +18 -0
- package/dist/src/render/snapshot.js +126 -0
- package/dist/src/renderer/default.d.ts +3 -0
- package/dist/src/renderer/default.js +3 -0
- package/dist/src/renderer/index.d.ts +11 -0
- package/dist/src/renderer/index.js +116 -0
- package/dist/src/renderer/node.d.ts +44 -0
- package/dist/src/renderer/node.js +153 -0
- package/dist/src/terminal/screen.d.ts +10 -0
- package/dist/src/terminal/screen.js +31 -0
- package/dist/src/terminal/stdin-router.d.ts +31 -0
- package/dist/src/terminal/stdin-router.js +133 -0
- package/package.json +64 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const ESC = '\x1b';
|
|
2
|
+
const CSI = `${ESC}[`;
|
|
3
|
+
function expandHex(color) {
|
|
4
|
+
if (color.length === 4) {
|
|
5
|
+
return '#' + color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
|
|
6
|
+
}
|
|
7
|
+
return color;
|
|
8
|
+
}
|
|
9
|
+
export function moveTo(col, row) {
|
|
10
|
+
return `${CSI}${row};${col}H`;
|
|
11
|
+
}
|
|
12
|
+
export function clearScreen() {
|
|
13
|
+
return `${CSI}2J`;
|
|
14
|
+
}
|
|
15
|
+
export function hideCursor() {
|
|
16
|
+
return `${CSI}?25l`;
|
|
17
|
+
}
|
|
18
|
+
export function showCursor() {
|
|
19
|
+
return `${CSI}?25h`;
|
|
20
|
+
}
|
|
21
|
+
export function enterAltScreen() {
|
|
22
|
+
return `${CSI}?1049h`;
|
|
23
|
+
}
|
|
24
|
+
export function exitAltScreen() {
|
|
25
|
+
return `${CSI}?1049l`;
|
|
26
|
+
}
|
|
27
|
+
export function resetStyle() {
|
|
28
|
+
return `${CSI}0m`;
|
|
29
|
+
}
|
|
30
|
+
export function bold() {
|
|
31
|
+
return `${CSI}1m`;
|
|
32
|
+
}
|
|
33
|
+
export function dim() {
|
|
34
|
+
return `${CSI}2m`;
|
|
35
|
+
}
|
|
36
|
+
export function italic() {
|
|
37
|
+
return `${CSI}3m`;
|
|
38
|
+
}
|
|
39
|
+
export function underline() {
|
|
40
|
+
return `${CSI}4m`;
|
|
41
|
+
}
|
|
42
|
+
export function strikethrough() {
|
|
43
|
+
return `${CSI}9m`;
|
|
44
|
+
}
|
|
45
|
+
export function fgColor(color) {
|
|
46
|
+
const code = ANSI_FG[color];
|
|
47
|
+
if (code !== undefined)
|
|
48
|
+
return `${CSI}${code}m`;
|
|
49
|
+
if (color.startsWith('#')) {
|
|
50
|
+
const hex = expandHex(color);
|
|
51
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
52
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
53
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
54
|
+
return `${CSI}38;2;${r};${g};${b}m`;
|
|
55
|
+
}
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
export function bgColor(color) {
|
|
59
|
+
const code = ANSI_BG[color];
|
|
60
|
+
if (code !== undefined)
|
|
61
|
+
return `${CSI}${code}m`;
|
|
62
|
+
if (color.startsWith('#')) {
|
|
63
|
+
const hex = expandHex(color);
|
|
64
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
65
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
66
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
67
|
+
return `${CSI}48;2;${r};${g};${b}m`;
|
|
68
|
+
}
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
const ANSI_FG = {
|
|
72
|
+
black: 30, red: 31, green: 32, yellow: 33,
|
|
73
|
+
blue: 34, magenta: 35, cyan: 36, white: 37,
|
|
74
|
+
default: 39,
|
|
75
|
+
};
|
|
76
|
+
export function hyperlinkOpen(url) {
|
|
77
|
+
return `\x1b]8;;${url}\x1b\\`;
|
|
78
|
+
}
|
|
79
|
+
export function hyperlinkClose() {
|
|
80
|
+
return `\x1b]8;;\x1b\\`;
|
|
81
|
+
}
|
|
82
|
+
export function enableMouse() {
|
|
83
|
+
return `${CSI}?1006h${CSI}?1003h`; // enable SGR mode, then any-event tracking
|
|
84
|
+
}
|
|
85
|
+
export function disableMouse() {
|
|
86
|
+
return `${CSI}?1003l${CSI}?1006l`;
|
|
87
|
+
}
|
|
88
|
+
export function setCursorShape(shape) {
|
|
89
|
+
const code = shape === 'block' ? 2 : shape === 'underline' ? 4 : 6;
|
|
90
|
+
return `${CSI}${code} q`;
|
|
91
|
+
}
|
|
92
|
+
export function enableBracketedPaste() {
|
|
93
|
+
return `${CSI}?2004h`;
|
|
94
|
+
}
|
|
95
|
+
export function disableBracketedPaste() {
|
|
96
|
+
return `${CSI}?2004l`;
|
|
97
|
+
}
|
|
98
|
+
export function beginSyncUpdate() {
|
|
99
|
+
return `${CSI}?2026h`;
|
|
100
|
+
}
|
|
101
|
+
export function endSyncUpdate() {
|
|
102
|
+
return `${CSI}?2026l`;
|
|
103
|
+
}
|
|
104
|
+
const ANSI_BG = {
|
|
105
|
+
black: 40, red: 41, green: 42, yellow: 43,
|
|
106
|
+
blue: 44, magenta: 45, cyan: 46, white: 47,
|
|
107
|
+
default: 49,
|
|
108
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const BORDER_SETS = {
|
|
2
|
+
single: { topLeft: '┌', topRight: '┐', bottomLeft: '└', bottomRight: '┘', horizontal: '─', vertical: '│' },
|
|
3
|
+
double: { topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝', horizontal: '═', vertical: '║' },
|
|
4
|
+
rounded: { topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯', horizontal: '─', vertical: '│' },
|
|
5
|
+
heavy: { topLeft: '┏', topRight: '┓', bottomLeft: '┗', bottomRight: '┛', horizontal: '━', vertical: '┃' },
|
|
6
|
+
};
|
|
7
|
+
export function renderBorder(buffer, box, style) {
|
|
8
|
+
if (style.borderStyle === 'none')
|
|
9
|
+
return;
|
|
10
|
+
const chars = BORDER_SETS[style.borderStyle];
|
|
11
|
+
if (!chars)
|
|
12
|
+
return;
|
|
13
|
+
const fg = style.borderColor !== 'default' ? style.borderColor : undefined;
|
|
14
|
+
const { x, y, width, height } = box;
|
|
15
|
+
const top = style.borderTop;
|
|
16
|
+
const right = style.borderRight;
|
|
17
|
+
const bottom = style.borderBottom;
|
|
18
|
+
const left = style.borderLeft;
|
|
19
|
+
// Corners (only where two adjacent sides meet)
|
|
20
|
+
if (top && left)
|
|
21
|
+
buffer.setCell(x, y, { char: chars.topLeft, fg });
|
|
22
|
+
if (top && right)
|
|
23
|
+
buffer.setCell(x + width - 1, y, { char: chars.topRight, fg });
|
|
24
|
+
if (bottom && left)
|
|
25
|
+
buffer.setCell(x, y + height - 1, { char: chars.bottomLeft, fg });
|
|
26
|
+
if (bottom && right)
|
|
27
|
+
buffer.setCell(x + width - 1, y + height - 1, { char: chars.bottomRight, fg });
|
|
28
|
+
// Top edge
|
|
29
|
+
if (top) {
|
|
30
|
+
const startCol = left ? x + 1 : x;
|
|
31
|
+
const endCol = right ? x + width - 1 : x + width;
|
|
32
|
+
for (let col = startCol; col < endCol; col++) {
|
|
33
|
+
buffer.setCell(col, y, { char: chars.horizontal, fg });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Bottom edge
|
|
37
|
+
if (bottom) {
|
|
38
|
+
const startCol = left ? x + 1 : x;
|
|
39
|
+
const endCol = right ? x + width - 1 : x + width;
|
|
40
|
+
for (let col = startCol; col < endCol; col++) {
|
|
41
|
+
buffer.setCell(col, y + height - 1, { char: chars.horizontal, fg });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Left edge
|
|
45
|
+
if (left) {
|
|
46
|
+
const startRow = top ? y + 1 : y;
|
|
47
|
+
const endRow = bottom ? y + height - 1 : y + height;
|
|
48
|
+
for (let row = startRow; row < endRow; row++) {
|
|
49
|
+
buffer.setCell(x, row, { char: chars.vertical, fg });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Right edge
|
|
53
|
+
if (right) {
|
|
54
|
+
const startRow = top ? y + 1 : y;
|
|
55
|
+
const endRow = bottom ? y + height - 1 : y + height;
|
|
56
|
+
for (let row = startRow; row < endRow; row++) {
|
|
57
|
+
buffer.setCell(x + width - 1, row, { char: chars.vertical, fg });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface Cell {
|
|
2
|
+
char: string;
|
|
3
|
+
fg: string;
|
|
4
|
+
bg: string;
|
|
5
|
+
bold: boolean;
|
|
6
|
+
italic: boolean;
|
|
7
|
+
underline: boolean;
|
|
8
|
+
strikethrough: boolean;
|
|
9
|
+
dim: boolean;
|
|
10
|
+
hyperlink?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class CellBuffer {
|
|
13
|
+
readonly width: number;
|
|
14
|
+
readonly height: number;
|
|
15
|
+
private cells;
|
|
16
|
+
constructor(width: number, height: number);
|
|
17
|
+
clear(): void;
|
|
18
|
+
getCell(col: number, row: number): Cell | undefined;
|
|
19
|
+
setCell(col: number, row: number, cell: Partial<Cell>): void;
|
|
20
|
+
writeText(col: number, row: number, text: string, style?: Partial<Cell>): void;
|
|
21
|
+
clone(): CellBuffer;
|
|
22
|
+
}
|
|
23
|
+
export declare function cellsEqual(a: Cell, b: Cell): boolean;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const EMPTY_CELL = {
|
|
2
|
+
char: ' ', fg: 'default', bg: 'default',
|
|
3
|
+
bold: false, italic: false, underline: false,
|
|
4
|
+
strikethrough: false, dim: false,
|
|
5
|
+
};
|
|
6
|
+
export class CellBuffer {
|
|
7
|
+
width;
|
|
8
|
+
height;
|
|
9
|
+
cells;
|
|
10
|
+
constructor(width, height) {
|
|
11
|
+
this.width = width;
|
|
12
|
+
this.height = height;
|
|
13
|
+
this.cells = new Array(width * height);
|
|
14
|
+
this.clear();
|
|
15
|
+
}
|
|
16
|
+
clear() {
|
|
17
|
+
for (let i = 0; i < this.cells.length; i++) {
|
|
18
|
+
this.cells[i] = { ...EMPTY_CELL };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
getCell(col, row) {
|
|
22
|
+
if (col < 0 || col >= this.width || row < 0 || row >= this.height)
|
|
23
|
+
return undefined;
|
|
24
|
+
return this.cells[row * this.width + col];
|
|
25
|
+
}
|
|
26
|
+
setCell(col, row, cell) {
|
|
27
|
+
if (col < 0 || col >= this.width || row < 0 || row >= this.height)
|
|
28
|
+
return;
|
|
29
|
+
const idx = row * this.width + col;
|
|
30
|
+
const existing = this.cells[idx];
|
|
31
|
+
this.cells[idx] = {
|
|
32
|
+
char: cell.char ?? existing.char,
|
|
33
|
+
fg: cell.fg ?? existing.fg,
|
|
34
|
+
bg: cell.bg ?? existing.bg,
|
|
35
|
+
bold: cell.bold ?? existing.bold,
|
|
36
|
+
italic: cell.italic ?? existing.italic,
|
|
37
|
+
underline: cell.underline ?? existing.underline,
|
|
38
|
+
strikethrough: cell.strikethrough ?? existing.strikethrough,
|
|
39
|
+
dim: cell.dim ?? existing.dim,
|
|
40
|
+
hyperlink: cell.hyperlink ?? existing.hyperlink,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
writeText(col, row, text, style) {
|
|
44
|
+
for (let i = 0; i < text.length; i++) {
|
|
45
|
+
this.setCell(col + i, row, {
|
|
46
|
+
char: text[i],
|
|
47
|
+
fg: style?.fg,
|
|
48
|
+
bg: style?.bg,
|
|
49
|
+
bold: style?.bold,
|
|
50
|
+
italic: style?.italic,
|
|
51
|
+
underline: style?.underline,
|
|
52
|
+
strikethrough: style?.strikethrough,
|
|
53
|
+
dim: style?.dim,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
clone() {
|
|
58
|
+
const copy = new CellBuffer(this.width, this.height);
|
|
59
|
+
for (let i = 0; i < this.cells.length; i++) {
|
|
60
|
+
copy.cells[i] = { ...this.cells[i] };
|
|
61
|
+
}
|
|
62
|
+
return copy;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function cellsEqual(a, b) {
|
|
66
|
+
return a.char === b.char && a.fg === b.fg && a.bg === b.bg
|
|
67
|
+
&& a.bold === b.bold && a.italic === b.italic
|
|
68
|
+
&& a.underline === b.underline && a.strikethrough === b.strikethrough
|
|
69
|
+
&& a.dim === b.dim && a.hyperlink === b.hyperlink;
|
|
70
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TermNode } from '../renderer/node.js';
|
|
2
|
+
import { RenderQueue } from './queue.js';
|
|
3
|
+
/**
|
|
4
|
+
* RenderContext tracks mutations and determines the minimum rendering path.
|
|
5
|
+
* Each renderer method calls the appropriate onX method, which enqueues
|
|
6
|
+
* the minimum work needed.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RenderContext {
|
|
9
|
+
readonly queue: RenderQueue;
|
|
10
|
+
onScheduleRender?: () => void;
|
|
11
|
+
onSetText(node: TermNode, newText: string): void;
|
|
12
|
+
onSetAttribute(node: TermNode, key: string, value: string): void;
|
|
13
|
+
onRemoveAttribute(node: TermNode, key: string): void;
|
|
14
|
+
onInsert(parent: TermNode, child: TermNode): void;
|
|
15
|
+
onRemove(child: TermNode, parent: TermNode): void;
|
|
16
|
+
onScroll(node: TermNode): void;
|
|
17
|
+
onResize(): void;
|
|
18
|
+
private invalidateDescendantStyles;
|
|
19
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { RenderQueue } from './queue.js';
|
|
2
|
+
/**
|
|
3
|
+
* RenderContext tracks mutations and determines the minimum rendering path.
|
|
4
|
+
* Each renderer method calls the appropriate onX method, which enqueues
|
|
5
|
+
* the minimum work needed.
|
|
6
|
+
*/
|
|
7
|
+
export class RenderContext {
|
|
8
|
+
queue = new RenderQueue();
|
|
9
|
+
onScheduleRender;
|
|
10
|
+
onSetText(node, newText) {
|
|
11
|
+
const oldText = node.text ?? '';
|
|
12
|
+
node.text = newText;
|
|
13
|
+
if (oldText.length === newText.length) {
|
|
14
|
+
this.queue.enqueuePaintOnly(node);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
this.queue.enqueueLayoutBubble(node);
|
|
18
|
+
}
|
|
19
|
+
this.onScheduleRender?.();
|
|
20
|
+
}
|
|
21
|
+
onSetAttribute(node, key, value) {
|
|
22
|
+
if (key === 'class') {
|
|
23
|
+
if (node.cache.classAttr === value)
|
|
24
|
+
return; // no change
|
|
25
|
+
node.cache.classAttr = value;
|
|
26
|
+
node.invalidateStyle();
|
|
27
|
+
this.queue.enqueueStyleResolve(node);
|
|
28
|
+
// Also invalidate descendants — descendant selectors may change
|
|
29
|
+
this.invalidateDescendantStyles(node);
|
|
30
|
+
}
|
|
31
|
+
else if (key === 'id' || key === 'data-focused' || key === 'data-hovered') {
|
|
32
|
+
node.invalidateStyle();
|
|
33
|
+
this.queue.enqueueStyleResolve(node);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.queue.enqueuePaintOnly(node);
|
|
37
|
+
}
|
|
38
|
+
node.attributes.set(key, value);
|
|
39
|
+
this.onScheduleRender?.();
|
|
40
|
+
}
|
|
41
|
+
onRemoveAttribute(node, key) {
|
|
42
|
+
node.attributes.delete(key);
|
|
43
|
+
if (key === 'class' || key === 'id' || key === 'data-focused' || key === 'data-hovered') {
|
|
44
|
+
node.cache.classAttr = '';
|
|
45
|
+
node.invalidateStyle();
|
|
46
|
+
this.queue.enqueueStyleResolve(node);
|
|
47
|
+
this.invalidateDescendantStyles(node);
|
|
48
|
+
}
|
|
49
|
+
this.onScheduleRender?.();
|
|
50
|
+
}
|
|
51
|
+
onInsert(parent, child) {
|
|
52
|
+
// New node needs full computation
|
|
53
|
+
child.invalidateAll();
|
|
54
|
+
this.queue.enqueueStyleResolve(child);
|
|
55
|
+
// Parent needs re-layout
|
|
56
|
+
if (hasFixedDimensions(parent)) {
|
|
57
|
+
this.queue.enqueueLayoutSubtree(parent);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.queue.enqueueLayoutBubble(parent);
|
|
61
|
+
}
|
|
62
|
+
this.onScheduleRender?.();
|
|
63
|
+
}
|
|
64
|
+
onRemove(child, parent) {
|
|
65
|
+
if (hasFixedDimensions(parent)) {
|
|
66
|
+
this.queue.enqueueLayoutSubtree(parent);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
this.queue.enqueueLayoutBubble(parent);
|
|
70
|
+
}
|
|
71
|
+
// Paint the area where the removed node was
|
|
72
|
+
this.queue.enqueuePaintOnly(parent);
|
|
73
|
+
this.onScheduleRender?.();
|
|
74
|
+
}
|
|
75
|
+
onScroll(node) {
|
|
76
|
+
this.queue.setFullRecompute();
|
|
77
|
+
this.onScheduleRender?.();
|
|
78
|
+
}
|
|
79
|
+
onResize() {
|
|
80
|
+
this.queue.setFullRecompute();
|
|
81
|
+
this.onScheduleRender?.();
|
|
82
|
+
}
|
|
83
|
+
invalidateDescendantStyles(node) {
|
|
84
|
+
for (const child of node.children) {
|
|
85
|
+
if (child.nodeType === 'element') {
|
|
86
|
+
child.invalidateStyle();
|
|
87
|
+
this.queue.enqueueStyleResolve(child);
|
|
88
|
+
this.invalidateDescendantStyles(child);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function hasFixedDimensions(node) {
|
|
94
|
+
const style = node.cache.resolvedStyle;
|
|
95
|
+
if (!style)
|
|
96
|
+
return false;
|
|
97
|
+
return style.width !== null && style.height !== null;
|
|
98
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { cellsEqual } from './buffer.js';
|
|
2
|
+
import * as ansi from './ansi.js';
|
|
3
|
+
export function diffBuffers(prev, next) {
|
|
4
|
+
const parts = [];
|
|
5
|
+
let lastStyle = null;
|
|
6
|
+
let currentHyperlink = undefined;
|
|
7
|
+
for (let row = 0; row < next.height; row++) {
|
|
8
|
+
for (let col = 0; col < next.width; col++) {
|
|
9
|
+
const cell = next.getCell(col, row);
|
|
10
|
+
const prevCell = prev?.getCell(col, row);
|
|
11
|
+
if (prevCell && cellsEqual(prevCell, cell))
|
|
12
|
+
continue;
|
|
13
|
+
parts.push(ansi.moveTo(col + 1, row + 1));
|
|
14
|
+
const styleCode = buildStyleCode(cell);
|
|
15
|
+
if (styleCode !== lastStyle) {
|
|
16
|
+
parts.push(ansi.resetStyle());
|
|
17
|
+
parts.push(styleCode);
|
|
18
|
+
lastStyle = styleCode;
|
|
19
|
+
}
|
|
20
|
+
if (cell.hyperlink !== currentHyperlink) {
|
|
21
|
+
if (currentHyperlink)
|
|
22
|
+
parts.push(ansi.hyperlinkClose());
|
|
23
|
+
if (cell.hyperlink)
|
|
24
|
+
parts.push(ansi.hyperlinkOpen(cell.hyperlink));
|
|
25
|
+
currentHyperlink = cell.hyperlink;
|
|
26
|
+
}
|
|
27
|
+
parts.push(cell.char);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (currentHyperlink)
|
|
31
|
+
parts.push(ansi.hyperlinkClose());
|
|
32
|
+
if (parts.length > 0)
|
|
33
|
+
parts.push(ansi.resetStyle());
|
|
34
|
+
return parts.join('');
|
|
35
|
+
}
|
|
36
|
+
function buildStyleCode(cell) {
|
|
37
|
+
const parts = [];
|
|
38
|
+
if (cell.fg !== 'default')
|
|
39
|
+
parts.push(ansi.fgColor(cell.fg));
|
|
40
|
+
if (cell.bg !== 'default')
|
|
41
|
+
parts.push(ansi.bgColor(cell.bg));
|
|
42
|
+
if (cell.bold)
|
|
43
|
+
parts.push(ansi.bold());
|
|
44
|
+
if (cell.dim)
|
|
45
|
+
parts.push(ansi.dim());
|
|
46
|
+
if (cell.italic)
|
|
47
|
+
parts.push(ansi.italic());
|
|
48
|
+
if (cell.underline)
|
|
49
|
+
parts.push(ansi.underline());
|
|
50
|
+
if (cell.strikethrough)
|
|
51
|
+
parts.push(ansi.strikethrough());
|
|
52
|
+
return parts.join('');
|
|
53
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TermNode } from '../renderer/node.js';
|
|
2
|
+
import { CellBuffer } from './buffer.js';
|
|
3
|
+
import { ResolvedStyle } from '../css/compute.js';
|
|
4
|
+
import { LayoutBox } from '../layout/engine.js';
|
|
5
|
+
/**
|
|
6
|
+
* Repaint only specific nodes' cells in the buffer.
|
|
7
|
+
* Clears the old area and writes the new content.
|
|
8
|
+
* Handles text-align, text-overflow, and white-space from ancestors.
|
|
9
|
+
*/
|
|
10
|
+
export declare function paintNodes(nodes: Set<TermNode>, buffer: CellBuffer, styles: Map<number, ResolvedStyle>, layout: Map<number, LayoutBox>, root: TermNode): void;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { renderBorder } from './border.js';
|
|
2
|
+
import { paintTextContent } from './paint-text.js';
|
|
3
|
+
/**
|
|
4
|
+
* Repaint only specific nodes' cells in the buffer.
|
|
5
|
+
* Clears the old area and writes the new content.
|
|
6
|
+
* Handles text-align, text-overflow, and white-space from ancestors.
|
|
7
|
+
*/
|
|
8
|
+
export function paintNodes(nodes, buffer, styles, layout, root) {
|
|
9
|
+
for (const node of nodes) {
|
|
10
|
+
const box = layout.get(node.id);
|
|
11
|
+
if (!box)
|
|
12
|
+
continue;
|
|
13
|
+
const oldBox = node.cache.layoutBox;
|
|
14
|
+
if (oldBox)
|
|
15
|
+
clearArea(buffer, oldBox);
|
|
16
|
+
if (node.nodeType === 'text') {
|
|
17
|
+
paintTextShared(node, buffer, box, styles, layout);
|
|
18
|
+
}
|
|
19
|
+
else if (node.nodeType === 'element') {
|
|
20
|
+
paintElementNode(node, buffer, box, styles, layout);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function paintElementNode(node, buffer, box, styles, layout) {
|
|
25
|
+
const style = styles.get(node.id);
|
|
26
|
+
if (!style || style.display === 'none')
|
|
27
|
+
return;
|
|
28
|
+
// Background fill
|
|
29
|
+
const visuals = resolveInheritedVisuals(node, styles);
|
|
30
|
+
// Element's own style overrides inherited
|
|
31
|
+
const bg = style.bg !== 'default' ? style.bg : visuals.bg;
|
|
32
|
+
if (bg !== 'default') {
|
|
33
|
+
for (let row = box.y; row < box.y + box.height; row++) {
|
|
34
|
+
for (let col = box.x; col < box.x + box.width; col++) {
|
|
35
|
+
buffer.setCell(col, row, { bg });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Border
|
|
40
|
+
if (style.borderStyle !== 'none') {
|
|
41
|
+
renderBorder(buffer, box, style);
|
|
42
|
+
}
|
|
43
|
+
// Repaint text children
|
|
44
|
+
for (const child of node.children) {
|
|
45
|
+
const childBox = layout.get(child.id);
|
|
46
|
+
if (!childBox)
|
|
47
|
+
continue;
|
|
48
|
+
if (child.nodeType === 'text') {
|
|
49
|
+
paintTextShared(child, buffer, childBox, styles, layout);
|
|
50
|
+
}
|
|
51
|
+
else if (child.nodeType === 'element') {
|
|
52
|
+
paintElementNode(child, buffer, childBox, styles, layout);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function paintTextShared(node, buffer, box, styles, layout) {
|
|
57
|
+
const visuals = resolveInheritedVisuals(node, styles);
|
|
58
|
+
paintTextContent(node, buffer, box, visuals, styles, layout);
|
|
59
|
+
}
|
|
60
|
+
function clearArea(buffer, box) {
|
|
61
|
+
for (let row = box.y; row < box.y + box.height; row++) {
|
|
62
|
+
for (let col = box.x; col < box.x + box.width; col++) {
|
|
63
|
+
buffer.setCell(col, row, { char: ' ', fg: 'default', bg: 'default', bold: false, italic: false, underline: false, strikethrough: false, dim: false });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function resolveInheritedVisuals(node, styles) {
|
|
68
|
+
const result = { fg: 'default', bg: 'default', bold: false, italic: false, underline: false, strikethrough: false, dim: false };
|
|
69
|
+
let current = node.parent;
|
|
70
|
+
while (current) {
|
|
71
|
+
const style = styles.get(current.id);
|
|
72
|
+
if (style) {
|
|
73
|
+
if (result.fg === 'default' && style.fg !== 'default')
|
|
74
|
+
result.fg = style.fg;
|
|
75
|
+
if (result.bg === 'default' && style.bg !== 'default')
|
|
76
|
+
result.bg = style.bg;
|
|
77
|
+
if (!result.bold && style.bold)
|
|
78
|
+
result.bold = true;
|
|
79
|
+
if (!result.italic && style.italic)
|
|
80
|
+
result.italic = true;
|
|
81
|
+
if (!result.underline && style.underline)
|
|
82
|
+
result.underline = true;
|
|
83
|
+
if (!result.strikethrough && style.strikethrough)
|
|
84
|
+
result.strikethrough = true;
|
|
85
|
+
if (!result.dim && style.dim)
|
|
86
|
+
result.dim = true;
|
|
87
|
+
}
|
|
88
|
+
if (!result.hyperlink && current.tag === 'a') {
|
|
89
|
+
result.hyperlink = current.attributes.get('href');
|
|
90
|
+
}
|
|
91
|
+
current = current.parent;
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared text painting logic used by both full and incremental paint.
|
|
3
|
+
* Single implementation prevents divergence.
|
|
4
|
+
*/
|
|
5
|
+
import { TermNode } from '../renderer/node.js';
|
|
6
|
+
import { CellBuffer } from './buffer.js';
|
|
7
|
+
import { ResolvedStyle } from '../css/compute.js';
|
|
8
|
+
import { LayoutBox } from '../layout/engine.js';
|
|
9
|
+
interface TextVisuals {
|
|
10
|
+
fg: string;
|
|
11
|
+
bg: string;
|
|
12
|
+
bold: boolean;
|
|
13
|
+
italic: boolean;
|
|
14
|
+
underline: boolean;
|
|
15
|
+
strikethrough: boolean;
|
|
16
|
+
dim: boolean;
|
|
17
|
+
hyperlink?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Paint a text node's content into the buffer, respecting inherited
|
|
21
|
+
* text-align, white-space, text-overflow from ancestors.
|
|
22
|
+
*/
|
|
23
|
+
export declare function paintTextContent(node: TermNode, buffer: CellBuffer, box: LayoutBox, visuals: TextVisuals, styles: Map<number, ResolvedStyle>, layout: Map<number, LayoutBox>, clip?: {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
} | null): void;
|
|
29
|
+
export {};
|