@earendil-works/pi-tui 0.82.1 → 0.84.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 (93) hide show
  1. package/README.md +77 -14
  2. package/dist/components/alt-screen-flash.d.ts +13 -0
  3. package/dist/components/alt-screen-flash.d.ts.map +1 -0
  4. package/dist/components/alt-screen-flash.js +37 -0
  5. package/dist/components/alt-screen-flash.js.map +1 -0
  6. package/dist/components/editor.d.ts.map +1 -1
  7. package/dist/components/editor.js +11 -0
  8. package/dist/components/editor.js.map +1 -1
  9. package/dist/components/h-stack.d.ts +7 -0
  10. package/dist/components/h-stack.d.ts.map +1 -0
  11. package/dist/components/h-stack.js +43 -0
  12. package/dist/components/h-stack.js.map +1 -0
  13. package/dist/components/image.d.ts.map +1 -1
  14. package/dist/components/image.js +3 -2
  15. package/dist/components/image.js.map +1 -1
  16. package/dist/components/markdown.d.ts +4 -0
  17. package/dist/components/markdown.d.ts.map +1 -1
  18. package/dist/components/markdown.js +128 -2
  19. package/dist/components/markdown.js.map +1 -1
  20. package/dist/components/scroll-view.d.ts +51 -0
  21. package/dist/components/scroll-view.d.ts.map +1 -0
  22. package/dist/components/scroll-view.js +173 -0
  23. package/dist/components/scroll-view.js.map +1 -0
  24. package/dist/components/settings-list.d.ts.map +1 -1
  25. package/dist/components/settings-list.js +3 -6
  26. package/dist/components/settings-list.js.map +1 -1
  27. package/dist/components/stack.d.ts +32 -0
  28. package/dist/components/stack.d.ts.map +1 -0
  29. package/dist/components/stack.js +111 -0
  30. package/dist/components/stack.js.map +1 -0
  31. package/dist/components/v-stack.d.ts +8 -0
  32. package/dist/components/v-stack.d.ts.map +1 -0
  33. package/dist/components/v-stack.js +26 -0
  34. package/dist/components/v-stack.js.map +1 -0
  35. package/dist/index.d.ts +9 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +10 -2
  38. package/dist/index.js.map +1 -1
  39. package/dist/keybindings.d.ts +44 -4
  40. package/dist/keybindings.d.ts.map +1 -1
  41. package/dist/keybindings.js +31 -4
  42. package/dist/keybindings.js.map +1 -1
  43. package/dist/latex.d.ts +10 -0
  44. package/dist/latex.d.ts.map +1 -0
  45. package/dist/latex.js +1124 -0
  46. package/dist/latex.js.map +1 -0
  47. package/dist/layout-node.d.ts +40 -0
  48. package/dist/layout-node.d.ts.map +1 -0
  49. package/dist/layout-node.js +6 -0
  50. package/dist/layout-node.js.map +1 -0
  51. package/dist/layout.d.ts +40 -0
  52. package/dist/layout.d.ts.map +1 -0
  53. package/dist/layout.js +314 -0
  54. package/dist/layout.js.map +1 -0
  55. package/dist/native-modifiers.d.ts.map +1 -1
  56. package/dist/native-modifiers.js +10 -3
  57. package/dist/native-modifiers.js.map +1 -1
  58. package/dist/terminal-colors.d.ts.map +1 -1
  59. package/dist/terminal-colors.js +1 -1
  60. package/dist/terminal-colors.js.map +1 -1
  61. package/dist/terminal-image.d.ts +26 -0
  62. package/dist/terminal-image.d.ts.map +1 -1
  63. package/dist/terminal-image.js +153 -5
  64. package/dist/terminal-image.js.map +1 -1
  65. package/dist/terminal.d.ts +1 -0
  66. package/dist/terminal.d.ts.map +1 -1
  67. package/dist/terminal.js +10 -7
  68. package/dist/terminal.js.map +1 -1
  69. package/dist/tui-alt-screen.d.ts +85 -0
  70. package/dist/tui-alt-screen.d.ts.map +1 -0
  71. package/dist/tui-alt-screen.js +768 -0
  72. package/dist/tui-alt-screen.js.map +1 -0
  73. package/dist/tui-main-screen.d.ts +39 -0
  74. package/dist/tui-main-screen.d.ts.map +1 -0
  75. package/dist/tui-main-screen.js +533 -0
  76. package/dist/tui-main-screen.js.map +1 -0
  77. package/dist/tui.d.ts +75 -38
  78. package/dist/tui.d.ts.map +1 -1
  79. package/dist/tui.js +98 -573
  80. package/dist/tui.js.map +1 -1
  81. package/dist/utils.d.ts +11 -0
  82. package/dist/utils.d.ts.map +1 -1
  83. package/dist/utils.js +119 -6
  84. package/dist/utils.js.map +1 -1
  85. package/native/darwin/README.md +20 -0
  86. package/native/darwin/build.sh +62 -0
  87. package/native/darwin/src/darwin-modifiers.c +70 -0
  88. package/native/win32/README.md +22 -0
  89. package/native/win32/build.mjs +229 -0
  90. package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  91. package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  92. package/native/win32/src/win32-console-mode.c +119 -0
  93. package/package.json +9 -1
