@earendil-works/pi-tui 0.84.4 → 0.85.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -4
- package/dist/alt-screen-search.d.ts +21 -1
- package/dist/alt-screen-search.d.ts.map +1 -1
- package/dist/alt-screen-search.js +202 -42
- package/dist/alt-screen-search.js.map +1 -1
- package/dist/components/box.d.ts +3 -1
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js +29 -0
- package/dist/components/box.js.map +1 -1
- package/dist/components/editor.d.ts +6 -1
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +98 -18
- package/dist/components/editor.js.map +1 -1
- package/dist/components/input.d.ts +12 -1
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +44 -5
- package/dist/components/input.js.map +1 -1
- package/dist/components/loader.d.ts +2 -0
- package/dist/components/loader.d.ts.map +1 -1
- package/dist/components/loader.js +10 -3
- package/dist/components/loader.js.map +1 -1
- package/dist/components/mouse-region.d.ts +12 -0
- package/dist/components/mouse-region.d.ts.map +1 -0
- package/dist/components/mouse-region.js +21 -0
- package/dist/components/mouse-region.js.map +1 -0
- package/dist/components/scroll-view.d.ts +6 -3
- package/dist/components/scroll-view.d.ts.map +1 -1
- package/dist/components/scroll-view.js +8 -2
- package/dist/components/scroll-view.js.map +1 -1
- package/dist/components/select-list.d.ts +4 -1
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +49 -2
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/settings-list.d.ts +5 -1
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +55 -5
- package/dist/components/settings-list.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/latex.d.ts.map +1 -1
- package/dist/latex.js +14 -0
- package/dist/latex.js.map +1 -1
- package/dist/layout.d.ts +3 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +32 -9
- package/dist/layout.js.map +1 -1
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +1 -4
- package/dist/terminal-image.js.map +1 -1
- package/dist/terminal.d.ts +6 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +17 -4
- package/dist/terminal.js.map +1 -1
- package/dist/tui-alt-screen.d.ts +31 -1
- package/dist/tui-alt-screen.d.ts.map +1 -1
- package/dist/tui-alt-screen.js +342 -41
- package/dist/tui-alt-screen.js.map +1 -1
- package/dist/tui-main-screen.d.ts.map +1 -1
- package/dist/tui-main-screen.js +6 -5
- package/dist/tui-main-screen.js.map +1 -1
- package/dist/tui.d.ts +84 -6
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +123 -9
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +9 -0
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/tui-alt-screen.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { AltScreenSearchComponent,
|
|
1
|
+
import { AltScreenSearchComponent, AltScreenSearchIndex, getAltScreenSearchMatchKey, } from "./alt-screen-search.js";
|
|
2
2
|
import { AltScreenFlashContainer } from "./components/alt-screen-flash.js";
|
|
3
3
|
import { ScrollView } from "./components/scroll-view.js";
|
|
4
4
|
import { getKeybindings } from "./keybindings.js";
|
|
5
5
|
import { isKeyRelease } from "./keys.js";
|
|
6
|
-
import { getScrollbarGeometry, getScrollViewBox, getScrollViewsAt, renderLayoutFrame, } from "./layout.js";
|
|
6
|
+
import { getLayoutBoxesAt, getScrollbarGeometry, getScrollViewBox, getScrollViewsAt, renderLayoutFrame, } from "./layout.js";
|
|
7
|
+
import { getLayoutNode } from "./layout-node.js";
|
|
7
8
|
import { deleteAllKittyImages, deleteAllKittyPlacements, deleteKittyImage, getCapabilities, getKittyImagePlacement, isImageLine, setCapabilities, } from "./terminal-image.js";
|
|
8
|
-
import { CURSOR_MARKER, compositeTuiLine, TuiBase, VIEWPORT_TUI, } from "./tui.js";
|
|
9
|
-
import { extractAnsiCode, getGraphemeCellRange, getOsc8LinkAtColumn, getWordSegmenter, sliceByColumn, stripTerminalSequences, visibleWidth, } from "./utils.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";
|
|
10
11
|
const ENTER_ALT_SCREEN = "\x1b[?1049h";
|
|
11
12
|
const EXIT_ALT_SCREEN = "\x1b[?1049l";
|
|
12
13
|
const DISABLE_AUTOWRAP = "\x1b[?7l";
|
|
@@ -21,6 +22,7 @@ const END_SYNCHRONIZED_OUTPUT = "\x1b[?2026l";
|
|
|
21
22
|
const OSC133_ZONE_PREFIX = /^(?:\x1b\]133;[ABC](?:\x07|\x1b\\))+/;
|
|
22
23
|
const OSC133_PROMPT_START = /^\x1b\]133;A(?:\x07|\x1b\\)/;
|
|
23
24
|
const PAGE_SCROLL_OVERLAP = 4;
|
|
25
|
+
const ALT_WHEEL_SCROLL_MULTIPLIER = 5;
|
|
24
26
|
const MAX_CACHED_OFFSCREEN_KITTY_IMAGES = 16;
|
|
25
27
|
const MAX_CACHED_OFFSCREEN_KITTY_TRANSMISSION_BYTES = 32 * 1024 * 1024;
|
|
26
28
|
const MAX_CACHED_OFFSCREEN_KITTY_DECODED_BYTES = 64 * 1024 * 1024;
|
|
@@ -57,13 +59,21 @@ export class TuiAltScreen extends TuiBase {
|
|
|
57
59
|
selectionPressActive = false;
|
|
58
60
|
scrollbarDrag;
|
|
59
61
|
scrollbarHover;
|
|
62
|
+
scrollToEndIndicatorRect;
|
|
60
63
|
activeSearch;
|
|
61
64
|
pressedUrl;
|
|
62
65
|
selectionDragged = false;
|
|
66
|
+
mouseCapture;
|
|
67
|
+
mousePressTarget;
|
|
68
|
+
mousePressPoint;
|
|
69
|
+
mousePressMoved = false;
|
|
70
|
+
lastComponentClick;
|
|
63
71
|
wheelScrollLines;
|
|
64
72
|
mouseEnabled;
|
|
65
73
|
searchMatchStyle;
|
|
66
74
|
searchCurrentMatchStyle;
|
|
75
|
+
searchNavigationButtonStyle;
|
|
76
|
+
scrollToEndIndicator;
|
|
67
77
|
openUrl;
|
|
68
78
|
onRightClickPaste;
|
|
69
79
|
copyOnSelect;
|
|
@@ -72,6 +82,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
72
82
|
super(terminal, showHardwareCursor, logDirectory);
|
|
73
83
|
this.implicitDocument = {
|
|
74
84
|
render: (width) => super.render(width),
|
|
85
|
+
handleMouse: (event) => super.handleMouse(event),
|
|
75
86
|
invalidate: () => {
|
|
76
87
|
for (const child of this.children)
|
|
77
88
|
child.invalidate();
|
|
@@ -83,6 +94,8 @@ export class TuiAltScreen extends TuiBase {
|
|
|
83
94
|
this.mouseEnabled = options.mouse ?? true;
|
|
84
95
|
this.searchMatchStyle = options.searchMatchStyle ?? ((text) => `\x1b[4m${text}\x1b[24m`);
|
|
85
96
|
this.searchCurrentMatchStyle = options.searchCurrentMatchStyle ?? ((text) => `\x1b[1;7m${text}\x1b[22;27m`);
|
|
97
|
+
this.searchNavigationButtonStyle = options.searchNavigationButtonStyle ?? ((text) => text);
|
|
98
|
+
this.scrollToEndIndicator = options.scrollToEndIndicator;
|
|
86
99
|
this.openUrl = options.openUrl;
|
|
87
100
|
this.onRightClickPaste = options.onRightClickPaste;
|
|
88
101
|
this.copyOnSelect = options.copyOnSelect ?? true;
|
|
@@ -151,6 +164,8 @@ export class TuiAltScreen extends TuiBase {
|
|
|
151
164
|
this.lastClick = undefined;
|
|
152
165
|
this.pressedUrl = undefined;
|
|
153
166
|
this.selectionDragged = false;
|
|
167
|
+
this.clearComponentMouseGesture();
|
|
168
|
+
this.lastComponentClick = undefined;
|
|
154
169
|
this.resetRenderState();
|
|
155
170
|
const term = process.env.TERM?.toLowerCase() ?? "";
|
|
156
171
|
// Multiplexers can lag when every pointer movement is forwarded. Button-motion
|
|
@@ -170,6 +185,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
170
185
|
this.selectionPressActive = false;
|
|
171
186
|
this.stopScrollbarHover();
|
|
172
187
|
this.stopScrollbarDrag();
|
|
188
|
+
this.clearComponentMouseGesture();
|
|
173
189
|
this.flashes.dispose();
|
|
174
190
|
if (!this.altScreenActive)
|
|
175
191
|
return;
|
|
@@ -284,14 +300,15 @@ export class TuiAltScreen extends TuiBase {
|
|
|
284
300
|
return;
|
|
285
301
|
}
|
|
286
302
|
}
|
|
287
|
-
|
|
303
|
+
toggleSearch() {
|
|
288
304
|
if (this.activeSearch) {
|
|
289
|
-
this.
|
|
305
|
+
this.closeSearch();
|
|
290
306
|
return;
|
|
291
307
|
}
|
|
292
|
-
const component = new AltScreenSearchComponent((query) => this.updateSearchQuery(query));
|
|
308
|
+
const component = new AltScreenSearchComponent((query) => this.updateSearchQuery(query), this.searchNavigationButtonStyle);
|
|
293
309
|
const search = {
|
|
294
310
|
component,
|
|
311
|
+
index: new AltScreenSearchIndex(),
|
|
295
312
|
query: "",
|
|
296
313
|
matches: [],
|
|
297
314
|
selectedIndex: -1,
|
|
@@ -302,7 +319,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
302
319
|
search.overlay = this.showOverlay(component, {
|
|
303
320
|
anchor: "top-right",
|
|
304
321
|
width: "40%",
|
|
305
|
-
minWidth:
|
|
322
|
+
minWidth: 32,
|
|
306
323
|
margin: 1,
|
|
307
324
|
});
|
|
308
325
|
}
|
|
@@ -332,6 +349,29 @@ export class TuiAltScreen extends TuiBase {
|
|
|
332
349
|
search.selectionMode = direction < 0 ? "previous" : "next";
|
|
333
350
|
this.requestRender();
|
|
334
351
|
}
|
|
352
|
+
getSearchNavigationDirectionAt(x, y) {
|
|
353
|
+
const search = this.activeSearch;
|
|
354
|
+
const bounds = search?.overlay?.getBounds();
|
|
355
|
+
if (!search || !bounds)
|
|
356
|
+
return undefined;
|
|
357
|
+
if (x < bounds.col || x >= bounds.col + bounds.width || y < bounds.row || y >= bounds.row + bounds.height) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
return search.component.getNavigationDirectionAt(y - bounds.row, x - bounds.col);
|
|
361
|
+
}
|
|
362
|
+
handleSearchMouseEvent(event) {
|
|
363
|
+
const search = this.activeSearch;
|
|
364
|
+
if (!search)
|
|
365
|
+
return false;
|
|
366
|
+
const direction = this.getSearchNavigationDirectionAt(event.x, event.y);
|
|
367
|
+
if (search.component.setHoveredNavigationDirection(direction))
|
|
368
|
+
this.requestRender();
|
|
369
|
+
if (direction === undefined || event.release || (event.button & 32) !== 0 || (event.button & 3) !== 0) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
this.navigateSearch(direction);
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
335
375
|
refreshSearch(layout) {
|
|
336
376
|
const search = this.activeSearch;
|
|
337
377
|
if (!search)
|
|
@@ -348,16 +388,29 @@ export class TuiAltScreen extends TuiBase {
|
|
|
348
388
|
return false;
|
|
349
389
|
}
|
|
350
390
|
const shouldRevealSelection = search.selectionMode !== "retain";
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
391
|
+
const result = search.index.search(lines, search.query);
|
|
392
|
+
const matches = result.matches;
|
|
393
|
+
search.matches = matches;
|
|
394
|
+
if (!result.changed && search.selectionMode === "retain")
|
|
395
|
+
return false;
|
|
396
|
+
const exactIndex = result.changed
|
|
397
|
+
? search.selectedKey
|
|
398
|
+
? matches.findIndex((match) => getAltScreenSearchMatchKey(match) === search.selectedKey)
|
|
399
|
+
: -1
|
|
400
|
+
: search.selectedIndex;
|
|
355
401
|
let selectedIndex = -1;
|
|
356
402
|
if (matches.length > 0) {
|
|
357
403
|
if (search.selectionMode === "query") {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
404
|
+
let low = 0;
|
|
405
|
+
let high = matches.length;
|
|
406
|
+
while (low < high) {
|
|
407
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
408
|
+
if ((matches[middle].segments[0]?.row ?? 0) < search.anchorRow)
|
|
409
|
+
low = middle + 1;
|
|
410
|
+
else
|
|
411
|
+
high = middle;
|
|
412
|
+
}
|
|
413
|
+
selectedIndex = low < matches.length ? low : 0;
|
|
361
414
|
}
|
|
362
415
|
else if (search.selectionMode === "next") {
|
|
363
416
|
const baseIndex = exactIndex >= 0 ? exactIndex : Math.min(search.selectedIndex, matches.length - 1);
|
|
@@ -372,7 +425,6 @@ export class TuiAltScreen extends TuiBase {
|
|
|
372
425
|
exactIndex >= 0 ? exactIndex : Math.min(Math.max(0, search.selectedIndex), matches.length - 1);
|
|
373
426
|
}
|
|
374
427
|
}
|
|
375
|
-
search.matches = matches;
|
|
376
428
|
search.selectedIndex = selectedIndex;
|
|
377
429
|
search.selectedKey = selectedIndex >= 0 ? getAltScreenSearchMatchKey(matches[selectedIndex]) : undefined;
|
|
378
430
|
search.selectionMode = "retain";
|
|
@@ -400,6 +452,12 @@ export class TuiAltScreen extends TuiBase {
|
|
|
400
452
|
shouldDeferViewportInputToOverlay() {
|
|
401
453
|
return this.isOverlayFocused() && this.activeSearch?.overlay?.isFocused() !== true;
|
|
402
454
|
}
|
|
455
|
+
clearComponentMouseGesture() {
|
|
456
|
+
this.mouseCapture = undefined;
|
|
457
|
+
this.mousePressTarget = undefined;
|
|
458
|
+
this.mousePressPoint = undefined;
|
|
459
|
+
this.mousePressMoved = false;
|
|
460
|
+
}
|
|
403
461
|
handleViewportInput(data) {
|
|
404
462
|
if (data === FOCUS_OUT) {
|
|
405
463
|
const hadActiveSelection = this.selectionPressActive;
|
|
@@ -407,9 +465,13 @@ export class TuiAltScreen extends TuiBase {
|
|
|
407
465
|
this.selectionPressActive = false;
|
|
408
466
|
this.stopSelectionAutoScroll();
|
|
409
467
|
this.stopScrollbarHover();
|
|
468
|
+
if (this.activeSearch?.component.setHoveredNavigationDirection(undefined))
|
|
469
|
+
this.requestRender();
|
|
410
470
|
this.stopScrollbarDrag();
|
|
411
471
|
this.pressedUrl = undefined;
|
|
412
472
|
this.selectionDragged = false;
|
|
473
|
+
this.clearComponentMouseGesture();
|
|
474
|
+
this.lastComponentClick = undefined;
|
|
413
475
|
if (hadActiveSelection) {
|
|
414
476
|
this.selectionAnchor = undefined;
|
|
415
477
|
this.selectionFocus = undefined;
|
|
@@ -425,6 +487,16 @@ export class TuiAltScreen extends TuiBase {
|
|
|
425
487
|
return { consume: true };
|
|
426
488
|
const wheelEvent = this.parseWheelEvent(data);
|
|
427
489
|
if (wheelEvent) {
|
|
490
|
+
const event = this.createMouseEvent("wheel", wheelEvent.button, wheelEvent.x, wheelEvent.y, {
|
|
491
|
+
wheelDelta: wheelEvent.direction * this.getWheelScrollLines(wheelEvent.button),
|
|
492
|
+
});
|
|
493
|
+
const overlay = this.dispatchMouseToOverlay(event);
|
|
494
|
+
const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(event));
|
|
495
|
+
if (result) {
|
|
496
|
+
if (this.applyMouseDispatchResult(event, result))
|
|
497
|
+
this.requestRender();
|
|
498
|
+
return { consume: true };
|
|
499
|
+
}
|
|
428
500
|
if (this.shouldDeferViewportInputToOverlay())
|
|
429
501
|
return undefined;
|
|
430
502
|
this.routeWheel(wheelEvent);
|
|
@@ -432,13 +504,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
432
504
|
}
|
|
433
505
|
const mouseEvent = this.parseSgrMouseEvent(data);
|
|
434
506
|
if (mouseEvent) {
|
|
435
|
-
|
|
436
|
-
return { consume: true };
|
|
437
|
-
const handled = this.handleScrollbarMouseEvent(mouseEvent);
|
|
438
|
-
if (!this.scrollbarDrag)
|
|
439
|
-
this.updateScrollbarHover(mouseEvent.x, mouseEvent.y);
|
|
440
|
-
if (!handled)
|
|
441
|
-
this.handleSelectionMouseEvent(mouseEvent);
|
|
507
|
+
this.handleMouseEvent(mouseEvent);
|
|
442
508
|
return { consume: true };
|
|
443
509
|
}
|
|
444
510
|
if (this.isMouseSequence(data))
|
|
@@ -447,7 +513,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
447
513
|
const isRelease = isKeyRelease(data);
|
|
448
514
|
if (keybindings.matches(data, "tui.altScreen.search")) {
|
|
449
515
|
if (!isRelease)
|
|
450
|
-
this.
|
|
516
|
+
this.toggleSearch();
|
|
451
517
|
return { consume: true };
|
|
452
518
|
}
|
|
453
519
|
if (this.activeSearch?.overlay?.isFocused()) {
|
|
@@ -523,6 +589,165 @@ export class TuiAltScreen extends TuiBase {
|
|
|
523
589
|
}
|
|
524
590
|
return undefined;
|
|
525
591
|
}
|
|
592
|
+
decodeMouseButton(button) {
|
|
593
|
+
switch (button & 3) {
|
|
594
|
+
case 0:
|
|
595
|
+
return "left";
|
|
596
|
+
case 1:
|
|
597
|
+
return "middle";
|
|
598
|
+
case 2:
|
|
599
|
+
return "right";
|
|
600
|
+
default:
|
|
601
|
+
return "none";
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
createMouseEvent(type, button, x, y, extra = {}) {
|
|
605
|
+
return {
|
|
606
|
+
type,
|
|
607
|
+
button: type === "wheel" ? "none" : this.decodeMouseButton(button),
|
|
608
|
+
x,
|
|
609
|
+
y,
|
|
610
|
+
screenX: x,
|
|
611
|
+
screenY: y,
|
|
612
|
+
width: Math.max(1, this.terminal.columns),
|
|
613
|
+
height: Math.max(1, this.terminal.rows),
|
|
614
|
+
shift: (button & 4) !== 0,
|
|
615
|
+
alt: (button & 8) !== 0,
|
|
616
|
+
ctrl: (button & 16) !== 0,
|
|
617
|
+
...(extra.wheelDelta === undefined ? {} : { wheelDelta: extra.wheelDelta }),
|
|
618
|
+
...(extra.clickCount === undefined ? {} : { clickCount: extra.clickCount }),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
dispatchMouseToLayout(event) {
|
|
622
|
+
if (!this.currentLayout)
|
|
623
|
+
return undefined;
|
|
624
|
+
const visited = new Set();
|
|
625
|
+
const boxes = getLayoutBoxesAt(this.currentLayout, event.screenX, event.screenY);
|
|
626
|
+
for (const box of boxes) {
|
|
627
|
+
if (visited.has(box.component))
|
|
628
|
+
continue;
|
|
629
|
+
if (getLayoutNode(box.component) && box.component.handleMouse === Container.prototype.handleMouse)
|
|
630
|
+
continue;
|
|
631
|
+
visited.add(box.component);
|
|
632
|
+
const result = dispatchMouseEvent(box.component, {
|
|
633
|
+
...event,
|
|
634
|
+
x: event.screenX - box.rect.x,
|
|
635
|
+
y: event.screenY - box.rect.y,
|
|
636
|
+
width: box.rect.width,
|
|
637
|
+
height: box.rect.height,
|
|
638
|
+
});
|
|
639
|
+
if (result)
|
|
640
|
+
return result;
|
|
641
|
+
}
|
|
642
|
+
return undefined;
|
|
643
|
+
}
|
|
644
|
+
applyMouseDispatchResult(event, result) {
|
|
645
|
+
const focusTarget = this.resolveMouseFocusTarget(result.focusTarget ?? result.target.component);
|
|
646
|
+
const focusChanged = result.focus === true && this.getFocusedComponent() !== focusTarget;
|
|
647
|
+
if (result.focus)
|
|
648
|
+
this.setFocus(focusTarget);
|
|
649
|
+
if (result.capture)
|
|
650
|
+
this.mouseCapture = result.target;
|
|
651
|
+
return (result.render ??
|
|
652
|
+
(focusChanged ||
|
|
653
|
+
event.type === "press" ||
|
|
654
|
+
event.type === "click" ||
|
|
655
|
+
event.type === "drag" ||
|
|
656
|
+
event.type === "wheel"));
|
|
657
|
+
}
|
|
658
|
+
dispatchMouseToTarget(event, target) {
|
|
659
|
+
return dispatchMouseEvent(target.component, retargetMouseEvent(event, target));
|
|
660
|
+
}
|
|
661
|
+
getComponentClickCount(target, x, y) {
|
|
662
|
+
const now = Date.now();
|
|
663
|
+
const previous = this.lastComponentClick;
|
|
664
|
+
const count = previous &&
|
|
665
|
+
now - previous.timestamp <= DOUBLE_CLICK_INTERVAL_MS &&
|
|
666
|
+
previous.component === target.component &&
|
|
667
|
+
previous.x === x &&
|
|
668
|
+
previous.y === y
|
|
669
|
+
? (previous.count % 3) + 1
|
|
670
|
+
: 1;
|
|
671
|
+
this.lastComponentClick = { timestamp: now, count, component: target.component, x, y };
|
|
672
|
+
return count;
|
|
673
|
+
}
|
|
674
|
+
clearTextSelection() {
|
|
675
|
+
this.stopSelectionAutoScroll();
|
|
676
|
+
this.selectionPressActive = false;
|
|
677
|
+
this.selectionAnchor = undefined;
|
|
678
|
+
this.selectionFocus = undefined;
|
|
679
|
+
this.selectionGranularity = "character";
|
|
680
|
+
this.selectionInitialRange = undefined;
|
|
681
|
+
this.pressedUrl = undefined;
|
|
682
|
+
this.selectionDragged = false;
|
|
683
|
+
}
|
|
684
|
+
handleMouseEvent(raw) {
|
|
685
|
+
const isMotion = (raw.button & 32) !== 0;
|
|
686
|
+
const type = raw.release
|
|
687
|
+
? "release"
|
|
688
|
+
: isMotion
|
|
689
|
+
? this.decodeMouseButton(raw.button) === "none"
|
|
690
|
+
? "move"
|
|
691
|
+
: "drag"
|
|
692
|
+
: "press";
|
|
693
|
+
const event = this.createMouseEvent(type, raw.button, raw.x, raw.y);
|
|
694
|
+
if (this.mouseCapture || this.mousePressTarget) {
|
|
695
|
+
const target = this.mouseCapture ?? this.mousePressTarget;
|
|
696
|
+
if (this.mousePressPoint && (raw.x !== this.mousePressPoint.x || raw.y !== this.mousePressPoint.y)) {
|
|
697
|
+
this.mousePressMoved = true;
|
|
698
|
+
this.lastComponentClick = undefined;
|
|
699
|
+
}
|
|
700
|
+
let render = false;
|
|
701
|
+
const targetResult = this.dispatchMouseToTarget(event, target);
|
|
702
|
+
if (targetResult)
|
|
703
|
+
render = this.applyMouseDispatchResult(event, targetResult);
|
|
704
|
+
if (raw.release) {
|
|
705
|
+
if (!this.mousePressMoved && this.mousePressPoint?.x === raw.x && this.mousePressPoint.y === raw.y) {
|
|
706
|
+
const clickEvent = this.createMouseEvent("click", raw.button, raw.x, raw.y, {
|
|
707
|
+
clickCount: this.getComponentClickCount(target, raw.x, raw.y),
|
|
708
|
+
});
|
|
709
|
+
const clickResult = this.dispatchMouseToTarget(clickEvent, target);
|
|
710
|
+
if (clickResult)
|
|
711
|
+
render = this.applyMouseDispatchResult(clickEvent, clickResult) || render;
|
|
712
|
+
}
|
|
713
|
+
this.clearComponentMouseGesture();
|
|
714
|
+
}
|
|
715
|
+
if (render)
|
|
716
|
+
this.requestRender();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (this.handleSearchMouseEvent(raw))
|
|
720
|
+
return;
|
|
721
|
+
const overlay = this.dispatchMouseToOverlay(event);
|
|
722
|
+
if (!overlay.hit) {
|
|
723
|
+
if (this.handleScrollToEndIndicatorMouseEvent(raw))
|
|
724
|
+
return;
|
|
725
|
+
const scrollbarHandled = this.handleScrollbarMouseEvent(raw);
|
|
726
|
+
if (!this.scrollbarDrag)
|
|
727
|
+
this.updateScrollbarHover(raw.x, raw.y);
|
|
728
|
+
if (scrollbarHandled)
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
this.stopScrollbarHover();
|
|
733
|
+
}
|
|
734
|
+
const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(event));
|
|
735
|
+
if (result) {
|
|
736
|
+
const render = this.applyMouseDispatchResult(event, result);
|
|
737
|
+
if (type === "press") {
|
|
738
|
+
this.clearTextSelection();
|
|
739
|
+
this.mousePressTarget = result.target;
|
|
740
|
+
this.mousePressPoint = { x: raw.x, y: raw.y };
|
|
741
|
+
this.mousePressMoved = false;
|
|
742
|
+
}
|
|
743
|
+
if (render)
|
|
744
|
+
this.requestRender();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (this.handleRightClickPaste(raw))
|
|
748
|
+
return;
|
|
749
|
+
this.handleSelectionMouseEvent(raw);
|
|
750
|
+
}
|
|
526
751
|
parseWheelEvent(data) {
|
|
527
752
|
const sgr = /^\x1b\[<(\d+);(\d+);(\d+)[Mm]$/.exec(data);
|
|
528
753
|
if (sgr) {
|
|
@@ -536,6 +761,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
536
761
|
direction: direction === 0 ? -1 : 1,
|
|
537
762
|
x: Number.parseInt(sgr[2], 10) - 1,
|
|
538
763
|
y: Number.parseInt(sgr[3], 10) - 1,
|
|
764
|
+
button,
|
|
539
765
|
};
|
|
540
766
|
}
|
|
541
767
|
if (data.length === 6 && data.startsWith("\x1b[M")) {
|
|
@@ -549,12 +775,17 @@ export class TuiAltScreen extends TuiBase {
|
|
|
549
775
|
direction: direction === 0 ? -1 : 1,
|
|
550
776
|
x: data.charCodeAt(4) - 33,
|
|
551
777
|
y: data.charCodeAt(5) - 33,
|
|
778
|
+
button,
|
|
552
779
|
};
|
|
553
780
|
}
|
|
554
781
|
return undefined;
|
|
555
782
|
}
|
|
783
|
+
getWheelScrollLines(button) {
|
|
784
|
+
// SGR mouse button codes use bit 3 (value 8) for the Alt modifier.
|
|
785
|
+
return (button & 8) !== 0 ? this.wheelScrollLines * ALT_WHEEL_SCROLL_MULTIPLIER : this.wheelScrollLines;
|
|
786
|
+
}
|
|
556
787
|
routeWheel(event) {
|
|
557
|
-
let remaining = event.direction * this.
|
|
788
|
+
let remaining = event.direction * this.getWheelScrollLines(event.button);
|
|
558
789
|
const seen = new Set();
|
|
559
790
|
for (const scrollView of this.currentLayout ? getScrollViewsAt(this.currentLayout, event.x, event.y) : []) {
|
|
560
791
|
seen.add(scrollView);
|
|
@@ -595,16 +826,25 @@ export class TuiAltScreen extends TuiBase {
|
|
|
595
826
|
}
|
|
596
827
|
return true;
|
|
597
828
|
}
|
|
598
|
-
|
|
829
|
+
handleScrollToEndIndicatorMouseEvent(event) {
|
|
830
|
+
const rect = this.scrollToEndIndicatorRect;
|
|
831
|
+
if (!rect || event.release || (event.button & 32) !== 0 || (event.button & 3) !== 0)
|
|
832
|
+
return false;
|
|
833
|
+
if (event.y !== rect.row || event.x < rect.column || event.x >= rect.column + rect.width)
|
|
834
|
+
return false;
|
|
835
|
+
this.scrollToBottom();
|
|
836
|
+
return true;
|
|
837
|
+
}
|
|
838
|
+
getScrollbarTargetAt(x, y, includeHiddenAuto = false) {
|
|
599
839
|
if (this.hasOverlay() || !this.currentLayout)
|
|
600
840
|
return undefined;
|
|
601
841
|
for (const scrollView of getScrollViewsAt(this.currentLayout, x, y)) {
|
|
602
842
|
const box = getScrollViewBox(this.currentLayout, scrollView);
|
|
603
|
-
const geometry = box ? getScrollbarGeometry(box) : undefined;
|
|
843
|
+
const geometry = box ? getScrollbarGeometry(box, includeHiddenAuto) : undefined;
|
|
604
844
|
if (geometry &&
|
|
605
845
|
x === geometry.column &&
|
|
606
|
-
y >= geometry.
|
|
607
|
-
y < geometry.
|
|
846
|
+
y >= geometry.trackTop &&
|
|
847
|
+
y < geometry.trackTop + geometry.trackHeight) {
|
|
608
848
|
return { scrollView, geometry };
|
|
609
849
|
}
|
|
610
850
|
}
|
|
@@ -618,11 +858,17 @@ export class TuiAltScreen extends TuiBase {
|
|
|
618
858
|
this.scrollbarHover?.setScrollbarActive(true);
|
|
619
859
|
}
|
|
620
860
|
updateScrollbarHover(x, y) {
|
|
621
|
-
this.setScrollbarHover(this.getScrollbarTargetAt(x, y)?.scrollView);
|
|
861
|
+
this.setScrollbarHover(this.getScrollbarTargetAt(x, y, true)?.scrollView);
|
|
622
862
|
}
|
|
623
863
|
stopScrollbarHover() {
|
|
624
864
|
this.setScrollbarHover(undefined);
|
|
625
865
|
}
|
|
866
|
+
scrollScrollbarToPointer(scrollView, geometry, pointerY, grabOffset) {
|
|
867
|
+
const maxThumbOffset = geometry.trackHeight - geometry.thumbHeight;
|
|
868
|
+
const thumbOffset = Math.max(0, Math.min(maxThumbOffset, pointerY - geometry.trackTop - grabOffset));
|
|
869
|
+
const scrollTop = maxThumbOffset === 0 ? 0 : Math.round((thumbOffset / maxThumbOffset) * geometry.maxScrollTop);
|
|
870
|
+
scrollView.scrollTo(scrollTop);
|
|
871
|
+
}
|
|
626
872
|
handleScrollbarMouseEvent(event) {
|
|
627
873
|
if (this.scrollbarDrag) {
|
|
628
874
|
if (event.release) {
|
|
@@ -634,10 +880,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
634
880
|
: undefined;
|
|
635
881
|
const geometry = box ? getScrollbarGeometry(box) : undefined;
|
|
636
882
|
if (geometry) {
|
|
637
|
-
|
|
638
|
-
const thumbOffset = Math.max(0, Math.min(maxThumbOffset, event.y - geometry.trackTop - this.scrollbarDrag.grabOffset));
|
|
639
|
-
const scrollTop = maxThumbOffset === 0 ? 0 : Math.round((thumbOffset / maxThumbOffset) * geometry.maxScrollTop);
|
|
640
|
-
this.scrollbarDrag.scrollView.scrollTo(scrollTop);
|
|
883
|
+
this.scrollScrollbarToPointer(this.scrollbarDrag.scrollView, geometry, event.y, this.scrollbarDrag.grabOffset);
|
|
641
884
|
}
|
|
642
885
|
return true;
|
|
643
886
|
}
|
|
@@ -656,9 +899,13 @@ export class TuiAltScreen extends TuiBase {
|
|
|
656
899
|
this.pressedUrl = undefined;
|
|
657
900
|
this.selectionDragged = false;
|
|
658
901
|
this.setScrollbarHover(target.scrollView);
|
|
902
|
+
const onThumb = event.y >= target.geometry.thumbTop && event.y < target.geometry.thumbTop + target.geometry.thumbHeight;
|
|
903
|
+
const grabOffset = onThumb ? event.y - target.geometry.thumbTop : Math.floor(target.geometry.thumbHeight / 2);
|
|
904
|
+
if (!onThumb)
|
|
905
|
+
this.scrollScrollbarToPointer(target.scrollView, target.geometry, event.y, grabOffset);
|
|
659
906
|
this.scrollbarDrag = {
|
|
660
907
|
scrollView: target.scrollView,
|
|
661
|
-
grabOffset
|
|
908
|
+
grabOffset,
|
|
662
909
|
};
|
|
663
910
|
return true;
|
|
664
911
|
}
|
|
@@ -843,12 +1090,11 @@ export class TuiAltScreen extends TuiBase {
|
|
|
843
1090
|
if (!this.selectionAnchor)
|
|
844
1091
|
return;
|
|
845
1092
|
this.updateSelectionFocus(point);
|
|
846
|
-
const
|
|
1093
|
+
const isClick = !this.selectionDragged &&
|
|
847
1094
|
this.selectionAnchor.scrollView === point.scrollView &&
|
|
848
1095
|
this.selectionAnchor.row === point.row &&
|
|
849
|
-
this.selectionAnchor.col === point.col
|
|
850
|
-
|
|
851
|
-
: undefined;
|
|
1096
|
+
this.selectionAnchor.col === point.col;
|
|
1097
|
+
const clickedUrl = isClick ? this.pressedUrl : undefined;
|
|
852
1098
|
this.pressedUrl = undefined;
|
|
853
1099
|
if (clickedUrl && this.openUrl) {
|
|
854
1100
|
this.selectionAnchor = undefined;
|
|
@@ -862,6 +1108,20 @@ export class TuiAltScreen extends TuiBase {
|
|
|
862
1108
|
this.requestRender();
|
|
863
1109
|
return;
|
|
864
1110
|
}
|
|
1111
|
+
if (isClick) {
|
|
1112
|
+
const clickEvent = this.createMouseEvent("click", event.button, event.x, event.y, {
|
|
1113
|
+
clickCount: this.lastClick?.count ?? 1,
|
|
1114
|
+
});
|
|
1115
|
+
const overlay = this.dispatchMouseToOverlay(clickEvent);
|
|
1116
|
+
const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(clickEvent));
|
|
1117
|
+
if (result) {
|
|
1118
|
+
const render = this.applyMouseDispatchResult(clickEvent, result);
|
|
1119
|
+
this.clearTextSelection();
|
|
1120
|
+
if (render)
|
|
1121
|
+
this.requestRender();
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
865
1125
|
if (this.copyOnSelect)
|
|
866
1126
|
void this.copySelectionToClipboard();
|
|
867
1127
|
this.requestRender();
|
|
@@ -1003,8 +1263,24 @@ export class TuiAltScreen extends TuiBase {
|
|
|
1003
1263
|
const maxRow = Math.min(screen.length, box.rect.y + box.rect.height, box.clip.y + box.clip.height);
|
|
1004
1264
|
const minColumn = Math.max(0, box.rect.x, box.clip.x);
|
|
1005
1265
|
const maxColumn = Math.min(this.terminal.columns, box.rect.x + box.rect.width, box.clip.x + box.clip.width, scrollbarColumn ?? Number.POSITIVE_INFINITY);
|
|
1006
|
-
|
|
1007
|
-
|
|
1266
|
+
const minContentRow = scrollView.scrollTop + minRow - box.rect.y;
|
|
1267
|
+
const maxContentRow = scrollView.scrollTop + maxRow - box.rect.y - 1;
|
|
1268
|
+
let low = 0;
|
|
1269
|
+
let high = search.matches.length;
|
|
1270
|
+
while (low < high) {
|
|
1271
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
1272
|
+
const match = search.matches[middle];
|
|
1273
|
+
const lastRow = match.segments[match.segments.length - 1]?.row ?? -1;
|
|
1274
|
+
if (lastRow < minContentRow)
|
|
1275
|
+
low = middle + 1;
|
|
1276
|
+
else
|
|
1277
|
+
high = middle;
|
|
1278
|
+
}
|
|
1279
|
+
for (let matchIndex = low; matchIndex < search.matches.length; matchIndex++) {
|
|
1280
|
+
const match = search.matches[matchIndex];
|
|
1281
|
+
if ((match.segments[0]?.row ?? 0) > maxContentRow)
|
|
1282
|
+
break;
|
|
1283
|
+
for (const segment of match.segments) {
|
|
1008
1284
|
const row = box.rect.y + segment.row - scrollView.scrollTop;
|
|
1009
1285
|
if (row < minRow || row >= maxRow)
|
|
1010
1286
|
continue;
|
|
@@ -1107,6 +1383,30 @@ export class TuiAltScreen extends TuiBase {
|
|
|
1107
1383
|
isMouseSequence(data) {
|
|
1108
1384
|
return /^\x1b\[<\d+;\d+;\d+[Mm]$/.test(data) || (data.length === 6 && data.startsWith("\x1b[M"));
|
|
1109
1385
|
}
|
|
1386
|
+
compositeScrollToEndIndicator(screen, layout, width) {
|
|
1387
|
+
this.scrollToEndIndicatorRect = undefined;
|
|
1388
|
+
const scrollView = layout.primaryScrollView ?? this.implicitScrollView;
|
|
1389
|
+
if (!this.scrollToEndIndicator || !scrollView.followEnd || scrollView.isFollowingEnd)
|
|
1390
|
+
return screen;
|
|
1391
|
+
const box = getScrollViewBox(layout, scrollView);
|
|
1392
|
+
const clip = box?.clip;
|
|
1393
|
+
if (!clip || clip.width <= 0 || clip.height <= 0)
|
|
1394
|
+
return screen;
|
|
1395
|
+
const row = clip.y + clip.height - 1;
|
|
1396
|
+
if (row >= screen.length || isImageLine(screen[row] ?? ""))
|
|
1397
|
+
return screen;
|
|
1398
|
+
const scrollbarColumn = box ? getScrollbarGeometry(box)?.column : undefined;
|
|
1399
|
+
const availableWidth = Math.max(0, (scrollbarColumn ?? clip.x + clip.width) - clip.x);
|
|
1400
|
+
const text = truncateToWidth(this.scrollToEndIndicator(), availableWidth, "");
|
|
1401
|
+
const textWidth = visibleWidth(text);
|
|
1402
|
+
if (textWidth === 0)
|
|
1403
|
+
return screen;
|
|
1404
|
+
const column = clip.x + Math.floor((availableWidth - textWidth) / 2);
|
|
1405
|
+
const result = [...screen];
|
|
1406
|
+
result[row] = compositeTuiLine(result[row] ?? "", text, column, textWidth, width);
|
|
1407
|
+
this.scrollToEndIndicatorRect = { row, column, width: textWidth };
|
|
1408
|
+
return result;
|
|
1409
|
+
}
|
|
1110
1410
|
compositeFlashes(screen, width, height) {
|
|
1111
1411
|
const flashLines = this.flashes.render(width).slice(-height);
|
|
1112
1412
|
if (flashLines.length === 0)
|
|
@@ -1135,6 +1435,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
1135
1435
|
}
|
|
1136
1436
|
let screen = nextLayout.lines.map((line) => line.replace(OSC133_ZONE_PREFIX, ""));
|
|
1137
1437
|
screen = this.applySearchHighlights(screen, nextLayout);
|
|
1438
|
+
screen = this.compositeScrollToEndIndicator(screen, nextLayout, width);
|
|
1138
1439
|
screen = this.compositeOverlays(screen, width, height);
|
|
1139
1440
|
if (screen.length > height)
|
|
1140
1441
|
screen = screen.slice(screen.length - height);
|