@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.
Files changed (107) hide show
  1. package/README.md +61 -4
  2. package/dist/alt-screen-search.d.ts +21 -1
  3. package/dist/alt-screen-search.d.ts.map +1 -1
  4. package/dist/alt-screen-search.js +199 -42
  5. package/dist/alt-screen-search.js.map +1 -1
  6. package/dist/components/box.d.ts +3 -1
  7. package/dist/components/box.d.ts.map +1 -1
  8. package/dist/components/box.js +28 -0
  9. package/dist/components/box.js.map +1 -1
  10. package/dist/components/editor.d.ts +6 -1
  11. package/dist/components/editor.d.ts.map +1 -1
  12. package/dist/components/editor.js +98 -18
  13. package/dist/components/editor.js.map +1 -1
  14. package/dist/components/input.d.ts +12 -1
  15. package/dist/components/input.d.ts.map +1 -1
  16. package/dist/components/input.js +40 -6
  17. package/dist/components/input.js.map +1 -1
  18. package/dist/components/latex.d.ts.map +1 -1
  19. package/dist/components/latex.js +7 -0
  20. package/dist/components/latex.js.map +1 -1
  21. package/dist/components/loader.d.ts +3 -0
  22. package/dist/components/loader.d.ts.map +1 -1
  23. package/dist/components/loader.js +20 -8
  24. package/dist/components/loader.js.map +1 -1
  25. package/dist/components/mouse-region.d.ts +12 -0
  26. package/dist/components/mouse-region.d.ts.map +1 -0
  27. package/dist/components/mouse-region.js +19 -0
  28. package/dist/components/mouse-region.js.map +1 -0
  29. package/dist/components/scroll-view.d.ts +6 -3
  30. package/dist/components/scroll-view.d.ts.map +1 -1
  31. package/dist/components/scroll-view.js +6 -1
  32. package/dist/components/scroll-view.js.map +1 -1
  33. package/dist/components/select-list.d.ts +4 -1
  34. package/dist/components/select-list.d.ts.map +1 -1
  35. package/dist/components/select-list.js +48 -2
  36. package/dist/components/select-list.js.map +1 -1
  37. package/dist/components/settings-list.d.ts +5 -1
  38. package/dist/components/settings-list.d.ts.map +1 -1
  39. package/dist/components/settings-list.js +54 -5
  40. package/dist/components/settings-list.js.map +1 -1
  41. package/dist/index.d.ts +5 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +4 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/layout.d.ts +3 -1
  46. package/dist/layout.d.ts.map +1 -1
  47. package/dist/layout.js +32 -9
  48. package/dist/layout.js.map +1 -1
  49. package/dist/mouse-input.d.ts +37 -0
  50. package/dist/mouse-input.d.ts.map +1 -0
  51. package/dist/mouse-input.js +97 -0
  52. package/dist/mouse-input.js.map +1 -0
  53. package/dist/native-modifiers.d.ts +2 -1
  54. package/dist/native-modifiers.d.ts.map +1 -1
  55. package/dist/native-modifiers.js +3 -44
  56. package/dist/native-modifiers.js.map +1 -1
  57. package/dist/native-platform.d.ts +18 -0
  58. package/dist/native-platform.d.ts.map +1 -0
  59. package/dist/native-platform.js +42 -0
  60. package/dist/native-platform.js.map +1 -0
  61. package/dist/stdin-buffer.d.ts +2 -0
  62. package/dist/stdin-buffer.d.ts.map +1 -1
  63. package/dist/stdin-buffer.js +38 -0
  64. package/dist/stdin-buffer.js.map +1 -1
  65. package/dist/terminal.d.ts +25 -1
  66. package/dist/terminal.d.ts.map +1 -1
  67. package/dist/terminal.js +112 -24
  68. package/dist/terminal.js.map +1 -1
  69. package/dist/tui-alt-screen.d.ts +31 -1
  70. package/dist/tui-alt-screen.d.ts.map +1 -1
  71. package/dist/tui-alt-screen.js +327 -78
  72. package/dist/tui-alt-screen.js.map +1 -1
  73. package/dist/tui-main-screen.d.ts +12 -0
  74. package/dist/tui-main-screen.d.ts.map +1 -1
  75. package/dist/tui-main-screen.js +129 -3
  76. package/dist/tui-main-screen.js.map +1 -1
  77. package/dist/tui.d.ts +111 -5
  78. package/dist/tui.d.ts.map +1 -1
  79. package/dist/tui.js +278 -4
  80. package/dist/tui.js.map +1 -1
  81. package/dist/utils.d.ts +2 -0
  82. package/dist/utils.d.ts.map +1 -1
  83. package/dist/utils.js +9 -0
  84. package/dist/utils.js.map +1 -1
  85. package/native/clipboard.h +170 -0
  86. package/native/darwin/README.md +8 -6
  87. package/native/darwin/build.sh +12 -5
  88. package/native/darwin/prebuilds/darwin-arm64/{darwin-modifiers.node → darwin-platform.node} +0 -0
  89. package/native/darwin/prebuilds/darwin-x64/{darwin-modifiers.node → darwin-platform.node} +0 -0
  90. package/native/darwin/src/darwin-platform.m +93 -0
  91. package/native/linux/README.md +23 -0
  92. package/native/linux/build.sh +65 -0
  93. package/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  94. package/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  95. package/native/linux/src/clipboard-worker.h +70 -0
  96. package/native/linux/src/linux-platform-x11.c +274 -0
  97. package/native/napi.h +89 -0
  98. package/native/win32/README.md +18 -7
  99. package/native/win32/build.mjs +7 -4
  100. package/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  101. package/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  102. package/native/win32/src/win32-platform.c +241 -0
  103. package/package.json +10 -3
  104. package/native/darwin/src/darwin-modifiers.c +0 -70
  105. package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  106. package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  107. package/native/win32/src/win32-console-mode.c +0 -119