@@ -0,0 +1,768 @@
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, 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_MOUSE = "\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1004h\x1b[?1006h";
14
+ const DISABLE_MOUSE = "\x1b[?1006l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l";
15
+ const FOCUS_IN = "\x1b[I";
16
+ const FOCUS_OUT = "\x1b[O";
17
+ const BEGIN_SYNCHRONIZED_OUTPUT = "\x1b[?2026h";
18
+ const END_SYNCHRONIZED_OUTPUT = "\x1b[?2026l";
19
+ const OSC133_ZONE_PREFIX = /^(?:\x1b\]133;[ABC](?:\x07|\x1b\\))+/;
20
+ const OSC133_PROMPT_START = /^\x1b\]133;A(?:\x07|\x1b\\)/;
21
+ const PAGE_SCROLL_OVERLAP = 4;
22
+ const MAX_CACHED_OFFSCREEN_KITTY_IMAGES = 16;
23
+ const MAX_CACHED_OFFSCREEN_KITTY_TRANSMISSION_BYTES = 32 * 1024 * 1024;
24
+ const MAX_CACHED_OFFSCREEN_KITTY_DECODED_BYTES = 64 * 1024 * 1024;
25
+ /** Alternate-screen TUI with a scrollable, application-owned viewport. */
26
+ export class TuiAltScreen extends TuiBase {
27
+ mode = "fullscreen";
28
+ [VIEWPORT_TUI] = true;
29
+ previousScreen = [];
30
+ lastDocument = [];
31
+ previousScreenWidth = 0;
32
+ previousScreenHeight = 0;
33
+ layoutRoot;
34
+ currentLayout;
35
+ implicitDocument;
36
+ implicitScrollView;
37
+ flashes;
38
+ altScreenActive = false;
39
+ imageProtocol = null;
40
+ savedCapabilities;
41
+ uploadedKittyImages = new Map();
42
+ selectionAnchor;
43
+ selectionFocus;
44
+ selectionDragPointer;
45
+ selectionAutoScrollDirection = 0;
46
+ selectionAutoScrollTimer;
47
+ selectionPressActive = false;
48
+ scrollbarDrag;
49
+ scrollbarHover;
50
+ pressedUrl;
51
+ selectionDragged = false;
52
+ wheelScrollLines;
53
+ mouseEnabled;
54
+ openUrl;
55
+ constructor(terminal, showHardwareCursor, logDirectory, options = {}) {
56
+ super(terminal, showHardwareCursor, logDirectory);
57
+ this.implicitDocument = {
58
+ render: (width) => super.render(width),
59
+ invalidate: () => {
60
+ for (const child of this.children)
61
+ child.invalidate();
62
+ },
63
+ };
64
+ this.implicitScrollView = new ScrollView(this.implicitDocument, { follow: "end", primary: true });
65
+ this.flashes = new AltScreenFlashContainer(() => this.requestRender());
66
+ this.wheelScrollLines = Math.max(1, Math.floor(options.wheelScrollLines ?? 1));
67
+ this.mouseEnabled = options.mouse ?? true;
68
+ this.openUrl = options.openUrl;
69
+ this.addInputListener((data) => this.handleViewportInput(data));
70
+ }
71
+ get viewportTop() {
72
+ return this.getPrimaryScrollView().scrollTop;
73
+ }
74
+ get isFollowingOutput() {
75
+ return this.getPrimaryScrollView().isFollowingEnd;
76
+ }
77
+ setLayoutRoot(component) {
78
+ if (this.layoutRoot === component)
79
+ return;
80
+ this.layoutRoot = component;
81
+ this.currentLayout = undefined;
82
+ this.requestRender();
83
+ }
84
+ render(width) {
85
+ return this.layoutRoot?.render(width) ?? super.render(width);
86
+ }
87
+ getMountedRoots() {
88
+ return this.layoutRoot ? [this.layoutRoot] : this.children;
89
+ }
90
+ getPrimaryScrollView() {
91
+ return this.currentLayout?.primaryScrollView ?? this.implicitScrollView;
92
+ }
93
+ beforeTerminalStart() {
94
+ this.stopSelectionAutoScroll();
95
+ this.selectionPressActive = false;
96
+ this.stopScrollbarHover();
97
+ this.stopScrollbarDrag();
98
+ this.flashes.dispose();
99
+ this.altScreenActive = true;
100
+ const capabilities = getCapabilities();
101
+ this.imageProtocol = capabilities.images;
102
+ this.uploadedKittyImages.clear();
103
+ if (capabilities.images === "iterm2") {
104
+ this.savedCapabilities = capabilities;
105
+ setCapabilities({ ...capabilities, images: null });
106
+ this.invalidate();
107
+ }
108
+ this.lastDocument = [];
109
+ this.selectionAnchor = undefined;
110
+ this.selectionFocus = undefined;
111
+ this.pressedUrl = undefined;
112
+ this.selectionDragged = false;
113
+ this.resetRenderState();
114
+ this.terminal.write(`${ENTER_ALT_SCREEN}${DISABLE_AUTOWRAP}${this.mouseEnabled ? ENABLE_MOUSE : ""}\x1b[2J\x1b[H\x1b[?25l`);
115
+ }
116
+ beforeTerminalStop(_options) {
117
+ this.stopSelectionAutoScroll();
118
+ this.selectionPressActive = false;
119
+ this.stopScrollbarHover();
120
+ this.stopScrollbarDrag();
121
+ this.flashes.dispose();
122
+ if (!this.altScreenActive)
123
+ return;
124
+ this.terminal.write(`${BEGIN_SYNCHRONIZED_OUTPUT}${this.deleteKittyImages()}${this.mouseEnabled ? DISABLE_MOUSE : ""}${ENABLE_AUTOWRAP}${END_SYNCHRONIZED_OUTPUT}`);
125
+ this.uploadedKittyImages.clear();
126
+ }
127
+ afterTerminalStop(options) {
128
+ if (!this.altScreenActive)
129
+ return;
130
+ this.altScreenActive = false;
131
+ if (options.preserveScreen) {
132
+ this.terminal.write(`${BEGIN_SYNCHRONIZED_OUTPUT}${EXIT_ALT_SCREEN}\x1b[?25h${END_SYNCHRONIZED_OUTPUT}`);
133
+ }
134
+ else {
135
+ const width = Math.max(1, this.terminal.columns);
136
+ const documentLines = this.render(width).map((line) => line.replace(OSC133_ZONE_PREFIX, ""));
137
+ this.lastDocument = this.applyLineResets(documentLines.map((line) => line.replaceAll(CURSOR_MARKER, ""))).map((line) => (isImageLine(line) || visibleWidth(line) <= width ? line : sliceByColumn(line, 0, width, true)));
138
+ let buffer = `${BEGIN_SYNCHRONIZED_OUTPUT}${EXIT_ALT_SCREEN}${DISABLE_AUTOWRAP}`;
139
+ for (let row = 0; row < this.lastDocument.length; row++) {
140
+ if (row > 0)
141
+ buffer += "\r\n";
142
+ buffer += `\r\x1b[2K${this.lastDocument[row] ?? ""}`;
143
+ }
144
+ buffer += `\x1b[0m${ENABLE_AUTOWRAP}\r\n\x1b[?25h${END_SYNCHRONIZED_OUTPUT}`;
145
+ this.terminal.write(buffer);
146
+ }
147
+ if (this.savedCapabilities) {
148
+ setCapabilities(this.savedCapabilities);
149
+ this.savedCapabilities = undefined;
150
+ }
151
+ }
152
+ deleteKittyImages() {
153
+ return this.imageProtocol === "kitty" ? deleteAllKittyImages() : "";
154
+ }
155
+ prepareKittyScreen(screen) {
156
+ const visibleImageIds = new Set();
157
+ const lines = screen.map((line) => {
158
+ const placement = getKittyImagePlacement(line);
159
+ if (!placement)
160
+ return line;
161
+ visibleImageIds.add(placement.imageId);
162
+ const cachedImage = this.uploadedKittyImages.get(placement.imageId);
163
+ const nextCachedImage = {
164
+ transmissionGeneration: placement.transmissionGeneration,
165
+ transmissionBytes: placement.transmissionBytes,
166
+ estimatedDecodedBytes: placement.estimatedDecodedBytes,
167
+ };
168
+ if (cachedImage)
169
+ this.uploadedKittyImages.delete(placement.imageId);
170
+ this.uploadedKittyImages.set(placement.imageId, nextCachedImage);
171
+ return cachedImage?.transmissionGeneration === placement.transmissionGeneration
172
+ ? placement.replacementLine
173
+ : line;
174
+ });
175
+ let cachedOffscreenImageCount = 0;
176
+ let cachedOffscreenTransmissionBytes = 0;
177
+ let cachedOffscreenDecodedBytes = 0;
178
+ for (const [imageId, cachedImage] of this.uploadedKittyImages) {
179
+ if (visibleImageIds.has(imageId))
180
+ continue;
181
+ cachedOffscreenImageCount += 1;
182
+ cachedOffscreenTransmissionBytes += cachedImage.transmissionBytes;
183
+ cachedOffscreenDecodedBytes += cachedImage.estimatedDecodedBytes;
184
+ }
185
+ let evictedImageDeletion = "";
186
+ for (const [imageId, cachedImage] of this.uploadedKittyImages) {
187
+ if (cachedOffscreenImageCount <= MAX_CACHED_OFFSCREEN_KITTY_IMAGES &&
188
+ cachedOffscreenTransmissionBytes <= MAX_CACHED_OFFSCREEN_KITTY_TRANSMISSION_BYTES &&
189
+ cachedOffscreenDecodedBytes <= MAX_CACHED_OFFSCREEN_KITTY_DECODED_BYTES) {
190
+ break;
191
+ }
192
+ if (visibleImageIds.has(imageId))
193
+ continue;
194
+ evictedImageDeletion += deleteKittyImage(imageId);
195
+ this.uploadedKittyImages.delete(imageId);
196
+ cachedOffscreenImageCount -= 1;
197
+ cachedOffscreenTransmissionBytes -= cachedImage.transmissionBytes;
198
+ cachedOffscreenDecodedBytes -= cachedImage.estimatedDecodedBytes;
199
+ }
200
+ return { lines, evictedImageDeletion };
201
+ }
202
+ resetRenderState() {
203
+ this.previousScreen = [];
204
+ this.previousScreenWidth = 0;
205
+ this.previousScreenHeight = 0;
206
+ this.currentLayout = undefined;
207
+ }
208
+ scrollBy(lines) {
209
+ this.getPrimaryScrollView().scrollBy(lines);
210
+ this.requestRender();
211
+ }
212
+ scrollToTop() {
213
+ this.getPrimaryScrollView().scrollToStart();
214
+ this.requestRender();
215
+ }
216
+ scrollToBottom() {
217
+ this.getPrimaryScrollView().scrollToEnd();
218
+ this.requestRender();
219
+ }
220
+ scrollToPrompt(direction) {
221
+ if (!this.currentLayout)
222
+ return;
223
+ const scrollView = this.getPrimaryScrollView();
224
+ const lines = getScrollViewBox(this.currentLayout, scrollView)?.scrollContentLines;
225
+ if (!lines)
226
+ return;
227
+ for (let row = scrollView.scrollTop + direction; row >= 0 && row < lines.length; row += direction) {
228
+ if (!OSC133_PROMPT_START.test(lines[row] ?? ""))
229
+ continue;
230
+ scrollView.scrollTo(row);
231
+ this.requestRender();
232
+ return;
233
+ }
234
+ }
235
+ /** Show a transient message in the alternate-screen flash stack. */
236
+ flash(message, durationMs) {
237
+ this.flashes.flash(message, durationMs);
238
+ }
239
+ handleViewportInput(data) {
240
+ if (data === FOCUS_OUT) {
241
+ const hadActiveSelection = this.selectionPressActive;
242
+ this.selectionPressActive = false;
243
+ this.stopSelectionAutoScroll();
244
+ this.stopScrollbarHover();
245
+ this.stopScrollbarDrag();
246
+ this.pressedUrl = undefined;
247
+ this.selectionDragged = false;
248
+ if (hadActiveSelection) {
249
+ this.selectionAnchor = undefined;
250
+ this.selectionFocus = undefined;
251
+ }
252
+ this.requestRender();
253
+ return { consume: true };
254
+ }
255
+ if (data === FOCUS_IN)
256
+ return { consume: true };
257
+ const wheelEvent = this.parseWheelEvent(data);
258
+ if (wheelEvent) {
259
+ this.routeWheel(wheelEvent);
260
+ return { consume: true };
261
+ }
262
+ const mouseEvent = this.parseSgrMouseEvent(data);
263
+ if (mouseEvent) {
264
+ const handled = this.handleScrollbarMouseEvent(mouseEvent);
265
+ if (!this.scrollbarDrag)
266
+ this.updateScrollbarHover(mouseEvent.x, mouseEvent.y);
267
+ if (!handled)
268
+ this.handleSelectionMouseEvent(mouseEvent);
269
+ return { consume: true };
270
+ }
271
+ if (this.isMouseSequence(data))
272
+ return { consume: true };
273
+ const keybindings = getKeybindings();
274
+ const isRelease = isKeyRelease(data);
275
+ if (keybindings.matches(data, "tui.altScreen.pageUp")) {
276
+ if (!isRelease) {
277
+ this.scrollBy(-Math.max(1, this.getPrimaryScrollView().viewportHeight - PAGE_SCROLL_OVERLAP));
278
+ }
279
+ return { consume: true };
280
+ }
281
+ if (keybindings.matches(data, "tui.altScreen.pageDown")) {
282
+ if (!isRelease) {
283
+ this.scrollBy(Math.max(1, this.getPrimaryScrollView().viewportHeight - PAGE_SCROLL_OVERLAP));
284
+ }
285
+ return { consume: true };
286
+ }
287
+ if (keybindings.matches(data, "tui.altScreen.previousPrompt")) {
288
+ if (!isRelease)
289
+ this.scrollToPrompt(-1);
290
+ return { consume: true };
291
+ }
292
+ if (keybindings.matches(data, "tui.altScreen.nextPrompt")) {
293
+ if (!isRelease)
294
+ this.scrollToPrompt(1);
295
+ return { consume: true };
296
+ }
297
+ if (keybindings.matches(data, "tui.altScreen.top")) {
298
+ if (!isRelease)
299
+ this.scrollToTop();
300
+ return { consume: true };
301
+ }
302
+ if (keybindings.matches(data, "tui.altScreen.bottom")) {
303
+ if (!isRelease)
304
+ this.scrollToBottom();
305
+ return { consume: true };
306
+ }
307
+ return undefined;
308
+ }
309
+ parseWheelEvent(data) {
310
+ const sgr = /^\x1b\[<(\d+);(\d+);(\d+)[Mm]$/.exec(data);
311
+ if (sgr) {
312
+ const button = Number.parseInt(sgr[1], 10);
313
+ if ((button & 64) === 0)
314
+ return undefined;
315
+ const direction = button & 3;
316
+ if (direction !== 0 && direction !== 1)
317
+ return undefined;
318
+ return {
319
+ direction: direction === 0 ? -1 : 1,
320
+ x: Number.parseInt(sgr[2], 10) - 1,
321
+ y: Number.parseInt(sgr[3], 10) - 1,
322
+ };
323
+ }
324
+ if (data.length === 6 && data.startsWith("\x1b[M")) {
325
+ const button = data.charCodeAt(3) - 32;
326
+ if ((button & 64) === 0)
327
+ return undefined;
328
+ const direction = button & 3;
329
+ if (direction !== 0 && direction !== 1)
330
+ return undefined;
331
+ return {
332
+ direction: direction === 0 ? -1 : 1,
333
+ x: data.charCodeAt(4) - 33,
334
+ y: data.charCodeAt(5) - 33,
335
+ };
336
+ }
337
+ return undefined;
338
+ }
339
+ routeWheel(event) {
340
+ let remaining = event.direction * this.wheelScrollLines;
341
+ const seen = new Set();
342
+ for (const scrollView of this.currentLayout ? getScrollViewsAt(this.currentLayout, event.x, event.y) : []) {
343
+ seen.add(scrollView);
344
+ remaining = scrollView.scrollBy(remaining);
345
+ if (remaining === 0 || scrollView.overscroll === "contain")
346
+ break;
347
+ }
348
+ const primary = this.getPrimaryScrollView();
349
+ if (remaining !== 0 && !seen.has(primary))
350
+ primary.scrollBy(remaining);
351
+ this.updateScrollbarHover(event.x, event.y);
352
+ this.requestRender();
353
+ }
354
+ parseSgrMouseEvent(data) {
355
+ const match = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(data);
356
+ if (!match)
357
+ return undefined;
358
+ return {
359
+ button: Number.parseInt(match[1], 10),
360
+ x: Number.parseInt(match[2], 10) - 1,
361
+ y: Number.parseInt(match[3], 10) - 1,
362
+ release: match[4] === "m",
363
+ };
364
+ }
365
+ getScrollbarTargetAt(x, y) {
366
+ if (this.hasOverlay() || !this.currentLayout)
367
+ return undefined;
368
+ for (const scrollView of getScrollViewsAt(this.currentLayout, x, y)) {
369
+ const box = getScrollViewBox(this.currentLayout, scrollView);
370
+ const geometry = box ? getScrollbarGeometry(box) : undefined;
371
+ if (geometry &&
372
+ x === geometry.column &&
373
+ y >= geometry.thumbTop &&
374
+ y < geometry.thumbTop + geometry.thumbHeight) {
375
+ return { scrollView, geometry };
376
+ }
377
+ }
378
+ return undefined;
379
+ }
380
+ setScrollbarHover(scrollView) {
381
+ if (scrollView === this.scrollbarHover)
382
+ return;
383
+ this.scrollbarHover?.setScrollbarActive(false);
384
+ this.scrollbarHover = scrollView;
385
+ this.scrollbarHover?.setScrollbarActive(true);
386
+ }
387
+ updateScrollbarHover(x, y) {
388
+ this.setScrollbarHover(this.getScrollbarTargetAt(x, y)?.scrollView);
389
+ }
390
+ stopScrollbarHover() {
391
+ this.setScrollbarHover(undefined);
392
+ }
393
+ handleScrollbarMouseEvent(event) {
394
+ if (this.scrollbarDrag) {
395
+ if (event.release) {
396
+ this.stopScrollbarDrag();
397
+ return true;
398
+ }
399
+ const box = this.currentLayout
400
+ ? getScrollViewBox(this.currentLayout, this.scrollbarDrag.scrollView)
401
+ : undefined;
402
+ const geometry = box ? getScrollbarGeometry(box) : undefined;
403
+ if (geometry) {
404
+ const maxThumbOffset = geometry.trackHeight - geometry.thumbHeight;
405
+ const thumbOffset = Math.max(0, Math.min(maxThumbOffset, event.y - geometry.trackTop - this.scrollbarDrag.grabOffset));
406
+ const scrollTop = maxThumbOffset === 0 ? 0 : Math.round((thumbOffset / maxThumbOffset) * geometry.maxScrollTop);
407
+ this.scrollbarDrag.scrollView.scrollTo(scrollTop);
408
+ }
409
+ return true;
410
+ }
411
+ if (event.release || (event.button & 32) !== 0 || (event.button & 3) !== 0)
412
+ return false;
413
+ const target = this.getScrollbarTargetAt(event.x, event.y);
414
+ if (!target)
415
+ return false;
416
+ this.stopSelectionAutoScroll();
417
+ this.selectionPressActive = false;
418
+ this.selectionAnchor = undefined;
419
+ this.selectionFocus = undefined;
420
+ this.pressedUrl = undefined;
421
+ this.selectionDragged = false;
422
+ this.setScrollbarHover(target.scrollView);
423
+ this.scrollbarDrag = {
424
+ scrollView: target.scrollView,
425
+ grabOffset: event.y - target.geometry.thumbTop,
426
+ };
427
+ return true;
428
+ }
429
+ stopScrollbarDrag() {
430
+ this.scrollbarDrag = undefined;
431
+ }
432
+ getScrollSelectionPoint(scrollView, x, y) {
433
+ if (!this.currentLayout)
434
+ return undefined;
435
+ const box = getScrollViewBox(this.currentLayout, scrollView);
436
+ if (!box || box.rect.height <= 0 || box.clip.height <= 0)
437
+ return undefined;
438
+ const visibleTop = Math.max(0, box.rect.y, box.clip.y);
439
+ const visibleBottom = Math.min(this.terminal.rows - 1, box.rect.y + box.rect.height - 1, box.clip.y + box.clip.height - 1);
440
+ if (visibleBottom < visibleTop)
441
+ return undefined;
442
+ const pointerRow = Math.max(visibleTop, Math.min(visibleBottom, y));
443
+ const maxContentRow = Math.max(0, (box.scrollContentLines?.length ?? 1) - 1);
444
+ return {
445
+ row: Math.max(0, Math.min(maxContentRow, scrollView.scrollTop + pointerRow - box.rect.y)),
446
+ col: Math.max(0, Math.min(box.rect.width - 1, x - box.rect.x)),
447
+ scrollView,
448
+ };
449
+ }
450
+ getSelectionPoint(event, scrollView) {
451
+ if (scrollView) {
452
+ const point = this.getScrollSelectionPoint(scrollView, event.x, event.y);
453
+ if (point)
454
+ return point;
455
+ }
456
+ return {
457
+ row: Math.max(0, Math.min(this.terminal.rows - 1, event.y)),
458
+ col: Math.max(0, Math.min(this.terminal.columns - 1, event.x)),
459
+ };
460
+ }
461
+ updateSelectionAutoScroll(event) {
462
+ const scrollView = this.selectionAnchor?.scrollView;
463
+ if (!scrollView || !this.currentLayout) {
464
+ this.stopSelectionAutoScroll();
465
+ return;
466
+ }
467
+ const box = getScrollViewBox(this.currentLayout, scrollView);
468
+ if (!box || box.rect.height <= 0 || box.clip.height <= 0) {
469
+ this.stopSelectionAutoScroll();
470
+ return;
471
+ }
472
+ const visibleTop = Math.max(0, box.rect.y, box.clip.y);
473
+ const visibleBottom = Math.min(this.terminal.rows - 1, box.rect.y + box.rect.height - 1, box.clip.y + box.clip.height - 1);
474
+ this.selectionDragPointer = { x: event.x, y: event.y };
475
+ this.selectionAutoScrollDirection = event.y <= visibleTop ? -1 : event.y >= visibleBottom ? 1 : 0;
476
+ if (this.selectionAutoScrollDirection === 0) {
477
+ this.stopSelectionAutoScroll();
478
+ return;
479
+ }
480
+ if (this.selectionAutoScrollTimer)
481
+ return;
482
+ this.selectionAutoScrollTimer = setInterval(() => this.autoScrollSelection(), 50);
483
+ this.selectionAutoScrollTimer.unref();
484
+ }
485
+ autoScrollSelection() {
486
+ const scrollView = this.selectionAnchor?.scrollView;
487
+ const pointer = this.selectionDragPointer;
488
+ const direction = this.selectionAutoScrollDirection;
489
+ if (!scrollView || !pointer || direction === 0) {
490
+ this.stopSelectionAutoScroll();
491
+ return;
492
+ }
493
+ const remaining = scrollView.scrollBy(direction);
494
+ if (remaining === direction) {
495
+ this.stopSelectionAutoScroll();
496
+ return;
497
+ }
498
+ const point = this.getScrollSelectionPoint(scrollView, pointer.x, pointer.y);
499
+ if (point)
500
+ this.selectionFocus = point;
501
+ this.requestRender();
502
+ }
503
+ stopSelectionAutoScroll() {
504
+ if (this.selectionAutoScrollTimer) {
505
+ clearInterval(this.selectionAutoScrollTimer);
506
+ this.selectionAutoScrollTimer = undefined;
507
+ }
508
+ this.selectionAutoScrollDirection = 0;
509
+ this.selectionDragPointer = undefined;
510
+ }
511
+ handleSelectionMouseEvent(event) {
512
+ if ((event.button & 3) !== 0)
513
+ return;
514
+ const anchorScrollView = this.selectionAnchor?.scrollView;
515
+ const point = this.getSelectionPoint(event, anchorScrollView);
516
+ if (event.release) {
517
+ if (!this.selectionPressActive)
518
+ return;
519
+ this.selectionPressActive = false;
520
+ this.stopSelectionAutoScroll();
521
+ if (!this.selectionAnchor)
522
+ return;
523
+ this.selectionFocus = point;
524
+ const clickedUrl = !this.selectionDragged &&
525
+ this.selectionAnchor.scrollView === point.scrollView &&
526
+ this.selectionAnchor.row === point.row &&
527
+ this.selectionAnchor.col === point.col
528
+ ? this.pressedUrl
529
+ : undefined;
530
+ this.pressedUrl = undefined;
531
+ if (clickedUrl && this.openUrl) {
532
+ this.selectionAnchor = undefined;
533
+ this.selectionFocus = undefined;
534
+ try {
535
+ this.openUrl(clickedUrl);
536
+ }
537
+ catch {
538
+ // URL activation is best-effort.
539
+ }
540
+ this.requestRender();
541
+ return;
542
+ }
543
+ this.copySelectionToClipboard();
544
+ this.requestRender();
545
+ return;
546
+ }
547
+ if ((event.button & 32) !== 0) {
548
+ if (!this.selectionPressActive || !this.selectionAnchor)
549
+ return;
550
+ this.selectionDragged = true;
551
+ this.pressedUrl = undefined;
552
+ this.selectionFocus = point;
553
+ this.updateSelectionAutoScroll(event);
554
+ this.requestRender();
555
+ return;
556
+ }
557
+ this.stopSelectionAutoScroll();
558
+ this.selectionPressActive = true;
559
+ const scrollView = !this.hasOverlay() && this.currentLayout
560
+ ? getScrollViewsAt(this.currentLayout, event.x, event.y)[0]
561
+ : undefined;
562
+ const anchor = this.getSelectionPoint(event, scrollView);
563
+ this.selectionAnchor = anchor;
564
+ this.selectionFocus = anchor;
565
+ this.selectionDragged = false;
566
+ this.pressedUrl = 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)));
567
+ this.requestRender();
568
+ }
569
+ getSelectionBounds() {
570
+ if (!this.selectionAnchor || !this.selectionFocus)
571
+ return undefined;
572
+ if (this.selectionAnchor.scrollView !== this.selectionFocus.scrollView)
573
+ return undefined;
574
+ const anchorBeforeFocus = this.selectionAnchor.row < this.selectionFocus.row ||
575
+ (this.selectionAnchor.row === this.selectionFocus.row && this.selectionAnchor.col < this.selectionFocus.col);
576
+ if (this.selectionAnchor.row === this.selectionFocus.row &&
577
+ this.selectionAnchor.col === this.selectionFocus.col) {
578
+ return undefined;
579
+ }
580
+ return anchorBeforeFocus
581
+ ? { start: this.selectionAnchor, end: this.selectionFocus }
582
+ : { start: this.selectionFocus, end: this.selectionAnchor };
583
+ }
584
+ getSelectionColumns(line, row, selection, minColumn = 0, maxColumn = visibleWidth(line)) {
585
+ const lineWidth = visibleWidth(line);
586
+ let start = Math.max(0, minColumn);
587
+ let end = Math.min(lineWidth, maxColumn);
588
+ if (row === selection.start.row) {
589
+ start = getGraphemeCellRange(line, selection.start.col)?.start ?? Math.min(selection.start.col, lineWidth);
590
+ }
591
+ if (row === selection.end.row) {
592
+ end = getGraphemeCellRange(line, selection.end.col)?.end ?? Math.min(selection.end.col + 1, lineWidth);
593
+ }
594
+ return { start: Math.max(minColumn, start), end: Math.min(maxColumn, end) };
595
+ }
596
+ copySelectionToClipboard() {
597
+ const selection = this.getSelectionBounds();
598
+ if (!selection)
599
+ return;
600
+ let sourceLines = this.previousScreen;
601
+ if (selection.start.scrollView) {
602
+ if (!this.currentLayout)
603
+ return;
604
+ const box = getScrollViewBox(this.currentLayout, selection.start.scrollView);
605
+ if (!box?.scrollContentLines)
606
+ return;
607
+ sourceLines = box.scrollContentLines;
608
+ }
609
+ const lines = [];
610
+ for (let row = selection.start.row; row <= selection.end.row; row++) {
611
+ const line = sourceLines[row] ?? "";
612
+ const columns = this.getSelectionColumns(line, row, selection);
613
+ lines.push(stripTerminalSequences(sliceByColumn(line, columns.start, Math.max(0, columns.end - columns.start), true)).trimEnd());
614
+ }
615
+ const text = lines.join("\n");
616
+ if (text.length === 0)
617
+ return;
618
+ this.terminal.write(`\x1b]52;c;${Buffer.from(text).toString("base64")}\x07`);
619
+ this.flash("Copied!");
620
+ }
621
+ applySelectionHighlight(text) {
622
+ let result = "\x1b[7m";
623
+ let index = 0;
624
+ while (index < text.length) {
625
+ const ansi = extractAnsiCode(text, index);
626
+ if (!ansi) {
627
+ result += text[index];
628
+ index += 1;
629
+ continue;
630
+ }
631
+ result += ansi.code;
632
+ if (ansi.code.endsWith("m"))
633
+ result += "\x1b[7m";
634
+ index += ansi.length;
635
+ }
636
+ return `${result}\x1b[27m`;
637
+ }
638
+ applySelection(screen, layout = this.currentLayout) {
639
+ const selection = this.getSelectionBounds();
640
+ if (!selection)
641
+ return screen;
642
+ let screenSelection = selection;
643
+ let minRow = 0;
644
+ let maxRow = screen.length - 1;
645
+ let minColumn = 0;
646
+ let maxColumn = this.terminal.columns;
647
+ if (selection.start.scrollView) {
648
+ if (!layout)
649
+ return screen;
650
+ const box = getScrollViewBox(layout, selection.start.scrollView);
651
+ if (!box)
652
+ return screen;
653
+ minRow = Math.max(0, box.rect.y, box.clip.y);
654
+ maxRow = Math.min(screen.length - 1, box.rect.y + box.rect.height - 1, box.clip.y + box.clip.height - 1);
655
+ minColumn = Math.max(0, box.rect.x, box.clip.x);
656
+ maxColumn = Math.min(this.terminal.columns, box.rect.x + box.rect.width, box.clip.x + box.clip.width);
657
+ screenSelection = {
658
+ start: {
659
+ row: box.rect.y + selection.start.row - selection.start.scrollView.scrollTop,
660
+ col: box.rect.x + selection.start.col,
661
+ scrollView: selection.start.scrollView,
662
+ },
663
+ end: {
664
+ row: box.rect.y + selection.end.row - selection.start.scrollView.scrollTop,
665
+ col: box.rect.x + selection.end.col,
666
+ scrollView: selection.start.scrollView,
667
+ },
668
+ };
669
+ }
670
+ return screen.map((line, row) => {
671
+ if (row < minRow ||
672
+ row > maxRow ||
673
+ row < screenSelection.start.row ||
674
+ row > screenSelection.end.row ||
675
+ isImageLine(line)) {
676
+ return line;
677
+ }
678
+ const lineWidth = visibleWidth(line);
679
+ const columns = this.getSelectionColumns(line, row, screenSelection, minColumn, maxColumn);
680
+ if (columns.end <= columns.start)
681
+ return line;
682
+ const before = sliceByColumn(line, 0, columns.start, true);
683
+ const selected = sliceByColumn(line, columns.start, columns.end - columns.start, true);
684
+ const after = sliceByColumn(line, columns.end, Math.max(0, lineWidth - columns.end), true);
685
+ return `${before}${this.applySelectionHighlight(selected)}${after}`;
686
+ });
687
+ }
688
+ isMouseSequence(data) {
689
+ return /^\x1b\[<\d+;\d+;\d+[Mm]$/.test(data) || (data.length === 6 && data.startsWith("\x1b[M"));
690
+ }
691
+ compositeFlashes(screen, width, height) {
692
+ const flashLines = this.flashes.render(width).slice(-height);
693
+ if (flashLines.length === 0)
694
+ return screen;
695
+ const result = [...screen];
696
+ while (result.length < height)
697
+ result.push("");
698
+ for (let row = 0; row < flashLines.length; row++) {
699
+ const line = flashLines[row];
700
+ const flashWidth = visibleWidth(line);
701
+ if (flashWidth === 0)
702
+ continue;
703
+ result[row] = compositeTuiLine(result[row] ?? "", line, width - flashWidth, flashWidth, width);
704
+ }
705
+ return result;
706
+ }
707
+ doRender() {
708
+ if (this.stopped || !this.altScreenActive)
709
+ return;
710
+ const width = Math.max(1, this.terminal.columns);
711
+ const height = Math.max(1, this.terminal.rows);
712
+ const root = this.layoutRoot ?? this.implicitScrollView;
713
+ const nextLayout = renderLayoutFrame(root, width, height, () => this.requestRender());
714
+ let screen = nextLayout.lines.map((line) => line.replace(OSC133_ZONE_PREFIX, ""));
715
+ screen = this.compositeOverlays(screen, width, height);
716
+ if (screen.length > height)
717
+ screen = screen.slice(screen.length - height);
718
+ screen = this.applySelection(screen, nextLayout);
719
+ screen = this.compositeFlashes(screen, width, height);
720
+ const cursorPos = this.extractCursorPosition(screen, height);
721
+ screen = this.applyLineResets(screen).map((line) => {
722
+ if (isImageLine(line) || visibleWidth(line) <= width)
723
+ return line;
724
+ return sliceByColumn(line, 0, width, true);
725
+ });
726
+ const fullRedraw = this.previousScreen.length === 0 || this.previousScreenWidth !== width || this.previousScreenHeight !== height;
727
+ const imagesNeedRedraw = screen.some((line, row) => line !== this.previousScreen[row] && (isImageLine(line) || isImageLine(this.previousScreen[row] ?? "")));
728
+ const redrawImages = fullRedraw || imagesNeedRedraw;
729
+ const hadUploadedKittyImages = this.uploadedKittyImages.size > 0;
730
+ const preparedKittyScreen = redrawImages && this.imageProtocol === "kitty"
731
+ ? this.prepareKittyScreen(screen)
732
+ : { lines: screen, evictedImageDeletion: "" };
733
+ let buffer = BEGIN_SYNCHRONIZED_OUTPUT;
734
+ if (fullRedraw) {
735
+ this.fullRedrawCount += 1;
736
+ const clearImages = this.imageProtocol === "kitty" && hadUploadedKittyImages
737
+ ? deleteAllKittyPlacements()
738
+ : this.deleteKittyImages();
739
+ buffer += `${clearImages}\x1b[2J`;
740
+ }
741
+ else if (imagesNeedRedraw) {
742
+ if (this.imageProtocol === "iterm2")
743
+ buffer += "\x1b[2J";
744
+ else if (this.imageProtocol === "kitty")
745
+ buffer += deleteAllKittyPlacements();
746
+ }
747
+ buffer += preparedKittyScreen.evictedImageDeletion;
748
+ for (let row = 0; row < height; row++) {
749
+ if (!fullRedraw && !imagesNeedRedraw && screen[row] === this.previousScreen[row])
750
+ continue;
751
+ buffer += `\x1b[${row + 1};1H\x1b[2K${preparedKittyScreen.lines[row] ?? ""}`;
752
+ }
753
+ if (cursorPos) {
754
+ buffer += `\x1b[${cursorPos.row + 1};${Math.min(width, cursorPos.col) + 1}H`;
755
+ buffer += this.getShowHardwareCursor() ? "\x1b[?25h" : "\x1b[?25l";
756
+ }
757
+ else {
758
+ buffer += "\x1b[?25l";
759
+ }
760
+ buffer += END_SYNCHRONIZED_OUTPUT;
761
+ this.terminal.write(buffer);
762
+ this.previousScreen = screen;
763
+ this.previousScreenWidth = width;
764
+ this.previousScreenHeight = height;
765
+ this.currentLayout = nextLayout;
766
+ }
767
+ }
768
+ //# sourceMappingURL=tui-alt-screen.js.map