@fleetagent/pi-tui 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +47 -7
  2. package/dist/autocomplete.d.ts +17 -23
  3. package/dist/autocomplete.d.ts.map +1 -1
  4. package/dist/autocomplete.js +129 -195
  5. package/dist/autocomplete.js.map +1 -1
  6. package/dist/components/alt-screen-flash.d.ts +13 -0
  7. package/dist/components/alt-screen-flash.d.ts.map +1 -0
  8. package/dist/components/alt-screen-flash.js +37 -0
  9. package/dist/components/alt-screen-flash.js.map +1 -0
  10. package/dist/components/editor.d.ts +47 -4
  11. package/dist/components/editor.d.ts.map +1 -1
  12. package/dist/components/editor.js +615 -585
  13. package/dist/components/editor.js.map +1 -1
  14. package/dist/components/h-stack.d.ts +7 -0
  15. package/dist/components/h-stack.d.ts.map +1 -0
  16. package/dist/components/h-stack.js +43 -0
  17. package/dist/components/h-stack.js.map +1 -0
  18. package/dist/components/image.d.ts +4 -0
  19. package/dist/components/image.d.ts.map +1 -1
  20. package/dist/components/image.js +38 -51
  21. package/dist/components/image.js.map +1 -1
  22. package/dist/components/input.d.ts +9 -0
  23. package/dist/components/input.d.ts.map +1 -1
  24. package/dist/components/input.js +121 -147
  25. package/dist/components/input.js.map +1 -1
  26. package/dist/components/markdown.d.ts +34 -0
  27. package/dist/components/markdown.d.ts.map +1 -1
  28. package/dist/components/markdown.js +491 -429
  29. package/dist/components/markdown.js.map +1 -1
  30. package/dist/components/scroll-view.d.ts +53 -0
  31. package/dist/components/scroll-view.d.ts.map +1 -0
  32. package/dist/components/scroll-view.js +173 -0
  33. package/dist/components/scroll-view.js.map +1 -0
  34. package/dist/components/select-list.d.ts.map +1 -1
  35. package/dist/components/select-list.js.map +1 -1
  36. package/dist/components/settings-list.d.ts +2 -0
  37. package/dist/components/settings-list.d.ts.map +1 -1
  38. package/dist/components/settings-list.js +31 -29
  39. package/dist/components/settings-list.js.map +1 -1
  40. package/dist/components/stack.d.ts +32 -0
  41. package/dist/components/stack.d.ts.map +1 -0
  42. package/dist/components/stack.js +122 -0
  43. package/dist/components/stack.js.map +1 -0
  44. package/dist/components/v-stack.d.ts +8 -0
  45. package/dist/components/v-stack.d.ts.map +1 -0
  46. package/dist/components/v-stack.js +27 -0
  47. package/dist/components/v-stack.js.map +1 -0
  48. package/dist/fuzzy.d.ts.map +1 -1
  49. package/dist/fuzzy.js +37 -46
  50. package/dist/fuzzy.js.map +1 -1
  51. package/dist/index.d.ts +10 -3
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +11 -2
  54. package/dist/index.js.map +1 -1
  55. package/dist/keybindings.d.ts +54 -4
  56. package/dist/keybindings.d.ts.map +1 -1
  57. package/dist/keybindings.js +39 -4
  58. package/dist/keybindings.js.map +1 -1
  59. package/dist/keys.d.ts.map +1 -1
  60. package/dist/keys.js +359 -479
  61. package/dist/keys.js.map +1 -1
  62. package/dist/kill-ring.d.ts +6 -4
  63. package/dist/kill-ring.d.ts.map +1 -1
  64. package/dist/kill-ring.js +0 -7
  65. package/dist/kill-ring.js.map +1 -1
  66. package/dist/latex.d.ts +10 -0
  67. package/dist/latex.d.ts.map +1 -0
  68. package/dist/latex.js +1406 -0
  69. package/dist/latex.js.map +1 -0
  70. package/dist/layout-node.d.ts +43 -0
  71. package/dist/layout-node.d.ts.map +1 -0
  72. package/dist/layout-node.js +6 -0
  73. package/dist/layout-node.js.map +1 -0
  74. package/dist/layout.d.ts +40 -0
  75. package/dist/layout.d.ts.map +1 -0
  76. package/dist/layout.js +386 -0
  77. package/dist/layout.js.map +1 -0
  78. package/dist/stdin-buffer.d.ts +8 -0
  79. package/dist/stdin-buffer.d.ts.map +1 -1
  80. package/dist/stdin-buffer.js +116 -159
  81. package/dist/stdin-buffer.js.map +1 -1
  82. package/dist/terminal-image.d.ts +41 -19
  83. package/dist/terminal-image.d.ts.map +1 -1
  84. package/dist/terminal-image.js +171 -32
  85. package/dist/terminal-image.js.map +1 -1
  86. package/dist/terminal.d.ts +3 -0
  87. package/dist/terminal.d.ts.map +1 -1
  88. package/dist/terminal.js +27 -23
  89. package/dist/terminal.js.map +1 -1
  90. package/dist/tui-alt-screen.d.ts +114 -0
  91. package/dist/tui-alt-screen.d.ts.map +1 -0
  92. package/dist/tui-alt-screen.js +950 -0
  93. package/dist/tui-alt-screen.js.map +1 -0
  94. package/dist/tui-main-screen.d.ts +52 -0
  95. package/dist/tui-main-screen.d.ts.map +1 -0
  96. package/dist/tui-main-screen.js +511 -0
  97. package/dist/tui-main-screen.js.map +1 -0
  98. package/dist/tui.d.ts +112 -44
  99. package/dist/tui.d.ts.map +1 -1
  100. package/dist/tui.js +489 -699
  101. package/dist/tui.js.map +1 -1
  102. package/dist/utils.d.ts +29 -17
  103. package/dist/utils.d.ts.map +1 -1
  104. package/dist/utils.js +605 -528
  105. package/dist/utils.js.map +1 -1
  106. package/dist/word-navigation.d.ts.map +1 -1
  107. package/dist/word-navigation.js +37 -39
  108. package/dist/word-navigation.js.map +1 -1
  109. package/package.json +1 -1
