@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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/dist/src/components/spinner.d.ts +11 -0
  4. package/dist/src/components/spinner.js +19 -0
  5. package/dist/src/components/text-buffer.d.ts +21 -0
  6. package/dist/src/components/text-buffer.js +87 -0
  7. package/dist/src/css/animation-runner.d.ts +17 -0
  8. package/dist/src/css/animation-runner.js +72 -0
  9. package/dist/src/css/animation.d.ts +5 -0
  10. package/dist/src/css/animation.js +6 -0
  11. package/dist/src/css/calc.d.ts +5 -0
  12. package/dist/src/css/calc.js +130 -0
  13. package/dist/src/css/color.d.ts +1 -0
  14. package/dist/src/css/color.js +157 -0
  15. package/dist/src/css/compute.d.ts +63 -0
  16. package/dist/src/css/compute.js +606 -0
  17. package/dist/src/css/defaults.d.ts +8 -0
  18. package/dist/src/css/defaults.js +44 -0
  19. package/dist/src/css/incremental.d.ts +9 -0
  20. package/dist/src/css/incremental.js +46 -0
  21. package/dist/src/css/index.d.ts +5 -0
  22. package/dist/src/css/index.js +3 -0
  23. package/dist/src/css/media.d.ts +11 -0
  24. package/dist/src/css/media.js +59 -0
  25. package/dist/src/css/parser.d.ts +20 -0
  26. package/dist/src/css/parser.js +241 -0
  27. package/dist/src/css/selector.d.ts +17 -0
  28. package/dist/src/css/selector.js +272 -0
  29. package/dist/src/css/specificity.d.ts +7 -0
  30. package/dist/src/css/specificity.js +89 -0
  31. package/dist/src/css/values.d.ts +17 -0
  32. package/dist/src/css/values.js +58 -0
  33. package/dist/src/css/variables.d.ts +6 -0
  34. package/dist/src/css/variables.js +42 -0
  35. package/dist/src/debug/console.d.ts +16 -0
  36. package/dist/src/debug/console.js +65 -0
  37. package/dist/src/debug/server.d.ts +22 -0
  38. package/dist/src/debug/server.js +90 -0
  39. package/dist/src/headless.d.ts +21 -0
  40. package/dist/src/headless.js +26 -0
  41. package/dist/src/index.d.ts +18 -0
  42. package/dist/src/index.js +485 -0
  43. package/dist/src/input/dispatch.d.ts +18 -0
  44. package/dist/src/input/dispatch.js +70 -0
  45. package/dist/src/input/focus.d.ts +18 -0
  46. package/dist/src/input/focus.js +81 -0
  47. package/dist/src/input/hit.d.ts +3 -0
  48. package/dist/src/input/hit.js +29 -0
  49. package/dist/src/input/keyboard.d.ts +9 -0
  50. package/dist/src/input/keyboard.js +100 -0
  51. package/dist/src/input/mouse.d.ts +7 -0
  52. package/dist/src/input/mouse.js +35 -0
  53. package/dist/src/input/scroll.d.ts +2 -0
  54. package/dist/src/input/scroll.js +24 -0
  55. package/dist/src/layout/cache.d.ts +4 -0
  56. package/dist/src/layout/cache.js +8 -0
  57. package/dist/src/layout/engine.d.ts +9 -0
  58. package/dist/src/layout/engine.js +455 -0
  59. package/dist/src/layout/flex.d.ts +4 -0
  60. package/dist/src/layout/flex.js +30 -0
  61. package/dist/src/layout/incremental.d.ts +8 -0
  62. package/dist/src/layout/incremental.js +58 -0
  63. package/dist/src/layout/size.d.ts +2 -0
  64. package/dist/src/layout/size.js +25 -0
  65. package/dist/src/layout/text.d.ts +7 -0
  66. package/dist/src/layout/text.js +52 -0
  67. package/dist/src/render/ansi.d.ts +23 -0
  68. package/dist/src/render/ansi.js +108 -0
  69. package/dist/src/render/border.d.ts +4 -0
  70. package/dist/src/render/border.js +60 -0
  71. package/dist/src/render/buffer.d.ts +23 -0
  72. package/dist/src/render/buffer.js +70 -0
  73. package/dist/src/render/context.d.ts +19 -0
  74. package/dist/src/render/context.js +98 -0
  75. package/dist/src/render/diff.d.ts +2 -0
  76. package/dist/src/render/diff.js +53 -0
  77. package/dist/src/render/incremental-paint.d.ts +10 -0
  78. package/dist/src/render/incremental-paint.js +94 -0
  79. package/dist/src/render/paint-text.d.ts +29 -0
  80. package/dist/src/render/paint-text.js +120 -0
  81. package/dist/src/render/paint.d.ts +5 -0
  82. package/dist/src/render/paint.js +220 -0
  83. package/dist/src/render/queue.d.ts +24 -0
  84. package/dist/src/render/queue.js +54 -0
  85. package/dist/src/render/scrollbar.d.ts +3 -0
  86. package/dist/src/render/scrollbar.js +19 -0
  87. package/dist/src/render/snapshot.d.ts +18 -0
  88. package/dist/src/render/snapshot.js +126 -0
  89. package/dist/src/renderer/default.d.ts +3 -0
  90. package/dist/src/renderer/default.js +3 -0
  91. package/dist/src/renderer/index.d.ts +11 -0
  92. package/dist/src/renderer/index.js +116 -0
  93. package/dist/src/renderer/node.d.ts +44 -0
  94. package/dist/src/renderer/node.js +153 -0
  95. package/dist/src/terminal/screen.d.ts +10 -0
  96. package/dist/src/terminal/screen.js +31 -0
  97. package/dist/src/terminal/stdin-router.d.ts +31 -0
  98. package/dist/src/terminal/stdin-router.js +133 -0
  99. package/package.json +64 -0
