@greppleai/grip-tui 0.0.0-stage → 0.0.1

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 (158) hide show
  1. package/README.md +818 -2
  2. package/dist/autocomplete.d.ts +48 -0
  3. package/dist/autocomplete.d.ts.map +1 -0
  4. package/dist/autocomplete.js +566 -0
  5. package/dist/autocomplete.js.map +1 -0
  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/box.d.ts +22 -0
  11. package/dist/components/box.d.ts.map +1 -0
  12. package/dist/components/box.js +104 -0
  13. package/dist/components/box.js.map +1 -0
  14. package/dist/components/cancellable-loader.d.ts +22 -0
  15. package/dist/components/cancellable-loader.d.ts.map +1 -0
  16. package/dist/components/cancellable-loader.js +35 -0
  17. package/dist/components/cancellable-loader.js.map +1 -0
  18. package/dist/components/editor.d.ts +292 -0
  19. package/dist/components/editor.d.ts.map +1 -0
  20. package/dist/components/editor.js +1887 -0
  21. package/dist/components/editor.js.map +1 -0
  22. package/dist/components/h-stack.d.ts +7 -0
  23. package/dist/components/h-stack.d.ts.map +1 -0
  24. package/dist/components/h-stack.js +43 -0
  25. package/dist/components/h-stack.js.map +1 -0
  26. package/dist/components/image.d.ts +32 -0
  27. package/dist/components/image.d.ts.map +1 -0
  28. package/dist/components/image.js +76 -0
  29. package/dist/components/image.js.map +1 -0
  30. package/dist/components/input.d.ts +46 -0
  31. package/dist/components/input.d.ts.map +1 -0
  32. package/dist/components/input.js +352 -0
  33. package/dist/components/input.js.map +1 -0
  34. package/dist/components/loader.d.ts +32 -0
  35. package/dist/components/loader.d.ts.map +1 -0
  36. package/dist/components/loader.js +80 -0
  37. package/dist/components/loader.js.map +1 -0
  38. package/dist/components/markdown.d.ts +132 -0
  39. package/dist/components/markdown.d.ts.map +1 -0
  40. package/dist/components/markdown.js +717 -0
  41. package/dist/components/markdown.js.map +1 -0
  42. package/dist/components/scroll-view.d.ts +53 -0
  43. package/dist/components/scroll-view.d.ts.map +1 -0
  44. package/dist/components/scroll-view.js +173 -0
  45. package/dist/components/scroll-view.js.map +1 -0
  46. package/dist/components/select-list.d.ts +50 -0
  47. package/dist/components/select-list.d.ts.map +1 -0
  48. package/dist/components/select-list.js +159 -0
  49. package/dist/components/select-list.js.map +1 -0
  50. package/dist/components/settings-list.d.ts +52 -0
  51. package/dist/components/settings-list.d.ts.map +1 -0
  52. package/dist/components/settings-list.js +187 -0
  53. package/dist/components/settings-list.js.map +1 -0
  54. package/dist/components/spacer.d.ts +12 -0
  55. package/dist/components/spacer.d.ts.map +1 -0
  56. package/dist/components/spacer.js +23 -0
  57. package/dist/components/spacer.js.map +1 -0
  58. package/dist/components/stack.d.ts +32 -0
  59. package/dist/components/stack.d.ts.map +1 -0
  60. package/dist/components/stack.js +122 -0
  61. package/dist/components/stack.js.map +1 -0
  62. package/dist/components/text.d.ts +19 -0
  63. package/dist/components/text.d.ts.map +1 -0
  64. package/dist/components/text.js +89 -0
  65. package/dist/components/text.js.map +1 -0
  66. package/dist/components/truncated-text.d.ts +13 -0
  67. package/dist/components/truncated-text.d.ts.map +1 -0
  68. package/dist/components/truncated-text.js +51 -0
  69. package/dist/components/truncated-text.js.map +1 -0
  70. package/dist/components/v-stack.d.ts +8 -0
  71. package/dist/components/v-stack.d.ts.map +1 -0
  72. package/dist/components/v-stack.js +27 -0
  73. package/dist/components/v-stack.js.map +1 -0
  74. package/dist/editor-component.d.ts +39 -0
  75. package/dist/editor-component.d.ts.map +1 -0
  76. package/dist/editor-component.js +2 -0
  77. package/dist/editor-component.js.map +1 -0
  78. package/dist/fuzzy.d.ts +16 -0
  79. package/dist/fuzzy.d.ts.map +1 -0
  80. package/dist/fuzzy.js +101 -0
  81. package/dist/fuzzy.js.map +1 -0
  82. package/dist/index.d.ts +30 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +41 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/keybindings.d.ts +243 -0
  87. package/dist/keybindings.d.ts.map +1 -0
  88. package/dist/keybindings.js +209 -0
  89. package/dist/keybindings.js.map +1 -0
  90. package/dist/keys.d.ts +184 -0
  91. package/dist/keys.d.ts.map +1 -0
  92. package/dist/keys.js +1053 -0
  93. package/dist/keys.js.map +1 -0
  94. package/dist/kill-ring.d.ts +30 -0
  95. package/dist/kill-ring.d.ts.map +1 -0
  96. package/dist/kill-ring.js +37 -0
  97. package/dist/kill-ring.js.map +1 -0
  98. package/dist/latex.d.ts +10 -0
  99. package/dist/latex.d.ts.map +1 -0
  100. package/dist/latex.js +1406 -0
  101. package/dist/latex.js.map +1 -0
  102. package/dist/layout-node.d.ts +43 -0
  103. package/dist/layout-node.d.ts.map +1 -0
  104. package/dist/layout-node.js +6 -0
  105. package/dist/layout-node.js.map +1 -0
  106. package/dist/layout.d.ts +40 -0
  107. package/dist/layout.d.ts.map +1 -0
  108. package/dist/layout.js +386 -0
  109. package/dist/layout.js.map +1 -0
  110. package/dist/native-modifiers.d.ts +3 -0
  111. package/dist/native-modifiers.d.ts.map +1 -0
  112. package/dist/native-modifiers.js +53 -0
  113. package/dist/native-modifiers.js.map +1 -0
  114. package/dist/stdin-buffer.d.ts +58 -0
  115. package/dist/stdin-buffer.d.ts.map +1 -0
  116. package/dist/stdin-buffer.js +318 -0
  117. package/dist/stdin-buffer.js.map +1 -0
  118. package/dist/terminal-control.d.ts +5 -0
  119. package/dist/terminal-control.d.ts.map +1 -0
  120. package/dist/terminal-control.js +26 -0
  121. package/dist/terminal-control.js.map +1 -0
  122. package/dist/terminal-image.d.ts +112 -0
  123. package/dist/terminal-image.d.ts.map +1 -0
  124. package/dist/terminal-image.js +508 -0
  125. package/dist/terminal-image.js.map +1 -0
  126. package/dist/terminal.d.ts +117 -0
  127. package/dist/terminal.d.ts.map +1 -0
  128. package/dist/terminal.js +490 -0
  129. package/dist/terminal.js.map +1 -0
  130. package/dist/tui-alt-screen.d.ts +114 -0
  131. package/dist/tui-alt-screen.d.ts.map +1 -0
  132. package/dist/tui-alt-screen.js +950 -0
  133. package/dist/tui-alt-screen.js.map +1 -0
  134. package/dist/tui-main-screen.d.ts +52 -0
  135. package/dist/tui-main-screen.d.ts.map +1 -0
  136. package/dist/tui-main-screen.js +511 -0
  137. package/dist/tui-main-screen.js.map +1 -0
  138. package/dist/tui.d.ts +295 -0
  139. package/dist/tui.d.ts.map +1 -0
  140. package/dist/tui.js +868 -0
  141. package/dist/tui.js.map +1 -0
  142. package/dist/undo-stack.d.ts +17 -0
  143. package/dist/undo-stack.d.ts.map +1 -0
  144. package/dist/undo-stack.js +25 -0
  145. package/dist/undo-stack.js.map +1 -0
  146. package/dist/utils.d.ts +96 -0
  147. package/dist/utils.d.ts.map +1 -0
  148. package/dist/utils.js +1106 -0
  149. package/dist/utils.js.map +1 -0
  150. package/dist/word-navigation.d.ts +25 -0
  151. package/dist/word-navigation.d.ts.map +1 -0
  152. package/dist/word-navigation.js +94 -0
  153. package/dist/word-navigation.js.map +1 -0
  154. package/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  155. package/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  156. package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  157. package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  158. package/package.json +47 -5
@@ -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