@@ -0,0 +1,950 @@
1
+ import { AltScreenFlashContainer } from "./components/alt-screen-flash.js";
2
+ import { ScrollView } from "./components/scroll-view.js";
3
+ import { getKeybindings } from "./keybindings.js";
4
+ import { isKeyRelease } from "./keys.js";
5
+ import { getScrollbarGeometry, getScrollViewBox, getScrollViewsAt, renderLayoutFrame, } from "./layout.js";
6
+ import { deleteAllKittyImages, deleteAllKittyPlacements, deleteKittyImage, getCapabilities, getKittyImagePlacement, isImageLine, setCapabilities, } from "./terminal-image.js";
7
+ import { CURSOR_MARKER, compositeTuiLine, TuiBase, VIEWPORT_TUI, } from "./tui.js";
8
+ import { extractAnsiCode, getGraphemeCellRange, getOsc8LinkAtColumn, getWordSegmenter, sliceByColumn, stripTerminalSequences, visibleWidth, } from "./utils.js";
9
+ const ENTER_ALT_SCREEN = "\x1b[?1049h";
10
+ const EXIT_ALT_SCREEN = "\x1b[?1049l";
11
+ const DISABLE_AUTOWRAP = "\x1b[?7l";
12
+ const ENABLE_AUTOWRAP = "\x1b[?7h";
13
+ const ENABLE_BUTTON_MOTION_MOUSE = "\x1b[?1000h\x1b[?1002h\x1b[?1004h\x1b[?1006h";
14
+ const ENABLE_ALL_MOTION_MOUSE = "\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1004h\x1b[?1006h";
15
+ const DISABLE_MOUSE = "\x1b[?1006l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l";
16
+ const FOCUS_IN = "\x1b[I";
17
+ const FOCUS_OUT = "\x1b[O";
18
+ const BEGIN_SYNCHRONIZED_OUTPUT = "\x1b[?2026h";
19
+ const END_SYNCHRONIZED_OUTPUT = "\x1b[?2026l";
20
+ const OSC133_ZONE_PREFIX = /^(?:\x1b\]133;[ABC](?:\x07|\x1b\\))+/;
21
+ const OSC133_PROMPT_START = /^\x1b\]133;A(?:\x07|\x1b\\)/;
22
+ const PAGE_SCROLL_OVERLAP = 4;
23
+ const MAX_CACHED_OFFSCREEN_KITTY_IMAGES = 16;
24
+ const MAX_CACHED_OFFSCREEN_KITTY_TRANSMISSION_BYTES = 32 * 1024 * 1024;
25
+ const MAX_CACHED_OFFSCREEN_KITTY_DECODED_BYTES = 64 * 1024 * 1024;
26
+ const DOUBLE_CLICK_INTERVAL_MS = 500;
27
+ const wordSegmenter = getWordSegmenter();
28
+ function createWheelEvent(button, x, y) {
29
+ if ((button & 64) === 0)
30
+ return undefined;
31
+ const direction = button & 3;
32
+ if (direction !== 0 && direction !== 1)
33
+ return undefined;
34
+ return { direction: direction === 0 ? -1 : 1, x, y };
35
+ }
36
+ /** Alternate-screen TUI with a scrollable, application-owned viewport. */
37
+ export class TuiAltScreen extends TuiBase {
38
+ mode = "fullscreen";
39
+ [VIEWPORT_TUI] = true;
40
+ previousScreen = [];
41
+ lastDocument = [];
42
+ previousScreenWidth = 0;
43
+ previousScreenHeight = 0;
44
+ layoutRoot;
45
+ currentLayout;
46
+ implicitDocument;
47
+ implicitScrollView;
48
+ altScreenActive = false;
49
+ imageProtocol = null;
50
+ savedCapabilities;
51
+ uploadedKittyImages = new Map();
52
+ flashes;
53
+ selectionAnchor;
54
+ selectionFocus;
55
+ selectionGranularity = "character";
56
+ selectionInitialRange;
57
+ lastClick;
58
+ selectionPressActive = false;
59
+ selectionDragPointer;
60
+ selectionAutoScrollDirection = 0;
61
+ selectionAutoScrollTimer;
62
+ scrollbarDrag;
63
+ scrollbarHover;
64
+ pressedUrl;
65
+ selectionDragged = false;
66
+ wheelScrollLines;
67
+ mouseEnabled;
68
+ openUrl;
69
+ onRightClickPaste;
70
+ constructor(terminal, showHardwareCursor, logDirectory, options = {}) {
71
+ super(terminal, showHardwareCursor, logDirectory);
72
+ this.implicitDocument = {
73
+ render: (width) => super.render(width),
74
+ invalidate: () => {
75
+ for (const child of this.children)
76
+ child.invalidate();
77
+ },
78
+ };
79
+ this.implicitScrollView = new ScrollView(this.implicitDocument, { follow: "end", primary: true });
80
+ this.wheelScrollLines = Math.max(1, Math.floor(options.wheelScrollLines ?? 1));
81
+ this.mouseEnabled = options.mouse ?? true;
82
+ this.openUrl = options.openUrl;
83
+ this.onRightClickPaste = options.onRightClickPaste;
84
+ this.flashes = new AltScreenFlashContainer(() => this.requestRender());
85
+ this.addInputListener((data) => this.handleViewportInput(data));
86
+ }
87
+ get viewportTop() {
88
+ return this.getPrimaryScrollView().scrollTop;
89
+ }
90
+ get isFollowingOutput() {
91
+ return this.getPrimaryScrollView().isFollowingEnd;
92
+ }
93
+ setLayoutRoot(component) {
94
+ if (this.layoutRoot === component)
95
+ return;
96
+ this.layoutRoot = component;
97
+ this.currentLayout = undefined;
98
+ this.requestRender();
99
+ }
100
+ render(width) {
101
+ return this.layoutRoot?.render(width) ?? super.render(width);
102
+ }
103
+ getMountedRoots() {
104
+ return this.layoutRoot ? [this.layoutRoot] : this.children;
105
+ }
106
+ getPrimaryScrollView() {
107
+ return this.currentLayout?.primaryScrollView ?? this.implicitScrollView;
108
+ }
109
+ cancelActivePointerInteraction() {
110
+ const hadActiveSelection = this.selectionPressActive;
111
+ this.stopSelectionAutoScroll();
112
+ this.stopScrollbarHover();
113
+ this.stopScrollbarDrag();
114
+ this.selectionPressActive = false;
115
+ this.pressedUrl = undefined;
116
+ this.selectionDragged = false;
117
+ this.lastClick = undefined;
118
+ if (!hadActiveSelection)
119
+ return;
120
+ this.selectionAnchor = undefined;
121
+ this.selectionFocus = undefined;
122
+ this.selectionGranularity = "character";
123
+ this.selectionInitialRange = undefined;
124
+ }
125
+ onTerminalResize() {
126
+ this.cancelActivePointerInteraction();
127
+ }
128
+ onOverlayStackChanged() {
129
+ if (this.hasCapturingOverlay())
130
+ this.cancelActivePointerInteraction();
131
+ }
132
+ beforeTerminalStart() {
133
+ this.stopSelectionAutoScroll();
134
+ this.stopScrollbarHover();
135
+ this.stopScrollbarDrag();
136
+ this.flashes.dispose();
137
+ this.altScreenActive = true;
138
+ const capabilities = getCapabilities();
139
+ this.imageProtocol = capabilities.images;
140
+ this.uploadedKittyImages.clear();
141
+ if (capabilities.images === "iterm2") {
142
+ this.savedCapabilities = capabilities;
143
+ setCapabilities({ ...capabilities, images: null });
144
+ this.invalidate();
145
+ }
146
+ this.lastDocument = [];
147
+ this.selectionAnchor = undefined;
148
+ this.selectionFocus = undefined;
149
+ this.selectionGranularity = "character";
150
+ this.selectionInitialRange = undefined;
151
+ this.lastClick = undefined;
152
+ this.pressedUrl = undefined;
153
+ this.selectionDragged = false;
154
+ this.selectionPressActive = false;
155
+ this.resetRenderState();
156
+ const term = process.env.TERM?.toLowerCase() ?? "";
157
+ const mouseSequence = process.env.TMUX !== undefined ||
158
+ process.env.ZELLIJ !== undefined ||
159
+ process.env.STY !== undefined ||
160
+ term.startsWith("tmux") ||
161
+ term.startsWith("screen")
162
+ ? ENABLE_BUTTON_MOTION_MOUSE
163
+ : ENABLE_ALL_MOTION_MOUSE;
164
+ this.terminal.write(`${ENTER_ALT_SCREEN}${DISABLE_AUTOWRAP}${this.mouseEnabled ? mouseSequence : ""}\x1b[2J\x1b[H\x1b[?25l`);
165
+ }
166
+ beforeTerminalStop() {
167
+ this.stopSelectionAutoScroll();
168
+ this.stopScrollbarHover();
169
+ this.stopScrollbarDrag();
170
+ this.flashes.dispose();
171
+ this.selectionPressActive = false;
172
+ if (!this.altScreenActive)
173
+ return;
174
+ this.terminal.write(`${BEGIN_SYNCHRONIZED_OUTPUT}${this.deleteKittyImages()}${this.mouseEnabled ? DISABLE_MOUSE : ""}${ENABLE_AUTOWRAP}${END_SYNCHRONIZED_OUTPUT}`);
175
+ this.uploadedKittyImages.clear();
176
+ }
177
+ afterTerminalStop(options) {
178
+ if (!this.altScreenActive)
179
+ return;
180
+ this.altScreenActive = false;
181
+ try {
182
+ let buffer = `${BEGIN_SYNCHRONIZED_OUTPUT}${EXIT_ALT_SCREEN}`;
183
+ if (!options?.preserveScreen) {
184
+ const width = Math.max(1, this.terminal.columns);
185
+ const documentLines = this.render(width).map((line) => line.replace(OSC133_ZONE_PREFIX, ""));
186
+ this.lastDocument = this.applyLineResets(documentLines.map((line) => line.replaceAll(CURSOR_MARKER, ""))).map((line) => isImageLine(line) || visibleWidth(line) <= width ? line : sliceByColumn(line, 0, width, true));
187
+ buffer += DISABLE_AUTOWRAP;
188
+ for (let row = 0; row < this.lastDocument.length; row++) {
189
+ if (row > 0)
190
+ buffer += "\r\n";
191
+ buffer += `\r\x1b[2K${this.lastDocument[row] ?? ""}`;
192
+ }
193
+ buffer += "\x1b[0m";
194
+ }
195
+ buffer += `${ENABLE_AUTOWRAP}${options?.preserveScreen ? "" : "\r\n"}\x1b[?25h${END_SYNCHRONIZED_OUTPUT}`;
196
+ this.terminal.write(buffer);
197
+ }
198
+ finally {
199
+ if (this.savedCapabilities) {
200
+ setCapabilities(this.savedCapabilities);
201
+ this.savedCapabilities = undefined;
202
+ }
203
+ }
204
+ }
205
+ deleteKittyImages() {
206
+ return this.imageProtocol === "kitty" ? deleteAllKittyImages() : "";
207
+ }
208
+ prepareKittyScreen(screen) {
209
+ const visibleImageIds = new Set();
210
+ const lines = screen.map((line) => {
211
+ const placement = getKittyImagePlacement(line);
212
+ if (!placement)
213
+ return line;
214
+ visibleImageIds.add(placement.imageId);
215
+ const cachedImage = this.uploadedKittyImages.get(placement.imageId);
216
+ const nextCachedImage = {
217
+ transmissionGeneration: placement.transmissionGeneration,
218
+ transmissionBytes: placement.transmissionBytes,
219
+ estimatedDecodedBytes: placement.estimatedDecodedBytes,
220
+ };
221
+ if (cachedImage)
222
+ this.uploadedKittyImages.delete(placement.imageId);
223
+ this.uploadedKittyImages.set(placement.imageId, nextCachedImage);
224
+ return cachedImage?.transmissionGeneration === placement.transmissionGeneration
225
+ ? placement.replacementLine
226
+ : line;
227
+ });
228
+ let cachedOffscreenImageCount = 0;
229
+ let cachedOffscreenTransmissionBytes = 0;
230
+ let cachedOffscreenDecodedBytes = 0;
231
+ for (const [imageId, cachedImage] of this.uploadedKittyImages) {
232
+ if (visibleImageIds.has(imageId))
233
+ continue;
234
+ cachedOffscreenImageCount += 1;
235
+ cachedOffscreenTransmissionBytes += cachedImage.transmissionBytes;
236
+ cachedOffscreenDecodedBytes += cachedImage.estimatedDecodedBytes;
237
+ }
238
+ let evictedImageDeletion = "";
239
+ for (const [imageId, cachedImage] of this.uploadedKittyImages) {
240
+ if (cachedOffscreenImageCount <= MAX_CACHED_OFFSCREEN_KITTY_IMAGES &&
241
+ cachedOffscreenTransmissionBytes <= MAX_CACHED_OFFSCREEN_KITTY_TRANSMISSION_BYTES &&
242
+ cachedOffscreenDecodedBytes <= MAX_CACHED_OFFSCREEN_KITTY_DECODED_BYTES) {
243
+ break;
244
+ }
245
+ if (visibleImageIds.has(imageId))
246
+ continue;
247
+ evictedImageDeletion += deleteKittyImage(imageId);
248
+ this.uploadedKittyImages.delete(imageId);
249
+ cachedOffscreenImageCount -= 1;
250
+ cachedOffscreenTransmissionBytes -= cachedImage.transmissionBytes;
251
+ cachedOffscreenDecodedBytes -= cachedImage.estimatedDecodedBytes;
252
+ }
253
+ return { lines, evictedImageDeletion };
254
+ }
255
+ resetRenderState() {
256
+ this.previousScreen = [];
257
+ this.previousScreenWidth = 0;
258
+ this.previousScreenHeight = 0;
259
+ this.currentLayout = undefined;
260
+ }
261
+ scrollBy(lines) {
262
+ this.getPrimaryScrollView().scrollBy(lines);
263
+ this.requestRender();
264
+ }
265
+ scrollToTop() {
266
+ this.getPrimaryScrollView().scrollToStart();
267
+ this.requestRender();
268
+ }
269
+ scrollToBottom() {
270
+ this.getPrimaryScrollView().scrollToEnd();
271
+ this.requestRender();
272
+ }
273
+ scrollToPrompt(direction) {
274
+ if (!this.currentLayout)
275
+ return;
276
+ const scrollView = this.getPrimaryScrollView();
277
+ const lines = getScrollViewBox(this.currentLayout, scrollView)?.scrollContentLines;
278
+ if (!lines)
279
+ return;
280
+ for (let row = scrollView.scrollTop + direction; row >= 0 && row < lines.length; row += direction) {
281
+ if (!OSC133_PROMPT_START.test(lines[row] ?? ""))
282
+ continue;
283
+ scrollView.scrollTo(row);
284
+ this.requestRender();
285
+ return;
286
+ }
287
+ }
288
+ /** Show a transient message in the alternate-screen flash stack. */
289
+ flash(message, durationMs) {
290
+ this.flashes.flash(message, durationMs);
291
+ }
292
+ handleFocusOut() {
293
+ if (!this.mouseEnabled)
294
+ return undefined;
295
+ this.stopSelectionAutoScroll();
296
+ this.stopScrollbarHover();
297
+ this.stopScrollbarDrag();
298
+ const hadActiveSelection = this.selectionPressActive;
299
+ this.selectionPressActive = false;
300
+ this.pressedUrl = undefined;
301
+ this.selectionDragged = false;
302
+ if (hadActiveSelection) {
303
+ this.selectionAnchor = undefined;
304
+ this.selectionFocus = undefined;
305
+ this.selectionGranularity = "character";
306
+ this.selectionInitialRange = undefined;
307
+ this.requestRender();
308
+ }
309
+ this.lastClick = undefined;
310
+ return { consume: true };
311
+ }
312
+ handleWheelInput(event, overlayActive) {
313
+ if (!this.mouseEnabled)
314
+ return undefined;
315
+ if (!overlayActive)
316
+ this.routeWheel(event);
317
+ return { consume: true };
318
+ }
319
+ handleMouseInput(event, overlayActive) {
320
+ if (!this.mouseEnabled)
321
+ return undefined;
322
+ if (this.handleRightClickPaste(event))
323
+ return { consume: true };
324
+ if (overlayActive)
325
+ return { consume: true };
326
+ const handled = this.handleScrollbarMouseEvent(event);
327
+ if (!this.scrollbarDrag)
328
+ this.updateScrollbarHover(event.x, event.y);
329
+ if (!handled)
330
+ this.handleSelectionMouseEvent(event);
331
+ return { consume: true };
332
+ }
333
+ handleViewportNavigationInput(data) {
334
+ const keybindings = getKeybindings();
335
+ const bindings = [
336
+ {
337
+ keybinding: "tui.altScreen.pageUp",
338
+ navigate: () => this.scrollBy(-Math.max(1, this.getPrimaryScrollView().viewportHeight - PAGE_SCROLL_OVERLAP)),
339
+ },
340
+ {
341
+ keybinding: "tui.altScreen.pageDown",
342
+ navigate: () => this.scrollBy(Math.max(1, this.getPrimaryScrollView().viewportHeight - PAGE_SCROLL_OVERLAP)),
343
+ },
344
+ {
345
+ keybinding: "tui.altScreen.halfPageUp",
346
+ navigate: () => this.scrollBy(-Math.max(1, Math.floor(this.getPrimaryScrollView().viewportHeight / 2))),
347
+ },
348
+ {
349
+ keybinding: "tui.altScreen.halfPageDown",
350
+ navigate: () => this.scrollBy(Math.max(1, Math.floor(this.getPrimaryScrollView().viewportHeight / 2))),
351
+ },
352
+ { keybinding: "tui.altScreen.previousPrompt", navigate: () => this.scrollToPrompt(-1) },
353
+ { keybinding: "tui.altScreen.nextPrompt", navigate: () => this.scrollToPrompt(1) },
354
+ { keybinding: "tui.altScreen.top", navigate: () => this.scrollToTop() },
355
+ { keybinding: "tui.altScreen.bottom", navigate: () => this.scrollToBottom() },
356
+ ];
357
+ const binding = bindings.find(({ keybinding }) => keybindings.matches(data, keybinding));
358
+ if (!binding)
359
+ return undefined;
360
+ if (!isKeyRelease(data))
361
+ binding.navigate();
362
+ return { consume: true };
363
+ }
364
+ handleViewportInput(data) {
365
+ if (data === FOCUS_OUT)
366
+ return this.handleFocusOut();
367
+ if (data === FOCUS_IN)
368
+ return this.mouseEnabled ? { consume: true } : undefined;
369
+ const overlayActive = this.hasCapturingOverlay();
370
+ const wheelEvent = this.parseWheelEvent(data);
371
+ if (wheelEvent)
372
+ return this.handleWheelInput(wheelEvent, overlayActive);
373
+ const mouseEvent = this.parseSgrMouseEvent(data);
374
+ if (mouseEvent)
375
+ return this.handleMouseInput(mouseEvent, overlayActive);
376
+ if (this.isMouseSequence(data))
377
+ return this.mouseEnabled ? { consume: true } : undefined;
378
+ return overlayActive ? undefined : this.handleViewportNavigationInput(data);
379
+ }
380
+ parseWheelEvent(data) {
381
+ const sgr = /^\x1b\[<(\d+);(\d+);(\d+)[Mm]$/.exec(data);
382
+ if (sgr) {
383
+ return createWheelEvent(Number.parseInt(sgr[1], 10), Number.parseInt(sgr[2], 10) - 1, Number.parseInt(sgr[3], 10) - 1);
384
+ }
385
+ if (data.length !== 6 || !data.startsWith("\x1b[M"))
386
+ return undefined;
387
+ return createWheelEvent(data.charCodeAt(3) - 32, data.charCodeAt(4) - 33, data.charCodeAt(5) - 33);
388
+ }
389
+ routeWheel(event) {
390
+ let remaining = event.direction * this.wheelScrollLines;
391
+ const seen = new Set();
392
+ let contained = false;
393
+ for (const scrollView of this.currentLayout ? getScrollViewsAt(this.currentLayout, event.x, event.y) : []) {
394
+ seen.add(scrollView);
395
+ remaining = scrollView.scrollBy(remaining);
396
+ if (remaining === 0)
397
+ break;
398
+ if (scrollView.overscroll === "contain") {
399
+ contained = true;
400
+ break;
401
+ }
402
+ }
403
+ const primary = this.getPrimaryScrollView();
404
+ if (remaining !== 0 && !contained && !seen.has(primary))
405
+ primary.scrollBy(remaining);
406
+ this.updateScrollbarHover(event.x, event.y);
407
+ this.requestRender();
408
+ }
409
+ parseSgrMouseEvent(data) {
410
+ const match = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(data);
411
+ if (!match)
412
+ return undefined;
413
+ return {
414
+ button: Number.parseInt(match[1], 10),
415
+ x: Number.parseInt(match[2], 10) - 1,
416
+ y: Number.parseInt(match[3], 10) - 1,
417
+ release: match[4] === "m",
418
+ };
419
+ }
420
+ handleRightClickPaste(event) {
421
+ if (!this.onRightClickPaste || process.platform !== "win32" || event.release || event.button !== 2) {
422
+ return false;
423
+ }
424
+ try {
425
+ this.onRightClickPaste();
426
+ }
427
+ catch {
428
+ // Clipboard paste is best-effort.
429
+ }
430
+ return true;
431
+ }
432
+ getScrollbarTargetAt(x, y) {
433
+ if (this.hasCapturingOverlay() || !this.currentLayout)
434
+ return undefined;
435
+ for (const scrollView of getScrollViewsAt(this.currentLayout, x, y)) {
436
+ const box = getScrollViewBox(this.currentLayout, scrollView);
437
+ const geometry = box ? getScrollbarGeometry(box) : undefined;
438
+ if (geometry &&
439
+ x === geometry.column &&
440
+ y >= geometry.thumbTop &&
441
+ y < geometry.thumbTop + geometry.thumbHeight) {
442
+ return { scrollView, geometry };
443
+ }
444
+ }
445
+ return undefined;
446
+ }
447
+ setScrollbarHover(scrollView) {
448
+ if (scrollView === this.scrollbarHover)
449
+ return;
450
+ this.scrollbarHover?.setScrollbarActive(false);
451
+ this.scrollbarHover = scrollView;
452
+ this.scrollbarHover?.setScrollbarActive(true);
453
+ }
454
+ updateScrollbarHover(x, y) {
455
+ this.setScrollbarHover(this.getScrollbarTargetAt(x, y)?.scrollView);
456
+ }
457
+ stopScrollbarHover() {
458
+ this.setScrollbarHover(undefined);
459
+ }
460
+ handleScrollbarMouseEvent(event) {
461
+ if (this.scrollbarDrag) {
462
+ if (event.release) {
463
+ this.stopScrollbarDrag();
464
+ return true;
465
+ }
466
+ const box = this.currentLayout
467
+ ? getScrollViewBox(this.currentLayout, this.scrollbarDrag.scrollView)
468
+ : undefined;
469
+ const geometry = box ? getScrollbarGeometry(box) : undefined;
470
+ if (geometry) {
471
+ const maxThumbOffset = geometry.trackHeight - geometry.thumbHeight;
472
+ const thumbOffset = Math.max(0, Math.min(maxThumbOffset, event.y - geometry.trackTop - this.scrollbarDrag.grabOffset));
473
+ const scrollTop = maxThumbOffset === 0 ? 0 : Math.round((thumbOffset / maxThumbOffset) * geometry.maxScrollTop);
474
+ this.scrollbarDrag.scrollView.scrollTo(scrollTop);
475
+ }
476
+ return true;
477
+ }
478
+ if (event.release || (event.button & 32) !== 0 || (event.button & 3) !== 0)
479
+ return false;
480
+ const target = this.getScrollbarTargetAt(event.x, event.y);
481
+ if (!target)
482
+ return false;
483
+ this.stopSelectionAutoScroll();
484
+ this.selectionPressActive = false;
485
+ this.selectionAnchor = undefined;
486
+ this.selectionFocus = undefined;
487
+ this.selectionGranularity = "character";
488
+ this.selectionInitialRange = undefined;
489
+ this.lastClick = undefined;
490
+ this.pressedUrl = undefined;
491
+ this.selectionDragged = false;
492
+ this.setScrollbarHover(target.scrollView);
493
+ this.scrollbarDrag = {
494
+ scrollView: target.scrollView,
495
+ grabOffset: event.y - target.geometry.thumbTop,
496
+ };
497
+ return true;
498
+ }
499
+ stopScrollbarDrag() {
500
+ this.scrollbarDrag = undefined;
501
+ }
502
+ getScrollSelectionPoint(scrollView, x, y) {
503
+ if (!this.currentLayout)
504
+ return undefined;
505
+ const box = getScrollViewBox(this.currentLayout, scrollView);
506
+ if (!box || box.rect.height <= 0 || box.clip.height <= 0)
507
+ return undefined;
508
+ const visibleTop = Math.max(0, box.rect.y, box.clip.y);
509
+ const visibleBottom = Math.min(this.terminal.rows - 1, box.rect.y + box.rect.height - 1, box.clip.y + box.clip.height - 1);
510
+ if (visibleBottom < visibleTop)
511
+ return undefined;
512
+ const pointerRow = Math.max(visibleTop, Math.min(visibleBottom, y));
513
+ const maxContentRow = Math.max(0, (box.scrollContentLines?.length ?? 1) - 1);
514
+ return {
515
+ row: Math.max(0, Math.min(maxContentRow, scrollView.scrollTop + pointerRow - box.rect.y)),
516
+ col: Math.max(0, Math.min(box.rect.width - 1, x - box.rect.x)),
517
+ scrollView,
518
+ };
519
+ }
520
+ getSelectionPoint(event, scrollView) {
521
+ if (scrollView) {
522
+ const point = this.getScrollSelectionPoint(scrollView, event.x, event.y);
523
+ if (point)
524
+ return point;
525
+ }
526
+ return {
527
+ row: Math.max(0, Math.min(this.terminal.rows - 1, event.y)),
528
+ col: Math.max(0, Math.min(this.terminal.columns - 1, event.x)),
529
+ };
530
+ }
531
+ getSelectionSourceLine(point) {
532
+ if (point.scrollView && this.currentLayout) {
533
+ const lines = getScrollViewBox(this.currentLayout, point.scrollView)?.scrollContentLines;
534
+ if (lines)
535
+ return lines[point.row] ?? "";
536
+ }
537
+ return this.previousScreen[point.row] ?? "";
538
+ }
539
+ getWordSelection(point) {
540
+ const line = stripTerminalSequences(this.getSelectionSourceLine(point));
541
+ let start = 0;
542
+ for (const segment of wordSegmenter.segment(line)) {
543
+ const end = start + visibleWidth(segment.segment);
544
+ if (point.col >= start && point.col < end) {
545
+ return {
546
+ start: { ...point, col: start },
547
+ end: { ...point, col: end, boundary: true },
548
+ };
549
+ }
550
+ start = end;
551
+ }
552
+ return undefined;
553
+ }
554
+ getLineSelection(point) {
555
+ return {
556
+ start: { ...point, col: 0 },
557
+ end: { ...point, col: visibleWidth(this.getSelectionSourceLine(point)), boundary: true },
558
+ };
559
+ }
560
+ updateSelectionFocus(point) {
561
+ if (this.selectionGranularity === "character" || !this.selectionInitialRange) {
562
+ this.selectionFocus = point;
563
+ return;
564
+ }
565
+ const range = this.selectionGranularity === "word" ? this.getWordSelection(point) : this.getLineSelection(point);
566
+ if (!range)
567
+ return;
568
+ const initial = this.selectionInitialRange;
569
+ const targetBeforeInitial = range.start.row < initial.start.row ||
570
+ (range.start.row === initial.start.row && range.start.col < initial.start.col);
571
+ if (targetBeforeInitial) {
572
+ this.selectionAnchor = initial.end;
573
+ this.selectionFocus = range.start;
574
+ }
575
+ else {
576
+ this.selectionAnchor = initial.start;
577
+ this.selectionFocus = range.end;
578
+ }
579
+ }
580
+ getClickCount(point, word) {
581
+ const now = Date.now();
582
+ const previous = this.lastClick;
583
+ const count = word &&
584
+ previous &&
585
+ now - previous.timestamp <= DOUBLE_CLICK_INTERVAL_MS &&
586
+ previous.row === point.row &&
587
+ previous.scrollView === point.scrollView &&
588
+ previous.wordStart === word.start.col &&
589
+ previous.wordEnd === word.end.col
590
+ ? (previous.count % 3) + 1
591
+ : 1;
592
+ this.lastClick = word
593
+ ? {
594
+ timestamp: now,
595
+ count,
596
+ row: point.row,
597
+ scrollView: point.scrollView,
598
+ wordStart: word.start.col,
599
+ wordEnd: word.end.col,
600
+ }
601
+ : undefined;
602
+ return count;
603
+ }
604
+ updateSelectionAutoScroll(event) {
605
+ const scrollView = this.selectionAnchor?.scrollView;
606
+ if (!scrollView || !this.currentLayout) {
607
+ this.stopSelectionAutoScroll();
608
+ return;
609
+ }
610
+ const box = getScrollViewBox(this.currentLayout, scrollView);
611
+ if (!box || box.rect.height <= 0 || box.clip.height <= 0) {
612
+ this.stopSelectionAutoScroll();
613
+ return;
614
+ }
615
+ const visibleTop = Math.max(0, box.rect.y, box.clip.y);
616
+ const visibleBottom = Math.min(this.terminal.rows - 1, box.rect.y + box.rect.height - 1, box.clip.y + box.clip.height - 1);
617
+ this.selectionDragPointer = { x: event.x, y: event.y };
618
+ this.selectionAutoScrollDirection = event.y <= visibleTop ? -1 : event.y >= visibleBottom ? 1 : 0;
619
+ if (this.selectionAutoScrollDirection === 0) {
620
+ this.stopSelectionAutoScroll();
621
+ return;
622
+ }
623
+ if (this.selectionAutoScrollTimer)
624
+ return;
625
+ this.selectionAutoScrollTimer = setInterval(() => this.autoScrollSelection(), 50);
626
+ this.selectionAutoScrollTimer.unref();
627
+ }
628
+ autoScrollSelection() {
629
+ const scrollView = this.selectionAnchor?.scrollView;
630
+ const pointer = this.selectionDragPointer;
631
+ const direction = this.selectionAutoScrollDirection;
632
+ if (!scrollView || !pointer || direction === 0) {
633
+ this.stopSelectionAutoScroll();
634
+ return;
635
+ }
636
+ const remaining = scrollView.scrollBy(direction);
637
+ if (remaining === direction) {
638
+ this.stopSelectionAutoScroll();
639
+ return;
640
+ }
641
+ const point = this.getScrollSelectionPoint(scrollView, pointer.x, pointer.y);
642
+ if (point)
643
+ this.updateSelectionFocus(point);
644
+ this.requestRender();
645
+ }
646
+ stopSelectionAutoScroll() {
647
+ if (this.selectionAutoScrollTimer) {
648
+ clearInterval(this.selectionAutoScrollTimer);
649
+ this.selectionAutoScrollTimer = undefined;
650
+ }
651
+ this.selectionAutoScrollDirection = 0;
652
+ this.selectionDragPointer = undefined;
653
+ }
654
+ activatePressedUrl(point) {
655
+ const anchor = this.selectionAnchor;
656
+ const clickedUrl = anchor &&
657
+ !this.selectionDragged &&
658
+ anchor.scrollView === point.scrollView &&
659
+ anchor.row === point.row &&
660
+ anchor.col === point.col
661
+ ? this.pressedUrl
662
+ : undefined;
663
+ this.pressedUrl = undefined;
664
+ if (!clickedUrl || !this.openUrl)
665
+ return false;
666
+ this.selectionAnchor = undefined;
667
+ this.selectionFocus = undefined;
668
+ try {
669
+ this.openUrl(clickedUrl);
670
+ }
671
+ catch {
672
+ // URL activation is best-effort.
673
+ }
674
+ this.requestRender();
675
+ return true;
676
+ }
677
+ completeSelectionPress(point) {
678
+ if (!this.selectionPressActive)
679
+ return;
680
+ this.selectionPressActive = false;
681
+ this.stopSelectionAutoScroll();
682
+ if (!this.selectionAnchor)
683
+ return;
684
+ this.updateSelectionFocus(point);
685
+ if (this.activatePressedUrl(point))
686
+ return;
687
+ this.copySelectionToClipboard();
688
+ this.requestRender();
689
+ }
690
+ updateSelectionDrag(event, point) {
691
+ if (!this.selectionPressActive || !this.selectionAnchor)
692
+ return;
693
+ this.selectionDragged = true;
694
+ this.lastClick = undefined;
695
+ this.pressedUrl = undefined;
696
+ this.updateSelectionFocus(point);
697
+ this.updateSelectionAutoScroll(event);
698
+ this.requestRender();
699
+ }
700
+ startSelectionPress(event) {
701
+ this.stopSelectionAutoScroll();
702
+ this.selectionPressActive = true;
703
+ const scrollView = !this.hasCapturingOverlay() && this.currentLayout
704
+ ? getScrollViewsAt(this.currentLayout, event.x, event.y)[0]
705
+ : undefined;
706
+ const anchor = this.getSelectionPoint(event, scrollView);
707
+ const word = this.getWordSelection(anchor);
708
+ const clickCount = this.getClickCount(anchor, word);
709
+ const range = clickCount === 2 ? word : clickCount === 3 ? this.getLineSelection(anchor) : undefined;
710
+ this.selectionGranularity = range ? (clickCount === 2 ? "word" : "line") : "character";
711
+ this.selectionInitialRange = range;
712
+ this.selectionAnchor = range?.start ?? anchor;
713
+ this.selectionFocus = range?.end ?? anchor;
714
+ this.selectionDragged = false;
715
+ this.pressedUrl = range
716
+ ? undefined
717
+ : getOsc8LinkAtColumn(this.previousScreen[Math.max(0, Math.min(this.terminal.rows - 1, event.y))] ?? "", Math.max(0, Math.min(this.terminal.columns - 1, event.x)));
718
+ this.requestRender();
719
+ }
720
+ handleSelectionMouseEvent(event) {
721
+ if ((event.button & 3) !== 0)
722
+ return;
723
+ const point = this.getSelectionPoint(event, this.selectionAnchor?.scrollView);
724
+ if (event.release) {
725
+ this.completeSelectionPress(point);
726
+ return;
727
+ }
728
+ if ((event.button & 32) !== 0) {
729
+ this.updateSelectionDrag(event, point);
730
+ return;
731
+ }
732
+ this.startSelectionPress(event);
733
+ }
734
+ getSelectionBounds() {
735
+ if (!this.selectionAnchor || !this.selectionFocus)
736
+ return undefined;
737
+ if (this.selectionAnchor.scrollView !== this.selectionFocus.scrollView)
738
+ return undefined;
739
+ const anchorBeforeFocus = this.selectionAnchor.row < this.selectionFocus.row ||
740
+ (this.selectionAnchor.row === this.selectionFocus.row && this.selectionAnchor.col < this.selectionFocus.col);
741
+ if (this.selectionAnchor.row === this.selectionFocus.row &&
742
+ this.selectionAnchor.col === this.selectionFocus.col) {
743
+ return undefined;
744
+ }
745
+ return anchorBeforeFocus
746
+ ? { start: this.selectionAnchor, end: this.selectionFocus }
747
+ : { start: this.selectionFocus, end: this.selectionAnchor };
748
+ }
749
+ getSelectionColumns(line, row, selection, minColumn = 0, maxColumn = visibleWidth(line)) {
750
+ const lineWidth = visibleWidth(line);
751
+ let start = Math.max(0, minColumn);
752
+ let end = Math.min(lineWidth, maxColumn);
753
+ if (row === selection.start.row) {
754
+ start = getGraphemeCellRange(line, selection.start.col)?.start ?? Math.min(selection.start.col, lineWidth);
755
+ }
756
+ if (row === selection.end.row) {
757
+ end = selection.end.boundary
758
+ ? Math.min(selection.end.col, lineWidth)
759
+ : (getGraphemeCellRange(line, selection.end.col)?.end ?? Math.min(selection.end.col + 1, lineWidth));
760
+ }
761
+ return { start: Math.max(minColumn, start), end: Math.min(maxColumn, end) };
762
+ }
763
+ copySelectionToClipboard() {
764
+ const selection = this.getSelectionBounds();
765
+ if (!selection)
766
+ return;
767
+ let sourceLines = this.previousScreen;
768
+ if (selection.start.scrollView) {
769
+ if (!this.currentLayout)
770
+ return;
771
+ const box = getScrollViewBox(this.currentLayout, selection.start.scrollView);
772
+ if (!box?.scrollContentLines)
773
+ return;
774
+ sourceLines = box.scrollContentLines;
775
+ }
776
+ const lines = [];
777
+ for (let row = selection.start.row; row <= selection.end.row; row++) {
778
+ const line = sourceLines[row] ?? "";
779
+ const columns = this.getSelectionColumns(line, row, selection);
780
+ lines.push(stripTerminalSequences(sliceByColumn(line, columns.start, Math.max(0, columns.end - columns.start), true)).trimEnd());
781
+ }
782
+ const text = lines.join("\n");
783
+ if (text.length === 0)
784
+ return;
785
+ this.terminal.write(`\x1b]52;c;${Buffer.from(text).toString("base64")}\x07`);
786
+ this.flash("Copied!");
787
+ }
788
+ applySelectionHighlight(text) {
789
+ let result = "\x1b[7m";
790
+ let index = 0;
791
+ while (index < text.length) {
792
+ const ansi = extractAnsiCode(text, index);
793
+ if (!ansi) {
794
+ result += text[index];
795
+ index++;
796
+ continue;
797
+ }
798
+ result += ansi.code;
799
+ if (ansi.code.endsWith("m"))
800
+ result += "\x1b[7m";
801
+ index += ansi.length;
802
+ }
803
+ return `${result}\x1b[27m`;
804
+ }
805
+ applySelection(screen, layout = this.currentLayout) {
806
+ const selection = this.getSelectionBounds();
807
+ if (!selection)
808
+ return screen;
809
+ let screenSelection = selection;
810
+ let minRow = 0;
811
+ let maxRow = screen.length - 1;
812
+ let minColumn = 0;
813
+ let maxColumn = this.terminal.columns;
814
+ if (selection.start.scrollView) {
815
+ if (!layout)
816
+ return screen;
817
+ const box = getScrollViewBox(layout, selection.start.scrollView);
818
+ if (!box)
819
+ return screen;
820
+ minRow = Math.max(0, box.rect.y, box.clip.y);
821
+ maxRow = Math.min(screen.length - 1, box.rect.y + box.rect.height - 1, box.clip.y + box.clip.height - 1);
822
+ minColumn = Math.max(0, box.rect.x, box.clip.x);
823
+ maxColumn = Math.min(this.terminal.columns, box.rect.x + box.rect.width, box.clip.x + box.clip.width);
824
+ screenSelection = {
825
+ start: {
826
+ ...selection.start,
827
+ row: box.rect.y + selection.start.row - selection.start.scrollView.scrollTop,
828
+ col: box.rect.x + selection.start.col,
829
+ },
830
+ end: {
831
+ ...selection.end,
832
+ row: box.rect.y + selection.end.row - selection.start.scrollView.scrollTop,
833
+ col: box.rect.x + selection.end.col,
834
+ },
835
+ };
836
+ }
837
+ return screen.map((line, row) => {
838
+ if (row < minRow ||
839
+ row > maxRow ||
840
+ row < screenSelection.start.row ||
841
+ row > screenSelection.end.row ||
842
+ isImageLine(line)) {
843
+ return line;
844
+ }
845
+ const lineWidth = visibleWidth(line);
846
+ const columns = this.getSelectionColumns(line, row, screenSelection, minColumn, maxColumn);
847
+ if (columns.end <= columns.start)
848
+ return line;
849
+ const before = sliceByColumn(line, 0, columns.start, true);
850
+ const selected = sliceByColumn(line, columns.start, columns.end - columns.start, true);
851
+ const after = sliceByColumn(line, columns.end, Math.max(0, lineWidth - columns.end), true);
852
+ return `${before}${this.applySelectionHighlight(selected)}${after}`;
853
+ });
854
+ }
855
+ isMouseSequence(data) {
856
+ return /^\x1b\[<\d+;\d+;\d+[Mm]$/.test(data) || (data.length === 6 && data.startsWith("\x1b[M"));
857
+ }
858
+ compositeFlashes(screen, width, height) {
859
+ const flashLines = this.flashes.render(width).slice(-height);
860
+ if (flashLines.length === 0)
861
+ return screen;
862
+ const result = [...screen];
863
+ while (result.length < height)
864
+ result.push("");
865
+ for (let row = 0; row < flashLines.length; row++) {
866
+ const line = flashLines[row];
867
+ const flashWidth = visibleWidth(line);
868
+ if (flashWidth === 0)
869
+ continue;
870
+ result[row] = compositeTuiLine(result[row] ?? "", line, width - flashWidth, flashWidth, width);
871
+ }
872
+ return result;
873
+ }
874
+ prepareRenderFrame(width, height) {
875
+ const root = this.layoutRoot ?? this.implicitScrollView;
876
+ const layout = renderLayoutFrame(root, width, height, () => this.requestRender());
877
+ let screen = layout.lines.map((line) => line.replace(OSC133_ZONE_PREFIX, ""));
878
+ screen = this.compositeOverlays(screen, width, height);
879
+ if (screen.length > height)
880
+ screen = screen.slice(screen.length - height);
881
+ screen = this.applySelection(screen, layout);
882
+ screen = this.compositeFlashes(screen, width, height);
883
+ const cursorPosition = this.extractCursorPosition(screen, height);
884
+ screen = this.applyLineResets(screen).map((line) => {
885
+ if (isImageLine(line) || visibleWidth(line) <= width)
886
+ return line;
887
+ return sliceByColumn(line, 0, width, true);
888
+ });
889
+ return { layout, screen, cursorPosition };
890
+ }
891
+ prepareRedrawPlan(screen, width, height) {
892
+ const fullRedraw = this.previousScreen.length === 0 || this.previousScreenWidth !== width || this.previousScreenHeight !== height;
893
+ const imagesNeedRedraw = screen.some((line, row) => line !== this.previousScreen[row] && (isImageLine(line) || isImageLine(this.previousScreen[row] ?? "")));
894
+ const redrawImages = fullRedraw || imagesNeedRedraw;
895
+ const hadUploadedKittyImages = this.uploadedKittyImages.size > 0;
896
+ const preparedKittyScreen = redrawImages && this.imageProtocol === "kitty"
897
+ ? this.prepareKittyScreen(screen)
898
+ : { lines: screen, evictedImageDeletion: "" };
899
+ return { fullRedraw, imagesNeedRedraw, hadUploadedKittyImages, preparedKittyScreen };
900
+ }
901
+ createImageRedrawSequence(plan) {
902
+ if (plan.fullRedraw) {
903
+ this.fullRedrawCount += 1;
904
+ const clearImages = this.imageProtocol === "kitty" && plan.hadUploadedKittyImages
905
+ ? deleteAllKittyPlacements()
906
+ : this.deleteKittyImages();
907
+ return `${clearImages}\x1b[2J`;
908
+ }
909
+ if (!plan.imagesNeedRedraw)
910
+ return "";
911
+ if (this.imageProtocol === "iterm2")
912
+ return "\x1b[2J";
913
+ return this.imageProtocol === "kitty" ? deleteAllKittyPlacements() : "";
914
+ }
915
+ appendChangedScreenRows(buffer, screen, preparedLines, height, redrawAllRows) {
916
+ for (let row = 0; row < height; row++) {
917
+ if (!redrawAllRows && screen[row] === this.previousScreen[row])
918
+ continue;
919
+ buffer += `\x1b[${row + 1};1H\x1b[2K${preparedLines[row] ?? ""}`;
920
+ }
921
+ return buffer;
922
+ }
923
+ appendCursorState(buffer, cursorPosition, width) {
924
+ if (!cursorPosition)
925
+ return `${buffer}\x1b[?25l`;
926
+ const position = `\x1b[${cursorPosition.row + 1};${Math.min(width, cursorPosition.col) + 1}H`;
927
+ const visibility = this.getShowHardwareCursor() ? "\x1b[?25h" : "\x1b[?25l";
928
+ return buffer + position + visibility;
929
+ }
930
+ createScreenUpdateBuffer(frame, plan, width, height) {
931
+ let buffer = BEGIN_SYNCHRONIZED_OUTPUT + this.createImageRedrawSequence(plan);
932
+ buffer += plan.preparedKittyScreen.evictedImageDeletion;
933
+ buffer = this.appendChangedScreenRows(buffer, frame.screen, plan.preparedKittyScreen.lines, height, plan.fullRedraw || plan.imagesNeedRedraw);
934
+ return this.appendCursorState(buffer, frame.cursorPosition, width) + END_SYNCHRONIZED_OUTPUT;
935
+ }
936
+ doRender() {
937
+ if (this.stopped || !this.altScreenActive)
938
+ return;
939
+ const width = Math.max(1, this.terminal.columns);
940
+ const height = Math.max(1, this.terminal.rows);
941
+ const frame = this.prepareRenderFrame(width, height);
942
+ const redrawPlan = this.prepareRedrawPlan(frame.screen, width, height);
943
+ this.terminal.write(this.createScreenUpdateBuffer(frame, redrawPlan, width, height));
944
+ this.previousScreen = frame.screen;
945
+ this.previousScreenWidth = width;
946
+ this.previousScreenHeight = height;
947
+ this.currentLayout = frame.layout;
948
+ }
949
+ }
950
+ //# sourceMappingURL=tui-alt-screen.js.map