@code-yeongyu/senpi-tui 2026.9.12 → 2026.9.13
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 +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 +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -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/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/terminal.d.ts.map +1 -1
- package/dist/terminal.js +2 -20
- 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 +335 -41
- package/dist/tui-alt-screen.js.map +1 -1
- package/dist/tui.d.ts +83 -5
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +118 -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,14 @@
|
|
|
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";
|
|
8
9
|
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";
|
|
10
|
+
import { Container, CURSOR_MARKER, compositeTuiLine, dispatchMouseEvent, retargetMouseEvent, TuiBase, VIEWPORT_TUI, } from "./tui.js";
|
|
11
|
+
import { extractAnsiCode, getGraphemeCellRange, getOsc8LinkAtColumn, getWordSegmenter, sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth, } from "./utils.js";
|
|
11
12
|
const ENTER_ALT_SCREEN = "\x1b[?1049h";
|
|
12
13
|
const EXIT_ALT_SCREEN = "\x1b[?1049l";
|
|
13
14
|
const DISABLE_AUTOWRAP = "\x1b[?7l";
|
|
@@ -22,6 +23,7 @@ const END_SYNCHRONIZED_OUTPUT = "\x1b[?2026l";
|
|
|
22
23
|
const OSC133_ZONE_PREFIX = /^(?:\x1b\]133;[ABC](?:\x07|\x1b\\))+/;
|
|
23
24
|
const OSC133_PROMPT_START = /^\x1b\]133;A(?:\x07|\x1b\\)/;
|
|
24
25
|
const PAGE_SCROLL_OVERLAP = 4;
|
|
26
|
+
const ALT_WHEEL_SCROLL_MULTIPLIER = 5;
|
|
25
27
|
const MAX_CACHED_OFFSCREEN_KITTY_IMAGES = 16;
|
|
26
28
|
const MAX_CACHED_OFFSCREEN_KITTY_TRANSMISSION_BYTES = 32 * 1024 * 1024;
|
|
27
29
|
const MAX_CACHED_OFFSCREEN_KITTY_DECODED_BYTES = 64 * 1024 * 1024;
|
|
@@ -48,8 +50,10 @@ export class TuiAltScreen extends TuiBase {
|
|
|
48
50
|
this.selectionAutoScrollDirection = 0;
|
|
49
51
|
this.selectionPressActive = false;
|
|
50
52
|
this.selectionDragged = false;
|
|
53
|
+
this.mousePressMoved = false;
|
|
51
54
|
this.implicitDocument = {
|
|
52
55
|
render: (width) => super.render(width),
|
|
56
|
+
handleMouse: (event) => super.handleMouse(event),
|
|
53
57
|
invalidate: () => {
|
|
54
58
|
for (const child of this.children)
|
|
55
59
|
child.invalidate();
|
|
@@ -61,6 +65,8 @@ export class TuiAltScreen extends TuiBase {
|
|
|
61
65
|
this.mouseEnabled = options.mouse ?? true;
|
|
62
66
|
this.searchMatchStyle = options.searchMatchStyle ?? ((text) => `\x1b[4m${text}\x1b[24m`);
|
|
63
67
|
this.searchCurrentMatchStyle = options.searchCurrentMatchStyle ?? ((text) => `\x1b[1;7m${text}\x1b[22;27m`);
|
|
68
|
+
this.searchNavigationButtonStyle = options.searchNavigationButtonStyle ?? ((text) => text);
|
|
69
|
+
this.scrollToEndIndicator = options.scrollToEndIndicator;
|
|
64
70
|
this.openUrl = options.openUrl;
|
|
65
71
|
this.onRightClickPaste = options.onRightClickPaste;
|
|
66
72
|
this.copyOnSelect = options.copyOnSelect ?? true;
|
|
@@ -129,6 +135,8 @@ export class TuiAltScreen extends TuiBase {
|
|
|
129
135
|
this.lastClick = undefined;
|
|
130
136
|
this.pressedUrl = undefined;
|
|
131
137
|
this.selectionDragged = false;
|
|
138
|
+
this.clearComponentMouseGesture();
|
|
139
|
+
this.lastComponentClick = undefined;
|
|
132
140
|
this.resetRenderState();
|
|
133
141
|
const term = process.env.TERM?.toLowerCase() ?? "";
|
|
134
142
|
// Multiplexers can lag when every pointer movement is forwarded. Button-motion
|
|
@@ -148,6 +156,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
148
156
|
this.selectionPressActive = false;
|
|
149
157
|
this.stopScrollbarHover();
|
|
150
158
|
this.stopScrollbarDrag();
|
|
159
|
+
this.clearComponentMouseGesture();
|
|
151
160
|
this.flashes.dispose();
|
|
152
161
|
if (!this.altScreenActive)
|
|
153
162
|
return;
|
|
@@ -262,14 +271,15 @@ export class TuiAltScreen extends TuiBase {
|
|
|
262
271
|
return;
|
|
263
272
|
}
|
|
264
273
|
}
|
|
265
|
-
|
|
274
|
+
toggleSearch() {
|
|
266
275
|
if (this.activeSearch) {
|
|
267
|
-
this.
|
|
276
|
+
this.closeSearch();
|
|
268
277
|
return;
|
|
269
278
|
}
|
|
270
|
-
const component = new AltScreenSearchComponent((query) => this.updateSearchQuery(query));
|
|
279
|
+
const component = new AltScreenSearchComponent((query) => this.updateSearchQuery(query), this.searchNavigationButtonStyle);
|
|
271
280
|
const search = {
|
|
272
281
|
component,
|
|
282
|
+
index: new AltScreenSearchIndex(),
|
|
273
283
|
query: "",
|
|
274
284
|
matches: [],
|
|
275
285
|
selectedIndex: -1,
|
|
@@ -280,7 +290,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
280
290
|
search.overlay = this.showOverlay(component, {
|
|
281
291
|
anchor: "top-right",
|
|
282
292
|
width: "40%",
|
|
283
|
-
minWidth:
|
|
293
|
+
minWidth: 32,
|
|
284
294
|
margin: 1,
|
|
285
295
|
});
|
|
286
296
|
}
|
|
@@ -310,6 +320,29 @@ export class TuiAltScreen extends TuiBase {
|
|
|
310
320
|
search.selectionMode = direction < 0 ? "previous" : "next";
|
|
311
321
|
this.requestRender();
|
|
312
322
|
}
|
|
323
|
+
getSearchNavigationDirectionAt(x, y) {
|
|
324
|
+
const search = this.activeSearch;
|
|
325
|
+
const bounds = search?.overlay?.getBounds();
|
|
326
|
+
if (!search || !bounds)
|
|
327
|
+
return undefined;
|
|
328
|
+
if (x < bounds.col || x >= bounds.col + bounds.width || y < bounds.row || y >= bounds.row + bounds.height) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
return search.component.getNavigationDirectionAt(y - bounds.row, x - bounds.col);
|
|
332
|
+
}
|
|
333
|
+
handleSearchMouseEvent(event) {
|
|
334
|
+
const search = this.activeSearch;
|
|
335
|
+
if (!search)
|
|
336
|
+
return false;
|
|
337
|
+
const direction = this.getSearchNavigationDirectionAt(event.x, event.y);
|
|
338
|
+
if (search.component.setHoveredNavigationDirection(direction))
|
|
339
|
+
this.requestRender();
|
|
340
|
+
if (direction === undefined || event.release || (event.button & 32) !== 0 || (event.button & 3) !== 0) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
this.navigateSearch(direction);
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
313
346
|
refreshSearch(layout) {
|
|
314
347
|
const search = this.activeSearch;
|
|
315
348
|
if (!search)
|
|
@@ -326,16 +359,29 @@ export class TuiAltScreen extends TuiBase {
|
|
|
326
359
|
return false;
|
|
327
360
|
}
|
|
328
361
|
const shouldRevealSelection = search.selectionMode !== "retain";
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
362
|
+
const result = search.index.search(lines, search.query);
|
|
363
|
+
const matches = result.matches;
|
|
364
|
+
search.matches = matches;
|
|
365
|
+
if (!result.changed && search.selectionMode === "retain")
|
|
366
|
+
return false;
|
|
367
|
+
const exactIndex = result.changed
|
|
368
|
+
? search.selectedKey
|
|
369
|
+
? matches.findIndex((match) => getAltScreenSearchMatchKey(match) === search.selectedKey)
|
|
370
|
+
: -1
|
|
371
|
+
: search.selectedIndex;
|
|
333
372
|
let selectedIndex = -1;
|
|
334
373
|
if (matches.length > 0) {
|
|
335
374
|
if (search.selectionMode === "query") {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
375
|
+
let low = 0;
|
|
376
|
+
let high = matches.length;
|
|
377
|
+
while (low < high) {
|
|
378
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
379
|
+
if ((matches[middle].segments[0]?.row ?? 0) < search.anchorRow)
|
|
380
|
+
low = middle + 1;
|
|
381
|
+
else
|
|
382
|
+
high = middle;
|
|
383
|
+
}
|
|
384
|
+
selectedIndex = low < matches.length ? low : 0;
|
|
339
385
|
}
|
|
340
386
|
else if (search.selectionMode === "next") {
|
|
341
387
|
const baseIndex = exactIndex >= 0 ? exactIndex : Math.min(search.selectedIndex, matches.length - 1);
|
|
@@ -350,7 +396,6 @@ export class TuiAltScreen extends TuiBase {
|
|
|
350
396
|
exactIndex >= 0 ? exactIndex : Math.min(Math.max(0, search.selectedIndex), matches.length - 1);
|
|
351
397
|
}
|
|
352
398
|
}
|
|
353
|
-
search.matches = matches;
|
|
354
399
|
search.selectedIndex = selectedIndex;
|
|
355
400
|
search.selectedKey = selectedIndex >= 0 ? getAltScreenSearchMatchKey(matches[selectedIndex]) : undefined;
|
|
356
401
|
search.selectionMode = "retain";
|
|
@@ -378,6 +423,12 @@ export class TuiAltScreen extends TuiBase {
|
|
|
378
423
|
shouldDeferViewportInputToOverlay() {
|
|
379
424
|
return this.isOverlayFocused() && this.activeSearch?.overlay?.isFocused() !== true;
|
|
380
425
|
}
|
|
426
|
+
clearComponentMouseGesture() {
|
|
427
|
+
this.mouseCapture = undefined;
|
|
428
|
+
this.mousePressTarget = undefined;
|
|
429
|
+
this.mousePressPoint = undefined;
|
|
430
|
+
this.mousePressMoved = false;
|
|
431
|
+
}
|
|
381
432
|
handleViewportInput(data) {
|
|
382
433
|
if (data === FOCUS_OUT) {
|
|
383
434
|
const hadActiveSelection = this.selectionPressActive;
|
|
@@ -385,9 +436,13 @@ export class TuiAltScreen extends TuiBase {
|
|
|
385
436
|
this.selectionPressActive = false;
|
|
386
437
|
this.stopSelectionAutoScroll();
|
|
387
438
|
this.stopScrollbarHover();
|
|
439
|
+
if (this.activeSearch?.component.setHoveredNavigationDirection(undefined))
|
|
440
|
+
this.requestRender();
|
|
388
441
|
this.stopScrollbarDrag();
|
|
389
442
|
this.pressedUrl = undefined;
|
|
390
443
|
this.selectionDragged = false;
|
|
444
|
+
this.clearComponentMouseGesture();
|
|
445
|
+
this.lastComponentClick = undefined;
|
|
391
446
|
if (hadActiveSelection) {
|
|
392
447
|
this.selectionAnchor = undefined;
|
|
393
448
|
this.selectionFocus = undefined;
|
|
@@ -403,6 +458,16 @@ export class TuiAltScreen extends TuiBase {
|
|
|
403
458
|
return { consume: true };
|
|
404
459
|
const wheelEvent = this.parseWheelEvent(data);
|
|
405
460
|
if (wheelEvent) {
|
|
461
|
+
const event = this.createMouseEvent("wheel", wheelEvent.button, wheelEvent.x, wheelEvent.y, {
|
|
462
|
+
wheelDelta: wheelEvent.direction * this.getWheelScrollLines(wheelEvent.button),
|
|
463
|
+
});
|
|
464
|
+
const overlay = this.dispatchMouseToOverlay(event);
|
|
465
|
+
const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(event));
|
|
466
|
+
if (result) {
|
|
467
|
+
if (this.applyMouseDispatchResult(event, result))
|
|
468
|
+
this.requestRender();
|
|
469
|
+
return { consume: true };
|
|
470
|
+
}
|
|
406
471
|
if (this.shouldDeferViewportInputToOverlay())
|
|
407
472
|
return undefined;
|
|
408
473
|
this.routeWheel(wheelEvent);
|
|
@@ -410,13 +475,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
410
475
|
}
|
|
411
476
|
const mouseEvent = this.parseSgrMouseEvent(data);
|
|
412
477
|
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);
|
|
478
|
+
this.handleMouseEvent(mouseEvent);
|
|
420
479
|
return { consume: true };
|
|
421
480
|
}
|
|
422
481
|
if (this.isMouseSequence(data))
|
|
@@ -425,7 +484,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
425
484
|
const isRelease = isKeyRelease(data);
|
|
426
485
|
if (keybindings.matches(data, "tui.altScreen.search")) {
|
|
427
486
|
if (!isRelease)
|
|
428
|
-
this.
|
|
487
|
+
this.toggleSearch();
|
|
429
488
|
return { consume: true };
|
|
430
489
|
}
|
|
431
490
|
if (this.activeSearch?.overlay?.isFocused()) {
|
|
@@ -501,6 +560,165 @@ export class TuiAltScreen extends TuiBase {
|
|
|
501
560
|
}
|
|
502
561
|
return undefined;
|
|
503
562
|
}
|
|
563
|
+
decodeMouseButton(button) {
|
|
564
|
+
switch (button & 3) {
|
|
565
|
+
case 0:
|
|
566
|
+
return "left";
|
|
567
|
+
case 1:
|
|
568
|
+
return "middle";
|
|
569
|
+
case 2:
|
|
570
|
+
return "right";
|
|
571
|
+
default:
|
|
572
|
+
return "none";
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
createMouseEvent(type, button, x, y, extra = {}) {
|
|
576
|
+
return {
|
|
577
|
+
type,
|
|
578
|
+
button: type === "wheel" ? "none" : this.decodeMouseButton(button),
|
|
579
|
+
x,
|
|
580
|
+
y,
|
|
581
|
+
screenX: x,
|
|
582
|
+
screenY: y,
|
|
583
|
+
width: Math.max(1, this.terminal.columns),
|
|
584
|
+
height: Math.max(1, this.terminal.rows),
|
|
585
|
+
shift: (button & 4) !== 0,
|
|
586
|
+
alt: (button & 8) !== 0,
|
|
587
|
+
ctrl: (button & 16) !== 0,
|
|
588
|
+
...(extra.wheelDelta === undefined ? {} : { wheelDelta: extra.wheelDelta }),
|
|
589
|
+
...(extra.clickCount === undefined ? {} : { clickCount: extra.clickCount }),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
dispatchMouseToLayout(event) {
|
|
593
|
+
if (!this.currentLayout)
|
|
594
|
+
return undefined;
|
|
595
|
+
const visited = new Set();
|
|
596
|
+
const boxes = getLayoutBoxesAt(this.currentLayout, event.screenX, event.screenY);
|
|
597
|
+
for (const box of boxes) {
|
|
598
|
+
if (visited.has(box.component))
|
|
599
|
+
continue;
|
|
600
|
+
if (getLayoutNode(box.component) && box.component.handleMouse === Container.prototype.handleMouse)
|
|
601
|
+
continue;
|
|
602
|
+
visited.add(box.component);
|
|
603
|
+
const result = dispatchMouseEvent(box.component, {
|
|
604
|
+
...event,
|
|
605
|
+
x: event.screenX - box.rect.x,
|
|
606
|
+
y: event.screenY - box.rect.y,
|
|
607
|
+
width: box.rect.width,
|
|
608
|
+
height: box.rect.height,
|
|
609
|
+
});
|
|
610
|
+
if (result)
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
615
|
+
applyMouseDispatchResult(event, result) {
|
|
616
|
+
const focusTarget = this.resolveMouseFocusTarget(result.focusTarget ?? result.target.component);
|
|
617
|
+
const focusChanged = result.focus === true && this.getFocusedComponent() !== focusTarget;
|
|
618
|
+
if (result.focus)
|
|
619
|
+
this.setFocus(focusTarget);
|
|
620
|
+
if (result.capture)
|
|
621
|
+
this.mouseCapture = result.target;
|
|
622
|
+
return (result.render ??
|
|
623
|
+
(focusChanged ||
|
|
624
|
+
event.type === "press" ||
|
|
625
|
+
event.type === "click" ||
|
|
626
|
+
event.type === "drag" ||
|
|
627
|
+
event.type === "wheel"));
|
|
628
|
+
}
|
|
629
|
+
dispatchMouseToTarget(event, target) {
|
|
630
|
+
return dispatchMouseEvent(target.component, retargetMouseEvent(event, target));
|
|
631
|
+
}
|
|
632
|
+
getComponentClickCount(target, x, y) {
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
const previous = this.lastComponentClick;
|
|
635
|
+
const count = previous &&
|
|
636
|
+
now - previous.timestamp <= DOUBLE_CLICK_INTERVAL_MS &&
|
|
637
|
+
previous.component === target.component &&
|
|
638
|
+
previous.x === x &&
|
|
639
|
+
previous.y === y
|
|
640
|
+
? (previous.count % 3) + 1
|
|
641
|
+
: 1;
|
|
642
|
+
this.lastComponentClick = { timestamp: now, count, component: target.component, x, y };
|
|
643
|
+
return count;
|
|
644
|
+
}
|
|
645
|
+
clearTextSelection() {
|
|
646
|
+
this.stopSelectionAutoScroll();
|
|
647
|
+
this.selectionPressActive = false;
|
|
648
|
+
this.selectionAnchor = undefined;
|
|
649
|
+
this.selectionFocus = undefined;
|
|
650
|
+
this.selectionGranularity = "character";
|
|
651
|
+
this.selectionInitialRange = undefined;
|
|
652
|
+
this.pressedUrl = undefined;
|
|
653
|
+
this.selectionDragged = false;
|
|
654
|
+
}
|
|
655
|
+
handleMouseEvent(raw) {
|
|
656
|
+
const isMotion = (raw.button & 32) !== 0;
|
|
657
|
+
const type = raw.release
|
|
658
|
+
? "release"
|
|
659
|
+
: isMotion
|
|
660
|
+
? this.decodeMouseButton(raw.button) === "none"
|
|
661
|
+
? "move"
|
|
662
|
+
: "drag"
|
|
663
|
+
: "press";
|
|
664
|
+
const event = this.createMouseEvent(type, raw.button, raw.x, raw.y);
|
|
665
|
+
if (this.mouseCapture || this.mousePressTarget) {
|
|
666
|
+
const target = this.mouseCapture ?? this.mousePressTarget;
|
|
667
|
+
if (this.mousePressPoint && (raw.x !== this.mousePressPoint.x || raw.y !== this.mousePressPoint.y)) {
|
|
668
|
+
this.mousePressMoved = true;
|
|
669
|
+
this.lastComponentClick = undefined;
|
|
670
|
+
}
|
|
671
|
+
let render = false;
|
|
672
|
+
const targetResult = this.dispatchMouseToTarget(event, target);
|
|
673
|
+
if (targetResult)
|
|
674
|
+
render = this.applyMouseDispatchResult(event, targetResult);
|
|
675
|
+
if (raw.release) {
|
|
676
|
+
if (!this.mousePressMoved && this.mousePressPoint?.x === raw.x && this.mousePressPoint.y === raw.y) {
|
|
677
|
+
const clickEvent = this.createMouseEvent("click", raw.button, raw.x, raw.y, {
|
|
678
|
+
clickCount: this.getComponentClickCount(target, raw.x, raw.y),
|
|
679
|
+
});
|
|
680
|
+
const clickResult = this.dispatchMouseToTarget(clickEvent, target);
|
|
681
|
+
if (clickResult)
|
|
682
|
+
render = this.applyMouseDispatchResult(clickEvent, clickResult) || render;
|
|
683
|
+
}
|
|
684
|
+
this.clearComponentMouseGesture();
|
|
685
|
+
}
|
|
686
|
+
if (render)
|
|
687
|
+
this.requestRender();
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (this.handleSearchMouseEvent(raw))
|
|
691
|
+
return;
|
|
692
|
+
const overlay = this.dispatchMouseToOverlay(event);
|
|
693
|
+
if (!overlay.hit) {
|
|
694
|
+
if (this.handleScrollToEndIndicatorMouseEvent(raw))
|
|
695
|
+
return;
|
|
696
|
+
const scrollbarHandled = this.handleScrollbarMouseEvent(raw);
|
|
697
|
+
if (!this.scrollbarDrag)
|
|
698
|
+
this.updateScrollbarHover(raw.x, raw.y);
|
|
699
|
+
if (scrollbarHandled)
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
this.stopScrollbarHover();
|
|
704
|
+
}
|
|
705
|
+
const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(event));
|
|
706
|
+
if (result) {
|
|
707
|
+
const render = this.applyMouseDispatchResult(event, result);
|
|
708
|
+
if (type === "press") {
|
|
709
|
+
this.clearTextSelection();
|
|
710
|
+
this.mousePressTarget = result.target;
|
|
711
|
+
this.mousePressPoint = { x: raw.x, y: raw.y };
|
|
712
|
+
this.mousePressMoved = false;
|
|
713
|
+
}
|
|
714
|
+
if (render)
|
|
715
|
+
this.requestRender();
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (this.handleRightClickPaste(raw))
|
|
719
|
+
return;
|
|
720
|
+
this.handleSelectionMouseEvent(raw);
|
|
721
|
+
}
|
|
504
722
|
parseWheelEvent(data) {
|
|
505
723
|
const sgr = /^\x1b\[<(\d+);(\d+);(\d+)[Mm]$/.exec(data);
|
|
506
724
|
if (sgr) {
|
|
@@ -514,6 +732,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
514
732
|
direction: direction === 0 ? -1 : 1,
|
|
515
733
|
x: Number.parseInt(sgr[2], 10) - 1,
|
|
516
734
|
y: Number.parseInt(sgr[3], 10) - 1,
|
|
735
|
+
button,
|
|
517
736
|
};
|
|
518
737
|
}
|
|
519
738
|
if (data.length === 6 && data.startsWith("\x1b[M")) {
|
|
@@ -527,12 +746,17 @@ export class TuiAltScreen extends TuiBase {
|
|
|
527
746
|
direction: direction === 0 ? -1 : 1,
|
|
528
747
|
x: data.charCodeAt(4) - 33,
|
|
529
748
|
y: data.charCodeAt(5) - 33,
|
|
749
|
+
button,
|
|
530
750
|
};
|
|
531
751
|
}
|
|
532
752
|
return undefined;
|
|
533
753
|
}
|
|
754
|
+
getWheelScrollLines(button) {
|
|
755
|
+
// SGR mouse button codes use bit 3 (value 8) for the Alt modifier.
|
|
756
|
+
return (button & 8) !== 0 ? this.wheelScrollLines * ALT_WHEEL_SCROLL_MULTIPLIER : this.wheelScrollLines;
|
|
757
|
+
}
|
|
534
758
|
routeWheel(event) {
|
|
535
|
-
let remaining = event.direction * this.
|
|
759
|
+
let remaining = event.direction * this.getWheelScrollLines(event.button);
|
|
536
760
|
const seen = new Set();
|
|
537
761
|
for (const scrollView of this.currentLayout ? getScrollViewsAt(this.currentLayout, event.x, event.y) : []) {
|
|
538
762
|
seen.add(scrollView);
|
|
@@ -573,16 +797,25 @@ export class TuiAltScreen extends TuiBase {
|
|
|
573
797
|
}
|
|
574
798
|
return true;
|
|
575
799
|
}
|
|
576
|
-
|
|
800
|
+
handleScrollToEndIndicatorMouseEvent(event) {
|
|
801
|
+
const rect = this.scrollToEndIndicatorRect;
|
|
802
|
+
if (!rect || event.release || (event.button & 32) !== 0 || (event.button & 3) !== 0)
|
|
803
|
+
return false;
|
|
804
|
+
if (event.y !== rect.row || event.x < rect.column || event.x >= rect.column + rect.width)
|
|
805
|
+
return false;
|
|
806
|
+
this.scrollToBottom();
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
getScrollbarTargetAt(x, y, includeHiddenAuto = false) {
|
|
577
810
|
if (this.hasOverlay() || !this.currentLayout)
|
|
578
811
|
return undefined;
|
|
579
812
|
for (const scrollView of getScrollViewsAt(this.currentLayout, x, y)) {
|
|
580
813
|
const box = getScrollViewBox(this.currentLayout, scrollView);
|
|
581
|
-
const geometry = box ? getScrollbarGeometry(box) : undefined;
|
|
814
|
+
const geometry = box ? getScrollbarGeometry(box, includeHiddenAuto) : undefined;
|
|
582
815
|
if (geometry &&
|
|
583
816
|
x === geometry.column &&
|
|
584
|
-
y >= geometry.
|
|
585
|
-
y < geometry.
|
|
817
|
+
y >= geometry.trackTop &&
|
|
818
|
+
y < geometry.trackTop + geometry.trackHeight) {
|
|
586
819
|
return { scrollView, geometry };
|
|
587
820
|
}
|
|
588
821
|
}
|
|
@@ -596,11 +829,17 @@ export class TuiAltScreen extends TuiBase {
|
|
|
596
829
|
this.scrollbarHover?.setScrollbarActive(true);
|
|
597
830
|
}
|
|
598
831
|
updateScrollbarHover(x, y) {
|
|
599
|
-
this.setScrollbarHover(this.getScrollbarTargetAt(x, y)?.scrollView);
|
|
832
|
+
this.setScrollbarHover(this.getScrollbarTargetAt(x, y, true)?.scrollView);
|
|
600
833
|
}
|
|
601
834
|
stopScrollbarHover() {
|
|
602
835
|
this.setScrollbarHover(undefined);
|
|
603
836
|
}
|
|
837
|
+
scrollScrollbarToPointer(scrollView, geometry, pointerY, grabOffset) {
|
|
838
|
+
const maxThumbOffset = geometry.trackHeight - geometry.thumbHeight;
|
|
839
|
+
const thumbOffset = Math.max(0, Math.min(maxThumbOffset, pointerY - geometry.trackTop - grabOffset));
|
|
840
|
+
const scrollTop = maxThumbOffset === 0 ? 0 : Math.round((thumbOffset / maxThumbOffset) * geometry.maxScrollTop);
|
|
841
|
+
scrollView.scrollTo(scrollTop);
|
|
842
|
+
}
|
|
604
843
|
handleScrollbarMouseEvent(event) {
|
|
605
844
|
if (this.scrollbarDrag) {
|
|
606
845
|
if (event.release) {
|
|
@@ -612,10 +851,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
612
851
|
: undefined;
|
|
613
852
|
const geometry = box ? getScrollbarGeometry(box) : undefined;
|
|
614
853
|
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);
|
|
854
|
+
this.scrollScrollbarToPointer(this.scrollbarDrag.scrollView, geometry, event.y, this.scrollbarDrag.grabOffset);
|
|
619
855
|
}
|
|
620
856
|
return true;
|
|
621
857
|
}
|
|
@@ -634,9 +870,13 @@ export class TuiAltScreen extends TuiBase {
|
|
|
634
870
|
this.pressedUrl = undefined;
|
|
635
871
|
this.selectionDragged = false;
|
|
636
872
|
this.setScrollbarHover(target.scrollView);
|
|
873
|
+
const onThumb = event.y >= target.geometry.thumbTop && event.y < target.geometry.thumbTop + target.geometry.thumbHeight;
|
|
874
|
+
const grabOffset = onThumb ? event.y - target.geometry.thumbTop : Math.floor(target.geometry.thumbHeight / 2);
|
|
875
|
+
if (!onThumb)
|
|
876
|
+
this.scrollScrollbarToPointer(target.scrollView, target.geometry, event.y, grabOffset);
|
|
637
877
|
this.scrollbarDrag = {
|
|
638
878
|
scrollView: target.scrollView,
|
|
639
|
-
grabOffset
|
|
879
|
+
grabOffset,
|
|
640
880
|
};
|
|
641
881
|
return true;
|
|
642
882
|
}
|
|
@@ -821,12 +1061,11 @@ export class TuiAltScreen extends TuiBase {
|
|
|
821
1061
|
if (!this.selectionAnchor)
|
|
822
1062
|
return;
|
|
823
1063
|
this.updateSelectionFocus(point);
|
|
824
|
-
const
|
|
1064
|
+
const isClick = !this.selectionDragged &&
|
|
825
1065
|
this.selectionAnchor.scrollView === point.scrollView &&
|
|
826
1066
|
this.selectionAnchor.row === point.row &&
|
|
827
|
-
this.selectionAnchor.col === point.col
|
|
828
|
-
|
|
829
|
-
: undefined;
|
|
1067
|
+
this.selectionAnchor.col === point.col;
|
|
1068
|
+
const clickedUrl = isClick ? this.pressedUrl : undefined;
|
|
830
1069
|
this.pressedUrl = undefined;
|
|
831
1070
|
if (clickedUrl && this.openUrl) {
|
|
832
1071
|
this.selectionAnchor = undefined;
|
|
@@ -840,6 +1079,20 @@ export class TuiAltScreen extends TuiBase {
|
|
|
840
1079
|
this.requestRender();
|
|
841
1080
|
return;
|
|
842
1081
|
}
|
|
1082
|
+
if (isClick) {
|
|
1083
|
+
const clickEvent = this.createMouseEvent("click", event.button, event.x, event.y, {
|
|
1084
|
+
clickCount: this.lastClick?.count ?? 1,
|
|
1085
|
+
});
|
|
1086
|
+
const overlay = this.dispatchMouseToOverlay(clickEvent);
|
|
1087
|
+
const result = overlay.result ?? (overlay.hit ? undefined : this.dispatchMouseToLayout(clickEvent));
|
|
1088
|
+
if (result) {
|
|
1089
|
+
const render = this.applyMouseDispatchResult(clickEvent, result);
|
|
1090
|
+
this.clearTextSelection();
|
|
1091
|
+
if (render)
|
|
1092
|
+
this.requestRender();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
843
1096
|
if (this.copyOnSelect)
|
|
844
1097
|
void this.copySelectionToClipboard();
|
|
845
1098
|
this.requestRender();
|
|
@@ -981,8 +1234,24 @@ export class TuiAltScreen extends TuiBase {
|
|
|
981
1234
|
const maxRow = Math.min(screen.length, box.rect.y + box.rect.height, box.clip.y + box.clip.height);
|
|
982
1235
|
const minColumn = Math.max(0, box.rect.x, box.clip.x);
|
|
983
1236
|
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
|
-
|
|
1237
|
+
const minContentRow = scrollView.scrollTop + minRow - box.rect.y;
|
|
1238
|
+
const maxContentRow = scrollView.scrollTop + maxRow - box.rect.y - 1;
|
|
1239
|
+
let low = 0;
|
|
1240
|
+
let high = search.matches.length;
|
|
1241
|
+
while (low < high) {
|
|
1242
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
1243
|
+
const match = search.matches[middle];
|
|
1244
|
+
const lastRow = match.segments[match.segments.length - 1]?.row ?? -1;
|
|
1245
|
+
if (lastRow < minContentRow)
|
|
1246
|
+
low = middle + 1;
|
|
1247
|
+
else
|
|
1248
|
+
high = middle;
|
|
1249
|
+
}
|
|
1250
|
+
for (let matchIndex = low; matchIndex < search.matches.length; matchIndex++) {
|
|
1251
|
+
const match = search.matches[matchIndex];
|
|
1252
|
+
if ((match.segments[0]?.row ?? 0) > maxContentRow)
|
|
1253
|
+
break;
|
|
1254
|
+
for (const segment of match.segments) {
|
|
986
1255
|
const row = box.rect.y + segment.row - scrollView.scrollTop;
|
|
987
1256
|
if (row < minRow || row >= maxRow)
|
|
988
1257
|
continue;
|
|
@@ -1085,6 +1354,30 @@ export class TuiAltScreen extends TuiBase {
|
|
|
1085
1354
|
isMouseSequence(data) {
|
|
1086
1355
|
return /^\x1b\[<\d+;\d+;\d+[Mm]$/.test(data) || (data.length === 6 && data.startsWith("\x1b[M"));
|
|
1087
1356
|
}
|
|
1357
|
+
compositeScrollToEndIndicator(screen, layout, width) {
|
|
1358
|
+
this.scrollToEndIndicatorRect = undefined;
|
|
1359
|
+
const scrollView = layout.primaryScrollView ?? this.implicitScrollView;
|
|
1360
|
+
if (!this.scrollToEndIndicator || !scrollView.followEnd || scrollView.isFollowingEnd)
|
|
1361
|
+
return screen;
|
|
1362
|
+
const box = getScrollViewBox(layout, scrollView);
|
|
1363
|
+
const clip = box?.clip;
|
|
1364
|
+
if (!clip || clip.width <= 0 || clip.height <= 0)
|
|
1365
|
+
return screen;
|
|
1366
|
+
const row = clip.y + clip.height - 1;
|
|
1367
|
+
if (row >= screen.length || isImageLine(screen[row] ?? ""))
|
|
1368
|
+
return screen;
|
|
1369
|
+
const scrollbarColumn = box ? getScrollbarGeometry(box)?.column : undefined;
|
|
1370
|
+
const availableWidth = Math.max(0, (scrollbarColumn ?? clip.x + clip.width) - clip.x);
|
|
1371
|
+
const text = truncateToWidth(this.scrollToEndIndicator(), availableWidth, "");
|
|
1372
|
+
const textWidth = visibleWidth(text);
|
|
1373
|
+
if (textWidth === 0)
|
|
1374
|
+
return screen;
|
|
1375
|
+
const column = clip.x + Math.floor((availableWidth - textWidth) / 2);
|
|
1376
|
+
const result = [...screen];
|
|
1377
|
+
result[row] = compositeTuiLine(result[row] ?? "", text, column, textWidth, width);
|
|
1378
|
+
this.scrollToEndIndicatorRect = { row, column, width: textWidth };
|
|
1379
|
+
return result;
|
|
1380
|
+
}
|
|
1088
1381
|
compositeFlashes(screen, width, height) {
|
|
1089
1382
|
const flashLines = this.flashes.render(width).slice(-height);
|
|
1090
1383
|
if (flashLines.length === 0)
|
|
@@ -1113,6 +1406,7 @@ export class TuiAltScreen extends TuiBase {
|
|
|
1113
1406
|
}
|
|
1114
1407
|
let screen = nextLayout.lines.map((line) => line.replace(OSC133_ZONE_PREFIX, ""));
|
|
1115
1408
|
screen = this.applySearchHighlights(screen, nextLayout);
|
|
1409
|
+
screen = this.compositeScrollToEndIndicator(screen, nextLayout, width);
|
|
1116
1410
|
screen = this.compositeOverlays(screen, width, height);
|
|
1117
1411
|
if (screen.length > height)
|
|
1118
1412
|
screen = screen.slice(screen.length - height);
|