@@ -1,13 +1,15 @@
1
1
  var _a;
2
- import { AltScreenSearchComponent, findAltScreenSearchMatches, getAltScreenSearchMatchKey, } from "./alt-screen-search.js";
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
- openSearch() {
275
+ toggleSearch() {
266
276
  if (this.activeSearch) {
267
- this.activeSearch.overlay?.focus();
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: 24,
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 matches = findAltScreenSearchMatches(lines, search.query);
330
- const exactIndex = search.selectedKey
331
- ? matches.findIndex((match) => getAltScreenSearchMatchKey(match) === search.selectedKey)
332
- : -1;
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
- selectedIndex = matches.findIndex((match) => (match.segments[0]?.row ?? 0) >= search.anchorRow);
337
- if (selectedIndex < 0)
338
- selectedIndex = 0;
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
- if (this.handleRightClickPaste(mouseEvent))
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.openSearch();
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
- parseWheelEvent(data) {
505
- const sgr = /^\x1b\[<(\d+);(\d+);(\d+)[Mm]$/.exec(data);
506
- if (sgr) {
507
- const button = Number.parseInt(sgr[1], 10);
508
- if ((button & 64) === 0)
509
- return undefined;
510
- const direction = button & 3;
511
- if (direction !== 0 && direction !== 1)
512
- return undefined;
513
- return {
514
- direction: direction === 0 ? -1 : 1,
515
- x: Number.parseInt(sgr[2], 10) - 1,
516
- y: Number.parseInt(sgr[3], 10) - 1,
517
- };
518
- }
519
- if (data.length === 6 && data.startsWith("\x1b[M")) {
520
- const button = data.charCodeAt(3) - 32;
521
- if ((button & 64) === 0)
522
- return undefined;
523
- const direction = button & 3;
524
- if (direction !== 0 && direction !== 1)
525
- return undefined;
526
- return {
527
- direction: direction === 0 ? -1 : 1,
528
- x: data.charCodeAt(4) - 33,
529
- y: data.charCodeAt(5) - 33,
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.wheelScrollLines;
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
- const match = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(data);
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
- getScrollbarTargetAt(x, y) {
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.thumbTop &&
585
- y < geometry.thumbTop + geometry.thumbHeight) {
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
- const maxThumbOffset = geometry.trackHeight - geometry.thumbHeight;
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: event.y - target.geometry.thumbTop,
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 clickedUrl = !this.selectionDragged &&
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
- ? this.pressedUrl
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
- for (let matchIndex = 0; matchIndex < search.matches.length; matchIndex++) {
985
- for (const segment of search.matches[matchIndex].segments) {
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 /^\x1b\[<\d+;\d+;\d+[Mm]$/.test(data) || (data.length === 6 && data.startsWith("\x1b[M"));
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);