@kingkoo1985/ink 6.6.6

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 (225) hide show
  1. package/build/colorize.d.ts +4 -0
  2. package/build/colorize.js +59 -0
  3. package/build/colorize.js.map +1 -0
  4. package/build/components/AccessibilityContext.d.ts +3 -0
  5. package/build/components/AccessibilityContext.js +5 -0
  6. package/build/components/AccessibilityContext.js.map +1 -0
  7. package/build/components/App.d.ts +68 -0
  8. package/build/components/App.js +290 -0
  9. package/build/components/App.js.map +1 -0
  10. package/build/components/AppContext.d.ts +52 -0
  11. package/build/components/AppContext.js +16 -0
  12. package/build/components/AppContext.js.map +1 -0
  13. package/build/components/BackgroundContext.d.ts +4 -0
  14. package/build/components/BackgroundContext.js +3 -0
  15. package/build/components/BackgroundContext.js.map +1 -0
  16. package/build/components/Box.d.ts +171 -0
  17. package/build/components/Box.js +40 -0
  18. package/build/components/Box.js.map +1 -0
  19. package/build/components/ErrorOverview.d.ts +6 -0
  20. package/build/components/ErrorOverview.js +84 -0
  21. package/build/components/ErrorOverview.js.map +1 -0
  22. package/build/components/FocusContext.d.ts +16 -0
  23. package/build/components/FocusContext.js +16 -0
  24. package/build/components/FocusContext.js.map +1 -0
  25. package/build/components/Newline.d.ts +13 -0
  26. package/build/components/Newline.js +8 -0
  27. package/build/components/Newline.js.map +1 -0
  28. package/build/components/Spacer.d.ts +7 -0
  29. package/build/components/Spacer.js +11 -0
  30. package/build/components/Spacer.js.map +1 -0
  31. package/build/components/Static.d.ts +24 -0
  32. package/build/components/Static.js +29 -0
  33. package/build/components/Static.js.map +1 -0
  34. package/build/components/StaticRender.d.ts +8 -0
  35. package/build/components/StaticRender.js +19 -0
  36. package/build/components/StaticRender.js.map +1 -0
  37. package/build/components/StderrContext.d.ts +15 -0
  38. package/build/components/StderrContext.js +12 -0
  39. package/build/components/StderrContext.js.map +1 -0
  40. package/build/components/StdinContext.d.ts +22 -0
  41. package/build/components/StdinContext.js +16 -0
  42. package/build/components/StdinContext.js.map +1 -0
  43. package/build/components/StdoutContext.d.ts +15 -0
  44. package/build/components/StdoutContext.js +12 -0
  45. package/build/components/StdoutContext.js.map +1 -0
  46. package/build/components/Text.d.ts +63 -0
  47. package/build/components/Text.js +50 -0
  48. package/build/components/Text.js.map +1 -0
  49. package/build/components/Transform.d.ts +16 -0
  50. package/build/components/Transform.js +15 -0
  51. package/build/components/Transform.js.map +1 -0
  52. package/build/data-limited-lru-map.d.ts +20 -0
  53. package/build/data-limited-lru-map.js +65 -0
  54. package/build/data-limited-lru-map.js.map +1 -0
  55. package/build/debug-log.d.ts +2 -0
  56. package/build/debug-log.js +44 -0
  57. package/build/debug-log.js.map +1 -0
  58. package/build/devtools-window-polyfill.d.ts +1 -0
  59. package/build/devtools-window-polyfill.js +65 -0
  60. package/build/devtools-window-polyfill.js.map +1 -0
  61. package/build/devtools.d.ts +1 -0
  62. package/build/devtools.js +8 -0
  63. package/build/devtools.js.map +1 -0
  64. package/build/dom.d.ts +114 -0
  65. package/build/dom.js +169 -0
  66. package/build/dom.js.map +1 -0
  67. package/build/get-max-width.d.ts +3 -0
  68. package/build/get-max-width.js +10 -0
  69. package/build/get-max-width.js.map +1 -0
  70. package/build/hooks/use-app.d.ts +5 -0
  71. package/build/hooks/use-app.js +8 -0
  72. package/build/hooks/use-app.js.map +1 -0
  73. package/build/hooks/use-focus-manager.d.ts +28 -0
  74. package/build/hooks/use-focus-manager.js +17 -0
  75. package/build/hooks/use-focus-manager.js.map +1 -0
  76. package/build/hooks/use-focus.d.ts +29 -0
  77. package/build/hooks/use-focus.js +42 -0
  78. package/build/hooks/use-focus.js.map +1 -0
  79. package/build/hooks/use-input.d.ts +93 -0
  80. package/build/hooks/use-input.js +92 -0
  81. package/build/hooks/use-input.js.map +1 -0
  82. package/build/hooks/use-is-screen-reader-enabled.d.ts +5 -0
  83. package/build/hooks/use-is-screen-reader-enabled.js +11 -0
  84. package/build/hooks/use-is-screen-reader-enabled.js.map +1 -0
  85. package/build/hooks/use-stderr.d.ts +5 -0
  86. package/build/hooks/use-stderr.js +8 -0
  87. package/build/hooks/use-stderr.js.map +1 -0
  88. package/build/hooks/use-stdin.d.ts +5 -0
  89. package/build/hooks/use-stdin.js +8 -0
  90. package/build/hooks/use-stdin.js.map +1 -0
  91. package/build/hooks/use-stdout.d.ts +5 -0
  92. package/build/hooks/use-stdout.js +8 -0
  93. package/build/hooks/use-stdout.js.map +1 -0
  94. package/build/index.d.ts +38 -0
  95. package/build/index.js +27 -0
  96. package/build/index.js.map +1 -0
  97. package/build/ink.d.ts +110 -0
  98. package/build/ink.js +576 -0
  99. package/build/ink.js.map +1 -0
  100. package/build/instances.d.ts +3 -0
  101. package/build/instances.js +8 -0
  102. package/build/instances.js.map +1 -0
  103. package/build/layout.d.ts +18 -0
  104. package/build/layout.js +54 -0
  105. package/build/layout.js.map +1 -0
  106. package/build/log-update.d.ts +28 -0
  107. package/build/log-update.js +529 -0
  108. package/build/log-update.js.map +1 -0
  109. package/build/measure-element.d.ts +119 -0
  110. package/build/measure-element.js +825 -0
  111. package/build/measure-element.js.map +1 -0
  112. package/build/measure-text.d.ts +50 -0
  113. package/build/measure-text.js +237 -0
  114. package/build/measure-text.js.map +1 -0
  115. package/build/output.d.ts +242 -0
  116. package/build/output.js +607 -0
  117. package/build/output.js.map +1 -0
  118. package/build/parse-keypress.d.ts +14 -0
  119. package/build/parse-keypress.js +225 -0
  120. package/build/parse-keypress.js.map +1 -0
  121. package/build/reconciler.d.ts +4 -0
  122. package/build/reconciler.js +326 -0
  123. package/build/reconciler.js.map +1 -0
  124. package/build/render-background.d.ts +4 -0
  125. package/build/render-background.js +37 -0
  126. package/build/render-background.js.map +1 -0
  127. package/build/render-border.d.ts +4 -0
  128. package/build/render-border.js +81 -0
  129. package/build/render-border.js.map +1 -0
  130. package/build/render-cached.d.ts +18 -0
  131. package/build/render-cached.js +66 -0
  132. package/build/render-cached.js.map +1 -0
  133. package/build/render-container.d.ts +27 -0
  134. package/build/render-container.js +169 -0
  135. package/build/render-container.js.map +1 -0
  136. package/build/render-node-to-output.d.ts +32 -0
  137. package/build/render-node-to-output.js +177 -0
  138. package/build/render-node-to-output.js.map +1 -0
  139. package/build/render-screen-reader.d.ts +5 -0
  140. package/build/render-screen-reader.js +54 -0
  141. package/build/render-screen-reader.js.map +1 -0
  142. package/build/render-scrollbar.d.ts +23 -0
  143. package/build/render-scrollbar.js +70 -0
  144. package/build/render-scrollbar.js.map +1 -0
  145. package/build/render-sticky.d.ts +53 -0
  146. package/build/render-sticky.js +317 -0
  147. package/build/render-sticky.js.map +1 -0
  148. package/build/render-text-node.d.ts +20 -0
  149. package/build/render-text-node.js +155 -0
  150. package/build/render-text-node.js.map +1 -0
  151. package/build/render.d.ts +165 -0
  152. package/build/render.js +60 -0
  153. package/build/render.js.map +1 -0
  154. package/build/renderer.d.ts +24 -0
  155. package/build/renderer.js +292 -0
  156. package/build/renderer.js.map +1 -0
  157. package/build/replay.d.ts +59 -0
  158. package/build/replay.js +128 -0
  159. package/build/replay.js.map +1 -0
  160. package/build/resize-observer.d.ts +24 -0
  161. package/build/resize-observer.js +102 -0
  162. package/build/resize-observer.js.map +1 -0
  163. package/build/scroll.d.ts +11 -0
  164. package/build/scroll.js +123 -0
  165. package/build/scroll.js.map +1 -0
  166. package/build/selection.d.ts +52 -0
  167. package/build/selection.js +359 -0
  168. package/build/selection.js.map +1 -0
  169. package/build/serialization.d.ts +25 -0
  170. package/build/serialization.js +224 -0
  171. package/build/serialization.js.map +1 -0
  172. package/build/squash-text-nodes.d.ts +16 -0
  173. package/build/squash-text-nodes.js +58 -0
  174. package/build/squash-text-nodes.js.map +1 -0
  175. package/build/styled-line.d.ts +58 -0
  176. package/build/styled-line.js +629 -0
  177. package/build/styled-line.js.map +1 -0
  178. package/build/styles.d.ts +286 -0
  179. package/build/styles.js +257 -0
  180. package/build/styles.js.map +1 -0
  181. package/build/terminal-buffer.d.ts +57 -0
  182. package/build/terminal-buffer.js +507 -0
  183. package/build/terminal-buffer.js.map +1 -0
  184. package/build/text-wrap.d.ts +12 -0
  185. package/build/text-wrap.js +154 -0
  186. package/build/text-wrap.js.map +1 -0
  187. package/build/tokenize.d.ts +47 -0
  188. package/build/tokenize.js +419 -0
  189. package/build/tokenize.js.map +1 -0
  190. package/build/vertical-gap.d.ts +17 -0
  191. package/build/vertical-gap.js +20 -0
  192. package/build/vertical-gap.js.map +1 -0
  193. package/build/worker/animation-controller.d.ts +72 -0
  194. package/build/worker/animation-controller.js +128 -0
  195. package/build/worker/animation-controller.js.map +1 -0
  196. package/build/worker/ansi-utils.d.ts +16 -0
  197. package/build/worker/ansi-utils.js +40 -0
  198. package/build/worker/ansi-utils.js.map +1 -0
  199. package/build/worker/canvas.d.ts +49 -0
  200. package/build/worker/canvas.js +90 -0
  201. package/build/worker/canvas.js.map +1 -0
  202. package/build/worker/compositor.d.ts +33 -0
  203. package/build/worker/compositor.js +308 -0
  204. package/build/worker/compositor.js.map +1 -0
  205. package/build/worker/platform.d.ts +15 -0
  206. package/build/worker/platform.js +19 -0
  207. package/build/worker/platform.js.map +1 -0
  208. package/build/worker/render-worker.d.ts +112 -0
  209. package/build/worker/render-worker.js +944 -0
  210. package/build/worker/render-worker.js.map +1 -0
  211. package/build/worker/scene-manager.d.ts +26 -0
  212. package/build/worker/scene-manager.js +109 -0
  213. package/build/worker/scene-manager.js.map +1 -0
  214. package/build/worker/scroll-optimizer.d.ts +32 -0
  215. package/build/worker/scroll-optimizer.js +110 -0
  216. package/build/worker/scroll-optimizer.js.map +1 -0
  217. package/build/worker/terminal-writer.d.ts +116 -0
  218. package/build/worker/terminal-writer.js +708 -0
  219. package/build/worker/terminal-writer.js.map +1 -0
  220. package/build/worker/worker-entry.d.ts +6 -0
  221. package/build/worker/worker-entry.js +130 -0
  222. package/build/worker/worker-entry.js.map +1 -0
  223. package/license +9 -0
  224. package/package.json +208 -0
  225. package/readme.md +2353 -0
