@code-yeongyu/senpi-tui 2026.7.31 → 2026.8.3

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