@ny4n/tui 0.0.0-stage → 1.0.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 (205) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +948 -2
  3. package/dist/alt-screen-search.d.ts +46 -0
  4. package/dist/alt-screen-search.d.ts.map +1 -0
  5. package/dist/alt-screen-search.js +282 -0
  6. package/dist/alt-screen-search.js.map +1 -0
  7. package/dist/autocomplete.d.ts +57 -0
  8. package/dist/autocomplete.d.ts.map +1 -0
  9. package/dist/autocomplete.js +693 -0
  10. package/dist/autocomplete.js.map +1 -0
  11. package/dist/colors.d.ts +73 -0
  12. package/dist/colors.d.ts.map +1 -0
  13. package/dist/colors.js +273 -0
  14. package/dist/colors.js.map +1 -0
  15. package/dist/components/alt-screen-flash.d.ts +13 -0
  16. package/dist/components/alt-screen-flash.d.ts.map +1 -0
  17. package/dist/components/alt-screen-flash.js +37 -0
  18. package/dist/components/alt-screen-flash.js.map +1 -0
  19. package/dist/components/box.d.ts +25 -0
  20. package/dist/components/box.d.ts.map +1 -0
  21. package/dist/components/box.js +138 -0
  22. package/dist/components/box.js.map +1 -0
  23. package/dist/components/cancellable-loader.d.ts +22 -0
  24. package/dist/components/cancellable-loader.d.ts.map +1 -0
  25. package/dist/components/cancellable-loader.js +35 -0
  26. package/dist/components/cancellable-loader.js.map +1 -0
  27. package/dist/components/editor.d.ts +260 -0
  28. package/dist/components/editor.d.ts.map +1 -0
  29. package/dist/components/editor.js +2044 -0
  30. package/dist/components/editor.js.map +1 -0
  31. package/dist/components/h-stack.d.ts +7 -0
  32. package/dist/components/h-stack.d.ts.map +1 -0
  33. package/dist/components/h-stack.js +43 -0
  34. package/dist/components/h-stack.js.map +1 -0
  35. package/dist/components/image.d.ts +40 -0
  36. package/dist/components/image.d.ts.map +1 -0
  37. package/dist/components/image.js +123 -0
  38. package/dist/components/image.js.map +1 -0
  39. package/dist/components/input.d.ts +48 -0
  40. package/dist/components/input.d.ts.map +1 -0
  41. package/dist/components/input.js +417 -0
  42. package/dist/components/input.js.map +1 -0
  43. package/dist/components/loader.d.ts +33 -0
  44. package/dist/components/loader.d.ts.map +1 -0
  45. package/dist/components/loader.js +76 -0
  46. package/dist/components/loader.js.map +1 -0
  47. package/dist/components/markdown.d.ts +104 -0
  48. package/dist/components/markdown.d.ts.map +1 -0
  49. package/dist/components/markdown.js +821 -0
  50. package/dist/components/markdown.js.map +1 -0
  51. package/dist/components/mouse-region.d.ts +12 -0
  52. package/dist/components/mouse-region.d.ts.map +1 -0
  53. package/dist/components/mouse-region.js +21 -0
  54. package/dist/components/mouse-region.js.map +1 -0
  55. package/dist/components/scroll-view.d.ts +59 -0
  56. package/dist/components/scroll-view.d.ts.map +1 -0
  57. package/dist/components/scroll-view.js +195 -0
  58. package/dist/components/scroll-view.js.map +1 -0
  59. package/dist/components/select-list.d.ts +53 -0
  60. package/dist/components/select-list.d.ts.map +1 -0
  61. package/dist/components/select-list.js +206 -0
  62. package/dist/components/select-list.js.map +1 -0
  63. package/dist/components/settings-list.d.ts +60 -0
  64. package/dist/components/settings-list.d.ts.map +1 -0
  65. package/dist/components/settings-list.js +252 -0
  66. package/dist/components/settings-list.js.map +1 -0
  67. package/dist/components/spacer.d.ts +12 -0
  68. package/dist/components/spacer.d.ts.map +1 -0
  69. package/dist/components/spacer.js +23 -0
  70. package/dist/components/spacer.js.map +1 -0
  71. package/dist/components/stack.d.ts +32 -0
  72. package/dist/components/stack.d.ts.map +1 -0
  73. package/dist/components/stack.js +111 -0
  74. package/dist/components/stack.js.map +1 -0
  75. package/dist/components/text.d.ts +20 -0
  76. package/dist/components/text.d.ts.map +1 -0
  77. package/dist/components/text.js +95 -0
  78. package/dist/components/text.js.map +1 -0
  79. package/dist/components/truncated-text.d.ts +13 -0
  80. package/dist/components/truncated-text.d.ts.map +1 -0
  81. package/dist/components/truncated-text.js +51 -0
  82. package/dist/components/truncated-text.js.map +1 -0
  83. package/dist/components/v-stack.d.ts +8 -0
  84. package/dist/components/v-stack.d.ts.map +1 -0
  85. package/dist/components/v-stack.js +26 -0
  86. package/dist/components/v-stack.js.map +1 -0
  87. package/dist/editor-component.d.ts +39 -0
  88. package/dist/editor-component.d.ts.map +1 -0
  89. package/dist/editor-component.js +2 -0
  90. package/dist/editor-component.js.map +1 -0
  91. package/dist/fuzzy.d.ts +16 -0
  92. package/dist/fuzzy.d.ts.map +1 -0
  93. package/dist/fuzzy.js +111 -0
  94. package/dist/fuzzy.js.map +1 -0
  95. package/dist/index.d.ts +37 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +50 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/keybindings.d.ts +273 -0
  100. package/dist/keybindings.d.ts.map +1 -0
  101. package/dist/keybindings.js +233 -0
  102. package/dist/keybindings.js.map +1 -0
  103. package/dist/keys.d.ts +184 -0
  104. package/dist/keys.d.ts.map +1 -0
  105. package/dist/keys.js +1206 -0
  106. package/dist/keys.js.map +1 -0
  107. package/dist/kill-ring.d.ts +28 -0
  108. package/dist/kill-ring.d.ts.map +1 -0
  109. package/dist/kill-ring.js +44 -0
  110. package/dist/kill-ring.js.map +1 -0
  111. package/dist/latex.d.ts +10 -0
  112. package/dist/latex.d.ts.map +1 -0
  113. package/dist/latex.js +1378 -0
  114. package/dist/latex.js.map +1 -0
  115. package/dist/layout-node.d.ts +40 -0
  116. package/dist/layout-node.d.ts.map +1 -0
  117. package/dist/layout-node.js +6 -0
  118. package/dist/layout-node.js.map +1 -0
  119. package/dist/layout.d.ts +42 -0
  120. package/dist/layout.d.ts.map +1 -0
  121. package/dist/layout.js +344 -0
  122. package/dist/layout.js.map +1 -0
  123. package/dist/native-modifiers.d.ts +4 -0
  124. package/dist/native-modifiers.d.ts.map +1 -0
  125. package/dist/native-modifiers.js +13 -0
  126. package/dist/native-modifiers.js.map +1 -0
  127. package/dist/native-module-path.d.ts +7 -0
  128. package/dist/native-module-path.d.ts.map +1 -0
  129. package/dist/native-module-path.js +19 -0
  130. package/dist/native-module-path.js.map +1 -0
  131. package/dist/native-platform.d.ts +20 -0
  132. package/dist/native-platform.d.ts.map +1 -0
  133. package/dist/native-platform.js +42 -0
  134. package/dist/native-platform.js.map +1 -0
  135. package/dist/oklab.d.ts +43 -0
  136. package/dist/oklab.d.ts.map +1 -0
  137. package/dist/oklab.js +210 -0
  138. package/dist/oklab.js.map +1 -0
  139. package/dist/program-status.d.ts +23 -0
  140. package/dist/program-status.d.ts.map +1 -0
  141. package/dist/program-status.js +43 -0
  142. package/dist/program-status.js.map +1 -0
  143. package/dist/stdin-buffer.d.ts +56 -0
  144. package/dist/stdin-buffer.d.ts.map +1 -0
  145. package/dist/stdin-buffer.js +366 -0
  146. package/dist/stdin-buffer.js.map +1 -0
  147. package/dist/terminal-colors.d.ts +27 -0
  148. package/dist/terminal-colors.d.ts.map +1 -0
  149. package/dist/terminal-colors.js +64 -0
  150. package/dist/terminal-colors.js.map +1 -0
  151. package/dist/terminal-image.d.ts +123 -0
  152. package/dist/terminal-image.d.ts.map +1 -0
  153. package/dist/terminal-image.js +603 -0
  154. package/dist/terminal-image.js.map +1 -0
  155. package/dist/terminal.d.ts +143 -0
  156. package/dist/terminal.d.ts.map +1 -0
  157. package/dist/terminal.js +493 -0
  158. package/dist/terminal.js.map +1 -0
  159. package/dist/tui-alt-screen.d.ts +168 -0
  160. package/dist/tui-alt-screen.d.ts.map +1 -0
  161. package/dist/tui-alt-screen.js +1552 -0
  162. package/dist/tui-alt-screen.js.map +1 -0
  163. package/dist/tui-main-screen.d.ts +39 -0
  164. package/dist/tui-main-screen.d.ts.map +1 -0
  165. package/dist/tui-main-screen.js +595 -0
  166. package/dist/tui-main-screen.js.map +1 -0
  167. package/dist/tui.d.ts +398 -0
  168. package/dist/tui.d.ts.map +1 -0
  169. package/dist/tui.js +1096 -0
  170. package/dist/tui.js.map +1 -0
  171. package/dist/undo-stack.d.ts +17 -0
  172. package/dist/undo-stack.d.ts.map +1 -0
  173. package/dist/undo-stack.js +25 -0
  174. package/dist/undo-stack.js.map +1 -0
  175. package/dist/utils.d.ts +107 -0
  176. package/dist/utils.d.ts.map +1 -0
  177. package/dist/utils.js +1263 -0
  178. package/dist/utils.js.map +1 -0
  179. package/dist/wheel-scroll.d.ts +23 -0
  180. package/dist/wheel-scroll.d.ts.map +1 -0
  181. package/dist/wheel-scroll.js +73 -0
  182. package/dist/wheel-scroll.js.map +1 -0
  183. package/dist/word-navigation.d.ts +25 -0
  184. package/dist/word-navigation.d.ts.map +1 -0
  185. package/dist/word-navigation.js +96 -0
  186. package/dist/word-navigation.js.map +1 -0
  187. package/native/clipboard.h +208 -0
  188. package/native/darwin/README.md +22 -0
  189. package/native/darwin/build.sh +69 -0
  190. package/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  191. package/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  192. package/native/darwin/src/darwin-platform.m +132 -0
  193. package/native/linux/README.md +23 -0
  194. package/native/linux/build.sh +65 -0
  195. package/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  196. package/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  197. package/native/linux/src/clipboard-worker.h +70 -0
  198. package/native/linux/src/linux-platform-x11.c +274 -0
  199. package/native/napi.h +91 -0
  200. package/native/win32/README.md +31 -0
  201. package/native/win32/build.mjs +232 -0
  202. package/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  203. package/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  204. package/native/win32/src/win32-platform.c +241 -0
  205. package/package.json +61 -5
