@code-yeongyu/senpi-tui 2026.9.12 → 2026.9.13-2
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 +61 -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 +199 -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 +28 -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 +40 -6
- package/dist/components/input.js.map +1 -1
- package/dist/components/latex.d.ts.map +1 -1
- package/dist/components/latex.js +7 -0
- package/dist/components/latex.js.map +1 -1
- package/dist/components/loader.d.ts +3 -0
- package/dist/components/loader.d.ts.map +1 -1
- package/dist/components/loader.js +20 -8
- 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 +19 -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 +6 -1
- 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 +48 -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 +54 -5
- package/dist/components/settings-list.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.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/mouse-input.d.ts +37 -0
- package/dist/mouse-input.d.ts.map +1 -0
- package/dist/mouse-input.js +97 -0
- package/dist/mouse-input.js.map +1 -0
- package/dist/native-modifiers.d.ts +2 -1
- package/dist/native-modifiers.d.ts.map +1 -1
- package/dist/native-modifiers.js +3 -44
- package/dist/native-modifiers.js.map +1 -1
- package/dist/native-platform.d.ts +18 -0
- package/dist/native-platform.d.ts.map +1 -0
- package/dist/native-platform.js +42 -0
- package/dist/native-platform.js.map +1 -0
- package/dist/stdin-buffer.d.ts +2 -0
- package/dist/stdin-buffer.d.ts.map +1 -1
- package/dist/stdin-buffer.js +38 -0
- package/dist/stdin-buffer.js.map +1 -1
- package/dist/terminal.d.ts +25 -1
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +112 -24
- 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 +327 -78
- package/dist/tui-alt-screen.js.map +1 -1
- package/dist/tui-main-screen.d.ts +12 -0
- package/dist/tui-main-screen.d.ts.map +1 -1
- package/dist/tui-main-screen.js +129 -3
- package/dist/tui-main-screen.js.map +1 -1
- package/dist/tui.d.ts +111 -5
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +278 -4
- 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/native/clipboard.h +170 -0
- package/native/darwin/README.md +8 -6
- package/native/darwin/build.sh +12 -5
- package/native/darwin/prebuilds/darwin-arm64/{darwin-modifiers.node → darwin-platform.node} +0 -0
- package/native/darwin/prebuilds/darwin-x64/{darwin-modifiers.node → darwin-platform.node} +0 -0
- package/native/darwin/src/darwin-platform.m +93 -0
- package/native/linux/README.md +23 -0
- package/native/linux/build.sh +65 -0
- package/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/native/linux/src/clipboard-worker.h +70 -0
- package/native/linux/src/linux-platform-x11.c +274 -0
- package/native/napi.h +89 -0
- package/native/win32/README.md +18 -7
- package/native/win32/build.mjs +7 -4
- package/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/native/win32/src/win32-platform.c +241 -0
- package/package.json +10 -3
- package/native/darwin/src/darwin-modifiers.c +0 -70
- package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/native/win32/src/win32-console-mode.c +0 -119
package/dist/tui-alt-screen.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
var _a;
|
|
2
|
-
import { AltScreenSearchComponent,
|
|
2
|
+
import { AltScreenSearchComponent, AltScreenSearchIndex, getAltScreenSearchMatchKey, } from "./alt-screen-search.js";
|
|
3
3
|
import { AltScreenFlashContainer } from "./components/alt-screen-flash.js";
|
|
4
4
|
import { ScrollView } from "./components/scroll-view.js";
|
|
5
5
|
import { getKeybindings } from "./keybindings.js";
|
|
6
6
|
import { isKeyRelease } from "./keys.js";
|
|
7
|
-
import { getScrollbarGeometry, getScrollViewBox, getScrollViewsAt, renderLayoutFrame, } from "./layout.js";
|
|
7
|
+
import { getLayoutBoxesAt, getScrollbarGeometry, getScrollViewBox, getScrollViewsAt, renderLayoutFrame, } from "./layout.js";
|
|
8
|
+
import { getLayoutNode } from "./layout-node.js";
|
|
9
|
+
import { decodeMouseButton, isMouseSequence, parseSgrMouseEvent, parseWheelEvent } from "./mouse-input.js";
|
|
8
10
|
import { deleteAllKittyImages, deleteAllKittyPlacements, deleteKittyImage, getCapabilities, getKittyImagePlacement, isImageLine, setCapabilities, } from "./terminal-image.js";
|
|
9
|
-
import { CURSOR_MARKER, compositeTuiLine, TuiBase, VIEWPORT_TUI, } from "./tui.js";
|
|
10
|
-
import { extractAnsiCode, getGraphemeCellRange, getOsc8LinkAtColumn, getWordSegmenter, sliceByColumn, stripTerminalSequences, visibleWidth, } from "./utils.js";
|
|
11
|
+
import { Container, CURSOR_MARKER, compositeTuiLine, dispatchMouseEvent, retargetMouseEvent, TuiBase, VIEWPORT_TUI, } from "./tui.js";
|
|
12
|
+
import { extractAnsiCode, getGraphemeCellRange, getOsc8LinkAtColumn, getWordSegmenter, sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth, } from "./utils.js";
|
|
11
13
|
const ENTER_ALT_SCREEN = "\x1b[?1049h";
|
|
12
14
|
const EXIT_ALT_SCREEN = "\x1b[?1049l";
|
|
13
15
|
const DISABLE_AUTOWRAP = "\x1b[?7l";
|
|
@@ -22,6 +24,7 @@ const END_SYNCHRONIZED_OUTPUT = "\x1b[?2026l";
|
|
|
22
24
|
const OSC133_ZONE_PREFIX = /^(?:\x1b\]133;[ABC](?:\x07|\x1b\\))+/;
|
|
23
25
|
const OSC133_PROMPT_START = /^\x1b\]133;A(?:\x07|\x1b\\)/;
|
|
24
26
|
const PAGE_SCROLL_OVERLAP = 4;
|
|
27
|
+
const ALT_WHEEL_SCROLL_MULTIPLIER = 5;
|
|
25
28
|
const MAX_CACHED_OFFSCREEN_KITTY_IMAGES = 16;
|
|
26
29
|
const MAX_CACHED_OFFSCREEN_KITTY_TRANSMISSION_BYTES = 32 * 1024 * 1024;
|
|
27
30
|
const MAX_CACHED_OFFSCREEN_KITTY_DECODED_BYTES = 64 * 1024 * 1024;
|
|
@@ -48,8 +51,10 @@ export class TuiAltScreen extends TuiBase {
|
|
|
48
51
|
this.selectionAutoScrollDirection = 0;
|
|
49
52
|
this.selectionPressActive = false;
|
|
50
53
|
this.selectionDragged = false;
|
|
54
|
+
this.mousePressMoved = false;
|
|
51
55
|
this.implicitDocument = {
|
|
52
56
|
render: (width) => super.render(width),
|
|
57
|
+
handleMouse: (event) => super.handleMouse(event),
|
|
53
58
|
invalidate: () => {
|
|
54
59
|
for (const child of this.children)
|
|
55
60
|
child.invalidate();
|
|
@@ -61,6 +66,8 @@ export class TuiAltScreen extends TuiBase {
|
|
|
61
66
|
this.mouseEnabled = options.mouse ?? true;
|
|
62
67
|
this.searchMatchStyle = options.searchMatchStyle ?? ((text) => `\x1b[4m${text}\x1b[24m`);
|
|
63
68
|
this.searchCurrentMatchStyle = options.searchCurrentMatchStyle ?? ((text) => `\x1b[1;7m${text}\x1b[22;27m`);
|
|
69
|
+
this.searchNavigationButtonStyle = options.searchNavigationButtonStyle ?? ((text) => text);
|
|
70
|
+
this.scrollToEndIndicator = options.scrollToEndIndicator;
|
|
64
71
|
this.openUrl = options.openUrl;
|
|
65
72
|
this.onRightClickPaste = options.onRightClickPaste;
|
|
66
73
|
this.copyOnSelect = options.copyOnSelect ?? true;
|
|
@@ -129,6 +136,8 @@ export class TuiAltScreen extends TuiBase {
|
|
|
129
136
|
this.lastClick = undefined;
|
|
130
137
|
this.pressedUrl = undefined;
|
|
131
138
|
this.selectionDragged = false;
|
|
139
|
+
this.clearComponentMouseGesture();
|
|
140
|
+
this.lastComponentClick = undefined;
|
|
132
141
|
this.resetRenderState();
|
|
133
142
|
const term = process.env.TERM?.toLowerCase() ?? "";
|
|
134
143
|
// Multiplexers can lag when every pointer movement is forwarded. Button-motion
|
|
@@ -148,6 +157,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
148
157
|
this.selectionPressActive = false;
|
|
149
158
|
this.stopScrollbarHover();
|
|
150
159
|
this.stopScrollbarDrag();
|
|
160
|
+
this.clearComponentMouseGesture();
|
|
151
161
|
this.flashes.dispose();
|
|
152
162
|
if (!this.altScreenActive)
|
|
153
163
|
return;
|
|
@@ -262,14 +272,15 @@ export class TuiAltScreen extends TuiBase {
|
|
|
262
272
|
return;
|
|
263
273
|
}
|
|
264
274
|
}
|
|
265
|
-
|
|
275
|
+
toggleSearch() {
|
|
266
276
|
if (this.activeSearch) {
|
|
267
|
-
this.
|
|
277
|
+
this.closeSearch();
|
|
268
278
|
return;
|
|
269
279
|
}
|
|
270
|
-
const component = new AltScreenSearchComponent((query) => this.updateSearchQuery(query));
|
|
280
|
+
const component = new AltScreenSearchComponent((query) => this.updateSearchQuery(query), this.searchNavigationButtonStyle);
|
|
271
281
|
const search = {
|
|
272
282
|
component,
|
|
283
|
+
index: new AltScreenSearchIndex(),
|
|
273
284
|
query: "",
|
|
274
285
|
matches: [],
|
|
275
286
|
selectedIndex: -1,
|
|
@@ -280,7 +291,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
280
291
|
search.overlay = this.showOverlay(component, {
|
|
281
292
|
anchor: "top-right",
|
|
282
293
|
width: "40%",
|
|
283
|
-
minWidth:
|
|
294
|
+
minWidth: 32,
|
|
284
295
|
margin: 1,
|
|
285
296
|
});
|
|
286
297
|
}
|
|
@@ -310,6 +321,29 @@ export class TuiAltScreen extends TuiBase {
|
|
|
310
321
|
search.selectionMode = direction < 0 ? "previous" : "next";
|
|
311
322
|
this.requestRender();
|
|
312
323
|
}
|
|
324
|
+
getSearchNavigationDirectionAt(x, y) {
|
|
325
|
+
const search = this.activeSearch;
|
|
326
|
+
const bounds = search?.overlay?.getBounds();
|
|
327
|
+
if (!search || !bounds)
|
|
328
|
+
return undefined;
|
|
329
|
+
if (x < bounds.col || x >= bounds.col + bounds.width || y < bounds.row || y >= bounds.row + bounds.height) {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
return search.component.getNavigationDirectionAt(y - bounds.row, x - bounds.col);
|
|
333
|
+
}
|
|
334
|
+
handleSearchMouseEvent(event) {
|
|
335
|
+
const search = this.activeSearch;
|
|
336
|
+
if (!search)
|
|
337
|
+
return false;
|
|
338
|
+
const direction = this.getSearchNavigationDirectionAt(event.x, event.y);
|
|
339
|
+
if (search.component.setHoveredNavigationDirection(direction))
|
|
340
|
+
this.requestRender();
|
|
341
|
+
if (direction === undefined || event.release || (event.button & 32) !== 0 || (event.button & 3) !== 0) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
this.navigateSearch(direction);
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
313
347
|
refreshSearch(layout) {
|
|
314
348
|
const search = this.activeSearch;
|
|
315
349
|
if (!search)
|
|
@@ -326,16 +360,29 @@ export class TuiAltScreen extends TuiBase {
|
|
|
326
360
|
return false;
|
|
327
361
|
}
|
|
328
362
|
const shouldRevealSelection = search.selectionMode !== "retain";
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
363
|
+
const result = search.index.search(lines, search.query);
|
|
364
|
+
const matches = result.matches;
|
|
365
|
+
search.matches = matches;
|
|
366
|
+
if (!result.changed && search.selectionMode === "retain")
|
|
367
|
+
return false;
|
|
368
|
+
const exactIndex = result.changed
|
|
369
|
+
? search.selectedKey
|
|
370
|
+
? matches.findIndex((match) => getAltScreenSearchMatchKey(match) === search.selectedKey)
|
|
371
|
+
: -1
|
|
372
|
+
: search.selectedIndex;
|
|
333
373
|
let selectedIndex = -1;
|
|
334
374
|
if (matches.length > 0) {
|
|
335
375
|
if (search.selectionMode === "query") {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
376
|
+
let low = 0;
|
|
377
|
+
let high = matches.length;
|
|
378
|
+
while (low < high) {
|
|
379
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
380
|
+
if ((matches[middle].segments[0]?.row ?? 0) < search.anchorRow)
|
|
381
|
+
low = middle + 1;
|
|
382
|
+
else
|
|
383
|
+
high = middle;
|
|
384
|
+
}
|
|
385
|
+
selectedIndex = low < matches.length ? low : 0;
|
|
339
386
|
}
|
|
340
387
|
else if (search.selectionMode === "next") {
|
|
341
388
|
const baseIndex = exactIndex >= 0 ? exactIndex : Math.min(search.selectedIndex, matches.length - 1);
|
|
@@ -350,7 +397,6 @@ export class TuiAltScreen extends TuiBase {
|
|
|
350
397
|
exactIndex >= 0 ? exactIndex : Math.min(Math.max(0, search.selectedIndex), matches.length - 1);
|
|
351
398
|
}
|
|
352
399
|
}
|
|
353
|
-
search.matches = matches;
|
|
354
400
|
search.selectedIndex = selectedIndex;
|
|
355
401
|
search.selectedKey = selectedIndex >= 0 ? getAltScreenSearchMatchKey(matches[selectedIndex]) : undefined;
|
|
356
402
|
search.selectionMode = "retain";
|
|
@@ -378,6 +424,12 @@ export class TuiAltScreen extends TuiBase {
|
|
|
378
424
|
shouldDeferViewportInputToOverlay() {
|
|
379
425
|
return this.isOverlayFocused() && this.activeSearch?.overlay?.isFocused() !== true;
|
|
380
426
|
}
|
|
427
|
+
clearComponentMouseGesture() {
|
|
428
|
+
this.mouseCapture = undefined;
|
|
429
|
+
this.mousePressTarget = undefined;
|
|
430
|
+
this.mousePressPoint = undefined;
|
|
431
|
+
this.mousePressMoved = false;
|
|
432
|
+
}
|
|
381
433
|
handleViewportInput(data) {
|
|
382
434
|
if (data === FOCUS_OUT) {
|
|
383
435
|
const hadActiveSelection = this.selectionPressActive;
|
|
@@ -385,9 +437,13 @@ export class TuiAltScreen extends TuiBase {
|
|
|
385
437
|
this.selectionPressActive = false;
|
|
386
438
|
this.stopSelectionAutoScroll();
|
|
387
439
|
this.stopScrollbarHover();
|
|
440
|
+
if (this.activeSearch?.component.setHoveredNavigationDirection(undefined))
|
|
441
|
+
this.requestRender();
|
|
388
442
|
this.stopScrollbarDrag();
|
|
389
443
|
this.pressedUrl = undefined;
|
|
390
444
|
this.selectionDragged = false;
|
|
445
|
+
this.clearComponentMouseGesture();
|
|
446
|
+
this.lastComponentClick = undefined;
|
|
391
447
|
if (hadActiveSelection) {
|
|
392
448
|
this.selectionAnchor = undefined;
|
|
393
449
|
this.selectionFocus = undefined;
|
|
@@ -403,6 +459,16 @@ export class TuiAltScreen extends TuiBase {
|
|
|
403
459
|
return { consume: true };
|
|
404
460
|
const wheelEvent = this.parseWheelEvent(data);
|
|
405
461
|
if (wheelEvent) {
|
|
462
|
+
const event = this.createMouseEvent("wheel", wheelEvent.button, wheelEvent.x, wheelEvent.y, {
|
|
463
|
+
wheelDelta: wheelEvent.direction * this.getWheelScrollLines(wheelEvent.button),
|
|
464
|
+
});
|
|
465
|
+
const overlay = this.dispatchMouseToOverlay(event);
|
|
466
|
+
const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(event));
|
|
467
|
+
if (result) {
|
|
468
|
+
if (this.applyMouseDispatchResult(event, result))
|
|
469
|
+
this.requestRender();
|
|
470
|
+
return { consume: true };
|
|
471
|
+
}
|
|
406
472
|
if (this.shouldDeferViewportInputToOverlay())
|
|
407
473
|
return undefined;
|
|
408
474
|
this.routeWheel(wheelEvent);
|
|
@@ -410,13 +476,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
410
476
|
}
|
|
411
477
|
const mouseEvent = this.parseSgrMouseEvent(data);
|
|
412
478
|
if (mouseEvent) {
|
|
413
|
-
|
|
414
|
-
return { consume: true };
|
|
415
|
-
const handled = this.handleScrollbarMouseEvent(mouseEvent);
|
|
416
|
-
if (!this.scrollbarDrag)
|
|
417
|
-
this.updateScrollbarHover(mouseEvent.x, mouseEvent.y);
|
|
418
|
-
if (!handled)
|
|
419
|
-
this.handleSelectionMouseEvent(mouseEvent);
|
|
479
|
+
this.handleMouseEvent(mouseEvent);
|
|
420
480
|
return { consume: true };
|
|
421
481
|
}
|
|
422
482
|
if (this.isMouseSequence(data))
|
|
@@ -425,7 +485,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
425
485
|
const isRelease = isKeyRelease(data);
|
|
426
486
|
if (keybindings.matches(data, "tui.altScreen.search")) {
|
|
427
487
|
if (!isRelease)
|
|
428
|
-
this.
|
|
488
|
+
this.toggleSearch();
|
|
429
489
|
return { consume: true };
|
|
430
490
|
}
|
|
431
491
|
if (this.activeSearch?.overlay?.isFocused()) {
|
|
@@ -501,38 +561,165 @@ export class TuiAltScreen extends TuiBase {
|
|
|
501
561
|
}
|
|
502
562
|
return undefined;
|
|
503
563
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
return
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
564
|
+
decodeMouseButton(button) {
|
|
565
|
+
return decodeMouseButton(button);
|
|
566
|
+
}
|
|
567
|
+
createMouseEvent(type, button, x, y, extra = {}) {
|
|
568
|
+
return {
|
|
569
|
+
type,
|
|
570
|
+
button: type === "wheel" ? "none" : this.decodeMouseButton(button),
|
|
571
|
+
x,
|
|
572
|
+
y,
|
|
573
|
+
screenX: x,
|
|
574
|
+
screenY: y,
|
|
575
|
+
width: Math.max(1, this.terminal.columns),
|
|
576
|
+
height: Math.max(1, this.terminal.rows),
|
|
577
|
+
shift: (button & 4) !== 0,
|
|
578
|
+
alt: (button & 8) !== 0,
|
|
579
|
+
ctrl: (button & 16) !== 0,
|
|
580
|
+
...(extra.wheelDelta === undefined ? {} : { wheelDelta: extra.wheelDelta }),
|
|
581
|
+
...(extra.clickCount === undefined ? {} : { clickCount: extra.clickCount }),
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
dispatchMouseToLayout(event) {
|
|
585
|
+
if (!this.currentLayout)
|
|
586
|
+
return undefined;
|
|
587
|
+
const visited = new Set();
|
|
588
|
+
const boxes = getLayoutBoxesAt(this.currentLayout, event.screenX, event.screenY);
|
|
589
|
+
for (const box of boxes) {
|
|
590
|
+
if (visited.has(box.component))
|
|
591
|
+
continue;
|
|
592
|
+
if (getLayoutNode(box.component) && box.component.handleMouse === Container.prototype.handleMouse)
|
|
593
|
+
continue;
|
|
594
|
+
visited.add(box.component);
|
|
595
|
+
const result = dispatchMouseEvent(box.component, {
|
|
596
|
+
...event,
|
|
597
|
+
x: event.screenX - box.rect.x,
|
|
598
|
+
y: event.screenY - box.rect.y,
|
|
599
|
+
width: box.rect.width,
|
|
600
|
+
height: box.rect.height,
|
|
601
|
+
});
|
|
602
|
+
if (result)
|
|
603
|
+
return result;
|
|
531
604
|
}
|
|
532
605
|
return undefined;
|
|
533
606
|
}
|
|
607
|
+
applyMouseDispatchResult(event, result) {
|
|
608
|
+
const focusTarget = this.resolveMouseFocusTarget(result.focusTarget ?? result.target.component);
|
|
609
|
+
const focusChanged = result.focus === true && this.getFocusedComponent() !== focusTarget;
|
|
610
|
+
if (result.focus)
|
|
611
|
+
this.setFocus(focusTarget);
|
|
612
|
+
if (result.capture)
|
|
613
|
+
this.mouseCapture = result.target;
|
|
614
|
+
return (result.render ??
|
|
615
|
+
(focusChanged ||
|
|
616
|
+
event.type === "press" ||
|
|
617
|
+
event.type === "click" ||
|
|
618
|
+
event.type === "drag" ||
|
|
619
|
+
event.type === "wheel"));
|
|
620
|
+
}
|
|
621
|
+
dispatchMouseToTarget(event, target) {
|
|
622
|
+
return dispatchMouseEvent(target.component, retargetMouseEvent(event, target));
|
|
623
|
+
}
|
|
624
|
+
getComponentClickCount(target, x, y) {
|
|
625
|
+
const now = Date.now();
|
|
626
|
+
const previous = this.lastComponentClick;
|
|
627
|
+
const count = previous &&
|
|
628
|
+
now - previous.timestamp <= DOUBLE_CLICK_INTERVAL_MS &&
|
|
629
|
+
previous.component === target.component &&
|
|
630
|
+
previous.x === x &&
|
|
631
|
+
previous.y === y
|
|
632
|
+
? (previous.count % 3) + 1
|
|
633
|
+
: 1;
|
|
634
|
+
this.lastComponentClick = { timestamp: now, count, component: target.component, x, y };
|
|
635
|
+
return count;
|
|
636
|
+
}
|
|
637
|
+
clearTextSelection() {
|
|
638
|
+
this.stopSelectionAutoScroll();
|
|
639
|
+
this.selectionPressActive = false;
|
|
640
|
+
this.selectionAnchor = undefined;
|
|
641
|
+
this.selectionFocus = undefined;
|
|
642
|
+
this.selectionGranularity = "character";
|
|
643
|
+
this.selectionInitialRange = undefined;
|
|
644
|
+
this.pressedUrl = undefined;
|
|
645
|
+
this.selectionDragged = false;
|
|
646
|
+
}
|
|
647
|
+
handleMouseEvent(raw) {
|
|
648
|
+
const isMotion = (raw.button & 32) !== 0;
|
|
649
|
+
const type = raw.release
|
|
650
|
+
? "release"
|
|
651
|
+
: isMotion
|
|
652
|
+
? this.decodeMouseButton(raw.button) === "none"
|
|
653
|
+
? "move"
|
|
654
|
+
: "drag"
|
|
655
|
+
: "press";
|
|
656
|
+
const event = this.createMouseEvent(type, raw.button, raw.x, raw.y);
|
|
657
|
+
if (this.mouseCapture || this.mousePressTarget) {
|
|
658
|
+
const target = this.mouseCapture ?? this.mousePressTarget;
|
|
659
|
+
if (this.mousePressPoint && (raw.x !== this.mousePressPoint.x || raw.y !== this.mousePressPoint.y)) {
|
|
660
|
+
this.mousePressMoved = true;
|
|
661
|
+
this.lastComponentClick = undefined;
|
|
662
|
+
}
|
|
663
|
+
let render = false;
|
|
664
|
+
const targetResult = this.dispatchMouseToTarget(event, target);
|
|
665
|
+
if (targetResult)
|
|
666
|
+
render = this.applyMouseDispatchResult(event, targetResult);
|
|
667
|
+
if (raw.release) {
|
|
668
|
+
if (!this.mousePressMoved && this.mousePressPoint?.x === raw.x && this.mousePressPoint.y === raw.y) {
|
|
669
|
+
const clickEvent = this.createMouseEvent("click", raw.button, raw.x, raw.y, {
|
|
670
|
+
clickCount: this.getComponentClickCount(target, raw.x, raw.y),
|
|
671
|
+
});
|
|
672
|
+
const clickResult = this.dispatchMouseToTarget(clickEvent, target);
|
|
673
|
+
if (clickResult)
|
|
674
|
+
render = this.applyMouseDispatchResult(clickEvent, clickResult) || render;
|
|
675
|
+
}
|
|
676
|
+
this.clearComponentMouseGesture();
|
|
677
|
+
}
|
|
678
|
+
if (render)
|
|
679
|
+
this.requestRender();
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (this.handleSearchMouseEvent(raw))
|
|
683
|
+
return;
|
|
684
|
+
const overlay = this.dispatchMouseToOverlay(event);
|
|
685
|
+
if (!overlay.hit) {
|
|
686
|
+
if (this.handleScrollToEndIndicatorMouseEvent(raw))
|
|
687
|
+
return;
|
|
688
|
+
const scrollbarHandled = this.handleScrollbarMouseEvent(raw);
|
|
689
|
+
if (!this.scrollbarDrag)
|
|
690
|
+
this.updateScrollbarHover(raw.x, raw.y);
|
|
691
|
+
if (scrollbarHandled)
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
this.stopScrollbarHover();
|
|
696
|
+
}
|
|
697
|
+
const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(event));
|
|
698
|
+
if (result) {
|
|
699
|
+
const render = this.applyMouseDispatchResult(event, result);
|
|
700
|
+
if (type === "press") {
|
|
701
|
+
this.clearTextSelection();
|
|
702
|
+
this.mousePressTarget = result.target;
|
|
703
|
+
this.mousePressPoint = { x: raw.x, y: raw.y };
|
|
704
|
+
this.mousePressMoved = false;
|
|
705
|
+
}
|
|
706
|
+
if (render)
|
|
707
|
+
this.requestRender();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (this.handleRightClickPaste(raw))
|
|
711
|
+
return;
|
|
712
|
+
this.handleSelectionMouseEvent(raw);
|
|
713
|
+
}
|
|
714
|
+
parseWheelEvent(data) {
|
|
715
|
+
return parseWheelEvent(data);
|
|
716
|
+
}
|
|
717
|
+
getWheelScrollLines(button) {
|
|
718
|
+
// SGR mouse button codes use bit 3 (value 8) for the Alt modifier.
|
|
719
|
+
return (button & 8) !== 0 ? this.wheelScrollLines * ALT_WHEEL_SCROLL_MULTIPLIER : this.wheelScrollLines;
|
|
720
|
+
}
|
|
534
721
|
routeWheel(event) {
|
|
535
|
-
let remaining = event.direction * this.
|
|
722
|
+
let remaining = event.direction * this.getWheelScrollLines(event.button);
|
|
536
723
|
const seen = new Set();
|
|
537
724
|
for (const scrollView of this.currentLayout ? getScrollViewsAt(this.currentLayout, event.x, event.y) : []) {
|
|
538
725
|
seen.add(scrollView);
|
|
@@ -547,15 +734,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
547
734
|
this.requestRender();
|
|
548
735
|
}
|
|
549
736
|
parseSgrMouseEvent(data) {
|
|
550
|
-
|
|
551
|
-
if (!match)
|
|
552
|
-
return undefined;
|
|
553
|
-
return {
|
|
554
|
-
button: Number.parseInt(match[1], 10),
|
|
555
|
-
x: Number.parseInt(match[2], 10) - 1,
|
|
556
|
-
y: Number.parseInt(match[3], 10) - 1,
|
|
557
|
-
release: match[4] === "m",
|
|
558
|
-
};
|
|
737
|
+
return parseSgrMouseEvent(data);
|
|
559
738
|
}
|
|
560
739
|
handleRightClickPaste(event) {
|
|
561
740
|
if (!this.onRightClickPaste ||
|
|
@@ -573,16 +752,25 @@ export class TuiAltScreen extends TuiBase {
|
|
|
573
752
|
}
|
|
574
753
|
return true;
|
|
575
754
|
}
|
|
576
|
-
|
|
755
|
+
handleScrollToEndIndicatorMouseEvent(event) {
|
|
756
|
+
const rect = this.scrollToEndIndicatorRect;
|
|
757
|
+
if (!rect || event.release || (event.button & 32) !== 0 || (event.button & 3) !== 0)
|
|
758
|
+
return false;
|
|
759
|
+
if (event.y !== rect.row || event.x < rect.column || event.x >= rect.column + rect.width)
|
|
760
|
+
return false;
|
|
761
|
+
this.scrollToBottom();
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
getScrollbarTargetAt(x, y, includeHiddenAuto = false) {
|
|
577
765
|
if (this.hasOverlay() || !this.currentLayout)
|
|
578
766
|
return undefined;
|
|
579
767
|
for (const scrollView of getScrollViewsAt(this.currentLayout, x, y)) {
|
|
580
768
|
const box = getScrollViewBox(this.currentLayout, scrollView);
|
|
581
|
-
const geometry = box ? getScrollbarGeometry(box) : undefined;
|
|
769
|
+
const geometry = box ? getScrollbarGeometry(box, includeHiddenAuto) : undefined;
|
|
582
770
|
if (geometry &&
|
|
583
771
|
x === geometry.column &&
|
|
584
|
-
y >= geometry.
|
|
585
|
-
y < geometry.
|
|
772
|
+
y >= geometry.trackTop &&
|
|
773
|
+
y < geometry.trackTop + geometry.trackHeight) {
|
|
586
774
|
return { scrollView, geometry };
|
|
587
775
|
}
|
|
588
776
|
}
|
|
@@ -596,11 +784,17 @@ export class TuiAltScreen extends TuiBase {
|
|
|
596
784
|
this.scrollbarHover?.setScrollbarActive(true);
|
|
597
785
|
}
|
|
598
786
|
updateScrollbarHover(x, y) {
|
|
599
|
-
this.setScrollbarHover(this.getScrollbarTargetAt(x, y)?.scrollView);
|
|
787
|
+
this.setScrollbarHover(this.getScrollbarTargetAt(x, y, true)?.scrollView);
|
|
600
788
|
}
|
|
601
789
|
stopScrollbarHover() {
|
|
602
790
|
this.setScrollbarHover(undefined);
|
|
603
791
|
}
|
|
792
|
+
scrollScrollbarToPointer(scrollView, geometry, pointerY, grabOffset) {
|
|
793
|
+
const maxThumbOffset = geometry.trackHeight - geometry.thumbHeight;
|
|
794
|
+
const thumbOffset = Math.max(0, Math.min(maxThumbOffset, pointerY - geometry.trackTop - grabOffset));
|
|
795
|
+
const scrollTop = maxThumbOffset === 0 ? 0 : Math.round((thumbOffset / maxThumbOffset) * geometry.maxScrollTop);
|
|
796
|
+
scrollView.scrollTo(scrollTop);
|
|
797
|
+
}
|
|
604
798
|
handleScrollbarMouseEvent(event) {
|
|
605
799
|
if (this.scrollbarDrag) {
|
|
606
800
|
if (event.release) {
|
|
@@ -612,10 +806,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
612
806
|
: undefined;
|
|
613
807
|
const geometry = box ? getScrollbarGeometry(box) : undefined;
|
|
614
808
|
if (geometry) {
|
|
615
|
-
|
|
616
|
-
const thumbOffset = Math.max(0, Math.min(maxThumbOffset, event.y - geometry.trackTop - this.scrollbarDrag.grabOffset));
|
|
617
|
-
const scrollTop = maxThumbOffset === 0 ? 0 : Math.round((thumbOffset / maxThumbOffset) * geometry.maxScrollTop);
|
|
618
|
-
this.scrollbarDrag.scrollView.scrollTo(scrollTop);
|
|
809
|
+
this.scrollScrollbarToPointer(this.scrollbarDrag.scrollView, geometry, event.y, this.scrollbarDrag.grabOffset);
|
|
619
810
|
}
|
|
620
811
|
return true;
|
|
621
812
|
}
|
|
@@ -634,9 +825,13 @@ export class TuiAltScreen extends TuiBase {
|
|
|
634
825
|
this.pressedUrl = undefined;
|
|
635
826
|
this.selectionDragged = false;
|
|
636
827
|
this.setScrollbarHover(target.scrollView);
|
|
828
|
+
const onThumb = event.y >= target.geometry.thumbTop && event.y < target.geometry.thumbTop + target.geometry.thumbHeight;
|
|
829
|
+
const grabOffset = onThumb ? event.y - target.geometry.thumbTop : Math.floor(target.geometry.thumbHeight / 2);
|
|
830
|
+
if (!onThumb)
|
|
831
|
+
this.scrollScrollbarToPointer(target.scrollView, target.geometry, event.y, grabOffset);
|
|
637
832
|
this.scrollbarDrag = {
|
|
638
833
|
scrollView: target.scrollView,
|
|
639
|
-
grabOffset
|
|
834
|
+
grabOffset,
|
|
640
835
|
};
|
|
641
836
|
return true;
|
|
642
837
|
}
|
|
@@ -821,12 +1016,11 @@ export class TuiAltScreen extends TuiBase {
|
|
|
821
1016
|
if (!this.selectionAnchor)
|
|
822
1017
|
return;
|
|
823
1018
|
this.updateSelectionFocus(point);
|
|
824
|
-
const
|
|
1019
|
+
const isClick = !this.selectionDragged &&
|
|
825
1020
|
this.selectionAnchor.scrollView === point.scrollView &&
|
|
826
1021
|
this.selectionAnchor.row === point.row &&
|
|
827
|
-
this.selectionAnchor.col === point.col
|
|
828
|
-
|
|
829
|
-
: undefined;
|
|
1022
|
+
this.selectionAnchor.col === point.col;
|
|
1023
|
+
const clickedUrl = isClick ? this.pressedUrl : undefined;
|
|
830
1024
|
this.pressedUrl = undefined;
|
|
831
1025
|
if (clickedUrl && this.openUrl) {
|
|
832
1026
|
this.selectionAnchor = undefined;
|
|
@@ -840,6 +1034,20 @@ export class TuiAltScreen extends TuiBase {
|
|
|
840
1034
|
this.requestRender();
|
|
841
1035
|
return;
|
|
842
1036
|
}
|
|
1037
|
+
if (isClick) {
|
|
1038
|
+
const clickEvent = this.createMouseEvent("click", event.button, event.x, event.y, {
|
|
1039
|
+
clickCount: this.lastClick?.count ?? 1,
|
|
1040
|
+
});
|
|
1041
|
+
const overlay = this.dispatchMouseToOverlay(clickEvent);
|
|
1042
|
+
const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(clickEvent));
|
|
1043
|
+
if (result) {
|
|
1044
|
+
const render = this.applyMouseDispatchResult(clickEvent, result);
|
|
1045
|
+
this.clearTextSelection();
|
|
1046
|
+
if (render)
|
|
1047
|
+
this.requestRender();
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
843
1051
|
if (this.copyOnSelect)
|
|
844
1052
|
void this.copySelectionToClipboard();
|
|
845
1053
|
this.requestRender();
|
|
@@ -981,8 +1189,24 @@ export class TuiAltScreen extends TuiBase {
|
|
|
981
1189
|
const maxRow = Math.min(screen.length, box.rect.y + box.rect.height, box.clip.y + box.clip.height);
|
|
982
1190
|
const minColumn = Math.max(0, box.rect.x, box.clip.x);
|
|
983
1191
|
const maxColumn = Math.min(this.terminal.columns, box.rect.x + box.rect.width, box.clip.x + box.clip.width, scrollbarColumn ?? Number.POSITIVE_INFINITY);
|
|
984
|
-
|
|
985
|
-
|
|
1192
|
+
const minContentRow = scrollView.scrollTop + minRow - box.rect.y;
|
|
1193
|
+
const maxContentRow = scrollView.scrollTop + maxRow - box.rect.y - 1;
|
|
1194
|
+
let low = 0;
|
|
1195
|
+
let high = search.matches.length;
|
|
1196
|
+
while (low < high) {
|
|
1197
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
1198
|
+
const match = search.matches[middle];
|
|
1199
|
+
const lastRow = match.segments[match.segments.length - 1]?.row ?? -1;
|
|
1200
|
+
if (lastRow < minContentRow)
|
|
1201
|
+
low = middle + 1;
|
|
1202
|
+
else
|
|
1203
|
+
high = middle;
|
|
1204
|
+
}
|
|
1205
|
+
for (let matchIndex = low; matchIndex < search.matches.length; matchIndex++) {
|
|
1206
|
+
const match = search.matches[matchIndex];
|
|
1207
|
+
if ((match.segments[0]?.row ?? 0) > maxContentRow)
|
|
1208
|
+
break;
|
|
1209
|
+
for (const segment of match.segments) {
|
|
986
1210
|
const row = box.rect.y + segment.row - scrollView.scrollTop;
|
|
987
1211
|
if (row < minRow || row >= maxRow)
|
|
988
1212
|
continue;
|
|
@@ -1083,7 +1307,31 @@ export class TuiAltScreen extends TuiBase {
|
|
|
1083
1307
|
});
|
|
1084
1308
|
}
|
|
1085
1309
|
isMouseSequence(data) {
|
|
1086
|
-
return
|
|
1310
|
+
return isMouseSequence(data);
|
|
1311
|
+
}
|
|
1312
|
+
compositeScrollToEndIndicator(screen, layout, width) {
|
|
1313
|
+
this.scrollToEndIndicatorRect = undefined;
|
|
1314
|
+
const scrollView = layout.primaryScrollView ?? this.implicitScrollView;
|
|
1315
|
+
if (!this.scrollToEndIndicator || !scrollView.followEnd || scrollView.isFollowingEnd)
|
|
1316
|
+
return screen;
|
|
1317
|
+
const box = getScrollViewBox(layout, scrollView);
|
|
1318
|
+
const clip = box?.clip;
|
|
1319
|
+
if (!clip || clip.width <= 0 || clip.height <= 0)
|
|
1320
|
+
return screen;
|
|
1321
|
+
const row = clip.y + clip.height - 1;
|
|
1322
|
+
if (row >= screen.length || isImageLine(screen[row] ?? ""))
|
|
1323
|
+
return screen;
|
|
1324
|
+
const scrollbarColumn = box ? getScrollbarGeometry(box)?.column : undefined;
|
|
1325
|
+
const availableWidth = Math.max(0, (scrollbarColumn ?? clip.x + clip.width) - clip.x);
|
|
1326
|
+
const text = truncateToWidth(this.scrollToEndIndicator(), availableWidth, "");
|
|
1327
|
+
const textWidth = visibleWidth(text);
|
|
1328
|
+
if (textWidth === 0)
|
|
1329
|
+
return screen;
|
|
1330
|
+
const column = clip.x + Math.floor((availableWidth - textWidth) / 2);
|
|
1331
|
+
const result = [...screen];
|
|
1332
|
+
result[row] = compositeTuiLine(result[row] ?? "", text, column, textWidth, width);
|
|
1333
|
+
this.scrollToEndIndicatorRect = { row, column, width: textWidth };
|
|
1334
|
+
return result;
|
|
1087
1335
|
}
|
|
1088
1336
|
compositeFlashes(screen, width, height) {
|
|
1089
1337
|
const flashLines = this.flashes.render(width).slice(-height);
|
|
@@ -1113,6 +1361,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
1113
1361
|
}
|
|
1114
1362
|
let screen = nextLayout.lines.map((line) => line.replace(OSC133_ZONE_PREFIX, ""));
|
|
1115
1363
|
screen = this.applySearchHighlights(screen, nextLayout);
|
|
1364
|
+
screen = this.compositeScrollToEndIndicator(screen, nextLayout, width);
|
|
1116
1365
|
screen = this.compositeOverlays(screen, width, height);
|
|
1117
1366
|
if (screen.length > height)
|
|
1118
1367
|
screen = screen.slice(screen.length - height);
|