@@ -0,0 +1,944 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import fs from 'node:fs';
7
+ import process from 'node:process';
8
+ import ansiEscapes from 'ansi-escapes';
9
+ import { debugLog, clearDebugLog } from '../debug-log.js';
10
+ import { regionLayoutProperties, copyRegionProperty, treesEqual, } from '../output.js';
11
+ import { Serializer } from '../serialization.js';
12
+ import { saveReplay, createHumanReadableDump, serializeReplayUpdate, } from '../replay.js';
13
+ import { TerminalWriter, rainbowColors, } from './terminal-writer.js';
14
+ import { Canvas } from './canvas.js';
15
+ import { SceneManager } from './scene-manager.js';
16
+ import { AnimationController } from './animation-controller.js';
17
+ import { Compositor } from './compositor.js';
18
+ import { ScrollOptimizer } from './scroll-optimizer.js';
19
+ const defaultAnimationInterval = 1;
20
+ const defaultMaxScrollbackLength = 1000;
21
+ export const debugWorker = false;
22
+ const clearDebugLogPerFrame = false;
23
+ /**
24
+ * Core renderer that composes together scrollable blocks of styled content.
25
+ */
26
+ export class TerminalBufferWorker {
27
+ columns;
28
+ rows;
29
+ frameIndex = 0;
30
+ debugRainbowEnabled = false;
31
+ isAlternateBufferEnabled = false;
32
+ stickyHeadersInBackbuffer = false;
33
+ animatedScroll = false;
34
+ animationInterval = defaultAnimationInterval;
35
+ backbufferUpdateDelay = 1000;
36
+ maxScrollbackLength = defaultMaxScrollbackLength;
37
+ forceScrollToBottomOnBackbufferRefresh = false;
38
+ resized = false;
39
+ cursorPosition;
40
+ forceNextRender = false;
41
+ isRecording = false;
42
+ recordingFilename = '';
43
+ recordedFrames = [];
44
+ recordingStartTime = 0;
45
+ // Ground truth on what lines should be rendered (composed frame)
46
+ screen = [];
47
+ backbuffer = [];
48
+ renderPromise;
49
+ sceneManager = new SceneManager();
50
+ animationController;
51
+ scrollOptimizer = new ScrollOptimizer();
52
+ primaryTerminalWriter;
53
+ alternateTerminalWriter;
54
+ get terminalWriter() {
55
+ return this.isAlternateBufferEnabled
56
+ ? this.alternateTerminalWriter
57
+ : this.primaryTerminalWriter;
58
+ }
59
+ get backbufferDirty() {
60
+ return this.terminalWriter.backbufferDirty;
61
+ }
62
+ set backbufferDirty(value) {
63
+ this.terminalWriter.backbufferDirty = value;
64
+ }
65
+ get backbufferDirtyCurrentFrame() {
66
+ return this.terminalWriter.backbufferDirtyCurrentFrame;
67
+ }
68
+ set backbufferDirtyCurrentFrame(value) {
69
+ this.terminalWriter.backbufferDirtyCurrentFrame = value;
70
+ }
71
+ constructor(columns, rows, options) {
72
+ this.columns = columns;
73
+ this.rows = rows;
74
+ const stdout = options?.stdout ?? process.stdout;
75
+ this.primaryTerminalWriter = new TerminalWriter(columns, rows, stdout);
76
+ this.alternateTerminalWriter = new TerminalWriter(columns, rows, stdout);
77
+ this.primaryTerminalWriter.writeRaw(ansiEscapes.cursorHide);
78
+ this.alternateTerminalWriter.writeRaw(ansiEscapes.cursorHide);
79
+ this.debugRainbowEnabled =
80
+ options?.debugRainbowEnabled ?? this.debugRainbowEnabled;
81
+ this.isAlternateBufferEnabled =
82
+ options?.isAlternateBufferEnabled ?? this.isAlternateBufferEnabled;
83
+ this.stickyHeadersInBackbuffer =
84
+ options?.stickyHeadersInBackbuffer ?? this.stickyHeadersInBackbuffer;
85
+ this.animatedScroll = options?.animatedScroll ?? this.animatedScroll;
86
+ this.animationInterval =
87
+ options?.animationInterval ?? this.animationInterval;
88
+ this.backbufferUpdateDelay =
89
+ options?.backbufferUpdateDelay ?? this.backbufferUpdateDelay;
90
+ this.maxScrollbackLength =
91
+ options?.maxScrollbackLength ?? this.maxScrollbackLength;
92
+ this.forceScrollToBottomOnBackbufferRefresh =
93
+ options?.forceScrollToBottomOnBackbufferRefresh ??
94
+ this.forceScrollToBottomOnBackbufferRefresh;
95
+ this.primaryTerminalWriter.maxScrollbackLength = this.maxScrollbackLength;
96
+ this.alternateTerminalWriter.maxScrollbackLength = this.maxScrollbackLength;
97
+ this.primaryTerminalWriter.forceScrollToBottomOnBackbufferRefresh =
98
+ this.forceScrollToBottomOnBackbufferRefresh;
99
+ this.alternateTerminalWriter.forceScrollToBottomOnBackbufferRefresh =
100
+ this.forceScrollToBottomOnBackbufferRefresh;
101
+ if (this.isAlternateBufferEnabled) {
102
+ this.alternateTerminalWriter.writeRaw(ansiEscapes.enterAlternativeScreen);
103
+ }
104
+ this.animationController = new AnimationController({
105
+ interval: this.animationInterval,
106
+ onTick: () => {
107
+ this.tickAnimation();
108
+ },
109
+ });
110
+ }
111
+ async waitForIdle() {
112
+ await this.flushPendingRender();
113
+ if (this.animatedScroll) {
114
+ await this.animationController.waitForIdle();
115
+ }
116
+ if (this.renderPromise) {
117
+ await this.renderPromise;
118
+ }
119
+ }
120
+ getExpectedState() {
121
+ return this.terminalWriter.getExpectedState();
122
+ }
123
+ getSceneManager() {
124
+ return this.sceneManager;
125
+ }
126
+ startRecording(filename) {
127
+ this.isRecording = true;
128
+ this.recordingFilename = filename;
129
+ this.recordedFrames = [];
130
+ this.recordingStartTime = Date.now();
131
+ const { root } = this.sceneManager;
132
+ if (root) {
133
+ const serializer = new Serializer();
134
+ const updates = this.serializeCurrentRegions(serializer);
135
+ this.recordedFrames.push({
136
+ tree: structuredClone(root),
137
+ updates: updates.map(u => serializeReplayUpdate(u)),
138
+ cursorPosition: this.cursorPosition,
139
+ timestamp: 0,
140
+ });
141
+ }
142
+ }
143
+ stopRecording() {
144
+ if (!this.isRecording)
145
+ return;
146
+ const data = {
147
+ type: 'sequence',
148
+ columns: this.columns,
149
+ rows: this.rows,
150
+ frames: this.recordedFrames,
151
+ };
152
+ saveReplay(data, this.recordingFilename);
153
+ this.isRecording = false;
154
+ this.recordedFrames = [];
155
+ }
156
+ dumpCurrentFrame(filename) {
157
+ const { root } = this.sceneManager;
158
+ if (!root)
159
+ return;
160
+ const serializer = new Serializer();
161
+ const updates = this.serializeCurrentRegions(serializer);
162
+ const data = {
163
+ type: 'single',
164
+ columns: this.columns,
165
+ rows: this.rows,
166
+ frames: [
167
+ {
168
+ tree: root,
169
+ updates: updates.map(u => serializeReplayUpdate(u)),
170
+ cursorPosition: this.cursorPosition,
171
+ timestamp: 0,
172
+ },
173
+ ],
174
+ };
175
+ saveReplay(data, filename);
176
+ const loadedData = {
177
+ type: 'single',
178
+ columns: this.columns,
179
+ rows: this.rows,
180
+ frames: [
181
+ {
182
+ tree: root,
183
+ updates,
184
+ cursorPosition: this.cursorPosition,
185
+ timestamp: 0,
186
+ },
187
+ ],
188
+ };
189
+ const dumpText = createHumanReadableDump(loadedData);
190
+ fs.writeFileSync(filename + '.dump.txt', dumpText);
191
+ }
192
+ updateOptions(options) {
193
+ if (options.isAlternateBufferEnabled !== undefined &&
194
+ this.isAlternateBufferEnabled !== options.isAlternateBufferEnabled) {
195
+ // Flush current writer before switching
196
+ this.terminalWriter.flush();
197
+ if (this.terminalWriter.fullRenderTimeout) {
198
+ clearTimeout(this.terminalWriter.fullRenderTimeout);
199
+ this.terminalWriter.fullRenderTimeout = undefined;
200
+ }
201
+ if (options.isAlternateBufferEnabled) {
202
+ this.primaryTerminalWriter.stdout.write(ansiEscapes.enterAlternativeScreen);
203
+ this.isAlternateBufferEnabled = true;
204
+ // The newly active alternate buffer is effectively blank
205
+ this.terminalWriter.clear();
206
+ }
207
+ else {
208
+ this.alternateTerminalWriter.stdout.write(ansiEscapes.exitAlternativeScreen);
209
+ this.isAlternateBufferEnabled = false;
210
+ // When returning to the primary buffer, we don't clear it (to preserve history/static output)
211
+ // but we mark it as potentially having an unknown cursor position and tainted lines.
212
+ this.terminalWriter.unkownCursorLocation();
213
+ this.terminalWriter.taintScreen();
214
+ this.terminalWriter.isTainted = true;
215
+ }
216
+ this.forceNextRender = true;
217
+ }
218
+ if (options.stickyHeadersInBackbuffer !== undefined &&
219
+ this.stickyHeadersInBackbuffer !== options.stickyHeadersInBackbuffer) {
220
+ this.stickyHeadersInBackbuffer = options.stickyHeadersInBackbuffer;
221
+ this.forceNextRender = true;
222
+ }
223
+ if (options.animatedScroll !== undefined &&
224
+ this.animatedScroll !== options.animatedScroll) {
225
+ this.animatedScroll = options.animatedScroll;
226
+ if (this.animatedScroll) {
227
+ this.animationController.start();
228
+ }
229
+ else {
230
+ this.animationController.stop();
231
+ }
232
+ }
233
+ if (options.animationInterval !== undefined &&
234
+ this.animationInterval !== options.animationInterval) {
235
+ this.animationInterval = options.animationInterval;
236
+ this.animationController.stop();
237
+ this.animationController.updateInterval(this.animationInterval);
238
+ if (this.animatedScroll) {
239
+ this.animationController.start();
240
+ }
241
+ }
242
+ if (options.backbufferUpdateDelay !== undefined) {
243
+ this.backbufferUpdateDelay = options.backbufferUpdateDelay;
244
+ }
245
+ if (options.maxScrollbackLength !== undefined) {
246
+ this.maxScrollbackLength = options.maxScrollbackLength;
247
+ this.primaryTerminalWriter.maxScrollbackLength = this.maxScrollbackLength;
248
+ this.alternateTerminalWriter.maxScrollbackLength =
249
+ this.maxScrollbackLength;
250
+ }
251
+ if (options.forceScrollToBottomOnBackbufferRefresh !== undefined &&
252
+ this.forceScrollToBottomOnBackbufferRefresh !==
253
+ options.forceScrollToBottomOnBackbufferRefresh) {
254
+ this.forceScrollToBottomOnBackbufferRefresh =
255
+ options.forceScrollToBottomOnBackbufferRefresh;
256
+ this.primaryTerminalWriter.forceScrollToBottomOnBackbufferRefresh =
257
+ this.forceScrollToBottomOnBackbufferRefresh;
258
+ this.alternateTerminalWriter.forceScrollToBottomOnBackbufferRefresh =
259
+ this.forceScrollToBottomOnBackbufferRefresh;
260
+ }
261
+ }
262
+ update(tree, updates, cursorPosition) {
263
+ const previousCursorPosition = this.cursorPosition;
264
+ this.cursorPosition = cursorPosition;
265
+ const treeChanged = !this.sceneManager.root || !treesEqual(this.sceneManager.root, tree);
266
+ if (this.isRecording) {
267
+ this.recordedFrames.push({
268
+ tree,
269
+ updates: updates.map(u => serializeReplayUpdate(u)),
270
+ cursorPosition,
271
+ timestamp: Date.now() - this.recordingStartTime,
272
+ });
273
+ }
274
+ if (this.animatedScroll) {
275
+ if (updates.length > 0) {
276
+ if (debugWorker) {
277
+ debugLog(`[RENDER-WORKER] Interrupting animation for jump\n`);
278
+ }
279
+ this.animationController.jumpToTargets(this.sceneManager.regions);
280
+ }
281
+ this.animationController.start();
282
+ }
283
+ this.sceneManager.update(tree, updates, {
284
+ animatedScroll: this.animatedScroll,
285
+ onScrollUpdate: (id, scrollTop, isNew) => {
286
+ if (this.animatedScroll && !isNew) {
287
+ this.animationController.setTargetScrollTop(id, scrollTop);
288
+ }
289
+ else {
290
+ const region = this.sceneManager.getRegion(id);
291
+ if (region) {
292
+ region.scrollTop = scrollTop;
293
+ if (this.animatedScroll) {
294
+ this.animationController.setTargetScrollTop(id, scrollTop);
295
+ }
296
+ }
297
+ }
298
+ },
299
+ onRegionDeleted: id => {
300
+ this.animationController.deleteTargetScrollTop(id);
301
+ this.scrollOptimizer.resetTracking(id);
302
+ },
303
+ });
304
+ // Track regionWasAtEnd for scrollbars
305
+ for (const update of updates) {
306
+ const region = this.sceneManager.getRegion(update.id);
307
+ if (region) {
308
+ const currentEffectiveScrollTop = this.animationController.getTargetScrollTop(region.id) ??
309
+ region.scrollTop ??
310
+ 0;
311
+ const wasAtEnd = currentEffectiveScrollTop >=
312
+ (region.scrollHeight ?? 0) - (region.height ?? 0);
313
+ this.sceneManager.regionWasAtEnd.set(region.id, wasAtEnd);
314
+ }
315
+ }
316
+ // Check backbuffer dirty
317
+ const rootRegion = this.sceneManager.getRootRegion();
318
+ if (rootRegion) {
319
+ const cameraY = Math.max(0, rootRegion.height - this.rows);
320
+ if (!this.isAlternateBufferEnabled) {
321
+ const maxPushedRoot = this.scrollOptimizer.maxRegionScrollTops.get(rootRegion.id) ?? 0;
322
+ if (cameraY < maxPushedRoot) {
323
+ this.terminalWriter.backbufferScrolledIncorrectly = true;
324
+ this.terminalWriter.backbufferDirtyCurrentFrame = true;
325
+ }
326
+ }
327
+ for (const update of updates) {
328
+ const region = this.sceneManager.getRegion(update.id);
329
+ if (region && update.lines) {
330
+ const scrollTop = region.scrollTop ?? 0;
331
+ for (const chunk of update.lines.updates) {
332
+ if (region.overflowToBackbuffer && chunk.start < scrollTop) {
333
+ this.terminalWriter.backbufferDirty = true;
334
+ this.terminalWriter.backbufferDirtyCurrentFrame = true;
335
+ }
336
+ const absStart = region.y + chunk.start;
337
+ if (absStart < cameraY) {
338
+ this.terminalWriter.backbufferDirty = true;
339
+ this.terminalWriter.backbufferDirtyCurrentFrame = true;
340
+ }
341
+ }
342
+ }
343
+ if (!this.isAlternateBufferEnabled &&
344
+ region &&
345
+ update.scrollTop !== undefined &&
346
+ region.overflowToBackbuffer) {
347
+ const maxPushed = this.scrollOptimizer.maxRegionScrollTops.get(region.id) ?? 0;
348
+ if (update.scrollTop < maxPushed) {
349
+ this.terminalWriter.backbufferScrolledIncorrectly = true;
350
+ this.terminalWriter.backbufferDirtyCurrentFrame = true;
351
+ }
352
+ }
353
+ }
354
+ }
355
+ const cursorChanged = cursorPosition !== previousCursorPosition &&
356
+ (!cursorPosition ||
357
+ !previousCursorPosition ||
358
+ cursorPosition.row !== previousCursorPosition.row ||
359
+ cursorPosition.col !== previousCursorPosition.col);
360
+ const shouldRender = updates.length > 0 ||
361
+ cursorChanged ||
362
+ treeChanged ||
363
+ this.terminalWriter.backbufferDirty ||
364
+ this.forceNextRender;
365
+ return shouldRender;
366
+ }
367
+ resize(columns, rows) {
368
+ if (this.columns === columns && this.rows === rows) {
369
+ return;
370
+ }
371
+ this.columns = columns;
372
+ this.rows = rows;
373
+ if (debugWorker) {
374
+ debugLog(`XXXXX [RENDER-WORKER] Resize to ${columns}x${rows}\n`);
375
+ }
376
+ this.primaryTerminalWriter.resize(columns, rows);
377
+ this.alternateTerminalWriter.resize(columns, rows);
378
+ this.terminalWriter.backbufferDirtyCurrentFrame = true;
379
+ this.resized = true;
380
+ void this.render();
381
+ }
382
+ async fullRender() {
383
+ if (clearDebugLogPerFrame) {
384
+ clearDebugLog();
385
+ }
386
+ if (this.terminalWriter.fullRenderTimeout) {
387
+ clearTimeout(this.terminalWriter.fullRenderTimeout);
388
+ this.terminalWriter.fullRenderTimeout = undefined;
389
+ }
390
+ if (!this.terminalWriter.backbufferDirty) {
391
+ await this.render();
392
+ return;
393
+ }
394
+ if (debugWorker) {
395
+ debugLog(`XXXXX [RENDER-WORKER] True full render triggered\n`);
396
+ }
397
+ this.terminalWriter.backbufferDirty = false;
398
+ this.terminalWriter.backbufferScrolledIncorrectly = false;
399
+ this.terminalWriter.backbufferDirtyCurrentFrame = false;
400
+ this.composeScene(true);
401
+ const rootRegion = this.sceneManager.getRootRegion();
402
+ const cameraY = rootRegion ? this.getCameraY(rootRegion) : 0;
403
+ this.syncCursor(cameraY);
404
+ this.terminalWriter.clear();
405
+ this.terminalWriter.writeLines([...this.backbuffer, ...this.screen]);
406
+ this.updateTrackingMaps(rootRegion, cameraY, true);
407
+ this.terminalWriter.finish();
408
+ this.terminalWriter.flush();
409
+ this.terminalWriter.validateLinesConsistent(this.screen);
410
+ this.logScene();
411
+ }
412
+ async render() {
413
+ const renderTask = this._render();
414
+ this.renderPromise = renderTask;
415
+ try {
416
+ await renderTask;
417
+ }
418
+ finally {
419
+ if (this.renderPromise === renderTask) {
420
+ this.renderPromise = undefined;
421
+ }
422
+ }
423
+ }
424
+ async flushPendingRender() {
425
+ if (this.terminalWriter.fullRenderTimeout) {
426
+ clearTimeout(this.terminalWriter.fullRenderTimeout);
427
+ this.terminalWriter.fullRenderTimeout = undefined;
428
+ await this.fullRender();
429
+ }
430
+ }
431
+ done() {
432
+ if (this.isRecording) {
433
+ this.stopRecording();
434
+ }
435
+ this.animationController.stop();
436
+ this.terminalWriter.done();
437
+ this.terminalWriter.stdout.write(ansiEscapes.cursorShow);
438
+ if (this.isAlternateBufferEnabled) {
439
+ this.terminalWriter.stdout.write(ansiEscapes.exitAlternativeScreen);
440
+ }
441
+ }
442
+ getLinesUpdated() {
443
+ return this.terminalWriter.getLinesUpdated();
444
+ }
445
+ resetLinesUpdated() {
446
+ this.terminalWriter.resetLinesUpdated();
447
+ }
448
+ clear() {
449
+ this.animationController.stop();
450
+ this.sceneManager.regions.clear();
451
+ this.sceneManager.root = undefined;
452
+ this.sceneManager.regionWasAtEnd.clear();
453
+ this.scrollOptimizer.maxRegionScrollTops.clear();
454
+ this.scrollOptimizer.lastRegionScrollTops.clear();
455
+ this.screen = [];
456
+ this.backbuffer = [];
457
+ this.forceNextRender = false;
458
+ this.cursorPosition = undefined;
459
+ this.primaryTerminalWriter.clear();
460
+ this.alternateTerminalWriter.clear();
461
+ this.terminalWriter.flush();
462
+ }
463
+ serializeCurrentRegions(serializer) {
464
+ const updates = [];
465
+ for (const region of this.sceneManager.regions.values()) {
466
+ const update = {
467
+ id: region.id,
468
+ stickyHeaders: region.stickyHeaders.map(h => {
469
+ const { node: _node, ...rest } = h;
470
+ return {
471
+ ...rest,
472
+ lines: serializer.serialize(h.lines),
473
+ stuckLines: h.stuckLines
474
+ ? serializer.serialize(h.stuckLines)
475
+ : undefined,
476
+ styledOutput: serializer.serialize(h.styledOutput),
477
+ };
478
+ }),
479
+ };
480
+ for (const key of regionLayoutProperties) {
481
+ copyRegionProperty(update, region, key);
482
+ }
483
+ if (region.lines.length > 0) {
484
+ update.lines = {
485
+ updates: [
486
+ {
487
+ start: 0,
488
+ end: region.lines.length,
489
+ data: serializer.serialize(region.lines),
490
+ },
491
+ ],
492
+ totalLength: region.lines.length,
493
+ };
494
+ }
495
+ updates.push(update);
496
+ }
497
+ return updates;
498
+ }
499
+ async _render() {
500
+ if (clearDebugLogPerFrame) {
501
+ clearDebugLog();
502
+ }
503
+ const rootRegion = this.sceneManager.getRootRegion();
504
+ if (!rootRegion) {
505
+ return;
506
+ }
507
+ if (rootRegion.width === 0) {
508
+ rootRegion.width = this.columns;
509
+ }
510
+ if (rootRegion.height === 0) {
511
+ rootRegion.height = this.rows;
512
+ }
513
+ this.forceNextRender = false;
514
+ if (this.debugRainbowEnabled) {
515
+ this.frameIndex++;
516
+ this.terminalWriter.debugRainbowColor =
517
+ rainbowColors[this.frameIndex % rainbowColors.length];
518
+ }
519
+ const cameraY = this.getCameraY(rootRegion);
520
+ this.syncCursor(cameraY);
521
+ const scrolledToBackbuffer = new Map();
522
+ if (!this.terminalWriter.isFirstRender) {
523
+ // 0. Handle Global Scroll (Backbuffer growth)
524
+ const maxPushed = this.scrollOptimizer.maxRegionScrollTops.get(rootRegion.id) ?? 0;
525
+ const linesToScroll = cameraY - maxPushed;
526
+ if (linesToScroll > 0) {
527
+ if (debugWorker) {
528
+ debugLog(`[RENDER-WORKER] Root region ${rootRegion.id} pushing ${linesToScroll} lines to backbuffer (cameraY: ${cameraY}, maxPushed: ${maxPushed})`);
529
+ }
530
+ scrolledToBackbuffer.set(rootRegion.id, linesToScroll);
531
+ this.appendToBackbuffer(maxPushed, linesToScroll);
532
+ this.scrollOptimizer.updateMaxPushed(rootRegion.id, cameraY);
533
+ }
534
+ // 0.5 Handle Local Region Scrolls
535
+ const compositor = this.createCompositor({
536
+ skipStickyHeaders: true,
537
+ skipScrollbars: false,
538
+ });
539
+ for (const region of this.sceneManager.regions.values()) {
540
+ const operations = this.scrollOptimizer.calculateScrollOperations(region, this.rows, this.columns, cameraY, (scrollStart, count, start, end) => {
541
+ const originalScrollTop = region.scrollTop;
542
+ region.scrollTop = scrollStart;
543
+ try {
544
+ const getLines = (skipScrollbars) => {
545
+ const canvas = Canvas.create(this.columns, this.rows + count);
546
+ this.composeNode(this.sceneManager.root, canvas, {
547
+ clip: undefined,
548
+ offsetY: -cameraY,
549
+ }, { skipStickyHeaders: true, skipScrollbars });
550
+ const lines = canvas.getLines();
551
+ return start === 0 && end + count === lines.length
552
+ ? lines
553
+ : lines.slice(start, end + count);
554
+ };
555
+ const cleanLines = getLines(true);
556
+ if (count > 0 && start === 0) {
557
+ const dirtyLines = getLines(false);
558
+ // First 'count' lines are clean (for backbuffer), the rest are dirty (for viewport)
559
+ return [
560
+ ...cleanLines.slice(0, count),
561
+ ...dirtyLines.slice(count),
562
+ ];
563
+ }
564
+ return cleanLines;
565
+ }
566
+ finally {
567
+ region.scrollTop = originalScrollTop;
568
+ }
569
+ }, (r, s, a) => compositor.calculateActualStuckTopHeight(r, s, a), (r, s, a) => compositor.calculateActualStuckBottomHeight(r, s, a), this.stickyHeadersInBackbuffer);
570
+ for (const op of operations) {
571
+ if (op.scrollToBackbuffer) {
572
+ if (debugWorker) {
573
+ debugLog(`[RENDER-WORKER] Region ${op.regionId} scrolling ${op.linesToScroll} lines to backbuffer`);
574
+ }
575
+ scrolledToBackbuffer.set(op.regionId, (scrolledToBackbuffer.get(op.regionId) ?? 0) + op.linesToScroll);
576
+ }
577
+ this.terminalWriter.scrollLines(op);
578
+ if (op.newMaxPushed !== undefined) {
579
+ this.scrollOptimizer.updateMaxPushed(op.regionId, op.newMaxPushed);
580
+ }
581
+ }
582
+ }
583
+ }
584
+ // 2. Compose Frame
585
+ if (this.terminalWriter.isFirstRender) {
586
+ for (const region of this.sceneManager.regions.values()) {
587
+ if (region.overflowToBackbuffer) {
588
+ this.scrollOptimizer.updateMaxPushed(region.id, region.scrollTop ?? 0);
589
+ }
590
+ }
591
+ if (rootRegion.overflowToBackbuffer) {
592
+ this.scrollOptimizer.updateMaxPushed(rootRegion.id, cameraY);
593
+ }
594
+ }
595
+ this.composeScene(this.terminalWriter.isFirstRender);
596
+ if (this.terminalWriter.isFirstRender) {
597
+ this.terminalWriter.writeLines([...this.backbuffer, ...this.screen]);
598
+ }
599
+ else {
600
+ // 3. Sync
601
+ for (let row = 0; row < this.rows; row++) {
602
+ this.terminalWriter.syncLine(this.screen[row], row);
603
+ }
604
+ }
605
+ this.terminalWriter.finish();
606
+ this.terminalWriter.flush();
607
+ this.updateTrackingMaps(rootRegion, cameraY);
608
+ if (this.terminalWriter.backbufferDirtyCurrentFrame) {
609
+ this.terminalWriter.backbufferDirty = true;
610
+ if (this.terminalWriter.fullRenderTimeout) {
611
+ clearTimeout(this.terminalWriter.fullRenderTimeout);
612
+ }
613
+ this.terminalWriter.fullRenderTimeout = setTimeout(() => {
614
+ void this.fullRender();
615
+ }, this.backbufferUpdateDelay);
616
+ }
617
+ this.terminalWriter.backbufferDirtyCurrentFrame = false;
618
+ this.logScene(scrolledToBackbuffer);
619
+ }
620
+ tickAnimation() {
621
+ const { hasScrolled, canScrollMore } = this.animationController.updateRegions(this.sceneManager.regions);
622
+ if (hasScrolled) {
623
+ void this.render();
624
+ }
625
+ if (!canScrollMore) {
626
+ if (debugWorker) {
627
+ debugLog(`[RENDER-WORKER] Stopping animation: all targets reached\n`);
628
+ }
629
+ this.animationController.stop();
630
+ }
631
+ }
632
+ composeScene(computeBackbuffer) {
633
+ const rootRegion = this.sceneManager.getRootRegion();
634
+ if (!rootRegion) {
635
+ return;
636
+ }
637
+ const cameraY = this.getCameraY(rootRegion);
638
+ this.backbuffer = [];
639
+ if (!this.isAlternateBufferEnabled && computeBackbuffer) {
640
+ const composeToBackbuffer = (node, region, height, offset) => {
641
+ const canvas = Canvas.create(this.columns, height);
642
+ const originalScrollTop = region.scrollTop;
643
+ region.scrollTop = offset;
644
+ try {
645
+ this.composeNode(node, canvas, {
646
+ clip: undefined,
647
+ offsetY: -region.y,
648
+ offsetX: 0,
649
+ overrideHeight: height,
650
+ isExpanded: true,
651
+ }, {
652
+ skipStickyHeaders: true,
653
+ skipScrollbars: true,
654
+ });
655
+ }
656
+ finally {
657
+ region.scrollTop = originalScrollTop;
658
+ }
659
+ for (const line of canvas.getLines()) {
660
+ this.backbuffer.push(this.terminalWriter.clampLine(line.styledChars, this.columns));
661
+ }
662
+ };
663
+ const rootBackbufferHeight = cameraY;
664
+ composeToBackbuffer(this.sceneManager.root, rootRegion, rootBackbufferHeight, 0);
665
+ for (const region of this.sceneManager.regions.values()) {
666
+ if (region.overflowToBackbuffer && region.isScrollable) {
667
+ const scrollTop = region.scrollTop ?? 0;
668
+ const regionBackbufferHeight = scrollTop;
669
+ const node = this.findNodeForRegion(region.id);
670
+ if (node) {
671
+ composeToBackbuffer(node, region, regionBackbufferHeight, 0);
672
+ }
673
+ }
674
+ }
675
+ }
676
+ const canvas = Canvas.create(this.columns, this.rows, this.resized);
677
+ this.composeNode(this.sceneManager.root, canvas, {
678
+ clip: undefined,
679
+ offsetY: -cameraY,
680
+ });
681
+ this.screen = canvas.getLines();
682
+ this.resized = false;
683
+ }
684
+ /**
685
+ * Recursively composites a region and its children onto the provided canvas.
686
+ *
687
+ * @param node The hierarchical node to compose.
688
+ * @param canvas The target canvas to draw upon.
689
+ * @param layout Layout and clipping options for the current composition pass.
690
+ * @param layout.clip Optional bounding box to clip rendering.
691
+ * @param layout.offsetY The cumulative Y offset (used for scrolling/camera adjustments).
692
+ * @param layout.offsetX The cumulative X offset (used for scrolling/camera adjustments).
693
+ * @param layout.overrideHeight Optional height to force for the current region, typically used when composing to the backbuffer to capture content that has scrolled out of view.
694
+ * @param layout.isExpanded If true, forces the region to render at its full scroll height rather than its constrained layout height. When composing the backbuffer, this flag is passed down to the specific scrollable descendant that has `overflowToBackbuffer` enabled (typically only one such region should exist), allowing its complete content to be captured instead of just the visible viewport.
695
+ * @param options Additional composition flags.
696
+ * @param options.skipStickyHeaders If true, sticky headers will not be drawn.
697
+ * @param options.skipScrollbars If true, scrollbars will not be drawn.
698
+ */
699
+ composeNode(node, canvas, { clip, offsetY = 0, offsetX = 0, overrideHeight, isExpanded = false, }, options) {
700
+ const region = this.sceneManager.getRegion(node.id);
701
+ if (!region)
702
+ return;
703
+ const absX = Math.round(region.x + offsetX);
704
+ const absY = Math.round(region.y + offsetY);
705
+ const inExpandedContext = isExpanded || overrideHeight !== undefined;
706
+ const height = overrideHeight ??
707
+ (inExpandedContext && region.isScrollable
708
+ ? (region.scrollHeight ?? 0)
709
+ : (region.height ?? 0));
710
+ if (absY >= canvas.height)
711
+ return;
712
+ if (absY + height < 0 && !this.stickyHeadersInBackbuffer)
713
+ return;
714
+ let myClip = {
715
+ x: absX,
716
+ y: absY,
717
+ w: Math.round(region.width) - (region.marginRight ?? 0),
718
+ h: Math.round(height) - (region.marginBottom ?? 0),
719
+ };
720
+ if (clip) {
721
+ const x1 = Math.max(myClip.x, clip.x);
722
+ const y1 = Math.max(myClip.y, clip.y);
723
+ const x2 = Math.min(myClip.x + myClip.w, clip.x + clip.w);
724
+ const y2 = Math.min(myClip.y + myClip.h, clip.y + clip.h);
725
+ if (x2 <= x1 || y2 <= y1)
726
+ return;
727
+ myClip = { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
728
+ }
729
+ const originalScrollTop = region.scrollTop;
730
+ if (inExpandedContext &&
731
+ region.isScrollable &&
732
+ overrideHeight === undefined) {
733
+ region.scrollTop = 0;
734
+ }
735
+ try {
736
+ const compositor = this.createCompositor(options);
737
+ compositor.drawContent(canvas, region, absX, absY, myClip);
738
+ for (const child of node.children) {
739
+ const childRegion = this.sceneManager.getRegion(child.id);
740
+ let childOptions = options;
741
+ if (options &&
742
+ childRegion &&
743
+ childRegion.isScrollable &&
744
+ !childRegion.overflowToBackbuffer) {
745
+ childOptions = {
746
+ ...options,
747
+ skipStickyHeaders: false,
748
+ };
749
+ }
750
+ this.composeNode(child, canvas, {
751
+ clip: myClip,
752
+ offsetY: absY - (region.scrollTop ?? 0),
753
+ offsetX: absX - (region.scrollLeft ?? 0),
754
+ isExpanded: inExpandedContext && Boolean(childRegion?.overflowToBackbuffer),
755
+ }, childOptions);
756
+ }
757
+ compositor.drawStickyHeaders(canvas, region, absX, absY, myClip);
758
+ compositor.drawScrollbars(canvas, region, absX, absY, myClip);
759
+ }
760
+ finally {
761
+ if (inExpandedContext &&
762
+ region.isScrollable &&
763
+ overrideHeight === undefined) {
764
+ region.scrollTop = originalScrollTop;
765
+ }
766
+ }
767
+ }
768
+ createCompositor(options) {
769
+ return new Compositor({
770
+ skipStickyHeaders: options?.skipStickyHeaders,
771
+ skipScrollbars: options?.skipScrollbars,
772
+ stickyHeadersInBackbuffer: this.stickyHeadersInBackbuffer,
773
+ animatedScroll: this.animatedScroll,
774
+ targetScrollTops: this.animationController.allTargetScrollTops,
775
+ regionWasAtEnd: this.sceneManager.regionWasAtEnd,
776
+ canvasHeight: this.rows,
777
+ });
778
+ }
779
+ getCameraY(rootRegion) {
780
+ return Math.max(0, rootRegion.height - this.rows);
781
+ }
782
+ syncCursor(cameraY) {
783
+ let cursorRow = -1;
784
+ let cursorCol = -1;
785
+ if (this.cursorPosition) {
786
+ const row = this.cursorPosition.row - cameraY;
787
+ if (row >= 0 && row < this.rows) {
788
+ cursorRow = row;
789
+ cursorCol = this.cursorPosition.col;
790
+ }
791
+ }
792
+ this.terminalWriter.setTargetCursorPosition(cursorRow, cursorCol);
793
+ }
794
+ appendToBackbuffer(start, count) {
795
+ const rootNode = this.sceneManager.root;
796
+ if (!rootNode)
797
+ return;
798
+ const canvas = Canvas.create(this.columns, count);
799
+ this.composeNode(rootNode, canvas, { clip: undefined, offsetY: -start }, {
800
+ skipStickyHeaders: true,
801
+ skipScrollbars: true,
802
+ });
803
+ const linesScrollingOut = canvas
804
+ .getLines()
805
+ .map(line => this.terminalWriter.clampLine(line.styledChars, this.columns));
806
+ this.terminalWriter.appendLinesBackbuffer(linesScrollingOut);
807
+ }
808
+ updateTrackingMaps(rootRegion, cameraY, reset = false) {
809
+ if (rootRegion) {
810
+ if (reset) {
811
+ this.scrollOptimizer.setMaxPushed(rootRegion.id, cameraY);
812
+ }
813
+ else {
814
+ this.scrollOptimizer.updateMaxPushed(rootRegion.id, cameraY);
815
+ }
816
+ }
817
+ for (const region of this.sceneManager.regions.values()) {
818
+ if (region.isScrollable) {
819
+ this.scrollOptimizer.lastRegionScrollTops.set(region.id, region.scrollTop ?? 0);
820
+ if (region.overflowToBackbuffer) {
821
+ if (reset) {
822
+ this.scrollOptimizer.setMaxPushed(region.id, region.scrollTop ?? 0);
823
+ }
824
+ else {
825
+ this.scrollOptimizer.updateMaxPushed(region.id, region.scrollTop ?? 0);
826
+ }
827
+ }
828
+ }
829
+ }
830
+ }
831
+ findNodeForRegion(id) {
832
+ if (!this.sceneManager.root)
833
+ return undefined;
834
+ const visit = (node) => {
835
+ if (node.id === id)
836
+ return node;
837
+ for (const child of node.children) {
838
+ const found = visit(child);
839
+ if (found)
840
+ return found;
841
+ }
842
+ return undefined;
843
+ };
844
+ return visit(this.sceneManager.root);
845
+ }
846
+ logScene(scrolledToBackbuffer) {
847
+ if (!debugWorker) {
848
+ return;
849
+ }
850
+ const rootNode = this.sceneManager.root;
851
+ if (!rootNode) {
852
+ return;
853
+ }
854
+ const rootRegion = this.sceneManager.getRootRegion();
855
+ const cameraY = rootRegion ? this.getCameraY(rootRegion) : 0;
856
+ debugLog('='.repeat(80));
857
+ debugLog(`FRAME START (Index: ${this.frameIndex})`);
858
+ debugLog('='.repeat(80));
859
+ const traverse = (node, depth, offsetX, offsetY, clip) => {
860
+ const region = this.sceneManager.getRegion(node.id);
861
+ if (!region) {
862
+ return;
863
+ }
864
+ const absX = Math.round(region.x + offsetX);
865
+ const absY = Math.round(region.y + offsetY);
866
+ const width = Math.round(region.width);
867
+ const height = Math.round(region.height);
868
+ let myClip = {
869
+ x: absX,
870
+ y: absY,
871
+ w: width,
872
+ h: height,
873
+ };
874
+ if (clip) {
875
+ const x1 = Math.max(myClip.x, clip.x);
876
+ const y1 = Math.max(myClip.y, clip.y);
877
+ const x2 = Math.min(myClip.x + myClip.w, clip.x + clip.w);
878
+ const y2 = Math.min(myClip.y + myClip.h, clip.y + clip.h);
879
+ myClip =
880
+ x2 > x1 && y2 > y1
881
+ ? { x: x1, y: y1, w: x2 - x1, h: y2 - y1 }
882
+ : { x: 0, y: 0, w: 0, h: 0 };
883
+ }
884
+ const indent = ' '.repeat(depth);
885
+ const isStatic = region.node?.nodeName === 'ink-static-render';
886
+ debugLog(`${indent}Region: ${region.id} ${isStatic ? '(STATIC)' : ''}`);
887
+ debugLog(`${indent} Bounds: x=${absX}, y=${absY}, w=${width}, h=${height}`);
888
+ debugLog(`${indent} Scroll: top=${region.scrollTop ?? 0}, left=${region.scrollLeft ?? 0}, height=${region.scrollHeight ?? 0}, width=${region.scrollWidth ?? 0}`);
889
+ const linesPushed = this.scrollOptimizer.maxRegionScrollTops.get(region.id) ?? 0;
890
+ const justScrolled = scrolledToBackbuffer?.get(region.id) ?? 0;
891
+ debugLog(`${indent} Backbuffer: enabled=${region.overflowToBackbuffer ?? false}, linesPushed=${linesPushed}${justScrolled > 0 ? `, JUST_SCROLLED=${justScrolled}` : ''}`);
892
+ debugLog(`${indent} Clip: x=${myClip.x}, y=${myClip.y}, w=${myClip.w}, h=${myClip.h}`);
893
+ if (region.lines.length > 0) {
894
+ debugLog(`${indent} Content:`);
895
+ for (const line of region.lines) {
896
+ const plainText = line.getText().trimEnd();
897
+ if (plainText) {
898
+ debugLog(`${indent} ${plainText}`);
899
+ }
900
+ }
901
+ }
902
+ if (region.stickyHeaders.length > 0) {
903
+ debugLog(`${indent} Sticky Headers:`);
904
+ for (const header of region.stickyHeaders) {
905
+ const scrollTop = region.scrollTop ?? 0;
906
+ const isStuckState = header.type === 'bottom'
907
+ ? Math.round(header.naturalRow - scrollTop + header.lines.length) >=
908
+ Math.round(header.y + (header.stuckLines ?? header.lines).length)
909
+ : Math.round(header.naturalRow - scrollTop) <=
910
+ Math.round(header.y);
911
+ let isStuck = isStuckState;
912
+ if (isStuck && header.type === 'top') {
913
+ isStuck = this.stickyHeadersInBackbuffer || absY > 0;
914
+ }
915
+ debugLog(`${indent} Header nodeID: ${header.nodeId}, type: ${header.type}, isStuckOnly: ${header.isStuckOnly}, IS_STUCK: ${isStuck}`);
916
+ debugLog(`${indent} Range: ${header.startRow}-${header.endRow}, StuckPos: y=${header.y}, NaturalRow: ${header.naturalRow}`);
917
+ const linesToLog = isStuck
918
+ ? (header.stuckLines ?? header.lines)
919
+ : header.lines;
920
+ debugLog(`${indent} Header Content (${isStuck ? 'STUCK' : 'NATURAL'}):`);
921
+ for (const line of linesToLog) {
922
+ const plainText = line.getText().trimEnd();
923
+ if (plainText) {
924
+ debugLog(`${indent} ${plainText}`);
925
+ }
926
+ }
927
+ }
928
+ }
929
+ for (const child of node.children) {
930
+ traverse(child, depth + 1, absX - (region.scrollLeft ?? 0), absY - (region.scrollTop ?? 0), myClip);
931
+ }
932
+ };
933
+ traverse(rootNode, 0, 0, -cameraY, {
934
+ x: 0,
935
+ y: 0,
936
+ w: this.columns,
937
+ h: this.rows,
938
+ });
939
+ debugLog('='.repeat(80));
940
+ debugLog('FRAME END');
941
+ debugLog('='.repeat(80));
942
+ }
943
+ }
944
+ //# sourceMappingURL=render-worker.js.map