@@ -0,0 +1,120 @@
1
+ import { wrapText, truncateText } from '../layout/text.js';
2
+ /**
3
+ * Paint a text node's content into the buffer, respecting inherited
4
+ * text-align, white-space, text-overflow from ancestors.
5
+ */
6
+ export function paintTextContent(node, buffer, box, visuals, styles, layout, clip) {
7
+ const text = node.text ?? '';
8
+ if (!text)
9
+ return;
10
+ if (box.width === 0 && box.height === 0)
11
+ return;
12
+ // Find text properties from the ancestor that sets them
13
+ const alignResult = findAncestorWithBox(node, styles, layout, s => s.textAlign !== 'left' ? s.textAlign : undefined);
14
+ const align = alignResult?.value ?? 'left';
15
+ const whiteSpace = findAncestorProp(node, styles, s => s.whiteSpace !== 'normal' ? s.whiteSpace : undefined) ?? 'normal';
16
+ const textOverflow = findAncestorProp(node, styles, s => s.textOverflow !== 'clip' ? s.textOverflow : undefined) ?? 'clip';
17
+ const noWrap = whiteSpace === 'nowrap';
18
+ const ellipsis = textOverflow === 'ellipsis';
19
+ // For truncation, use the alignment container's inner width
20
+ const alignBox = alignResult?.box;
21
+ const parentBox = node.parent ? layout.get(node.parent.id) : undefined;
22
+ const truncWidth = alignBox ? innerWidth(alignBox, node, styles, layout) : (parentBox?.width ?? box.width);
23
+ // Determine text lines
24
+ let lines;
25
+ if (noWrap && ellipsis) {
26
+ lines = [truncateText(text, truncWidth)];
27
+ }
28
+ else if (noWrap) {
29
+ lines = [text.substring(0, truncWidth)];
30
+ }
31
+ else {
32
+ lines = wrapText(text, box.width > 0 ? box.width : buffer.width);
33
+ }
34
+ // Compute starting x with text-align
35
+ let startX = box.x;
36
+ if (align !== 'left' && alignBox) {
37
+ const inW = innerWidth(alignBox, node, styles, layout);
38
+ const inX = innerX(alignBox, node, styles, layout);
39
+ const textWidth = lines[0]?.length ?? 0;
40
+ if (align === 'center') {
41
+ startX = inX + Math.floor((inW - textWidth) / 2);
42
+ }
43
+ else if (align === 'right') {
44
+ startX = inX + inW - textWidth;
45
+ }
46
+ }
47
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
48
+ const line = lines[lineIdx];
49
+ const y = box.y + lineIdx;
50
+ for (let i = 0; i < line.length; i++) {
51
+ const cx = startX + i;
52
+ if (clip && (cx < clip.x || cx >= clip.x + clip.width || y < clip.y || y >= clip.y + clip.height))
53
+ continue;
54
+ buffer.setCell(cx, y, {
55
+ char: line[i],
56
+ fg: visuals.fg,
57
+ bg: visuals.bg,
58
+ bold: visuals.bold,
59
+ italic: visuals.italic,
60
+ underline: visuals.underline,
61
+ strikethrough: visuals.strikethrough,
62
+ dim: visuals.dim,
63
+ hyperlink: visuals.hyperlink,
64
+ });
65
+ }
66
+ }
67
+ }
68
+ /** Find an ancestor property value and the ancestor's layout box. */
69
+ function findAncestorWithBox(node, styles, layout, getter) {
70
+ let current = node.parent;
71
+ while (current) {
72
+ const s = styles.get(current.id);
73
+ if (s) {
74
+ const val = getter(s);
75
+ if (val !== undefined) {
76
+ const box = layout.get(current.id);
77
+ if (box)
78
+ return { value: val, box };
79
+ }
80
+ }
81
+ current = current.parent;
82
+ }
83
+ return undefined;
84
+ }
85
+ /** Find an ancestor property value (without needing the box). */
86
+ function findAncestorProp(node, styles, getter) {
87
+ let current = node.parent;
88
+ while (current) {
89
+ const s = styles.get(current.id);
90
+ if (s) {
91
+ const val = getter(s);
92
+ if (val !== undefined)
93
+ return val;
94
+ }
95
+ current = current.parent;
96
+ }
97
+ return undefined;
98
+ }
99
+ /** Get inner X position (accounting for border) of the alignment container. */
100
+ function innerX(alignBox, node, styles, layout) {
101
+ const inset = findBorderInset(alignBox, node, styles, layout);
102
+ return alignBox.x + inset;
103
+ }
104
+ /** Get inner width (accounting for border) of the alignment container. */
105
+ function innerWidth(alignBox, node, styles, layout) {
106
+ const inset = findBorderInset(alignBox, node, styles, layout);
107
+ return alignBox.width - inset * 2;
108
+ }
109
+ /** Find the border inset of the ancestor that owns alignBox. */
110
+ function findBorderInset(alignBox, node, styles, layout) {
111
+ let current = node.parent;
112
+ while (current) {
113
+ if (layout.get(current.id) === alignBox) {
114
+ const s = styles.get(current.id);
115
+ return (s?.borderStyle && s.borderStyle !== 'none') ? 1 : 0;
116
+ }
117
+ current = current.parent;
118
+ }
119
+ return 0;
120
+ }
@@ -0,0 +1,5 @@
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
+ export declare function paint(root: TermNode, buffer: CellBuffer, styles?: Map<number, ResolvedStyle>, layout?: Map<number, LayoutBox>): void;
@@ -0,0 +1,220 @@
1
+ import { renderBorder } from './border.js';
2
+ import { paintTextContent } from './paint-text.js';
3
+ const DEFAULT_VISUALS = {
4
+ fg: 'default', bg: 'default',
5
+ bold: false, italic: false, underline: false, strikethrough: false, dim: false,
6
+ };
7
+ const NO_SCROLL = { x: 0, y: 0 };
8
+ export function paint(root, buffer, styles, layout) {
9
+ paintNode(root, buffer, styles, layout, DEFAULT_VISUALS, null, NO_SCROLL);
10
+ }
11
+ function paintNode(node, buffer, styles, layout, inherited, clip, scroll) {
12
+ if (node.nodeType === 'comment')
13
+ return;
14
+ const visuals = resolveVisuals(node, styles, inherited);
15
+ const rawBox = layout?.get(node.id);
16
+ const box = rawBox ? applyScroll(rawBox, scroll) : undefined;
17
+ // Check display:none — element and all descendants are invisible and take no space
18
+ const ownStyle = node.nodeType === 'element' ? styles?.get(node.id) : undefined;
19
+ const parentStyle = node.parent ? styles?.get(node.parent.id) : undefined;
20
+ if (ownStyle?.display === 'none' || parentStyle?.display === 'none')
21
+ return;
22
+ // Check visibility — hidden elements take space but don't render
23
+ const isHidden = ownStyle?.visibility === 'hidden' || parentStyle?.visibility === 'hidden';
24
+ if (node.nodeType === 'text') {
25
+ if (!isHidden) {
26
+ const parentBox = node.parent ? layout?.get(node.parent.id) : undefined;
27
+ paintText(node, buffer, box, visuals, clip, parentStyle, parentBox, styles, layout);
28
+ }
29
+ return;
30
+ }
31
+ if (node.nodeType === 'element' && box && !isHidden) {
32
+ fillBackground(buffer, box, visuals, clip);
33
+ if (ownStyle && ownStyle.borderStyle !== 'none') {
34
+ renderBorder(buffer, box, ownStyle);
35
+ }
36
+ if (node.tag === 'hr') {
37
+ paintHorizontalRule(buffer, box, visuals, clip);
38
+ return;
39
+ }
40
+ if (node.tag === 'li') {
41
+ paintListMarker(node, buffer, box, visuals, clip);
42
+ }
43
+ if (node.tag === 'input' && box) {
44
+ paintInput(node, buffer, box, visuals, clip);
45
+ }
46
+ }
47
+ // Determine clip and scroll for children
48
+ let childClip = clip;
49
+ let childScroll = scroll;
50
+ if (node.nodeType === 'element' && box) {
51
+ const ownStyle = styles?.get(node.id);
52
+ if (ownStyle && ownStyle.overflow !== 'visible') {
53
+ childClip = intersectClip(clip, box);
54
+ }
55
+ if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
56
+ childScroll = { x: scroll.x + node.scrollLeft, y: scroll.y + node.scrollTop };
57
+ }
58
+ }
59
+ for (const child of node.children) {
60
+ paintNode(child, buffer, styles, layout, visuals, childClip, childScroll);
61
+ }
62
+ }
63
+ function applyScroll(box, scroll) {
64
+ if (scroll.x === 0 && scroll.y === 0)
65
+ return box;
66
+ return { x: box.x - scroll.x, y: box.y - scroll.y, width: box.width, height: box.height };
67
+ }
68
+ function paintText(node, buffer, box, visuals, clip, parentStyle, parentBox, styles, layout) {
69
+ if (!box || !styles || !layout)
70
+ return;
71
+ paintTextContent(node, buffer, box, visuals, styles, layout, clip);
72
+ }
73
+ function fillBackground(buffer, box, visuals, clip) {
74
+ if (visuals.bg === 'default')
75
+ return;
76
+ for (let row = box.y; row < box.y + box.height; row++) {
77
+ for (let col = box.x; col < box.x + box.width; col++) {
78
+ if (clip && !inClip(col, row, clip))
79
+ continue;
80
+ buffer.setCell(col, row, { bg: visuals.bg });
81
+ }
82
+ }
83
+ }
84
+ function paintListMarker(node, buffer, box, visuals, clip) {
85
+ const parent = node.parent;
86
+ if (!parent)
87
+ return;
88
+ const isOrdered = parent.tag === 'ol';
89
+ let marker;
90
+ if (isOrdered) {
91
+ const index = parent.children.filter(c => c.tag === 'li').indexOf(node);
92
+ marker = `${index + 1}. `;
93
+ }
94
+ else {
95
+ marker = '• ';
96
+ }
97
+ for (let i = 0; i < marker.length; i++) {
98
+ const cx = box.x + i;
99
+ if (clip && !inClip(cx, box.y, clip))
100
+ continue;
101
+ buffer.setCell(cx, box.y, { char: marker[i], fg: visuals.fg, dim: visuals.dim });
102
+ }
103
+ }
104
+ function paintInput(node, buffer, box, visuals, clip) {
105
+ const value = node.attributes.get('value') ?? '';
106
+ const isFocused = node.attributes.has('data-focused');
107
+ const cursor = node.textBuffer?.cursor ?? value.length;
108
+ const style = node.cache.resolvedStyle;
109
+ const borderInset = (style?.borderStyle && style.borderStyle !== 'none') ? 1 : 0;
110
+ const padL = resolvePadVal(style?.paddingLeft) + borderInset;
111
+ const padR = resolvePadVal(style?.paddingRight) + borderInset;
112
+ const contentX = box.x + padL;
113
+ const contentY = box.y + borderInset;
114
+ const contentW = box.width - padL - padR;
115
+ if (contentW <= 0)
116
+ return;
117
+ // Scroll offset: only adjust when cursor leaves visible range.
118
+ // Cursor at end of text (position == value.length) can sit 1 cell
119
+ // past the last character, using the right padding space.
120
+ let scrollOffset = node.scrollLeft ?? 0;
121
+ // Cursor scrolled off the left → snap left edge to cursor
122
+ if (cursor < scrollOffset) {
123
+ scrollOffset = cursor;
124
+ }
125
+ // Cursor scrolled off the right → scroll just enough to show it
126
+ if (cursor > scrollOffset + contentW) {
127
+ scrollOffset = cursor - contentW;
128
+ }
129
+ // If cursor is mid-text and at the rightmost visible cell,
130
+ // we need to be able to see what's after it
131
+ if (cursor < value.length && cursor === scrollOffset + contentW) {
132
+ scrollOffset = cursor - contentW + 1;
133
+ }
134
+ scrollOffset = Math.max(0, scrollOffset);
135
+ node.scrollLeft = scrollOffset;
136
+ // Determine what's visible and where overflow indicators go
137
+ const hasOverflowLeft = scrollOffset > 0;
138
+ const visibleEnd = Math.min(scrollOffset + contentW, value.length);
139
+ const hasOverflowRight = visibleEnd < value.length;
140
+ // Paint text — overflow indicators replace the first/last visible character
141
+ for (let i = 0; i < contentW; i++) {
142
+ const charIdx = scrollOffset + i;
143
+ if (charIdx >= value.length)
144
+ break;
145
+ const cx = contentX + i;
146
+ if (clip && !inClip(cx, contentY, clip))
147
+ continue;
148
+ buffer.setCell(cx, contentY, { char: value[charIdx], fg: visuals.fg });
149
+ }
150
+ // Overflow indicators (faint ellipsis on top of first/last char)
151
+ if (hasOverflowLeft) {
152
+ const cx = contentX;
153
+ if (!clip || inClip(cx, contentY, clip)) {
154
+ buffer.setCell(cx, contentY, { char: '…', fg: visuals.fg, dim: true });
155
+ }
156
+ }
157
+ if (hasOverflowRight) {
158
+ const cx = contentX + contentW - 1;
159
+ if (!clip || inClip(cx, contentY, clip)) {
160
+ buffer.setCell(cx, contentY, { char: '…', fg: visuals.fg, dim: true });
161
+ }
162
+ }
163
+ // Cursor (inverted colors)
164
+ if (isFocused) {
165
+ const cursorScreenX = contentX + (cursor - scrollOffset);
166
+ if (cursorScreenX >= contentX && cursorScreenX <= contentX + contentW) {
167
+ const cursorChar = cursor < value.length ? value[cursor] : ' ';
168
+ if (!clip || inClip(cursorScreenX, contentY, clip)) {
169
+ buffer.setCell(cursorScreenX, contentY, {
170
+ char: cursorChar,
171
+ fg: visuals.bg !== 'default' ? visuals.bg : 'black',
172
+ bg: visuals.fg !== 'default' ? visuals.fg : 'white',
173
+ });
174
+ }
175
+ }
176
+ }
177
+ }
178
+ function resolvePadVal(v) {
179
+ if (typeof v === 'number')
180
+ return v;
181
+ return 0;
182
+ }
183
+ function paintHorizontalRule(buffer, box, visuals, clip) {
184
+ for (let col = box.x; col < box.x + box.width; col++) {
185
+ if (clip && !inClip(col, box.y, clip))
186
+ continue;
187
+ buffer.setCell(col, box.y, { char: '─', fg: visuals.fg, dim: true });
188
+ }
189
+ }
190
+ function inClip(col, row, clip) {
191
+ return col >= clip.x && col < clip.x + clip.width
192
+ && row >= clip.y && row < clip.y + clip.height;
193
+ }
194
+ function intersectClip(existing, box) {
195
+ if (!existing)
196
+ return { x: box.x, y: box.y, width: box.width, height: box.height };
197
+ const x = Math.max(existing.x, box.x);
198
+ const y = Math.max(existing.y, box.y);
199
+ const right = Math.min(existing.x + existing.width, box.x + box.width);
200
+ const bottom = Math.min(existing.y + existing.height, box.y + box.height);
201
+ return { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) };
202
+ }
203
+ function resolveVisuals(node, styles, inherited) {
204
+ if (node.nodeType !== 'element')
205
+ return inherited;
206
+ const own = styles?.get(node.id);
207
+ if (!own)
208
+ return inherited;
209
+ const hyperlink = node.tag === 'a' ? node.attributes.get('href') : inherited.hyperlink;
210
+ return {
211
+ fg: own.fg !== 'default' ? own.fg : inherited.fg,
212
+ bg: own.bg !== 'default' ? own.bg : inherited.bg,
213
+ bold: own.bold || inherited.bold,
214
+ italic: own.italic || inherited.italic,
215
+ underline: own.underline || inherited.underline,
216
+ strikethrough: own.strikethrough || inherited.strikethrough,
217
+ dim: own.dim || inherited.dim,
218
+ hyperlink,
219
+ };
220
+ }
@@ -0,0 +1,24 @@
1
+ import { TermNode } from '../renderer/node.js';
2
+ export declare class RenderQueue {
3
+ paintOnly: Set<TermNode>;
4
+ styleResolve: Set<TermNode>;
5
+ layoutSubtree: Set<TermNode>;
6
+ layoutBubble: Set<TermNode>;
7
+ fullRecompute: boolean;
8
+ enqueuePaintOnly(node: TermNode): void;
9
+ enqueueStyleResolve(node: TermNode): void;
10
+ enqueueLayoutSubtree(node: TermNode): void;
11
+ enqueueLayoutBubble(node: TermNode): void;
12
+ setFullRecompute(): void;
13
+ isEmpty(): boolean;
14
+ clear(): void;
15
+ /** Returns a frozen copy of the current state and clears this queue. */
16
+ snapshot(): RenderQueueSnapshot;
17
+ }
18
+ export interface RenderQueueSnapshot {
19
+ readonly paintOnly: Set<TermNode>;
20
+ readonly styleResolve: Set<TermNode>;
21
+ readonly layoutSubtree: Set<TermNode>;
22
+ readonly layoutBubble: Set<TermNode>;
23
+ readonly fullRecompute: boolean;
24
+ }
@@ -0,0 +1,54 @@
1
+ export class RenderQueue {
2
+ paintOnly = new Set();
3
+ styleResolve = new Set();
4
+ layoutSubtree = new Set();
5
+ layoutBubble = new Set();
6
+ fullRecompute = false;
7
+ enqueuePaintOnly(node) {
8
+ // Don't add if already queued for more comprehensive work
9
+ if (this.styleResolve.has(node) || this.layoutSubtree.has(node) || this.layoutBubble.has(node))
10
+ return;
11
+ this.paintOnly.add(node);
12
+ }
13
+ enqueueStyleResolve(node) {
14
+ this.paintOnly.delete(node); // style resolve subsumes paint
15
+ this.styleResolve.add(node);
16
+ }
17
+ enqueueLayoutSubtree(node) {
18
+ this.paintOnly.delete(node); // layout subsumes paint
19
+ this.layoutSubtree.add(node);
20
+ }
21
+ enqueueLayoutBubble(node) {
22
+ this.paintOnly.delete(node);
23
+ this.layoutBubble.add(node);
24
+ }
25
+ setFullRecompute() {
26
+ this.fullRecompute = true;
27
+ }
28
+ isEmpty() {
29
+ return !this.fullRecompute
30
+ && this.paintOnly.size === 0
31
+ && this.styleResolve.size === 0
32
+ && this.layoutSubtree.size === 0
33
+ && this.layoutBubble.size === 0;
34
+ }
35
+ clear() {
36
+ this.paintOnly.clear();
37
+ this.styleResolve.clear();
38
+ this.layoutSubtree.clear();
39
+ this.layoutBubble.clear();
40
+ this.fullRecompute = false;
41
+ }
42
+ /** Returns a frozen copy of the current state and clears this queue. */
43
+ snapshot() {
44
+ const snap = {
45
+ paintOnly: new Set(this.paintOnly),
46
+ styleResolve: new Set(this.styleResolve),
47
+ layoutSubtree: new Set(this.layoutSubtree),
48
+ layoutBubble: new Set(this.layoutBubble),
49
+ fullRecompute: this.fullRecompute,
50
+ };
51
+ this.clear();
52
+ return snap;
53
+ }
54
+ }
@@ -0,0 +1,3 @@
1
+ import { CellBuffer } from './buffer.js';
2
+ import { LayoutBox } from '../layout/engine.js';
3
+ export declare function renderScrollbar(buffer: CellBuffer, box: LayoutBox, contentHeight: number, scrollTop: number): void;
@@ -0,0 +1,19 @@
1
+ const TRACK_CHAR = '│';
2
+ const THUMB_CHAR = '┃';
3
+ export function renderScrollbar(buffer, box, contentHeight, scrollTop) {
4
+ if (contentHeight <= box.height)
5
+ return;
6
+ const col = box.x + box.width - 1;
7
+ const trackHeight = box.height;
8
+ const thumbSize = Math.max(1, Math.round(trackHeight * (box.height / contentHeight)));
9
+ const maxScroll = contentHeight - box.height;
10
+ const thumbPos = Math.round((scrollTop / maxScroll) * (trackHeight - thumbSize));
11
+ for (let row = 0; row < trackHeight; row++) {
12
+ const isThumb = row >= thumbPos && row < thumbPos + thumbSize;
13
+ buffer.setCell(col, box.y + row, {
14
+ char: isThumb ? THUMB_CHAR : TRACK_CHAR,
15
+ fg: isThumb ? 'white' : 'default',
16
+ dim: !isThumb,
17
+ });
18
+ }
19
+ }
@@ -0,0 +1,18 @@
1
+ import { CellBuffer } from './buffer.js';
2
+ /**
3
+ * Serialize a cell buffer to a readable text format for snapshot testing.
4
+ * Each line shows the characters, followed by color/style annotations for non-default cells.
5
+ */
6
+ export declare function bufferToText(buffer: CellBuffer): string;
7
+ /**
8
+ * Serialize a cell buffer to a detailed format that includes style info.
9
+ * Format: each non-default cell shows [col,row char fg bg bold]
10
+ */
11
+ export declare function bufferToStyledText(buffer: CellBuffer): string;
12
+ /**
13
+ * Render a cell buffer to SVG for visual inspection.
14
+ */
15
+ export declare function bufferToSvg(buffer: CellBuffer, options?: {
16
+ cellWidth?: number;
17
+ cellHeight?: number;
18
+ }): string;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Serialize a cell buffer to a readable text format for snapshot testing.
3
+ * Each line shows the characters, followed by color/style annotations for non-default cells.
4
+ */
5
+ export function bufferToText(buffer) {
6
+ const lines = [];
7
+ for (let row = 0; row < buffer.height; row++) {
8
+ let line = '';
9
+ let hasContent = false;
10
+ for (let col = 0; col < buffer.width; col++) {
11
+ const cell = buffer.getCell(col, row);
12
+ line += cell.char;
13
+ if (cell.char !== ' ' || cell.bg !== 'default')
14
+ hasContent = true;
15
+ }
16
+ if (hasContent) {
17
+ lines.push(line.trimEnd());
18
+ }
19
+ else if (lines.length > 0) {
20
+ // Track empty lines between content, but trim trailing empties
21
+ lines.push('');
22
+ }
23
+ }
24
+ // Trim trailing empty lines
25
+ while (lines.length > 0 && lines[lines.length - 1] === '') {
26
+ lines.pop();
27
+ }
28
+ return lines.join('\n');
29
+ }
30
+ /**
31
+ * Serialize a cell buffer to a detailed format that includes style info.
32
+ * Format: each non-default cell shows [col,row char fg bg bold]
33
+ */
34
+ export function bufferToStyledText(buffer) {
35
+ const lines = [];
36
+ for (let row = 0; row < buffer.height; row++) {
37
+ let chars = '';
38
+ const styles = [];
39
+ for (let col = 0; col < buffer.width; col++) {
40
+ const cell = buffer.getCell(col, row);
41
+ chars += cell.char;
42
+ if (hasStyle(cell)) {
43
+ const parts = [];
44
+ if (cell.fg !== 'default')
45
+ parts.push(`fg:${cell.fg}`);
46
+ if (cell.bg !== 'default')
47
+ parts.push(`bg:${cell.bg}`);
48
+ if (cell.bold)
49
+ parts.push('bold');
50
+ if (cell.italic)
51
+ parts.push('italic');
52
+ if (cell.underline)
53
+ parts.push('underline');
54
+ if (cell.dim)
55
+ parts.push('dim');
56
+ styles.push(`[${col}:${parts.join(',')}]`);
57
+ }
58
+ }
59
+ const trimmed = chars.trimEnd();
60
+ if (trimmed || styles.length > 0) {
61
+ const line = styles.length > 0
62
+ ? `${trimmed} ${styles.join(' ')}`
63
+ : trimmed;
64
+ lines.push(line);
65
+ }
66
+ }
67
+ // Trim trailing empty lines
68
+ while (lines.length > 0 && lines[lines.length - 1] === '') {
69
+ lines.pop();
70
+ }
71
+ return lines.join('\n');
72
+ }
73
+ /**
74
+ * Render a cell buffer to SVG for visual inspection.
75
+ */
76
+ export function bufferToSvg(buffer, options) {
77
+ const cw = options?.cellWidth ?? 10;
78
+ const ch = options?.cellHeight ?? 18;
79
+ const fontSize = Math.floor(ch * 0.75);
80
+ const width = buffer.width * cw;
81
+ const height = buffer.height * ch;
82
+ const parts = [];
83
+ parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" style="background:#1a1a2e">`);
84
+ parts.push(`<style>text { font-family: 'Menlo', 'Monaco', 'Courier New', monospace; font-size: ${fontSize}px; }</style>`);
85
+ for (let row = 0; row < buffer.height; row++) {
86
+ for (let col = 0; col < buffer.width; col++) {
87
+ const cell = buffer.getCell(col, row);
88
+ const x = col * cw;
89
+ const y = row * ch;
90
+ // Background
91
+ if (cell.bg !== 'default') {
92
+ const bgColor = colorToHex(cell.bg);
93
+ parts.push(`<rect x="${x}" y="${y}" width="${cw}" height="${ch}" fill="${bgColor}"/>`);
94
+ }
95
+ // Text
96
+ if (cell.char !== ' ') {
97
+ const fgColor = colorToHex(cell.fg === 'default' ? 'white' : cell.fg);
98
+ const weight = cell.bold ? 'font-weight="bold"' : '';
99
+ const decoration = cell.underline ? 'text-decoration="underline"' : '';
100
+ const fontStyle = cell.italic ? 'font-style="italic"' : '';
101
+ const textY = y + ch - Math.floor(ch * 0.25);
102
+ parts.push(`<text x="${x + 1}" y="${textY}" fill="${fgColor}" ${weight} ${decoration} ${fontStyle}>${escapeXml(cell.char)}</text>`);
103
+ }
104
+ }
105
+ }
106
+ parts.push('</svg>');
107
+ return parts.join('\n');
108
+ }
109
+ function hasStyle(cell) {
110
+ return cell.fg !== 'default' || cell.bg !== 'default'
111
+ || cell.bold || cell.italic || cell.underline
112
+ || cell.strikethrough || cell.dim;
113
+ }
114
+ function colorToHex(color) {
115
+ if (color.startsWith('#'))
116
+ return color;
117
+ const map = {
118
+ black: '#000000', red: '#ff0000', green: '#00ff00', yellow: '#ffff00',
119
+ blue: '#0000ff', magenta: '#ff00ff', cyan: '#00ffff', white: '#ffffff',
120
+ default: '#cccccc',
121
+ };
122
+ return map[color] ?? '#cccccc';
123
+ }
124
+ function escapeXml(s) {
125
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
126
+ }
@@ -0,0 +1,3 @@
1
+ import { createTermRenderer } from './index.js';
2
+ declare const renderer: ReturnType<typeof createTermRenderer>;
3
+ export default renderer;
@@ -0,0 +1,3 @@
1
+ import { createTermRenderer } from './index.js';
2
+ const renderer = createTermRenderer();
3
+ export default renderer;
@@ -0,0 +1,11 @@
1
+ import { createRenderer as svelteCreateRenderer } from 'svelte/renderer';
2
+ import { TermNode } from './node.js';
3
+ export declare function createTermRenderer(): ReturnType<typeof svelteCreateRenderer<TermNode, TermNode, TermNode, TermNode>>;
4
+ /**
5
+ * Keep the custom renderer active globally so Svelte's effects
6
+ * use our renderer methods (setText, setAttribute, etc.) instead
7
+ * of falling back to DOM operations (node.nodeValue, etc.).
8
+ *
9
+ * Call this AFTER renderer.render() which pops the renderer.
10
+ */
11
+ export { TermNode } from './node.js';