@jrichman/ink 6.4.11 → 6.4.12
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/build/components/App.d.ts +6 -0
- package/build/components/App.js +5 -0
- package/build/components/App.js.map +1 -1
- package/build/components/AppContext.d.ts +36 -1
- package/build/components/AppContext.js +7 -2
- package/build/components/AppContext.js.map +1 -1
- package/build/components/Box.d.ts +26 -2
- package/build/components/Box.js +3 -2
- package/build/components/Box.js.map +1 -1
- package/build/components/StaticRender.d.ts +8 -0
- package/build/components/StaticRender.js +11 -0
- package/build/components/StaticRender.js.map +1 -0
- package/build/debug-log.d.ts +2 -0
- package/build/debug-log.js +44 -0
- package/build/debug-log.js.map +1 -0
- package/build/dom.d.ts +37 -3
- package/build/dom.js +19 -3
- package/build/dom.js.map +1 -1
- package/build/index.d.ts +4 -1
- package/build/index.js +3 -1
- package/build/index.js.map +1 -1
- package/build/ink.d.ts +29 -2
- package/build/ink.js +215 -102
- package/build/ink.js.map +1 -1
- package/build/measure-element.d.ts +20 -1
- package/build/measure-element.js +201 -51
- package/build/measure-element.js.map +1 -1
- package/build/output.d.ts +195 -10
- package/build/output.js +494 -160
- package/build/output.js.map +1 -1
- package/build/reconciler.js +19 -3
- package/build/reconciler.js.map +1 -1
- package/build/render-background.js +1 -1
- package/build/render-background.js.map +1 -1
- package/build/render-cached.d.ts +17 -0
- package/build/render-cached.js +62 -0
- package/build/render-cached.js.map +1 -0
- package/build/render-container.d.ts +24 -0
- package/build/render-container.js +169 -0
- package/build/render-container.js.map +1 -0
- package/build/render-node-to-output.d.ts +15 -7
- package/build/render-node-to-output.js +123 -485
- package/build/render-node-to-output.js.map +1 -1
- package/build/render-screen-reader.d.ts +5 -0
- package/build/render-screen-reader.js +54 -0
- package/build/render-screen-reader.js.map +1 -0
- package/build/render-scrollbar.d.ts +22 -0
- package/build/render-scrollbar.js +77 -0
- package/build/render-scrollbar.js.map +1 -0
- package/build/render-sticky.d.ts +56 -0
- package/build/render-sticky.js +314 -0
- package/build/render-sticky.js.map +1 -0
- package/build/render-text-node.d.ts +24 -0
- package/build/render-text-node.js +133 -0
- package/build/render-text-node.js.map +1 -0
- package/build/render.d.ts +39 -0
- package/build/render.js +5 -0
- package/build/render.js.map +1 -1
- package/build/renderer.d.ts +10 -2
- package/build/renderer.js +103 -7
- package/build/renderer.js.map +1 -1
- package/build/replay.d.ts +60 -0
- package/build/replay.js +138 -0
- package/build/replay.js.map +1 -0
- package/build/scroll.js +20 -1
- package/build/scroll.js.map +1 -1
- package/build/selection.d.ts +9 -0
- package/build/selection.js +47 -0
- package/build/selection.js.map +1 -1
- package/build/serialization.d.ts +28 -0
- package/build/serialization.js +267 -0
- package/build/serialization.js.map +1 -0
- package/build/styles.d.ts +18 -0
- package/build/styles.js.map +1 -1
- package/build/terminal-buffer.d.ts +53 -0
- package/build/terminal-buffer.js +441 -0
- package/build/terminal-buffer.js.map +1 -0
- package/build/worker/animation-controller.d.ts +72 -0
- package/build/worker/animation-controller.js +128 -0
- package/build/worker/animation-controller.js.map +1 -0
- package/build/worker/ansi-utils.d.ts +16 -0
- package/build/worker/ansi-utils.js +40 -0
- package/build/worker/ansi-utils.js.map +1 -0
- package/build/worker/canvas.d.ts +47 -0
- package/build/worker/canvas.js +94 -0
- package/build/worker/canvas.js.map +1 -0
- package/build/worker/compositor.d.ts +33 -0
- package/build/worker/compositor.js +314 -0
- package/build/worker/compositor.js.map +1 -0
- package/build/worker/platform.d.ts +15 -0
- package/build/worker/platform.js +19 -0
- package/build/worker/platform.js.map +1 -0
- package/build/worker/render-worker.d.ts +112 -0
- package/build/worker/render-worker.js +936 -0
- package/build/worker/render-worker.js.map +1 -0
- package/build/worker/scene-manager.d.ts +26 -0
- package/build/worker/scene-manager.js +99 -0
- package/build/worker/scene-manager.js.map +1 -0
- package/build/worker/scroll-optimizer.d.ts +32 -0
- package/build/worker/scroll-optimizer.js +110 -0
- package/build/worker/scroll-optimizer.js.map +1 -0
- package/build/worker/terminal-writer.d.ts +116 -0
- package/build/worker/terminal-writer.js +722 -0
- package/build/worker/terminal-writer.js.map +1 -0
- package/build/worker/worker-entry.d.ts +6 -0
- package/build/worker/worker-entry.js +130 -0
- package/build/worker/worker-entry.js.map +1 -0
- package/build/wrap-text.d.ts +6 -0
- package/build/wrap-text.js +120 -0
- package/build/wrap-text.js.map +1 -0
- package/package.json +3 -1
package/build/output.js
CHANGED
|
@@ -1,49 +1,207 @@
|
|
|
1
1
|
import { styledCharsToString } from '@alcalzone/ansi-tokenize';
|
|
2
|
-
import { toStyledCharacters, inkCharacterWidth, styledCharsWidth,
|
|
2
|
+
import { toStyledCharacters, inkCharacterWidth, styledCharsWidth, } from './measure-text.js';
|
|
3
|
+
import { calculateScrollbarLayout } from './measure-element.js';
|
|
4
|
+
import { renderScrollbar } from './render-scrollbar.js';
|
|
5
|
+
/**
|
|
6
|
+
"Virtual" output class
|
|
7
|
+
|
|
8
|
+
Handles the positioning and saving of the output of each node in the tree. Also responsible for applying transformations to each character of the output.
|
|
9
|
+
|
|
10
|
+
Used to generate the final output of all nodes before writing it to actual output stream (e.g. stdout)
|
|
11
|
+
*/
|
|
12
|
+
export function clampCursorColumn(line, col) {
|
|
13
|
+
let currentLineCol = 0;
|
|
14
|
+
let lastContentCol = 0;
|
|
15
|
+
for (const char of line) {
|
|
16
|
+
const charWidth = char.fullWidth ? 2 : 1;
|
|
17
|
+
if (char.value !== ' ' || char.styles.length > 0) {
|
|
18
|
+
lastContentCol = currentLineCol + charWidth;
|
|
19
|
+
}
|
|
20
|
+
currentLineCol += charWidth;
|
|
21
|
+
}
|
|
22
|
+
return col > lastContentCol ? lastContentCol : col;
|
|
23
|
+
}
|
|
24
|
+
export function intersectRect(a, b) {
|
|
25
|
+
const x1 = Math.max(a.x, b.x);
|
|
26
|
+
const y1 = Math.max(a.y, b.y);
|
|
27
|
+
const x2 = Math.min(a.x + a.w, b.x + b.w);
|
|
28
|
+
const y2 = Math.min(a.y + a.h, b.y + b.h);
|
|
29
|
+
if (x2 <= x1 || y2 <= y1) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
|
|
33
|
+
}
|
|
34
|
+
export function isRectIntersectingClip(rect, clip) {
|
|
35
|
+
const clipLeft = clip.x1 ?? -Infinity;
|
|
36
|
+
const clipRight = clip.x2 ?? Infinity;
|
|
37
|
+
const clipTop = clip.y1 ?? -Infinity;
|
|
38
|
+
const clipBottom = clip.y2 ?? Infinity;
|
|
39
|
+
return (rect.x2 > clipLeft &&
|
|
40
|
+
rect.x1 < clipRight &&
|
|
41
|
+
rect.y2 > clipTop &&
|
|
42
|
+
rect.y1 < clipBottom);
|
|
43
|
+
}
|
|
44
|
+
export const regionLayoutProperties = [
|
|
45
|
+
'x',
|
|
46
|
+
'y',
|
|
47
|
+
'width',
|
|
48
|
+
'height',
|
|
49
|
+
'scrollTop',
|
|
50
|
+
'scrollLeft',
|
|
51
|
+
'scrollHeight',
|
|
52
|
+
'scrollWidth',
|
|
53
|
+
'isScrollable',
|
|
54
|
+
'isVerticallyScrollable',
|
|
55
|
+
'isHorizontallyScrollable',
|
|
56
|
+
'scrollbarVisible',
|
|
57
|
+
'overflowToBackbuffer',
|
|
58
|
+
'marginRight',
|
|
59
|
+
'marginBottom',
|
|
60
|
+
'scrollbarThumbColor',
|
|
61
|
+
'backgroundColor',
|
|
62
|
+
'opaque',
|
|
63
|
+
'borderTop',
|
|
64
|
+
'borderBottom',
|
|
65
|
+
];
|
|
66
|
+
export function copyRegionProperty(target, source, key) {
|
|
67
|
+
const value = source[key];
|
|
68
|
+
if (value !== undefined) {
|
|
69
|
+
target[key] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
3
72
|
export default class Output {
|
|
4
73
|
width;
|
|
5
74
|
height;
|
|
6
|
-
|
|
7
|
-
|
|
75
|
+
// The root region represents the main screen area (non-scrollable background)
|
|
76
|
+
root;
|
|
77
|
+
activeRegionStack = [];
|
|
8
78
|
clips = [];
|
|
9
79
|
constructor(options) {
|
|
10
|
-
const { width, height } = options;
|
|
80
|
+
const { width, height, node, id = 'root' } = options;
|
|
11
81
|
this.width = width;
|
|
12
82
|
this.height = height;
|
|
83
|
+
this.root = {
|
|
84
|
+
id,
|
|
85
|
+
x: 0,
|
|
86
|
+
y: 0,
|
|
87
|
+
width,
|
|
88
|
+
height,
|
|
89
|
+
lines: [],
|
|
90
|
+
styledOutput: [],
|
|
91
|
+
isScrollable: false,
|
|
92
|
+
stickyHeaders: [],
|
|
93
|
+
children: [],
|
|
94
|
+
node,
|
|
95
|
+
selectableSpans: [],
|
|
96
|
+
};
|
|
97
|
+
this.initLines(this.root, width, height);
|
|
98
|
+
this.activeRegionStack.push(this.root);
|
|
13
99
|
}
|
|
14
100
|
getCurrentClip() {
|
|
15
101
|
return this.clips.at(-1);
|
|
16
102
|
}
|
|
103
|
+
getActiveRegion() {
|
|
104
|
+
return this.activeRegionStack.at(-1);
|
|
105
|
+
}
|
|
106
|
+
getRegionAbsoluteOffset() {
|
|
107
|
+
let x = 0;
|
|
108
|
+
let y = 0;
|
|
109
|
+
for (const region of this.activeRegionStack) {
|
|
110
|
+
x += region.x - (region.scrollLeft ?? 0);
|
|
111
|
+
y += region.y - (region.scrollTop ?? 0);
|
|
112
|
+
}
|
|
113
|
+
return { x, y };
|
|
114
|
+
}
|
|
115
|
+
startChildRegion(options) {
|
|
116
|
+
const { id, x, y, width, height, isScrollable, isVerticallyScrollable, isHorizontallyScrollable, scrollState, scrollbarVisible, overflowToBackbuffer, marginRight, marginBottom, scrollbarThumbColor, backgroundColor, opaque, nodeId, stableScrollback, borderTop, borderBottom, } = options;
|
|
117
|
+
// Create new region
|
|
118
|
+
// The buffer size should match scrollDimensions if scrollable, or bounds if not.
|
|
119
|
+
// If scrollable, we want to capture the FULL content.
|
|
120
|
+
const bufferWidth = scrollState?.scrollWidth ?? width;
|
|
121
|
+
const bufferHeight = scrollState?.scrollHeight ?? height;
|
|
122
|
+
const activeRegion = this.getActiveRegion();
|
|
123
|
+
const inheritedOverflowToBackbuffer = isScrollable
|
|
124
|
+
? overflowToBackbuffer
|
|
125
|
+
: (overflowToBackbuffer ?? activeRegion.overflowToBackbuffer);
|
|
126
|
+
const region = {
|
|
127
|
+
id,
|
|
128
|
+
x,
|
|
129
|
+
y,
|
|
130
|
+
width,
|
|
131
|
+
height,
|
|
132
|
+
lines: [],
|
|
133
|
+
styledOutput: [],
|
|
134
|
+
isScrollable,
|
|
135
|
+
isVerticallyScrollable,
|
|
136
|
+
isHorizontallyScrollable,
|
|
137
|
+
scrollTop: scrollState?.scrollTop,
|
|
138
|
+
scrollLeft: scrollState?.scrollLeft,
|
|
139
|
+
scrollHeight: scrollState?.scrollHeight,
|
|
140
|
+
scrollWidth: scrollState?.scrollWidth,
|
|
141
|
+
scrollbarVisible,
|
|
142
|
+
overflowToBackbuffer: inheritedOverflowToBackbuffer,
|
|
143
|
+
marginRight,
|
|
144
|
+
marginBottom,
|
|
145
|
+
scrollbarThumbColor,
|
|
146
|
+
backgroundColor,
|
|
147
|
+
opaque,
|
|
148
|
+
borderTop,
|
|
149
|
+
borderBottom,
|
|
150
|
+
stickyHeaders: [],
|
|
151
|
+
children: [],
|
|
152
|
+
nodeId,
|
|
153
|
+
stableScrollback,
|
|
154
|
+
selectableSpans: [],
|
|
155
|
+
};
|
|
156
|
+
this.initLines(region, bufferWidth, bufferHeight);
|
|
157
|
+
// Add to current active region's children
|
|
158
|
+
this.getActiveRegion().children.push(region);
|
|
159
|
+
// Push to stack
|
|
160
|
+
this.activeRegionStack.push(region);
|
|
161
|
+
}
|
|
162
|
+
endChildRegion() {
|
|
163
|
+
if (this.activeRegionStack.length > 1) {
|
|
164
|
+
this.activeRegionStack.pop();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
addStickyHeader(header) {
|
|
168
|
+
this.getActiveRegion().stickyHeaders.push(header);
|
|
169
|
+
}
|
|
17
170
|
write(x, y, items, options) {
|
|
18
|
-
const { transformers, lineIndex, preserveBackgroundColor, isTerminalCursorFocused, terminalCursorPosition, } = options;
|
|
19
|
-
|
|
171
|
+
const { transformers = [], lineIndex = 0, preserveBackgroundColor = false, isTerminalCursorFocused = false, terminalCursorPosition, isSelectable = false, } = options;
|
|
172
|
+
if (items.length === 0 && !isTerminalCursorFocused) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
20
175
|
if (isTerminalCursorFocused) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
176
|
+
const region = this.getActiveRegion();
|
|
177
|
+
let col = 0;
|
|
178
|
+
let row = 0;
|
|
179
|
+
const chars = typeof items === 'string' ? toStyledCharacters(items) : items;
|
|
180
|
+
let charOffset = 0;
|
|
181
|
+
const targetOffset = terminalCursorPosition ?? Number.POSITIVE_INFINITY;
|
|
182
|
+
for (const char of chars) {
|
|
183
|
+
if (charOffset >= targetOffset) {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
if (char.value === '\n') {
|
|
187
|
+
row++;
|
|
188
|
+
col = 0;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
col += inkCharacterWidth(char.value);
|
|
192
|
+
}
|
|
193
|
+
charOffset += char.value.length;
|
|
194
|
+
}
|
|
195
|
+
region.cursorPosition = {
|
|
196
|
+
row: y + row,
|
|
197
|
+
col: x + col,
|
|
27
198
|
};
|
|
28
199
|
}
|
|
29
|
-
if (items.length
|
|
30
|
-
|
|
200
|
+
if (items.length > 0) {
|
|
201
|
+
this.applyWrite(x, y, items, transformers, lineIndex, preserveBackgroundColor, isSelectable);
|
|
31
202
|
}
|
|
32
|
-
this.operations.push({
|
|
33
|
-
type: 'write',
|
|
34
|
-
x,
|
|
35
|
-
y,
|
|
36
|
-
items,
|
|
37
|
-
transformers,
|
|
38
|
-
lineIndex,
|
|
39
|
-
preserveBackgroundColor,
|
|
40
|
-
});
|
|
41
203
|
}
|
|
42
204
|
clip(clip) {
|
|
43
|
-
this.operations.push({
|
|
44
|
-
type: 'clip',
|
|
45
|
-
clip,
|
|
46
|
-
});
|
|
47
205
|
const previousClip = this.clips.at(-1);
|
|
48
206
|
const nextClip = { ...clip };
|
|
49
207
|
if (previousClip) {
|
|
@@ -75,149 +233,105 @@ export default class Output {
|
|
|
75
233
|
this.clips.push(nextClip);
|
|
76
234
|
}
|
|
77
235
|
unclip() {
|
|
78
|
-
this.operations.push({
|
|
79
|
-
type: 'unclip',
|
|
80
|
-
});
|
|
81
236
|
this.clips.pop();
|
|
82
237
|
}
|
|
83
238
|
get() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
nextClip.y2 =
|
|
123
|
-
previousClip.y2 === undefined
|
|
124
|
-
? nextClip.y2
|
|
125
|
-
: nextClip.y2 === undefined
|
|
126
|
-
? previousClip.y2
|
|
127
|
-
: Math.min(previousClip.y2, nextClip.y2);
|
|
239
|
+
this.clampCursorPosition(this.root);
|
|
240
|
+
this.trimRegionLines(this.root);
|
|
241
|
+
return this.root;
|
|
242
|
+
}
|
|
243
|
+
addRegionTree(region, x, y) {
|
|
244
|
+
const activeRegion = this.getActiveRegion();
|
|
245
|
+
const clonedRegion = this.cloneRegion(region, x, y, activeRegion.overflowToBackbuffer);
|
|
246
|
+
activeRegion.children.push(clonedRegion);
|
|
247
|
+
}
|
|
248
|
+
cloneRegion(region, x, y, inheritedOverflowToBackbuffer) {
|
|
249
|
+
const overflowToBackbuffer = region.isScrollable
|
|
250
|
+
? region.overflowToBackbuffer
|
|
251
|
+
: (region.overflowToBackbuffer ?? inheritedOverflowToBackbuffer);
|
|
252
|
+
const cloned = {
|
|
253
|
+
...region,
|
|
254
|
+
overflowToBackbuffer,
|
|
255
|
+
x: region.x + x,
|
|
256
|
+
y: region.y + y,
|
|
257
|
+
lines: region.lines.map(line => line.map(char => ({ ...char, styles: [...char.styles] }))),
|
|
258
|
+
selectableSpans: region.selectableSpans.map(span => ({ ...span })),
|
|
259
|
+
stickyHeaders: region.stickyHeaders.map(header => ({
|
|
260
|
+
...header,
|
|
261
|
+
x: header.x,
|
|
262
|
+
y: header.y,
|
|
263
|
+
})),
|
|
264
|
+
children: region.children.map(child => this.cloneRegion(child, 0, 0, overflowToBackbuffer)),
|
|
265
|
+
};
|
|
266
|
+
return cloned;
|
|
267
|
+
}
|
|
268
|
+
trimRegionLines(region) {
|
|
269
|
+
for (let y = 0; y < region.lines.length; y++) {
|
|
270
|
+
const line = region.lines[y];
|
|
271
|
+
let lastNonSpace = -1;
|
|
272
|
+
for (let i = line.length - 1; i >= 0; i--) {
|
|
273
|
+
const char = line[i];
|
|
274
|
+
if (char.value !== ' ' || char.styles.length > 0) {
|
|
275
|
+
lastNonSpace = i;
|
|
276
|
+
break;
|
|
128
277
|
}
|
|
129
|
-
clips.push(nextClip);
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (operation.type === 'unclip') {
|
|
133
|
-
clips.pop();
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (operation.type === 'write') {
|
|
137
|
-
this.applyWriteOperation(output, clips, operation);
|
|
138
278
|
}
|
|
279
|
+
region.styledOutput[y] = line.slice(0, lastNonSpace + 1);
|
|
139
280
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (this.cursorFocusInfo) {
|
|
143
|
-
const { x, y, styledChars, terminalCursorPosition: charIndex, } = this.cursorFocusInfo;
|
|
144
|
-
if (charIndex === undefined) {
|
|
145
|
-
// Use text end (backward compatible)
|
|
146
|
-
const lines = splitStyledCharsByNewline(styledChars);
|
|
147
|
-
const lastLineIndex = lines.length - 1;
|
|
148
|
-
const lastLine = lines[lastLineIndex] ?? [];
|
|
149
|
-
cursorPosition = {
|
|
150
|
-
row: y + lastLineIndex,
|
|
151
|
-
col: lastLineIndex === 0
|
|
152
|
-
? x + styledCharsWidth(lastLine)
|
|
153
|
-
: styledCharsWidth(lastLine),
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
// Use character index to calculate cursor position using StyledChar[]
|
|
158
|
-
const { row, col } = getPositionAtOffset(styledChars, charIndex);
|
|
159
|
-
cursorPosition = {
|
|
160
|
-
row: y + row,
|
|
161
|
-
col: x + col,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
281
|
+
for (const child of region.children) {
|
|
282
|
+
this.trimRegionLines(child);
|
|
164
283
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// Adjust cursor position based on actual output (after trimEnd)
|
|
173
|
-
if (cursorPosition) {
|
|
174
|
-
const lines = generatedOutput.split('\n');
|
|
175
|
-
const cursorLine = lines[cursorPosition.row];
|
|
176
|
-
if (cursorLine !== undefined) {
|
|
177
|
-
const actualLineWidth = styledCharsWidth(toStyledCharacters(cursorLine));
|
|
178
|
-
// Cursor should not go beyond the actual trimmed line width
|
|
179
|
-
cursorPosition.col = Math.min(cursorPosition.col, actualLineWidth);
|
|
284
|
+
}
|
|
285
|
+
clampCursorPosition(region) {
|
|
286
|
+
if (region.cursorPosition) {
|
|
287
|
+
const { row, col } = region.cursorPosition;
|
|
288
|
+
const line = region.lines[row];
|
|
289
|
+
if (line) {
|
|
290
|
+
region.cursorPosition.col = clampCursorColumn(line, col);
|
|
180
291
|
}
|
|
181
292
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
styledOutput: output,
|
|
186
|
-
cursorPosition,
|
|
187
|
-
};
|
|
293
|
+
for (const child of region.children) {
|
|
294
|
+
this.clampCursorPosition(child);
|
|
295
|
+
}
|
|
188
296
|
}
|
|
189
|
-
|
|
190
|
-
for (let
|
|
191
|
-
|
|
192
|
-
|
|
297
|
+
initLines(region, width, height) {
|
|
298
|
+
for (let y = 0; y < height; y++) {
|
|
299
|
+
const row = [];
|
|
300
|
+
for (let x = 0; x < width; x++) {
|
|
301
|
+
row.push({
|
|
193
302
|
type: 'char',
|
|
194
|
-
value,
|
|
303
|
+
value: ' ',
|
|
195
304
|
fullWidth: false,
|
|
196
|
-
styles,
|
|
197
|
-
};
|
|
305
|
+
styles: [],
|
|
306
|
+
});
|
|
198
307
|
}
|
|
308
|
+
region.lines.push(row);
|
|
309
|
+
region.styledOutput.push(row);
|
|
199
310
|
}
|
|
200
311
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
312
|
+
// Helper to apply write immediately
|
|
313
|
+
// eslint-disable-next-line max-params
|
|
314
|
+
applyWrite(x, y, items, transformers, lineIndex, _preserveBackgroundColor, isSelectable) {
|
|
315
|
+
const region = this.getActiveRegion();
|
|
316
|
+
const { lines } = region;
|
|
317
|
+
const bufferWidth = lines[0]?.length ?? 0;
|
|
204
318
|
let chars = typeof items === 'string' ? toStyledCharacters(items) : items;
|
|
205
|
-
const clip =
|
|
319
|
+
const clip = this.getCurrentClip();
|
|
206
320
|
let fromX;
|
|
207
321
|
let toX;
|
|
208
322
|
if (clip) {
|
|
209
|
-
const
|
|
323
|
+
const regionOffset = this.getRegionAbsoluteOffset();
|
|
324
|
+
const clipResult = this.clipChars(chars, x + regionOffset.x, y + regionOffset.y, clip);
|
|
210
325
|
if (!clipResult) {
|
|
211
326
|
return;
|
|
212
327
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
y = clipResult
|
|
216
|
-
|
|
217
|
-
|
|
328
|
+
let absoluteX;
|
|
329
|
+
let absoluteY;
|
|
330
|
+
({ chars, x: absoluteX, y: absoluteY, fromX, toX } = clipResult);
|
|
331
|
+
x = absoluteX - regionOffset.x;
|
|
332
|
+
y = absoluteY - regionOffset.y;
|
|
218
333
|
}
|
|
219
|
-
const currentLine =
|
|
220
|
-
// Line can be missing if `text` is taller than height of pre-initialized `this.output`
|
|
334
|
+
const currentLine = lines[y];
|
|
221
335
|
if (!currentLine) {
|
|
222
336
|
return;
|
|
223
337
|
}
|
|
@@ -230,18 +344,25 @@ export default class Output {
|
|
|
230
344
|
}
|
|
231
345
|
let offsetX = x;
|
|
232
346
|
let relativeX = 0;
|
|
347
|
+
let spanStartX = -1;
|
|
348
|
+
let spanText = '';
|
|
233
349
|
for (const character of chars) {
|
|
234
350
|
const characterWidth = inkCharacterWidth(character.value);
|
|
235
351
|
if (toX !== undefined && relativeX >= toX) {
|
|
236
352
|
break;
|
|
237
353
|
}
|
|
238
354
|
if (fromX === undefined || relativeX >= fromX) {
|
|
239
|
-
if (offsetX >=
|
|
355
|
+
if (offsetX >= bufferWidth) {
|
|
240
356
|
break;
|
|
241
357
|
}
|
|
242
358
|
currentLine[offsetX] = character;
|
|
359
|
+
if (isSelectable) {
|
|
360
|
+
if (spanStartX === -1)
|
|
361
|
+
spanStartX = offsetX;
|
|
362
|
+
spanText += character.value;
|
|
363
|
+
}
|
|
243
364
|
if (characterWidth > 1) {
|
|
244
|
-
this.clearRange(currentLine, { start: offsetX + 1, end: offsetX + characterWidth }, character.styles, '');
|
|
365
|
+
this.clearRange(currentLine, { start: offsetX + 1, end: offsetX + characterWidth }, character.styles, '', bufferWidth);
|
|
245
366
|
}
|
|
246
367
|
offsetX += characterWidth;
|
|
247
368
|
}
|
|
@@ -250,27 +371,45 @@ export default class Output {
|
|
|
250
371
|
relativeX < fromX &&
|
|
251
372
|
relativeX + characterWidth > fromX) {
|
|
252
373
|
const clearLength = relativeX + characterWidth - fromX;
|
|
253
|
-
this.clearRange(currentLine, { start: offsetX, end: offsetX + clearLength }, character.styles, ' ');
|
|
374
|
+
this.clearRange(currentLine, { start: offsetX, end: offsetX + clearLength }, character.styles, ' ', bufferWidth);
|
|
254
375
|
offsetX += clearLength;
|
|
255
376
|
}
|
|
256
377
|
relativeX += characterWidth;
|
|
257
378
|
}
|
|
379
|
+
if (isSelectable && spanStartX !== -1) {
|
|
380
|
+
region.selectableSpans.push({
|
|
381
|
+
y,
|
|
382
|
+
startX: spanStartX,
|
|
383
|
+
endX: offsetX,
|
|
384
|
+
text: spanText,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
258
387
|
if (toX !== undefined) {
|
|
259
388
|
const absoluteToX = x - (fromX ?? 0) + toX;
|
|
260
|
-
this.clearRange(currentLine, { start: offsetX, end: absoluteToX }, [], ' ');
|
|
389
|
+
this.clearRange(currentLine, { start: offsetX, end: absoluteToX }, [], ' ', bufferWidth);
|
|
261
390
|
}
|
|
262
391
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
392
|
+
// eslint-disable-next-line max-params
|
|
393
|
+
clearRange(currentLine, range, styles, value, maxWidth) {
|
|
394
|
+
for (let offset = range.start; offset < range.end; offset++) {
|
|
395
|
+
if (offset >= 0 && offset < maxWidth) {
|
|
396
|
+
currentLine[offset] = {
|
|
397
|
+
type: 'char',
|
|
398
|
+
value,
|
|
399
|
+
fullWidth: false,
|
|
400
|
+
styles,
|
|
401
|
+
};
|
|
271
402
|
}
|
|
272
403
|
}
|
|
273
|
-
|
|
404
|
+
}
|
|
405
|
+
clipChars(chars, x, y, clip) {
|
|
406
|
+
const { x1, x2 } = clip;
|
|
407
|
+
const clipHorizontally = typeof x1 === 'number' && typeof x2 === 'number';
|
|
408
|
+
const width = styledCharsWidth(chars);
|
|
409
|
+
const effectiveY1 = this.getActiveRegion().overflowToBackbuffer
|
|
410
|
+
? -Infinity
|
|
411
|
+
: (clip.y1 ?? -Infinity);
|
|
412
|
+
if (!isRectIntersectingClip({ x1: x, y1: y, x2: x + width, y2: y + 1 }, { ...clip, y1: effectiveY1 })) {
|
|
274
413
|
return undefined;
|
|
275
414
|
}
|
|
276
415
|
let fromX;
|
|
@@ -286,4 +425,199 @@ export default class Output {
|
|
|
286
425
|
return { chars, x, y, fromX, toX };
|
|
287
426
|
}
|
|
288
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Flattens a hierarchy of nested regions into a single 2D array of styled characters
|
|
430
|
+
* that represents the final visual output to be written to the terminal.
|
|
431
|
+
* This effectively renders the nested region tree (much like compositing layers
|
|
432
|
+
* in a web browser) into a single screen buffer.
|
|
433
|
+
*/
|
|
434
|
+
export function flattenRegion(root, options) {
|
|
435
|
+
const { width, height } = root;
|
|
436
|
+
const lines = Array.from({ length: height }, () => Array.from({ length: width }, () => ({
|
|
437
|
+
type: 'char',
|
|
438
|
+
value: ' ',
|
|
439
|
+
fullWidth: false,
|
|
440
|
+
styles: [],
|
|
441
|
+
})));
|
|
442
|
+
composeRegion(root, lines, {
|
|
443
|
+
clip: { x: 0, y: 0, w: width, h: height },
|
|
444
|
+
}, options);
|
|
445
|
+
return lines;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Recursively traverses a Region and its children, drawing its content
|
|
449
|
+
* into the given `targetLines` buffer while applying coordinate offsets
|
|
450
|
+
* and clipping boundaries. Handles scroll offsets, scrollbars, and floating
|
|
451
|
+
* elements like sticky headers.
|
|
452
|
+
*/
|
|
453
|
+
function composeRegion(region, targetLines, { clip, offsetX = 0, offsetY = 0, }, options) {
|
|
454
|
+
const { x, y, width, height, lines, children, stickyHeaders, scrollTop: regionScrollTop, scrollLeft: regionScrollLeft, cursorPosition: regionCursorPosition, } = region;
|
|
455
|
+
const absX = x + offsetX;
|
|
456
|
+
const absY = y + offsetY;
|
|
457
|
+
const myClip = intersectRect(clip, { x: absX, y: absY, w: width, h: height });
|
|
458
|
+
if (!myClip) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const scrollTop = regionScrollTop ?? 0;
|
|
462
|
+
const scrollLeft = regionScrollLeft ?? 0;
|
|
463
|
+
if (regionCursorPosition && options?.context) {
|
|
464
|
+
const cursorX = absX + regionCursorPosition.col - scrollLeft;
|
|
465
|
+
const cursorY = absY + regionCursorPosition.row - scrollTop;
|
|
466
|
+
if (cursorX >= myClip.x &&
|
|
467
|
+
cursorX <= myClip.x + myClip.w &&
|
|
468
|
+
cursorY >= myClip.y &&
|
|
469
|
+
cursorY <= myClip.y + myClip.h) {
|
|
470
|
+
options.context.cursorPosition = { row: cursorY, col: cursorX };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const { x: myClipX, y: myClipY, w: myClipW, h: myClipH } = myClip;
|
|
474
|
+
for (let sy = myClipY; sy < myClipY + myClipH; sy++) {
|
|
475
|
+
const row = targetLines[sy];
|
|
476
|
+
if (!row) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const localY = sy - absY + scrollTop;
|
|
480
|
+
const sourceLine = lines[localY];
|
|
481
|
+
if (!sourceLine) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
for (let sx = myClipX; sx < myClipX + myClipW; sx++) {
|
|
485
|
+
const localX = sx - absX + scrollLeft;
|
|
486
|
+
const char = sourceLine[localX];
|
|
487
|
+
if (char) {
|
|
488
|
+
row[sx] = char;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
for (const child of children) {
|
|
493
|
+
composeRegion(child, targetLines, {
|
|
494
|
+
clip: myClip,
|
|
495
|
+
offsetX: absX - scrollLeft,
|
|
496
|
+
offsetY: absY - scrollTop,
|
|
497
|
+
}, options);
|
|
498
|
+
}
|
|
499
|
+
if (!options?.skipStickyHeaders) {
|
|
500
|
+
for (const header of stickyHeaders) {
|
|
501
|
+
const headerY = header.y + absY; // Absolute Y
|
|
502
|
+
const headerH = header.styledOutput.length;
|
|
503
|
+
for (let i = 0; i < headerH; i++) {
|
|
504
|
+
const sy = headerY + i;
|
|
505
|
+
if (sy < myClipY || sy >= myClipY + myClipH) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const row = targetLines[sy];
|
|
509
|
+
if (!row) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const line = header.styledOutput[i];
|
|
513
|
+
if (!line) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const headerX = header.x + absX;
|
|
517
|
+
const headerW = line.length;
|
|
518
|
+
const hx1 = Math.max(headerX, myClipX);
|
|
519
|
+
const hx2 = Math.min(headerX + headerW, myClipX + myClipW);
|
|
520
|
+
for (let sx = hx1; sx < hx2; sx++) {
|
|
521
|
+
const cx = sx - headerX;
|
|
522
|
+
const char = line[cx];
|
|
523
|
+
if (char) {
|
|
524
|
+
row[sx] = char;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (!options?.skipScrollbars &&
|
|
531
|
+
region.isScrollable &&
|
|
532
|
+
(region.scrollbarVisible ?? true)) {
|
|
533
|
+
const scrollHeight = region.scrollHeight ?? 0;
|
|
534
|
+
const scrollWidth = region.scrollWidth ?? 0;
|
|
535
|
+
const isVerticalScrollbarVisible = (region.isVerticallyScrollable ?? false) && scrollHeight > region.height;
|
|
536
|
+
const isHorizontalScrollbarVisible = (region.isHorizontallyScrollable ?? false) && scrollWidth > region.width;
|
|
537
|
+
if (isVerticalScrollbarVisible) {
|
|
538
|
+
const verticalLayout = calculateScrollbarLayout({
|
|
539
|
+
x: absX,
|
|
540
|
+
y: absY,
|
|
541
|
+
width: region.width,
|
|
542
|
+
height: region.height,
|
|
543
|
+
marginRight: region.marginRight ?? 0,
|
|
544
|
+
marginBottom: region.marginBottom ?? 0,
|
|
545
|
+
clientDimension: region.height,
|
|
546
|
+
scrollDimension: scrollHeight,
|
|
547
|
+
scrollPosition: scrollTop,
|
|
548
|
+
hasOppositeScrollbar: false,
|
|
549
|
+
axis: 'vertical',
|
|
550
|
+
});
|
|
551
|
+
if (verticalLayout) {
|
|
552
|
+
renderScrollbar({
|
|
553
|
+
layout: verticalLayout,
|
|
554
|
+
clip: myClip,
|
|
555
|
+
axis: 'vertical',
|
|
556
|
+
color: region.scrollbarThumbColor,
|
|
557
|
+
setChar(x, y, char) {
|
|
558
|
+
if (y >= 0 &&
|
|
559
|
+
y < targetLines.length &&
|
|
560
|
+
x >= 0 &&
|
|
561
|
+
x < targetLines[0].length) {
|
|
562
|
+
targetLines[y][x] = char;
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (isHorizontalScrollbarVisible) {
|
|
569
|
+
const horizontalLayout = calculateScrollbarLayout({
|
|
570
|
+
x: absX,
|
|
571
|
+
y: absY,
|
|
572
|
+
width: region.width,
|
|
573
|
+
height: region.height,
|
|
574
|
+
marginRight: region.marginRight ?? 0,
|
|
575
|
+
marginBottom: region.marginBottom ?? 0,
|
|
576
|
+
clientDimension: region.width,
|
|
577
|
+
scrollDimension: scrollWidth,
|
|
578
|
+
scrollPosition: scrollLeft,
|
|
579
|
+
hasOppositeScrollbar: isVerticalScrollbarVisible,
|
|
580
|
+
axis: 'horizontal',
|
|
581
|
+
});
|
|
582
|
+
if (horizontalLayout) {
|
|
583
|
+
renderScrollbar({
|
|
584
|
+
layout: horizontalLayout,
|
|
585
|
+
clip: myClip,
|
|
586
|
+
axis: 'horizontal',
|
|
587
|
+
color: region.scrollbarThumbColor,
|
|
588
|
+
setChar(x, y, char) {
|
|
589
|
+
if (y >= 0 &&
|
|
590
|
+
y < targetLines.length &&
|
|
591
|
+
x >= 0 &&
|
|
592
|
+
x < targetLines[0].length) {
|
|
593
|
+
targetLines[y][x] = char;
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
export const extractSelectableText = (spans) => {
|
|
602
|
+
if (spans.length === 0) {
|
|
603
|
+
return '';
|
|
604
|
+
}
|
|
605
|
+
const sortedSpans = [...spans].sort((a, b) => a.y === b.y ? a.startX - b.startX : a.y - b.y);
|
|
606
|
+
let selectableText = '';
|
|
607
|
+
let currentY = sortedSpans[0]?.y ?? 0;
|
|
608
|
+
let currentX = sortedSpans[0]?.startX ?? 0;
|
|
609
|
+
for (const span of sortedSpans) {
|
|
610
|
+
if (span.y > currentY) {
|
|
611
|
+
selectableText += '\n'.repeat(span.y - currentY);
|
|
612
|
+
currentX = 0;
|
|
613
|
+
currentY = span.y;
|
|
614
|
+
}
|
|
615
|
+
if (span.startX > currentX) {
|
|
616
|
+
selectableText += ' '.repeat(span.startX - currentX);
|
|
617
|
+
}
|
|
618
|
+
selectableText += span.text;
|
|
619
|
+
currentX = span.endX;
|
|
620
|
+
}
|
|
621
|
+
return selectableText;
|
|
622
|
+
};
|
|
289
623
|
//# sourceMappingURL=output.js.map
|