@@ -0,0 +1,1552 @@
1
+ import { AltScreenSearchComponent, AltScreenSearchIndex, getAltScreenSearchMatchKey, } from "./alt-screen-search.js";
2
+ import { AltScreenFlashContainer } from "./components/alt-screen-flash.js";
3
+ import { ScrollView } from "./components/scroll-view.js";
4
+ import { getKeybindings } from "./keybindings.js";
5
+ import { isKeyRelease } from "./keys.js";
6
+ import { getLayoutBoxesAt, getScrollbarGeometry, getScrollViewBox, getScrollViewsAt, renderLayoutFrame, } from "./layout.js";
7
+ import { getLayoutNode } from "./layout-node.js";
8
+ import { deleteAllKittyImages, deleteAllKittyPlacements, deleteKittyImage, getCapabilities, getKittyImagePlacement, getKittyImagePlacementRows, isImageLine, setCapabilities, } from "./terminal-image.js";
9
+ import { Container, CURSOR_MARKER, compositeTuiLine, dispatchMouseEvent, retargetMouseEvent, TuiBase, VIEWPORT_TUI, } from "./tui.js";
10
+ import { extractAnsiCode, getGraphemeCellRange, getOsc8LinkAtColumn, getWordSegmenter, sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth, } from "./utils.js";
11
+ import { WheelScrollAccelerator } from "./wheel-scroll.js";
12
+ const ENTER_ALT_SCREEN = "\x1b[?1049h";
13
+ const EXIT_ALT_SCREEN = "\x1b[?1049l";
14
+ const DISABLE_AUTOWRAP = "\x1b[?7l";
15
+ const ENABLE_AUTOWRAP = "\x1b[?7h";
16
+ const ENABLE_BUTTON_MOTION_MOUSE = "\x1b[?1000h\x1b[?1002h\x1b[?1004h\x1b[?1006h";
17
+ const ENABLE_ALL_MOTION_MOUSE = "\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1004h\x1b[?1006h";
18
+ const DISABLE_MOUSE = "\x1b[?1006l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l";
19
+ const FOCUS_IN = "\x1b[I";
20
+ const FOCUS_OUT = "\x1b[O";
21
+ const BEGIN_SYNCHRONIZED_OUTPUT = "\x1b[?2026h";
22
+ const END_SYNCHRONIZED_OUTPUT = "\x1b[?2026l";
23
+ const OSC133_ZONE_PREFIX = /^(?:\x1b\]133;[ABC](?:\x07|\x1b\\))+/;
24
+ const OSC133_PROMPT_START = /^\x1b\]133;A(?:\x07|\x1b\\)/;
25
+ const PAGE_SCROLL_OVERLAP = 4;
26
+ const ALT_WHEEL_SCROLL_MULTIPLIER = 5;
27
+ const MAX_CACHED_OFFSCREEN_KITTY_IMAGES = 16;
28
+ const MAX_CACHED_OFFSCREEN_KITTY_TRANSMISSION_BYTES = 32 * 1024 * 1024;
29
+ const MAX_CACHED_OFFSCREEN_KITTY_DECODED_BYTES = 64 * 1024 * 1024;
30
+ const DOUBLE_CLICK_INTERVAL_MS = 500;
31
+ const COPY_ERROR_FLASH_DURATION_MS = 5000;
32
+ // Regular mode delegates double-click selection to the terminal emulator. Fullscreen owns mouse selection,
33
+ // so mirror common terminal word-selection behavior by keeping paths and kebab-case tokens whole.
34
+ const TERMINAL_WORD_SELECTION_JOINERS = new Set(["/", "-"]);
35
+ const wordSegmenter = getWordSegmenter();
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
+ flashes;
49
+ altScreenActive = false;
50
+ imageProtocol = null;
51
+ savedCapabilities;
52
+ uploadedKittyImages = new Map();
53
+ selectionAnchor;
54
+ selectionFocus;
55
+ selectionGranularity = "character";
56
+ selectionInitialRange;
57
+ lastClick;
58
+ selectionDragPointer;
59
+ selectionAutoScrollDirection = 0;
60
+ selectionAutoScrollTimer;
61
+ selectionPressActive = false;
62
+ scrollbarDrag;
63
+ scrollbarHover;
64
+ scrollToEndIndicatorRect;
65
+ activeSearch;
66
+ pressedUrl;
67
+ selectionDragged = false;
68
+ mouseCapture;
69
+ mousePressTarget;
70
+ mousePressPoint;
71
+ mousePressMoved = false;
72
+ lastComponentClick;
73
+ wheelScroll;
74
+ mouseEnabled;
75
+ searchMatchStyle;
76
+ searchCurrentMatchStyle;
77
+ searchNavigationButtonStyle;
78
+ scrollToEndIndicator;
79
+ openUrl;
80
+ onRightClickPaste;
81
+ copyOnSelect;
82
+ copySelection;
83
+ constructor(terminal, showHardwareCursor, logDirectory, options = {}) {
84
+ super(terminal, showHardwareCursor, logDirectory);
85
+ this.implicitDocument = {
86
+ render: (width) => super.render(width),
87
+ handleMouse: (event) => super.handleMouse(event),
88
+ invalidate: () => {
89
+ for (const child of this.children)
90
+ child.invalidate();
91
+ },
92
+ };
93
+ this.implicitScrollView = new ScrollView(this.implicitDocument, { follow: "end", primary: true });
94
+ this.flashes = new AltScreenFlashContainer(() => this.requestRender());
95
+ this.wheelScroll = new WheelScrollAccelerator(options.wheelScrollLines ?? 1);
96
+ this.mouseEnabled = options.mouse ?? true;
97
+ this.searchMatchStyle = options.searchMatchStyle ?? ((text) => `\x1b[4m${text}\x1b[24m`);
98
+ this.searchCurrentMatchStyle = options.searchCurrentMatchStyle ?? ((text) => `\x1b[1;7m${text}\x1b[22;27m`);
99
+ this.searchNavigationButtonStyle = options.searchNavigationButtonStyle ?? ((text) => text);
100
+ this.scrollToEndIndicator = options.scrollToEndIndicator;
101
+ this.openUrl = options.openUrl;
102
+ this.onRightClickPaste = options.onRightClickPaste;
103
+ this.copyOnSelect = options.copyOnSelect ?? true;
104
+ this.copySelection = options.copySelection;
105
+ this.addInputListener((data) => this.handleViewportInput(data));
106
+ }
107
+ get viewportTop() {
108
+ return this.getPrimaryScrollView().scrollTop;
109
+ }
110
+ get isFollowingOutput() {
111
+ return this.getPrimaryScrollView().isFollowingEnd;
112
+ }
113
+ setWheelScrollLines(lines) {
114
+ this.wheelScroll.setLines(lines);
115
+ }
116
+ getCopyOnSelect() {
117
+ return this.copyOnSelect;
118
+ }
119
+ setCopyOnSelect(enabled) {
120
+ this.copyOnSelect = enabled;
121
+ }
122
+ /** Whether the fullscreen viewport has a non-empty active text selection. */
123
+ hasActiveSelection() {
124
+ return this.getActiveSelectionText() !== undefined;
125
+ }
126
+ /** Copy the active fullscreen text selection, if any, using the configured selection clipboard path. */
127
+ async copyActiveSelectionToClipboard() {
128
+ const text = this.getActiveSelectionText();
129
+ if (!text)
130
+ return false;
131
+ return this.copyTextToClipboard(text);
132
+ }
133
+ /** Drop the text selection and multi-click history, e.g. before the host replaces the transcript. */
134
+ resetTextSelection() {
135
+ this.clearTextSelection();
136
+ this.lastClick = undefined;
137
+ }
138
+ /** The lines of the last rendered frame, one per terminal row, as written to the terminal. */
139
+ getScreenLines() {
140
+ return [...this.previousScreen];
141
+ }
142
+ setLayoutRoot(component) {
143
+ if (this.layoutRoot === component)
144
+ return;
145
+ this.layoutRoot = component;
146
+ this.currentLayout = undefined;
147
+ this.requestRender();
148
+ }
149
+ render(width) {
150
+ return this.layoutRoot?.render(width) ?? super.render(width);
151
+ }
152
+ getMountedRoots() {
153
+ return this.layoutRoot ? [this.layoutRoot] : this.children;
154
+ }
155
+ getPrimaryScrollView() {
156
+ return this.currentLayout?.primaryScrollView ?? this.implicitScrollView;
157
+ }
158
+ beforeTerminalStart() {
159
+ this.stopSelectionAutoScroll();
160
+ this.selectionPressActive = false;
161
+ this.stopScrollbarHover();
162
+ this.stopScrollbarDrag();
163
+ this.flashes.dispose();
164
+ this.altScreenActive = true;
165
+ const capabilities = getCapabilities();
166
+ this.imageProtocol = capabilities.images;
167
+ this.uploadedKittyImages.clear();
168
+ if (capabilities.images === "iterm2") {
169
+ this.savedCapabilities = capabilities;
170
+ setCapabilities({ ...capabilities, images: null });
171
+ this.invalidate();
172
+ }
173
+ this.lastDocument = [];
174
+ this.selectionAnchor = undefined;
175
+ this.selectionFocus = undefined;
176
+ this.selectionGranularity = "character";
177
+ this.selectionInitialRange = undefined;
178
+ this.lastClick = undefined;
179
+ this.pressedUrl = undefined;
180
+ this.selectionDragged = false;
181
+ this.clearComponentMouseGesture();
182
+ this.lastComponentClick = undefined;
183
+ this.resetRenderState();
184
+ const term = process.env.TERM?.toLowerCase() ?? "";
185
+ // Multiplexers can lag when every pointer movement is forwarded. Button-motion
186
+ // tracking preserves clicks, wheel events, selections, and scrollbar dragging.
187
+ const mouseSequence = process.env.TMUX !== undefined ||
188
+ process.env.ZELLIJ !== undefined ||
189
+ process.env.STY !== undefined ||
190
+ term.startsWith("tmux") ||
191
+ term.startsWith("screen")
192
+ ? ENABLE_BUTTON_MOTION_MOUSE
193
+ : ENABLE_ALL_MOTION_MOUSE;
194
+ this.terminal.write(`${ENTER_ALT_SCREEN}${DISABLE_AUTOWRAP}${this.mouseEnabled ? mouseSequence : ""}\x1b[2J\x1b[H\x1b[?25l`);
195
+ }
196
+ beforeTerminalStop(_options) {
197
+ this.closeSearch();
198
+ this.stopSelectionAutoScroll();
199
+ this.selectionPressActive = false;
200
+ this.stopScrollbarHover();
201
+ this.stopScrollbarDrag();
202
+ this.clearComponentMouseGesture();
203
+ this.flashes.dispose();
204
+ if (!this.altScreenActive)
205
+ return;
206
+ this.terminal.write(`${BEGIN_SYNCHRONIZED_OUTPUT}${this.deleteKittyImages()}${this.mouseEnabled ? DISABLE_MOUSE : ""}${ENABLE_AUTOWRAP}${END_SYNCHRONIZED_OUTPUT}`);
207
+ this.uploadedKittyImages.clear();
208
+ }
209
+ afterTerminalStop(options) {
210
+ if (!this.altScreenActive)
211
+ return;
212
+ this.altScreenActive = false;
213
+ if (options.preserveScreen) {
214
+ this.terminal.write(`${BEGIN_SYNCHRONIZED_OUTPUT}${EXIT_ALT_SCREEN}\x1b[?25h${END_SYNCHRONIZED_OUTPUT}`);
215
+ }
216
+ else {
217
+ const width = Math.max(1, this.terminal.columns);
218
+ const documentLines = this.resolveFakeCursors(this.render(width)).map((line) => line.replace(OSC133_ZONE_PREFIX, ""));
219
+ this.lastDocument = this.applyLineResets(documentLines.map((line) => line.replaceAll(CURSOR_MARKER, ""))).map((line) => (isImageLine(line) || visibleWidth(line) <= width ? line : sliceByColumn(line, 0, width, true)));
220
+ let buffer = `${BEGIN_SYNCHRONIZED_OUTPUT}${EXIT_ALT_SCREEN}${DISABLE_AUTOWRAP}`;
221
+ for (let row = 0; row < this.lastDocument.length; row++) {
222
+ if (row > 0)
223
+ buffer += "\r\n";
224
+ buffer += `\r\x1b[2K${this.lastDocument[row] ?? ""}`;
225
+ }
226
+ buffer += `\x1b[0m${ENABLE_AUTOWRAP}\r\n\x1b[?25h${END_SYNCHRONIZED_OUTPUT}`;
227
+ this.terminal.write(buffer);
228
+ }
229
+ if (this.savedCapabilities) {
230
+ setCapabilities(this.savedCapabilities);
231
+ this.savedCapabilities = undefined;
232
+ }
233
+ }
234
+ deleteKittyImages() {
235
+ return this.imageProtocol === "kitty" ? deleteAllKittyImages() : "";
236
+ }
237
+ prepareKittyScreen(screen) {
238
+ const visibleImageIds = new Set();
239
+ const lines = screen.map((line) => {
240
+ const placement = getKittyImagePlacement(line);
241
+ if (!placement)
242
+ return line;
243
+ visibleImageIds.add(placement.imageId);
244
+ const cachedImage = this.uploadedKittyImages.get(placement.imageId);
245
+ const nextCachedImage = {
246
+ transmissionGeneration: placement.transmissionGeneration,
247
+ transmissionBytes: placement.transmissionBytes,
248
+ estimatedDecodedBytes: placement.estimatedDecodedBytes,
249
+ };
250
+ if (cachedImage)
251
+ this.uploadedKittyImages.delete(placement.imageId);
252
+ this.uploadedKittyImages.set(placement.imageId, nextCachedImage);
253
+ return cachedImage?.transmissionGeneration === placement.transmissionGeneration
254
+ ? placement.replacementLine
255
+ : line;
256
+ });
257
+ let cachedOffscreenImageCount = 0;
258
+ let cachedOffscreenTransmissionBytes = 0;
259
+ let cachedOffscreenDecodedBytes = 0;
260
+ for (const [imageId, cachedImage] of this.uploadedKittyImages) {
261
+ if (visibleImageIds.has(imageId))
262
+ continue;
263
+ cachedOffscreenImageCount += 1;
264
+ cachedOffscreenTransmissionBytes += cachedImage.transmissionBytes;
265
+ cachedOffscreenDecodedBytes += cachedImage.estimatedDecodedBytes;
266
+ }
267
+ let evictedImageDeletion = "";
268
+ for (const [imageId, cachedImage] of this.uploadedKittyImages) {
269
+ if (cachedOffscreenImageCount <= MAX_CACHED_OFFSCREEN_KITTY_IMAGES &&
270
+ cachedOffscreenTransmissionBytes <= MAX_CACHED_OFFSCREEN_KITTY_TRANSMISSION_BYTES &&
271
+ cachedOffscreenDecodedBytes <= MAX_CACHED_OFFSCREEN_KITTY_DECODED_BYTES) {
272
+ break;
273
+ }
274
+ if (visibleImageIds.has(imageId))
275
+ continue;
276
+ evictedImageDeletion += deleteKittyImage(imageId);
277
+ this.uploadedKittyImages.delete(imageId);
278
+ cachedOffscreenImageCount -= 1;
279
+ cachedOffscreenTransmissionBytes -= cachedImage.transmissionBytes;
280
+ cachedOffscreenDecodedBytes -= cachedImage.estimatedDecodedBytes;
281
+ }
282
+ return { lines, evictedImageDeletion };
283
+ }
284
+ resetRenderState() {
285
+ this.previousScreen = [];
286
+ this.previousScreenWidth = 0;
287
+ this.previousScreenHeight = 0;
288
+ this.currentLayout = undefined;
289
+ }
290
+ scrollBy(lines) {
291
+ this.getPrimaryScrollView().scrollBy(lines);
292
+ this.requestRender();
293
+ }
294
+ scrollToTop() {
295
+ this.getPrimaryScrollView().scrollToStart();
296
+ this.requestRender();
297
+ }
298
+ scrollToBottom() {
299
+ this.getPrimaryScrollView().scrollToEnd();
300
+ this.requestRender();
301
+ }
302
+ scrollToPrompt(direction) {
303
+ if (!this.currentLayout)
304
+ return;
305
+ const scrollView = this.getPrimaryScrollView();
306
+ const lines = getScrollViewBox(this.currentLayout, scrollView)?.scrollContentLines;
307
+ if (!lines)
308
+ return;
309
+ for (let row = scrollView.scrollTop + direction; row >= 0 && row < lines.length; row += direction) {
310
+ if (!OSC133_PROMPT_START.test(lines[row] ?? ""))
311
+ continue;
312
+ scrollView.scrollTo(row);
313
+ this.requestRender();
314
+ return;
315
+ }
316
+ }
317
+ toggleSearch() {
318
+ if (this.activeSearch) {
319
+ this.closeSearch();
320
+ return;
321
+ }
322
+ const component = new AltScreenSearchComponent((query) => this.updateSearchQuery(query), this.searchNavigationButtonStyle);
323
+ const search = {
324
+ component,
325
+ index: new AltScreenSearchIndex(),
326
+ query: "",
327
+ matches: [],
328
+ selectedIndex: -1,
329
+ anchorRow: this.getPrimaryScrollView().scrollTop,
330
+ selectionMode: "query",
331
+ };
332
+ this.activeSearch = search;
333
+ search.overlay = this.showOverlay(component, {
334
+ anchor: "top-right",
335
+ width: "40%",
336
+ minWidth: 32,
337
+ margin: 1,
338
+ });
339
+ }
340
+ closeSearch() {
341
+ const search = this.activeSearch;
342
+ if (!search)
343
+ return;
344
+ this.activeSearch = undefined;
345
+ search.overlay?.hide();
346
+ this.requestRender();
347
+ }
348
+ updateSearchQuery(query) {
349
+ const search = this.activeSearch;
350
+ if (!search || query === search.query)
351
+ return;
352
+ const selected = search.matches[search.selectedIndex];
353
+ search.anchorRow = selected?.segments[0]?.row ?? this.getPrimaryScrollView().scrollTop;
354
+ search.query = query;
355
+ search.selectionMode = "query";
356
+ search.component.setResult(-1, 0);
357
+ this.requestRender();
358
+ }
359
+ navigateSearch(direction) {
360
+ const search = this.activeSearch;
361
+ if (!search?.query)
362
+ return;
363
+ search.selectionMode = direction < 0 ? "previous" : "next";
364
+ this.requestRender();
365
+ }
366
+ getSearchNavigationDirectionAt(x, y) {
367
+ const search = this.activeSearch;
368
+ const bounds = search?.overlay?.getBounds();
369
+ if (!search || !bounds)
370
+ return undefined;
371
+ if (x < bounds.col || x >= bounds.col + bounds.width || y < bounds.row || y >= bounds.row + bounds.height) {
372
+ return undefined;
373
+ }
374
+ return search.component.getNavigationDirectionAt(y - bounds.row, x - bounds.col);
375
+ }
376
+ handleSearchMouseEvent(event) {
377
+ const search = this.activeSearch;
378
+ if (!search)
379
+ return false;
380
+ const direction = this.getSearchNavigationDirectionAt(event.x, event.y);
381
+ if (search.component.setHoveredNavigationDirection(direction))
382
+ this.requestRender();
383
+ if (direction === undefined || event.release || (event.button & 32) !== 0 || (event.button & 3) !== 0) {
384
+ return false;
385
+ }
386
+ this.navigateSearch(direction);
387
+ return true;
388
+ }
389
+ refreshSearch(layout) {
390
+ const search = this.activeSearch;
391
+ if (!search)
392
+ return false;
393
+ const scrollView = layout.primaryScrollView ?? this.implicitScrollView;
394
+ const box = getScrollViewBox(layout, scrollView);
395
+ const lines = box?.scrollContentLines;
396
+ if (!lines || !search.query.trim()) {
397
+ search.matches = [];
398
+ search.selectedIndex = -1;
399
+ search.selectedKey = undefined;
400
+ search.selectionMode = "retain";
401
+ search.component.setResult(-1, 0);
402
+ return false;
403
+ }
404
+ const shouldRevealSelection = search.selectionMode !== "retain";
405
+ const result = search.index.search(lines, search.query);
406
+ const matches = result.matches;
407
+ search.matches = matches;
408
+ if (!result.changed && search.selectionMode === "retain")
409
+ return false;
410
+ const exactIndex = result.changed
411
+ ? search.selectedKey
412
+ ? matches.findIndex((match) => getAltScreenSearchMatchKey(match) === search.selectedKey)
413
+ : -1
414
+ : search.selectedIndex;
415
+ let selectedIndex = -1;
416
+ if (matches.length > 0) {
417
+ if (search.selectionMode === "query") {
418
+ let low = 0;
419
+ let high = matches.length;
420
+ while (low < high) {
421
+ const middle = low + Math.floor((high - low) / 2);
422
+ if ((matches[middle].segments[0]?.row ?? 0) < search.anchorRow)
423
+ low = middle + 1;
424
+ else
425
+ high = middle;
426
+ }
427
+ selectedIndex = low < matches.length ? low : 0;
428
+ }
429
+ else if (search.selectionMode === "next") {
430
+ const baseIndex = exactIndex >= 0 ? exactIndex : Math.min(search.selectedIndex, matches.length - 1);
431
+ selectedIndex = baseIndex < 0 ? 0 : (baseIndex + 1) % matches.length;
432
+ }
433
+ else if (search.selectionMode === "previous") {
434
+ const baseIndex = exactIndex >= 0 ? exactIndex : Math.min(search.selectedIndex, matches.length - 1);
435
+ selectedIndex = baseIndex < 0 ? matches.length - 1 : (baseIndex - 1 + matches.length) % matches.length;
436
+ }
437
+ else {
438
+ selectedIndex =
439
+ exactIndex >= 0 ? exactIndex : Math.min(Math.max(0, search.selectedIndex), matches.length - 1);
440
+ }
441
+ }
442
+ search.selectedIndex = selectedIndex;
443
+ search.selectedKey = selectedIndex >= 0 ? getAltScreenSearchMatchKey(matches[selectedIndex]) : undefined;
444
+ search.selectionMode = "retain";
445
+ search.component.setResult(selectedIndex, matches.length);
446
+ if (!shouldRevealSelection)
447
+ return false;
448
+ const selected = matches[selectedIndex];
449
+ const firstSegment = selected?.segments[0];
450
+ const lastSegment = selected?.segments[selected.segments.length - 1];
451
+ if (!box || !firstSegment || !lastSegment || scrollView.viewportHeight <= 0)
452
+ return false;
453
+ const before = scrollView.scrollTop;
454
+ const visibleBottom = before + scrollView.viewportHeight - 1;
455
+ let target = before;
456
+ if (firstSegment.row < before || lastSegment.row > visibleBottom) {
457
+ target = firstSegment.row - Math.floor(scrollView.viewportHeight / 3);
458
+ }
459
+ scrollView.scrollTo(target, { disableFollow: true });
460
+ return scrollView.scrollTop !== before;
461
+ }
462
+ /** Show a transient message in the alternate-screen flash stack. */
463
+ flash(message, durationMs) {
464
+ this.flashes.flash(message, durationMs);
465
+ }
466
+ shouldDeferViewportInputToOverlay() {
467
+ return this.isOverlayFocused() && this.activeSearch?.overlay?.isFocused() !== true;
468
+ }
469
+ clearComponentMouseGesture() {
470
+ this.mouseCapture = undefined;
471
+ this.mousePressTarget = undefined;
472
+ this.mousePressPoint = undefined;
473
+ this.mousePressMoved = false;
474
+ }
475
+ handleViewportInput(data) {
476
+ if (data === FOCUS_OUT) {
477
+ const hadActiveSelection = this.selectionPressActive;
478
+ const hadNonEmptyActiveSelection = hadActiveSelection && this.getSelectionBounds() !== undefined;
479
+ this.selectionPressActive = false;
480
+ this.stopSelectionAutoScroll();
481
+ this.stopScrollbarHover();
482
+ if (this.activeSearch?.component.setHoveredNavigationDirection(undefined))
483
+ this.requestRender();
484
+ this.stopScrollbarDrag();
485
+ this.pressedUrl = undefined;
486
+ this.selectionDragged = false;
487
+ this.clearComponentMouseGesture();
488
+ this.lastComponentClick = undefined;
489
+ if (hadActiveSelection) {
490
+ this.selectionAnchor = undefined;
491
+ this.selectionFocus = undefined;
492
+ this.selectionGranularity = "character";
493
+ this.selectionInitialRange = undefined;
494
+ if (hadNonEmptyActiveSelection)
495
+ this.requestRender();
496
+ }
497
+ this.lastClick = undefined;
498
+ return { consume: true };
499
+ }
500
+ if (data === FOCUS_IN)
501
+ return { consume: true };
502
+ const wheelEvent = this.parseWheelEvent(data);
503
+ if (wheelEvent) {
504
+ const lines = this.wheelScroll.next(wheelEvent.direction, performance.now());
505
+ // SGR mouse button codes use bit 3 (value 8) for the Alt modifier.
506
+ const wheelDelta = wheelEvent.direction * ((wheelEvent.button & 8) !== 0 ? lines * ALT_WHEEL_SCROLL_MULTIPLIER : lines);
507
+ const event = this.createMouseEvent("wheel", wheelEvent.button, wheelEvent.x, wheelEvent.y, { wheelDelta });
508
+ const overlay = this.dispatchMouseToOverlay(event);
509
+ const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(event));
510
+ if (result) {
511
+ if (this.applyMouseDispatchResult(event, result))
512
+ this.requestRender();
513
+ return { consume: true };
514
+ }
515
+ if (this.shouldDeferViewportInputToOverlay())
516
+ return undefined;
517
+ this.routeWheel(wheelEvent, wheelDelta);
518
+ return { consume: true };
519
+ }
520
+ const mouseEvent = this.parseSgrMouseEvent(data);
521
+ if (mouseEvent) {
522
+ this.handleMouseEvent(mouseEvent);
523
+ return { consume: true };
524
+ }
525
+ if (this.isMouseSequence(data))
526
+ return { consume: true };
527
+ const keybindings = getKeybindings();
528
+ const isRelease = isKeyRelease(data);
529
+ if (keybindings.matches(data, "tui.altScreen.search")) {
530
+ if (!isRelease)
531
+ this.toggleSearch();
532
+ return { consume: true };
533
+ }
534
+ if (this.activeSearch?.overlay?.isFocused()) {
535
+ if (keybindings.matches(data, "tui.altScreen.searchNext")) {
536
+ if (!isRelease)
537
+ this.navigateSearch(1);
538
+ return { consume: true };
539
+ }
540
+ if (keybindings.matches(data, "tui.altScreen.searchPrevious")) {
541
+ if (!isRelease)
542
+ this.navigateSearch(-1);
543
+ return { consume: true };
544
+ }
545
+ if (keybindings.matches(data, "tui.altScreen.searchClose")) {
546
+ if (!isRelease)
547
+ this.closeSearch();
548
+ return { consume: true };
549
+ }
550
+ }
551
+ if (this.shouldDeferViewportInputToOverlay())
552
+ return undefined;
553
+ if (keybindings.matches(data, "tui.altScreen.pageUp")) {
554
+ if (!isRelease) {
555
+ this.scrollBy(-Math.max(1, this.getPrimaryScrollView().viewportHeight - PAGE_SCROLL_OVERLAP));
556
+ }
557
+ return { consume: true };
558
+ }
559
+ if (keybindings.matches(data, "tui.altScreen.pageDown")) {
560
+ if (!isRelease) {
561
+ this.scrollBy(Math.max(1, this.getPrimaryScrollView().viewportHeight - PAGE_SCROLL_OVERLAP));
562
+ }
563
+ return { consume: true };
564
+ }
565
+ if (keybindings.matches(data, "tui.altScreen.halfPageUp")) {
566
+ if (!isRelease)
567
+ this.scrollBy(-Math.max(1, Math.floor(this.getPrimaryScrollView().viewportHeight / 2)));
568
+ return { consume: true };
569
+ }
570
+ if (keybindings.matches(data, "tui.altScreen.halfPageDown")) {
571
+ if (!isRelease)
572
+ this.scrollBy(Math.max(1, Math.floor(this.getPrimaryScrollView().viewportHeight / 2)));
573
+ return { consume: true };
574
+ }
575
+ if (keybindings.matches(data, "tui.altScreen.lineUp")) {
576
+ if (!isRelease)
577
+ this.scrollBy(-1);
578
+ return { consume: true };
579
+ }
580
+ if (keybindings.matches(data, "tui.altScreen.lineDown")) {
581
+ if (!isRelease)
582
+ this.scrollBy(1);
583
+ return { consume: true };
584
+ }
585
+ if (keybindings.matches(data, "tui.altScreen.previousPrompt")) {
586
+ if (!isRelease)
587
+ this.scrollToPrompt(-1);
588
+ return { consume: true };
589
+ }
590
+ if (keybindings.matches(data, "tui.altScreen.nextPrompt")) {
591
+ if (!isRelease)
592
+ this.scrollToPrompt(1);
593
+ return { consume: true };
594
+ }
595
+ if (keybindings.matches(data, "tui.altScreen.top")) {
596
+ if (!isRelease)
597
+ this.scrollToTop();
598
+ return { consume: true };
599
+ }
600
+ if (keybindings.matches(data, "tui.altScreen.bottom")) {
601
+ if (!isRelease)
602
+ this.scrollToBottom();
603
+ return { consume: true };
604
+ }
605
+ return undefined;
606
+ }
607
+ decodeMouseButton(button) {
608
+ switch (button & 3) {
609
+ case 0:
610
+ return "left";
611
+ case 1:
612
+ return "middle";
613
+ case 2:
614
+ return "right";
615
+ default:
616
+ return "none";
617
+ }
618
+ }
619
+ createMouseEvent(type, button, x, y, extra = {}) {
620
+ return {
621
+ type,
622
+ button: type === "wheel" ? "none" : this.decodeMouseButton(button),
623
+ x,
624
+ y,
625
+ screenX: x,
626
+ screenY: y,
627
+ width: Math.max(1, this.terminal.columns),
628
+ height: Math.max(1, this.terminal.rows),
629
+ shift: (button & 4) !== 0,
630
+ alt: (button & 8) !== 0,
631
+ ctrl: (button & 16) !== 0,
632
+ ...(extra.wheelDelta === undefined ? {} : { wheelDelta: extra.wheelDelta }),
633
+ ...(extra.clickCount === undefined ? {} : { clickCount: extra.clickCount }),
634
+ };
635
+ }
636
+ dispatchMouseToLayout(event) {
637
+ if (!this.currentLayout)
638
+ return undefined;
639
+ const visited = new Set();
640
+ const boxes = getLayoutBoxesAt(this.currentLayout, event.screenX, event.screenY);
641
+ for (const box of boxes) {
642
+ if (visited.has(box.component))
643
+ continue;
644
+ if (getLayoutNode(box.component) && box.component.handleMouse === Container.prototype.handleMouse)
645
+ continue;
646
+ visited.add(box.component);
647
+ const result = dispatchMouseEvent(box.component, {
648
+ ...event,
649
+ x: event.screenX - box.rect.x,
650
+ y: event.screenY - box.rect.y,
651
+ width: box.rect.width,
652
+ height: box.rect.height,
653
+ });
654
+ if (result)
655
+ return result;
656
+ }
657
+ return undefined;
658
+ }
659
+ applyMouseDispatchResult(event, result) {
660
+ const focusTarget = this.resolveMouseFocusTarget(result.focusTarget ?? result.target.component);
661
+ const focusChanged = result.focus === true && this.getFocusedComponent() !== focusTarget;
662
+ if (result.focus)
663
+ this.setFocus(focusTarget);
664
+ if (result.capture)
665
+ this.mouseCapture = result.target;
666
+ return (result.render ??
667
+ (focusChanged ||
668
+ event.type === "press" ||
669
+ event.type === "click" ||
670
+ event.type === "drag" ||
671
+ event.type === "wheel"));
672
+ }
673
+ dispatchMouseToTarget(event, target) {
674
+ return dispatchMouseEvent(target.component, retargetMouseEvent(event, target));
675
+ }
676
+ getComponentClickCount(target, x, y) {
677
+ const now = Date.now();
678
+ const previous = this.lastComponentClick;
679
+ const count = previous &&
680
+ now - previous.timestamp <= DOUBLE_CLICK_INTERVAL_MS &&
681
+ previous.component === target.component &&
682
+ previous.x === x &&
683
+ previous.y === y
684
+ ? (previous.count % 3) + 1
685
+ : 1;
686
+ this.lastComponentClick = { timestamp: now, count, component: target.component, x, y };
687
+ return count;
688
+ }
689
+ clearTextSelection() {
690
+ this.stopSelectionAutoScroll();
691
+ this.selectionPressActive = false;
692
+ this.selectionAnchor = undefined;
693
+ this.selectionFocus = undefined;
694
+ this.selectionGranularity = "character";
695
+ this.selectionInitialRange = undefined;
696
+ this.pressedUrl = undefined;
697
+ this.selectionDragged = false;
698
+ }
699
+ handleMouseEvent(raw) {
700
+ const isMotion = (raw.button & 32) !== 0;
701
+ const type = raw.release
702
+ ? "release"
703
+ : isMotion
704
+ ? this.decodeMouseButton(raw.button) === "none"
705
+ ? "move"
706
+ : "drag"
707
+ : "press";
708
+ const event = this.createMouseEvent(type, raw.button, raw.x, raw.y);
709
+ if (this.mouseCapture || this.mousePressTarget) {
710
+ const target = this.mouseCapture ?? this.mousePressTarget;
711
+ if (this.mousePressPoint && (raw.x !== this.mousePressPoint.x || raw.y !== this.mousePressPoint.y)) {
712
+ this.mousePressMoved = true;
713
+ this.lastComponentClick = undefined;
714
+ }
715
+ let render = false;
716
+ const targetResult = this.dispatchMouseToTarget(event, target);
717
+ if (targetResult)
718
+ render = this.applyMouseDispatchResult(event, targetResult);
719
+ if (raw.release) {
720
+ if (!this.mousePressMoved && this.mousePressPoint?.x === raw.x && this.mousePressPoint.y === raw.y) {
721
+ const clickEvent = this.createMouseEvent("click", raw.button, raw.x, raw.y, {
722
+ clickCount: this.getComponentClickCount(target, raw.x, raw.y),
723
+ });
724
+ const clickResult = this.dispatchMouseToTarget(clickEvent, target);
725
+ if (clickResult)
726
+ render = this.applyMouseDispatchResult(clickEvent, clickResult) || render;
727
+ }
728
+ this.clearComponentMouseGesture();
729
+ }
730
+ if (render)
731
+ this.requestRender();
732
+ return;
733
+ }
734
+ if (this.handleSearchMouseEvent(raw))
735
+ return;
736
+ const overlay = this.dispatchMouseToOverlay(event);
737
+ if (!overlay.hit) {
738
+ if (this.handleScrollToEndIndicatorMouseEvent(raw))
739
+ return;
740
+ const scrollbarHandled = this.handleScrollbarMouseEvent(raw);
741
+ if (!this.scrollbarDrag)
742
+ this.updateScrollbarHover(raw.x, raw.y);
743
+ if (scrollbarHandled)
744
+ return;
745
+ }
746
+ else {
747
+ this.stopScrollbarHover();
748
+ }
749
+ const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(event));
750
+ if (result) {
751
+ const render = this.applyMouseDispatchResult(event, result);
752
+ if (type === "press") {
753
+ this.clearTextSelection();
754
+ this.mousePressTarget = result.target;
755
+ this.mousePressPoint = { x: raw.x, y: raw.y };
756
+ this.mousePressMoved = false;
757
+ }
758
+ if (render)
759
+ this.requestRender();
760
+ return;
761
+ }
762
+ if (this.handleRightClickPaste(raw))
763
+ return;
764
+ this.handleSelectionMouseEvent(raw);
765
+ }
766
+ parseWheelEvent(data) {
767
+ const sgr = /^\x1b\[<(\d+);(\d+);(\d+)[Mm]$/.exec(data);
768
+ if (sgr) {
769
+ const button = Number.parseInt(sgr[1], 10);
770
+ if ((button & 64) === 0)
771
+ return undefined;
772
+ const direction = button & 3;
773
+ if (direction !== 0 && direction !== 1)
774
+ return undefined;
775
+ return {
776
+ direction: direction === 0 ? -1 : 1,
777
+ x: Number.parseInt(sgr[2], 10) - 1,
778
+ y: Number.parseInt(sgr[3], 10) - 1,
779
+ button,
780
+ };
781
+ }
782
+ if (data.length === 6 && data.startsWith("\x1b[M")) {
783
+ const button = data.charCodeAt(3) - 32;
784
+ if ((button & 64) === 0)
785
+ return undefined;
786
+ const direction = button & 3;
787
+ if (direction !== 0 && direction !== 1)
788
+ return undefined;
789
+ return {
790
+ direction: direction === 0 ? -1 : 1,
791
+ x: data.charCodeAt(4) - 33,
792
+ y: data.charCodeAt(5) - 33,
793
+ button,
794
+ };
795
+ }
796
+ return undefined;
797
+ }
798
+ routeWheel(event, delta) {
799
+ let remaining = delta;
800
+ const seen = new Set();
801
+ for (const scrollView of this.currentLayout ? getScrollViewsAt(this.currentLayout, event.x, event.y) : []) {
802
+ seen.add(scrollView);
803
+ remaining = scrollView.scrollBy(remaining);
804
+ if (remaining === 0 || scrollView.overscroll === "contain")
805
+ break;
806
+ }
807
+ const primary = this.getPrimaryScrollView();
808
+ if (remaining !== 0 && !seen.has(primary))
809
+ primary.scrollBy(remaining);
810
+ this.updateScrollbarHover(event.x, event.y);
811
+ this.requestRender();
812
+ }
813
+ parseSgrMouseEvent(data) {
814
+ const match = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(data);
815
+ if (!match)
816
+ return undefined;
817
+ return {
818
+ button: Number.parseInt(match[1], 10),
819
+ x: Number.parseInt(match[2], 10) - 1,
820
+ y: Number.parseInt(match[3], 10) - 1,
821
+ release: match[4] === "m",
822
+ };
823
+ }
824
+ handleRightClickPaste(event) {
825
+ if (!this.onRightClickPaste ||
826
+ process.platform !== "win32" ||
827
+ process.env.TERM_PROGRAM?.toLowerCase() === "vscode" ||
828
+ event.release ||
829
+ event.button !== 2) {
830
+ return false;
831
+ }
832
+ try {
833
+ this.onRightClickPaste();
834
+ }
835
+ catch {
836
+ // Clipboard paste is best-effort.
837
+ }
838
+ return true;
839
+ }
840
+ handleScrollToEndIndicatorMouseEvent(event) {
841
+ const rect = this.scrollToEndIndicatorRect;
842
+ if (!rect || event.release || (event.button & 32) !== 0 || (event.button & 3) !== 0)
843
+ return false;
844
+ if (event.y !== rect.row || event.x < rect.column || event.x >= rect.column + rect.width)
845
+ return false;
846
+ this.scrollToBottom();
847
+ return true;
848
+ }
849
+ getScrollbarTargetAt(x, y, includeHiddenAuto = false) {
850
+ if (this.hasOverlay() || !this.currentLayout)
851
+ return undefined;
852
+ for (const scrollView of getScrollViewsAt(this.currentLayout, x, y)) {
853
+ const box = getScrollViewBox(this.currentLayout, scrollView);
854
+ const geometry = box ? getScrollbarGeometry(box, includeHiddenAuto) : undefined;
855
+ if (geometry &&
856
+ x === geometry.column &&
857
+ y >= geometry.trackTop &&
858
+ y < geometry.trackTop + geometry.trackHeight) {
859
+ return { scrollView, geometry };
860
+ }
861
+ }
862
+ return undefined;
863
+ }
864
+ setScrollbarHover(scrollView) {
865
+ if (scrollView === this.scrollbarHover)
866
+ return;
867
+ this.scrollbarHover?.setScrollbarActive(false);
868
+ this.scrollbarHover = scrollView;
869
+ this.scrollbarHover?.setScrollbarActive(true);
870
+ }
871
+ updateScrollbarHover(x, y) {
872
+ this.setScrollbarHover(this.getScrollbarTargetAt(x, y, true)?.scrollView);
873
+ }
874
+ stopScrollbarHover() {
875
+ this.setScrollbarHover(undefined);
876
+ }
877
+ scrollScrollbarToPointer(scrollView, geometry, pointerY, grabOffset) {
878
+ const maxThumbOffset = geometry.trackHeight - geometry.thumbHeight;
879
+ const thumbOffset = Math.max(0, Math.min(maxThumbOffset, pointerY - geometry.trackTop - grabOffset));
880
+ const scrollTop = maxThumbOffset === 0 ? 0 : Math.round((thumbOffset / maxThumbOffset) * geometry.maxScrollTop);
881
+ scrollView.scrollTo(scrollTop);
882
+ }
883
+ handleScrollbarMouseEvent(event) {
884
+ if (this.scrollbarDrag) {
885
+ if (event.release) {
886
+ this.stopScrollbarDrag();
887
+ return true;
888
+ }
889
+ const box = this.currentLayout
890
+ ? getScrollViewBox(this.currentLayout, this.scrollbarDrag.scrollView)
891
+ : undefined;
892
+ const geometry = box ? getScrollbarGeometry(box) : undefined;
893
+ if (geometry) {
894
+ this.scrollScrollbarToPointer(this.scrollbarDrag.scrollView, geometry, event.y, this.scrollbarDrag.grabOffset);
895
+ }
896
+ return true;
897
+ }
898
+ if (event.release || (event.button & 32) !== 0 || (event.button & 3) !== 0)
899
+ return false;
900
+ const target = this.getScrollbarTargetAt(event.x, event.y);
901
+ if (!target)
902
+ return false;
903
+ this.stopSelectionAutoScroll();
904
+ this.selectionPressActive = false;
905
+ this.selectionAnchor = undefined;
906
+ this.selectionFocus = undefined;
907
+ this.selectionGranularity = "character";
908
+ this.selectionInitialRange = undefined;
909
+ this.lastClick = undefined;
910
+ this.pressedUrl = undefined;
911
+ this.selectionDragged = false;
912
+ this.setScrollbarHover(target.scrollView);
913
+ const onThumb = event.y >= target.geometry.thumbTop && event.y < target.geometry.thumbTop + target.geometry.thumbHeight;
914
+ const grabOffset = onThumb ? event.y - target.geometry.thumbTop : Math.floor(target.geometry.thumbHeight / 2);
915
+ if (!onThumb)
916
+ this.scrollScrollbarToPointer(target.scrollView, target.geometry, event.y, grabOffset);
917
+ this.scrollbarDrag = {
918
+ scrollView: target.scrollView,
919
+ grabOffset,
920
+ };
921
+ return true;
922
+ }
923
+ stopScrollbarDrag() {
924
+ this.scrollbarDrag = undefined;
925
+ }
926
+ getScrollSelectionPoint(scrollView, x, y) {
927
+ if (!this.currentLayout)
928
+ return undefined;
929
+ const box = getScrollViewBox(this.currentLayout, scrollView);
930
+ if (!box || box.rect.height <= 0 || box.clip.height <= 0)
931
+ return undefined;
932
+ const visibleTop = Math.max(0, box.rect.y, box.clip.y);
933
+ const visibleBottom = Math.min(this.terminal.rows - 1, box.rect.y + box.rect.height - 1, box.clip.y + box.clip.height - 1);
934
+ if (visibleBottom < visibleTop)
935
+ return undefined;
936
+ const pointerRow = Math.max(visibleTop, Math.min(visibleBottom, y));
937
+ const maxContentRow = Math.max(0, (box.scrollContentLines?.length ?? 1) - 1);
938
+ return {
939
+ row: Math.max(0, Math.min(maxContentRow, scrollView.scrollTop + pointerRow - box.rect.y)),
940
+ col: Math.max(0, Math.min(box.rect.width - 1, x - box.rect.x)),
941
+ scrollView,
942
+ };
943
+ }
944
+ getSelectionPoint(event, scrollView) {
945
+ if (scrollView) {
946
+ const point = this.getScrollSelectionPoint(scrollView, event.x, event.y);
947
+ if (point)
948
+ return point;
949
+ }
950
+ return {
951
+ row: Math.max(0, Math.min(this.terminal.rows - 1, event.y)),
952
+ col: Math.max(0, Math.min(this.terminal.columns - 1, event.x)),
953
+ };
954
+ }
955
+ getSelectionSourceLine(point) {
956
+ if (point.scrollView && this.currentLayout) {
957
+ const lines = getScrollViewBox(this.currentLayout, point.scrollView)?.scrollContentLines;
958
+ if (lines)
959
+ return lines[point.row] ?? "";
960
+ }
961
+ return this.previousScreen[point.row] ?? "";
962
+ }
963
+ getWordSelection(point) {
964
+ const line = stripTerminalSequences(this.getSelectionSourceLine(point));
965
+ const segments = [];
966
+ let start = 0;
967
+ for (const segment of wordSegmenter.segment(line)) {
968
+ const end = start + visibleWidth(segment.segment);
969
+ const joiner = TERMINAL_WORD_SELECTION_JOINERS.has(segment.segment);
970
+ segments.push({ start, end, selectable: segment.isWordLike === true || joiner, joiner });
971
+ start = end;
972
+ }
973
+ const clickedSegmentIndex = segments.findIndex((segment) => point.col >= segment.start && point.col < segment.end);
974
+ if (clickedSegmentIndex < 0)
975
+ return undefined;
976
+ const canJoin = (left, right) => left.selectable && right.selectable && (left.joiner || right.joiner);
977
+ let selectionStart = segments[clickedSegmentIndex].start;
978
+ let selectionEnd = segments[clickedSegmentIndex].end;
979
+ for (let index = clickedSegmentIndex; index > 0 && canJoin(segments[index - 1], segments[index]); index--) {
980
+ selectionStart = segments[index - 1].start;
981
+ }
982
+ for (let index = clickedSegmentIndex; index < segments.length - 1 && canJoin(segments[index], segments[index + 1]); index++) {
983
+ selectionEnd = segments[index + 1].end;
984
+ }
985
+ return {
986
+ start: { ...point, col: selectionStart },
987
+ end: { ...point, col: selectionEnd, boundary: true },
988
+ };
989
+ }
990
+ getLineSelection(point) {
991
+ return {
992
+ start: { ...point, col: 0 },
993
+ end: { ...point, col: visibleWidth(this.getSelectionSourceLine(point)), boundary: true },
994
+ };
995
+ }
996
+ updateSelectionFocus(point) {
997
+ if (this.selectionGranularity === "character" || !this.selectionInitialRange) {
998
+ this.selectionFocus = point;
999
+ return;
1000
+ }
1001
+ const range = this.selectionGranularity === "word" ? this.getWordSelection(point) : this.getLineSelection(point);
1002
+ if (!range)
1003
+ return;
1004
+ const initial = this.selectionInitialRange;
1005
+ const targetBeforeInitial = range.start.row < initial.start.row ||
1006
+ (range.start.row === initial.start.row && range.start.col < initial.start.col);
1007
+ if (targetBeforeInitial) {
1008
+ this.selectionAnchor = initial.end;
1009
+ this.selectionFocus = range.start;
1010
+ }
1011
+ else {
1012
+ this.selectionAnchor = initial.start;
1013
+ this.selectionFocus = range.end;
1014
+ }
1015
+ }
1016
+ getClickCount(point, word) {
1017
+ const now = Date.now();
1018
+ const previous = this.lastClick;
1019
+ const count = word &&
1020
+ previous &&
1021
+ now - previous.timestamp <= DOUBLE_CLICK_INTERVAL_MS &&
1022
+ previous.row === point.row &&
1023
+ previous.scrollView === point.scrollView &&
1024
+ previous.wordStart === word.start.col &&
1025
+ previous.wordEnd === word.end.col
1026
+ ? (previous.count % 3) + 1
1027
+ : 1;
1028
+ this.lastClick = word
1029
+ ? {
1030
+ timestamp: now,
1031
+ count,
1032
+ row: point.row,
1033
+ scrollView: point.scrollView,
1034
+ wordStart: word.start.col,
1035
+ wordEnd: word.end.col,
1036
+ }
1037
+ : undefined;
1038
+ return count;
1039
+ }
1040
+ updateSelectionAutoScroll(event) {
1041
+ const scrollView = this.selectionAnchor?.scrollView;
1042
+ if (!scrollView || !this.currentLayout) {
1043
+ this.stopSelectionAutoScroll();
1044
+ return;
1045
+ }
1046
+ const box = getScrollViewBox(this.currentLayout, scrollView);
1047
+ if (!box || box.rect.height <= 0 || box.clip.height <= 0) {
1048
+ this.stopSelectionAutoScroll();
1049
+ return;
1050
+ }
1051
+ const visibleTop = Math.max(0, box.rect.y, box.clip.y);
1052
+ const visibleBottom = Math.min(this.terminal.rows - 1, box.rect.y + box.rect.height - 1, box.clip.y + box.clip.height - 1);
1053
+ this.selectionDragPointer = { x: event.x, y: event.y };
1054
+ this.selectionAutoScrollDirection = event.y <= visibleTop ? -1 : event.y >= visibleBottom ? 1 : 0;
1055
+ if (this.selectionAutoScrollDirection === 0) {
1056
+ this.stopSelectionAutoScroll();
1057
+ return;
1058
+ }
1059
+ if (this.selectionAutoScrollTimer)
1060
+ return;
1061
+ this.selectionAutoScrollTimer = setInterval(() => this.autoScrollSelection(), 50);
1062
+ this.selectionAutoScrollTimer.unref();
1063
+ }
1064
+ autoScrollSelection() {
1065
+ const scrollView = this.selectionAnchor?.scrollView;
1066
+ const pointer = this.selectionDragPointer;
1067
+ const direction = this.selectionAutoScrollDirection;
1068
+ if (!scrollView || !pointer || direction === 0) {
1069
+ this.stopSelectionAutoScroll();
1070
+ return;
1071
+ }
1072
+ const remaining = scrollView.scrollBy(direction);
1073
+ if (remaining === direction) {
1074
+ this.stopSelectionAutoScroll();
1075
+ return;
1076
+ }
1077
+ const point = this.getScrollSelectionPoint(scrollView, pointer.x, pointer.y);
1078
+ if (point)
1079
+ this.updateSelectionFocus(point);
1080
+ this.requestRender();
1081
+ }
1082
+ stopSelectionAutoScroll() {
1083
+ if (this.selectionAutoScrollTimer) {
1084
+ clearInterval(this.selectionAutoScrollTimer);
1085
+ this.selectionAutoScrollTimer = undefined;
1086
+ }
1087
+ this.selectionAutoScrollDirection = 0;
1088
+ this.selectionDragPointer = undefined;
1089
+ }
1090
+ handleSelectionMouseEvent(event) {
1091
+ const button = event.button & 3;
1092
+ if (button !== 0 && !(event.release && button === 3))
1093
+ return;
1094
+ const anchorScrollView = this.selectionAnchor?.scrollView;
1095
+ const point = this.getSelectionPoint(event, anchorScrollView);
1096
+ if (event.release) {
1097
+ if (!this.selectionPressActive)
1098
+ return;
1099
+ this.selectionPressActive = false;
1100
+ this.stopSelectionAutoScroll();
1101
+ if (!this.selectionAnchor)
1102
+ return;
1103
+ this.updateSelectionFocus(point);
1104
+ const isClick = !this.selectionDragged &&
1105
+ this.selectionAnchor.scrollView === point.scrollView &&
1106
+ this.selectionAnchor.row === point.row &&
1107
+ this.selectionAnchor.col === point.col;
1108
+ const clickedUrl = isClick ? this.pressedUrl : undefined;
1109
+ this.pressedUrl = undefined;
1110
+ if (clickedUrl && this.openUrl) {
1111
+ this.selectionAnchor = undefined;
1112
+ this.selectionFocus = undefined;
1113
+ try {
1114
+ this.openUrl(clickedUrl);
1115
+ }
1116
+ catch {
1117
+ // URL activation is best-effort.
1118
+ }
1119
+ this.requestRender();
1120
+ return;
1121
+ }
1122
+ if (isClick) {
1123
+ const clickEvent = this.createMouseEvent("click", event.button, event.x, event.y, {
1124
+ clickCount: this.lastClick?.count ?? 1,
1125
+ });
1126
+ const overlay = this.dispatchMouseToOverlay(clickEvent);
1127
+ const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(clickEvent));
1128
+ if (result) {
1129
+ const render = this.applyMouseDispatchResult(clickEvent, result);
1130
+ this.clearTextSelection();
1131
+ if (render)
1132
+ this.requestRender();
1133
+ return;
1134
+ }
1135
+ }
1136
+ if (this.copyOnSelect)
1137
+ void this.copySelectionToClipboard();
1138
+ this.requestRender();
1139
+ return;
1140
+ }
1141
+ if ((event.button & 32) !== 0) {
1142
+ if (!this.selectionPressActive || !this.selectionAnchor)
1143
+ return;
1144
+ this.selectionDragged = true;
1145
+ this.lastClick = undefined;
1146
+ this.pressedUrl = undefined;
1147
+ this.updateSelectionFocus(point);
1148
+ this.updateSelectionAutoScroll(event);
1149
+ this.requestRender();
1150
+ return;
1151
+ }
1152
+ this.stopSelectionAutoScroll();
1153
+ this.selectionPressActive = true;
1154
+ const scrollView = !this.hasOverlay() && this.currentLayout
1155
+ ? getScrollViewsAt(this.currentLayout, event.x, event.y)[0]
1156
+ : undefined;
1157
+ const anchor = this.getSelectionPoint(event, scrollView);
1158
+ const word = this.getWordSelection(anchor);
1159
+ const clickCount = this.getClickCount(anchor, word);
1160
+ const range = clickCount === 2 ? word : clickCount === 3 ? this.getLineSelection(anchor) : undefined;
1161
+ this.selectionGranularity = range ? (clickCount === 2 ? "word" : "line") : "character";
1162
+ this.selectionInitialRange = range;
1163
+ this.selectionAnchor = range?.start ?? anchor;
1164
+ this.selectionFocus = range?.end ?? anchor;
1165
+ this.selectionDragged = false;
1166
+ this.pressedUrl = range
1167
+ ? undefined
1168
+ : 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)));
1169
+ this.requestRender();
1170
+ }
1171
+ getSelectionBounds() {
1172
+ if (!this.selectionAnchor || !this.selectionFocus)
1173
+ return undefined;
1174
+ if (this.selectionAnchor.scrollView !== this.selectionFocus.scrollView)
1175
+ return undefined;
1176
+ const anchorBeforeFocus = this.selectionAnchor.row < this.selectionFocus.row ||
1177
+ (this.selectionAnchor.row === this.selectionFocus.row && this.selectionAnchor.col < this.selectionFocus.col);
1178
+ if (this.selectionAnchor.row === this.selectionFocus.row &&
1179
+ this.selectionAnchor.col === this.selectionFocus.col) {
1180
+ return undefined;
1181
+ }
1182
+ return anchorBeforeFocus
1183
+ ? { start: this.selectionAnchor, end: this.selectionFocus }
1184
+ : { start: this.selectionFocus, end: this.selectionAnchor };
1185
+ }
1186
+ getSelectionColumns(line, row, selection, minColumn = 0, maxColumn = visibleWidth(line)) {
1187
+ const lineWidth = visibleWidth(line);
1188
+ let start = Math.max(0, minColumn);
1189
+ let end = Math.min(lineWidth, maxColumn);
1190
+ if (row === selection.start.row) {
1191
+ start = getGraphemeCellRange(line, selection.start.col)?.start ?? Math.min(selection.start.col, lineWidth);
1192
+ }
1193
+ if (row === selection.end.row) {
1194
+ end = selection.end.boundary
1195
+ ? Math.min(selection.end.col, lineWidth)
1196
+ : (getGraphemeCellRange(line, selection.end.col)?.end ?? Math.min(selection.end.col + 1, lineWidth));
1197
+ }
1198
+ return { start: Math.max(minColumn, start), end: Math.min(maxColumn, end) };
1199
+ }
1200
+ getActiveSelectionText() {
1201
+ const selection = this.getSelectionBounds();
1202
+ if (!selection)
1203
+ return undefined;
1204
+ let sourceLines = this.previousScreen;
1205
+ if (selection.start.scrollView) {
1206
+ if (!this.currentLayout)
1207
+ return undefined;
1208
+ const box = getScrollViewBox(this.currentLayout, selection.start.scrollView);
1209
+ if (!box?.scrollContentLines)
1210
+ return undefined;
1211
+ sourceLines = box.scrollContentLines;
1212
+ }
1213
+ const lines = [];
1214
+ for (let row = selection.start.row; row <= selection.end.row; row++) {
1215
+ const line = sourceLines[row] ?? "";
1216
+ const columns = this.getSelectionColumns(line, row, selection);
1217
+ lines.push(stripTerminalSequences(sliceByColumn(line, columns.start, Math.max(0, columns.end - columns.start), true)).trimEnd());
1218
+ }
1219
+ const text = lines.join("\n");
1220
+ return text.length === 0 ? undefined : text;
1221
+ }
1222
+ async copySelectionToClipboard() {
1223
+ const text = this.getActiveSelectionText();
1224
+ if (!text)
1225
+ return false;
1226
+ return this.copyTextToClipboard(text);
1227
+ }
1228
+ async copyTextToClipboard(text) {
1229
+ // Prefer an injected clipboard implementation (native clipboard + platform tools with a
1230
+ // verified success path) when the host app provides one. A bare OSC 52 write can show
1231
+ // "Copied!" while leaving the system clipboard untouched (e.g. macOS Terminal.app, tmux
1232
+ // without OSC 52 clipboard passthrough), so only report success when it actually copies.
1233
+ if (this.copySelection) {
1234
+ const result = await this.copySelection(text);
1235
+ const ok = result === true;
1236
+ this.flash(ok ? "Copied!" : typeof result === "string" ? result : "Copy failed", ok ? undefined : COPY_ERROR_FLASH_DURATION_MS);
1237
+ return ok;
1238
+ }
1239
+ this.terminal.write(`\x1b]52;c;${Buffer.from(text).toString("base64")}\x07`);
1240
+ this.flash("Copied!");
1241
+ return true;
1242
+ }
1243
+ applySearchTextHighlight(text, current) {
1244
+ const style = current ? this.searchCurrentMatchStyle : this.searchMatchStyle;
1245
+ let result = "";
1246
+ let plainStart = 0;
1247
+ let index = 0;
1248
+ while (index < text.length) {
1249
+ const ansi = extractAnsiCode(text, index);
1250
+ if (!ansi) {
1251
+ index += 1;
1252
+ continue;
1253
+ }
1254
+ if (index > plainStart)
1255
+ result += style(text.slice(plainStart, index));
1256
+ result += ansi.code;
1257
+ index += ansi.length;
1258
+ plainStart = index;
1259
+ }
1260
+ if (plainStart < text.length)
1261
+ result += style(text.slice(plainStart));
1262
+ return result;
1263
+ }
1264
+ applySearchHighlights(screen, layout) {
1265
+ const search = this.activeSearch;
1266
+ if (!search || search.selectedIndex < 0 || search.matches.length === 0)
1267
+ return screen;
1268
+ const scrollView = layout.primaryScrollView ?? this.implicitScrollView;
1269
+ const box = getScrollViewBox(layout, scrollView);
1270
+ if (!box)
1271
+ return screen;
1272
+ const rangesByRow = new Map();
1273
+ const scrollbarColumn = getScrollbarGeometry(box)?.column;
1274
+ const minRow = Math.max(0, box.rect.y, box.clip.y);
1275
+ const maxRow = Math.min(screen.length, box.rect.y + box.rect.height, box.clip.y + box.clip.height);
1276
+ const minColumn = Math.max(0, box.rect.x, box.clip.x);
1277
+ const maxColumn = Math.min(this.terminal.columns, box.rect.x + box.rect.width, box.clip.x + box.clip.width, scrollbarColumn ?? Number.POSITIVE_INFINITY);
1278
+ const minContentRow = scrollView.scrollTop + minRow - box.rect.y;
1279
+ const maxContentRow = scrollView.scrollTop + maxRow - box.rect.y - 1;
1280
+ let low = 0;
1281
+ let high = search.matches.length;
1282
+ while (low < high) {
1283
+ const middle = low + Math.floor((high - low) / 2);
1284
+ const match = search.matches[middle];
1285
+ const lastRow = match.segments[match.segments.length - 1]?.row ?? -1;
1286
+ if (lastRow < minContentRow)
1287
+ low = middle + 1;
1288
+ else
1289
+ high = middle;
1290
+ }
1291
+ for (let matchIndex = low; matchIndex < search.matches.length; matchIndex++) {
1292
+ const match = search.matches[matchIndex];
1293
+ if ((match.segments[0]?.row ?? 0) > maxContentRow)
1294
+ break;
1295
+ for (const segment of match.segments) {
1296
+ const row = box.rect.y + segment.row - scrollView.scrollTop;
1297
+ if (row < minRow || row >= maxRow)
1298
+ continue;
1299
+ const startCol = Math.max(minColumn, box.rect.x + segment.startCol);
1300
+ const endCol = Math.min(maxColumn, box.rect.x + segment.endCol);
1301
+ if (endCol <= startCol)
1302
+ continue;
1303
+ const ranges = rangesByRow.get(row) ?? [];
1304
+ ranges.push({ startCol, endCol, current: matchIndex === search.selectedIndex });
1305
+ rangesByRow.set(row, ranges);
1306
+ }
1307
+ }
1308
+ const result = [...screen];
1309
+ for (const [row, ranges] of rangesByRow) {
1310
+ let line = result[row] ?? "";
1311
+ if (isImageLine(line))
1312
+ continue;
1313
+ const lineWidth = visibleWidth(line);
1314
+ for (const range of ranges.sort((a, b) => b.startCol - a.startCol)) {
1315
+ const startCol = Math.min(range.startCol, lineWidth);
1316
+ const endCol = Math.min(range.endCol, lineWidth);
1317
+ if (endCol <= startCol)
1318
+ continue;
1319
+ const before = sliceByColumn(line, 0, startCol, true);
1320
+ const highlighted = sliceByColumn(line, startCol, endCol - startCol, true);
1321
+ const after = sliceByColumn(line, endCol, Math.max(0, lineWidth - endCol), true);
1322
+ line = `${before}${this.applySearchTextHighlight(highlighted, range.current)}${after}`;
1323
+ }
1324
+ result[row] = line;
1325
+ }
1326
+ return result;
1327
+ }
1328
+ applySelectionHighlight(text) {
1329
+ let result = "\x1b[7m";
1330
+ let index = 0;
1331
+ while (index < text.length) {
1332
+ const ansi = extractAnsiCode(text, index);
1333
+ if (!ansi) {
1334
+ result += text[index];
1335
+ index += 1;
1336
+ continue;
1337
+ }
1338
+ result += ansi.code;
1339
+ if (ansi.code.endsWith("m"))
1340
+ result += "\x1b[7m";
1341
+ index += ansi.length;
1342
+ }
1343
+ return `${result}\x1b[27m`;
1344
+ }
1345
+ applySelection(screen, layout = this.currentLayout) {
1346
+ const selection = this.getSelectionBounds();
1347
+ if (!selection)
1348
+ return screen;
1349
+ let screenSelection = selection;
1350
+ let minRow = 0;
1351
+ let maxRow = screen.length - 1;
1352
+ let minColumn = 0;
1353
+ let maxColumn = this.terminal.columns;
1354
+ if (selection.start.scrollView) {
1355
+ if (!layout)
1356
+ return screen;
1357
+ const box = getScrollViewBox(layout, selection.start.scrollView);
1358
+ if (!box)
1359
+ return screen;
1360
+ minRow = Math.max(0, box.rect.y, box.clip.y);
1361
+ maxRow = Math.min(screen.length - 1, box.rect.y + box.rect.height - 1, box.clip.y + box.clip.height - 1);
1362
+ minColumn = Math.max(0, box.rect.x, box.clip.x);
1363
+ maxColumn = Math.min(this.terminal.columns, box.rect.x + box.rect.width, box.clip.x + box.clip.width);
1364
+ screenSelection = {
1365
+ start: {
1366
+ ...selection.start,
1367
+ row: box.rect.y + selection.start.row - selection.start.scrollView.scrollTop,
1368
+ col: box.rect.x + selection.start.col,
1369
+ },
1370
+ end: {
1371
+ ...selection.end,
1372
+ row: box.rect.y + selection.end.row - selection.start.scrollView.scrollTop,
1373
+ col: box.rect.x + selection.end.col,
1374
+ },
1375
+ };
1376
+ }
1377
+ return screen.map((line, row) => {
1378
+ if (row < minRow ||
1379
+ row > maxRow ||
1380
+ row < screenSelection.start.row ||
1381
+ row > screenSelection.end.row ||
1382
+ isImageLine(line)) {
1383
+ return line;
1384
+ }
1385
+ const lineWidth = visibleWidth(line);
1386
+ const columns = this.getSelectionColumns(line, row, screenSelection, minColumn, maxColumn);
1387
+ if (columns.end <= columns.start)
1388
+ return line;
1389
+ const before = sliceByColumn(line, 0, columns.start, true);
1390
+ const selected = sliceByColumn(line, columns.start, columns.end - columns.start, true);
1391
+ const after = sliceByColumn(line, columns.end, Math.max(0, lineWidth - columns.end), true);
1392
+ return `${before}${this.applySelectionHighlight(selected)}${after}`;
1393
+ });
1394
+ }
1395
+ isMouseSequence(data) {
1396
+ return /^\x1b\[<\d+;\d+;\d+[Mm]$/.test(data) || (data.length === 6 && data.startsWith("\x1b[M"));
1397
+ }
1398
+ compositeScrollToEndIndicator(screen, layout, width) {
1399
+ this.scrollToEndIndicatorRect = undefined;
1400
+ const scrollView = layout.primaryScrollView ?? this.implicitScrollView;
1401
+ if (!this.scrollToEndIndicator || !scrollView.followEnd || scrollView.isFollowingEnd)
1402
+ return screen;
1403
+ const box = getScrollViewBox(layout, scrollView);
1404
+ const clip = box?.clip;
1405
+ if (!clip || clip.width <= 0 || clip.height <= 0)
1406
+ return screen;
1407
+ const row = clip.y + clip.height - 1;
1408
+ if (row >= screen.length || isImageLine(screen[row] ?? ""))
1409
+ return screen;
1410
+ const scrollbarColumn = box ? getScrollbarGeometry(box)?.column : undefined;
1411
+ const label = truncateToWidth(this.scrollToEndIndicator(), clip.width, "");
1412
+ const labelWidth = visibleWidth(label);
1413
+ const column = clip.x + Math.floor((clip.width - labelWidth) / 2);
1414
+ const rightEdge = scrollbarColumn ?? clip.x + clip.width;
1415
+ const availableWidth = Math.max(0, rightEdge - column);
1416
+ const text = truncateToWidth(label, availableWidth, "");
1417
+ const textWidth = visibleWidth(text);
1418
+ if (textWidth === 0)
1419
+ return screen;
1420
+ const result = [...screen];
1421
+ result[row] = compositeTuiLine(result[row] ?? "", text, column, textWidth, width);
1422
+ this.scrollToEndIndicatorRect = { row, column, width: textWidth };
1423
+ return result;
1424
+ }
1425
+ compositeFlashes(screen, width, height) {
1426
+ const flashLines = this.flashes.render(width).slice(-height);
1427
+ if (flashLines.length === 0)
1428
+ return screen;
1429
+ const result = [...screen];
1430
+ while (result.length < height)
1431
+ result.push("");
1432
+ for (let row = 0; row < flashLines.length; row++) {
1433
+ const line = flashLines[row];
1434
+ const flashWidth = visibleWidth(line);
1435
+ if (flashWidth === 0)
1436
+ continue;
1437
+ result[row] = compositeTuiLine(result[row] ?? "", line, width - flashWidth, flashWidth, width);
1438
+ }
1439
+ return result;
1440
+ }
1441
+ doRender() {
1442
+ if (this.stopped || !this.altScreenActive)
1443
+ return;
1444
+ const width = Math.max(1, this.terminal.columns);
1445
+ const height = Math.max(1, this.terminal.rows);
1446
+ const root = this.layoutRoot ?? this.implicitScrollView;
1447
+ let nextLayout = renderLayoutFrame(root, width, height, () => this.requestRender());
1448
+ if (this.refreshSearch(nextLayout)) {
1449
+ nextLayout = renderLayoutFrame(root, width, height, () => this.requestRender());
1450
+ }
1451
+ // Resolve fake cursors before highlighting and compositing, which only track SGR codes
1452
+ let screen = this.resolveFakeCursors(nextLayout.lines.map((line) => line.replace(OSC133_ZONE_PREFIX, "")));
1453
+ screen = this.applySearchHighlights(screen, nextLayout);
1454
+ screen = this.compositeScrollToEndIndicator(screen, nextLayout, width);
1455
+ screen = this.compositeOverlays(screen, width, height);
1456
+ if (screen.length > height)
1457
+ screen = screen.slice(screen.length - height);
1458
+ screen = this.applySelection(screen, nextLayout);
1459
+ screen = this.compositeFlashes(screen, width, height);
1460
+ const cursorPos = this.extractCursorPosition(screen, height);
1461
+ screen = this.applyLineResets(screen).map((line) => {
1462
+ if (isImageLine(line) || visibleWidth(line) <= width)
1463
+ return line;
1464
+ return sliceByColumn(line, 0, width, true);
1465
+ });
1466
+ const fullRedraw = this.previousScreen.length === 0 || this.previousScreenWidth !== width || this.previousScreenHeight !== height;
1467
+ const changedRows = screen.map((line, row) => line !== this.previousScreen[row]);
1468
+ const imageAnchorsNeedRedraw = screen.some((line, row) => changedRows[row] && (isImageLine(line) || isImageLine(this.previousScreen[row] ?? "")));
1469
+ const isWezTerm = Boolean(process.env.WEZTERM_PANE) || process.env.TERM_PROGRAM?.toLowerCase() === "wezterm";
1470
+ const imageCellsNeedRedraw = !imageAnchorsNeedRedraw &&
1471
+ isWezTerm &&
1472
+ this.imageProtocol === "kitty" &&
1473
+ changedRows.some(Boolean) &&
1474
+ screen.some((line, row) => {
1475
+ const placementRows = getKittyImagePlacementRows(line);
1476
+ if (placementRows === undefined)
1477
+ return false;
1478
+ for (let coveredRow = row; coveredRow < row + placementRows; coveredRow++) {
1479
+ if (changedRows[coveredRow])
1480
+ return true;
1481
+ }
1482
+ return false;
1483
+ });
1484
+ const imagesNeedRedraw = imageAnchorsNeedRedraw || imageCellsNeedRedraw;
1485
+ const redrawImages = fullRedraw || imagesNeedRedraw;
1486
+ const hadUploadedKittyImages = this.uploadedKittyImages.size > 0;
1487
+ const preparedKittyScreen = redrawImages && this.imageProtocol === "kitty"
1488
+ ? this.prepareKittyScreen(screen)
1489
+ : { lines: screen, evictedImageDeletion: "" };
1490
+ let buffer = BEGIN_SYNCHRONIZED_OUTPUT;
1491
+ if (fullRedraw) {
1492
+ this.fullRedrawCount += 1;
1493
+ const clearImages = this.imageProtocol === "kitty" && hadUploadedKittyImages
1494
+ ? deleteAllKittyPlacements()
1495
+ : this.deleteKittyImages();
1496
+ buffer += `${clearImages}\x1b[2J`;
1497
+ }
1498
+ else if (imagesNeedRedraw) {
1499
+ if (this.imageProtocol === "iterm2")
1500
+ buffer += "\x1b[2J";
1501
+ else if (this.imageProtocol === "kitty")
1502
+ buffer += deleteAllKittyPlacements();
1503
+ }
1504
+ buffer += preparedKittyScreen.evictedImageDeletion;
1505
+ // WezTerm erases intersecting Kitty image cells when a later row write touches a covered row.
1506
+ // Draw image placements after every clear and text write so nothing later intersects them; preserve
1507
+ // the existing interleaved output for text-only frames and every other terminal.
1508
+ const drawKittyImagesLast = redrawImages && this.imageProtocol === "kitty" && screen.some(isImageLine) && isWezTerm;
1509
+ if (drawKittyImagesLast) {
1510
+ for (let row = 0; row < height; row++) {
1511
+ if (!fullRedraw && !imagesNeedRedraw && screen[row] === this.previousScreen[row])
1512
+ continue;
1513
+ buffer += `\x1b[${row + 1};1H\x1b[2K`;
1514
+ }
1515
+ for (let row = 0; row < height; row++) {
1516
+ if (!fullRedraw && !imagesNeedRedraw && screen[row] === this.previousScreen[row])
1517
+ continue;
1518
+ if (isImageLine(preparedKittyScreen.lines[row] ?? ""))
1519
+ continue;
1520
+ buffer += `\x1b[${row + 1};1H${preparedKittyScreen.lines[row] ?? ""}`;
1521
+ }
1522
+ for (let row = 0; row < height; row++) {
1523
+ if (!fullRedraw && !imagesNeedRedraw && screen[row] === this.previousScreen[row])
1524
+ continue;
1525
+ if (!isImageLine(preparedKittyScreen.lines[row] ?? ""))
1526
+ continue;
1527
+ buffer += `\x1b[${row + 1};1H${preparedKittyScreen.lines[row] ?? ""}`;
1528
+ }
1529
+ }
1530
+ else {
1531
+ for (let row = 0; row < height; row++) {
1532
+ if (!fullRedraw && !imagesNeedRedraw && screen[row] === this.previousScreen[row])
1533
+ continue;
1534
+ buffer += `\x1b[${row + 1};1H\x1b[2K${preparedKittyScreen.lines[row] ?? ""}`;
1535
+ }
1536
+ }
1537
+ if (cursorPos) {
1538
+ buffer += `\x1b[${cursorPos.row + 1};${Math.min(width, cursorPos.col) + 1}H`;
1539
+ buffer += this.getShowHardwareCursor() ? "\x1b[?25h" : "\x1b[?25l";
1540
+ }
1541
+ else {
1542
+ buffer += "\x1b[?25l";
1543
+ }
1544
+ buffer += END_SYNCHRONIZED_OUTPUT;
1545
+ this.terminal.write(buffer);
1546
+ this.previousScreen = screen;
1547
+ this.previousScreenWidth = width;
1548
+ this.previousScreenHeight = height;
1549
+ this.currentLayout = nextLayout;
1550
+ }
1551
+ }
1552
+ //# sourceMappingURL=tui-alt-screen.js.map