@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.
Files changed (111) hide show
  1. package/build/components/App.d.ts +6 -0
  2. package/build/components/App.js +5 -0
  3. package/build/components/App.js.map +1 -1
  4. package/build/components/AppContext.d.ts +36 -1
  5. package/build/components/AppContext.js +7 -2
  6. package/build/components/AppContext.js.map +1 -1
  7. package/build/components/Box.d.ts +26 -2
  8. package/build/components/Box.js +3 -2
  9. package/build/components/Box.js.map +1 -1
  10. package/build/components/StaticRender.d.ts +8 -0
  11. package/build/components/StaticRender.js +11 -0
  12. package/build/components/StaticRender.js.map +1 -0
  13. package/build/debug-log.d.ts +2 -0
  14. package/build/debug-log.js +44 -0
  15. package/build/debug-log.js.map +1 -0
  16. package/build/dom.d.ts +37 -3
  17. package/build/dom.js +19 -3
  18. package/build/dom.js.map +1 -1
  19. package/build/index.d.ts +4 -1
  20. package/build/index.js +3 -1
  21. package/build/index.js.map +1 -1
  22. package/build/ink.d.ts +29 -2
  23. package/build/ink.js +215 -102
  24. package/build/ink.js.map +1 -1
  25. package/build/measure-element.d.ts +20 -1
  26. package/build/measure-element.js +201 -51
  27. package/build/measure-element.js.map +1 -1
  28. package/build/output.d.ts +195 -10
  29. package/build/output.js +494 -160
  30. package/build/output.js.map +1 -1
  31. package/build/reconciler.js +19 -3
  32. package/build/reconciler.js.map +1 -1
  33. package/build/render-background.js +1 -1
  34. package/build/render-background.js.map +1 -1
  35. package/build/render-cached.d.ts +17 -0
  36. package/build/render-cached.js +62 -0
  37. package/build/render-cached.js.map +1 -0
  38. package/build/render-container.d.ts +24 -0
  39. package/build/render-container.js +169 -0
  40. package/build/render-container.js.map +1 -0
  41. package/build/render-node-to-output.d.ts +15 -7
  42. package/build/render-node-to-output.js +123 -485
  43. package/build/render-node-to-output.js.map +1 -1
  44. package/build/render-screen-reader.d.ts +5 -0
  45. package/build/render-screen-reader.js +54 -0
  46. package/build/render-screen-reader.js.map +1 -0
  47. package/build/render-scrollbar.d.ts +22 -0
  48. package/build/render-scrollbar.js +77 -0
  49. package/build/render-scrollbar.js.map +1 -0
  50. package/build/render-sticky.d.ts +56 -0
  51. package/build/render-sticky.js +314 -0
  52. package/build/render-sticky.js.map +1 -0
  53. package/build/render-text-node.d.ts +24 -0
  54. package/build/render-text-node.js +133 -0
  55. package/build/render-text-node.js.map +1 -0
  56. package/build/render.d.ts +39 -0
  57. package/build/render.js +5 -0
  58. package/build/render.js.map +1 -1
  59. package/build/renderer.d.ts +10 -2
  60. package/build/renderer.js +103 -7
  61. package/build/renderer.js.map +1 -1
  62. package/build/replay.d.ts +60 -0
  63. package/build/replay.js +138 -0
  64. package/build/replay.js.map +1 -0
  65. package/build/scroll.js +20 -1
  66. package/build/scroll.js.map +1 -1
  67. package/build/selection.d.ts +9 -0
  68. package/build/selection.js +47 -0
  69. package/build/selection.js.map +1 -1
  70. package/build/serialization.d.ts +28 -0
  71. package/build/serialization.js +267 -0
  72. package/build/serialization.js.map +1 -0
  73. package/build/styles.d.ts +18 -0
  74. package/build/styles.js.map +1 -1
  75. package/build/terminal-buffer.d.ts +53 -0
  76. package/build/terminal-buffer.js +441 -0
  77. package/build/terminal-buffer.js.map +1 -0
  78. package/build/worker/animation-controller.d.ts +72 -0
  79. package/build/worker/animation-controller.js +128 -0
  80. package/build/worker/animation-controller.js.map +1 -0
  81. package/build/worker/ansi-utils.d.ts +16 -0
  82. package/build/worker/ansi-utils.js +40 -0
  83. package/build/worker/ansi-utils.js.map +1 -0
  84. package/build/worker/canvas.d.ts +47 -0
  85. package/build/worker/canvas.js +94 -0
  86. package/build/worker/canvas.js.map +1 -0
  87. package/build/worker/compositor.d.ts +33 -0
  88. package/build/worker/compositor.js +314 -0
  89. package/build/worker/compositor.js.map +1 -0
  90. package/build/worker/platform.d.ts +15 -0
  91. package/build/worker/platform.js +19 -0
  92. package/build/worker/platform.js.map +1 -0
  93. package/build/worker/render-worker.d.ts +112 -0
  94. package/build/worker/render-worker.js +936 -0
  95. package/build/worker/render-worker.js.map +1 -0
  96. package/build/worker/scene-manager.d.ts +26 -0
  97. package/build/worker/scene-manager.js +99 -0
  98. package/build/worker/scene-manager.js.map +1 -0
  99. package/build/worker/scroll-optimizer.d.ts +32 -0
  100. package/build/worker/scroll-optimizer.js +110 -0
  101. package/build/worker/scroll-optimizer.js.map +1 -0
  102. package/build/worker/terminal-writer.d.ts +116 -0
  103. package/build/worker/terminal-writer.js +722 -0
  104. package/build/worker/terminal-writer.js.map +1 -0
  105. package/build/worker/worker-entry.d.ts +6 -0
  106. package/build/worker/worker-entry.js +130 -0
  107. package/build/worker/worker-entry.js.map +1 -0
  108. package/build/wrap-text.d.ts +6 -0
  109. package/build/wrap-text.js +120 -0
  110. package/build/wrap-text.js.map +1 -0
  111. 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, splitStyledCharsByNewline, getPositionAtOffset, } from './measure-text.js';
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
- operations = [];
7
- cursorFocusInfo = undefined;
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
- // Track cursor target position for terminal cursor synchronization
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 styledChars = typeof items === 'string' ? toStyledCharacters(items) : items;
22
- this.cursorFocusInfo = {
23
- x,
24
- y,
25
- styledChars,
26
- terminalCursorPosition,
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 === 0) {
30
- return;
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
- // Initialize output array with a specific set of rows, so that margin/padding at the bottom is preserved
85
- const output = [];
86
- for (let y = 0; y < this.height; y++) {
87
- const row = [];
88
- for (let x = 0; x < this.width; x++) {
89
- row.push({
90
- type: 'char',
91
- value: ' ',
92
- fullWidth: false,
93
- styles: [],
94
- });
95
- }
96
- output.push(row);
97
- }
98
- const clips = [];
99
- for (const operation of this.operations) {
100
- if (operation.type === 'clip') {
101
- const previousClip = clips.at(-1);
102
- const nextClip = { ...operation.clip };
103
- if (previousClip) {
104
- nextClip.x1 =
105
- previousClip.x1 === undefined
106
- ? nextClip.x1
107
- : nextClip.x1 === undefined
108
- ? previousClip.x1
109
- : Math.max(previousClip.x1, nextClip.x1);
110
- nextClip.x2 =
111
- previousClip.x2 === undefined
112
- ? nextClip.x2
113
- : nextClip.x2 === undefined
114
- ? previousClip.x2
115
- : Math.min(previousClip.x2, nextClip.x2);
116
- nextClip.y1 =
117
- previousClip.y1 === undefined
118
- ? nextClip.y1
119
- : nextClip.y1 === undefined
120
- ? previousClip.y1
121
- : Math.max(previousClip.y1, nextClip.y1);
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
- // Calculate cursor position from cursor target (if exists)
141
- let cursorPosition;
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
- const generatedOutput = output
166
- .map(line => {
167
- // See https://github.com/vadimdemedes/ink/pull/564#issuecomment-1637022742
168
- const lineWithoutEmptyItems = line.filter(item => item !== undefined);
169
- return styledCharsToString(lineWithoutEmptyItems).trimEnd();
170
- })
171
- .join('\n');
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
- return {
183
- output: generatedOutput,
184
- height: output.length,
185
- styledOutput: output,
186
- cursorPosition,
187
- };
293
+ for (const child of region.children) {
294
+ this.clampCursorPosition(child);
295
+ }
188
296
  }
189
- clearRange(currentLine, range, styles, value = ' ') {
190
- for (let offset = range.start; offset < range.end; offset++) {
191
- if (offset >= 0 && offset < this.width) {
192
- currentLine[offset] = {
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
- applyWriteOperation(output, clips, operation) {
202
- const { transformers, lineIndex = 0 } = operation;
203
- let { x, y, items } = operation;
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 = clips.at(-1);
319
+ const clip = this.getCurrentClip();
206
320
  let fromX;
207
321
  let toX;
208
322
  if (clip) {
209
- const clipResult = this.clipChars(chars, x, y, clip);
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
- chars = clipResult.chars;
214
- x = clipResult.x;
215
- y = clipResult.y;
216
- fromX = clipResult.fromX;
217
- toX = clipResult.toX;
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 = output[y];
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 >= this.width) {
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
- clipChars(chars, x, y, clip) {
264
- const { x1, x2, y1, y2 } = clip;
265
- const clipHorizontally = typeof x1 === 'number' && typeof x2 === 'number';
266
- const clipVertically = typeof y1 === 'number' && typeof y2 === 'number';
267
- if (clipHorizontally) {
268
- const width = styledCharsWidth(chars);
269
- if (x + width < clip.x1 || x > clip.x2) {
270
- return undefined;
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
- if (clipVertically && (y < clip.y1 || y >= clip.y2)) {
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