@ashx-j/lunr-tui 0.2.7 → 0.2.9

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/dist/tui.js CHANGED
@@ -9,7 +9,7 @@ import { isKeyRelease, matchesKey } from "./keys.js";
9
9
  import { MOUSE_TRACKING_DISABLE, MOUSE_TRACKING_ENABLE, parseMouseEvent } from "./mouse.js";
10
10
  import { isOsc11BackgroundColorResponse, parseOsc11BackgroundColor, parseTerminalColorSchemeReport, } from "./terminal-colors.js";
11
11
  import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
12
- import { extractAnsiCode, extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth, } from "./utils.js";
12
+ import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
13
13
  const KITTY_SEQUENCE_PREFIX = "\x1b_G";
14
14
  function parseKittyImageHeader(line) {
15
15
  const sequenceStart = line.indexOf(KITTY_SEQUENCE_PREFIX);
@@ -104,6 +104,20 @@ export class Container {
104
104
  }
105
105
  return lines;
106
106
  }
107
+ handleClick(localY, width) {
108
+ let y = 0;
109
+ for (const child of this.children) {
110
+ const h = child.render(width).length;
111
+ if (localY >= y && localY < y + h) {
112
+ if (typeof child.handleClick === "function") {
113
+ return child.handleClick(localY - y, width);
114
+ }
115
+ return false;
116
+ }
117
+ y += h;
118
+ }
119
+ return false;
120
+ }
107
121
  }
108
122
  /**
109
123
  * TUI - Main class for managing terminal UI with differential rendering
@@ -118,7 +132,7 @@ export class TUI extends Container {
118
132
  inputListeners = new Set();
119
133
  /** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
120
134
  onDebug;
121
- /** Called with selected user/assistant text after an in-app drag-release. */
135
+ /** Kept for callers; left-drag no longer selects. Shift+drag stays native terminal selection. */
122
136
  onTextSelected;
123
137
  renderRequested = false;
124
138
  renderTimer;
@@ -155,9 +169,11 @@ export class TUI extends Container {
155
169
  alternateScreenActive = false;
156
170
  mouseTrackingActive = false;
157
171
  static WHEEL_LINES = 3;
158
- chatHitRegions = [];
159
172
  chatScrollbar;
160
- selection;
173
+ scrollbarDrag;
174
+ pendingClick;
175
+ lastChatStart = 0;
176
+ lastChatSliceHeight = 0;
161
177
  constructor(terminal, showHardwareCursor) {
162
178
  super();
163
179
  this.terminal = terminal;
@@ -387,21 +403,21 @@ export class TUI extends Container {
387
403
  return -1;
388
404
  return this.children.indexOf(this.pinComponent);
389
405
  }
390
- collectChildLines(from, to, width) {
406
+ collectChildLines(from, to, width, ranges) {
391
407
  const lines = [];
392
408
  for (let i = from; i < to; i++) {
393
409
  const child = this.children[i];
394
410
  if (!child)
395
411
  continue;
396
- this.appendRenderedChild(lines, child, width);
412
+ this.appendRenderedChild(lines, child, width, ranges);
397
413
  }
398
414
  return lines;
399
415
  }
400
- appendRenderedChild(lines, child, width) {
416
+ appendRenderedChild(lines, child, width, ranges) {
401
417
  const nested = "children" in child && Array.isArray(child.children);
402
418
  if (nested && child.children.length > 0 && child.render === Container.prototype.render) {
403
419
  for (const nestedChild of child.children) {
404
- this.appendRenderedChild(lines, nestedChild, width);
420
+ this.appendRenderedChild(lines, nestedChild, width, ranges);
405
421
  }
406
422
  return;
407
423
  }
@@ -409,9 +425,7 @@ export class TUI extends Container {
409
425
  for (const line of child.render(width)) {
410
426
  lines.push(line);
411
427
  }
412
- if (child.selectable && lines.length > start) {
413
- this.chatHitRegions.push({ y0: start, y1: lines.length - 1, lines: lines.slice(start) });
414
- }
428
+ ranges?.push({ component: child, start, end: lines.length });
415
429
  }
416
430
  snapStartToKittyImageHeader(lines, start) {
417
431
  if (start <= 0 || start >= lines.length)
@@ -434,8 +448,8 @@ export class TUI extends Container {
434
448
  render(width) {
435
449
  const pinIndex = this.getPinIndex();
436
450
  if (pinIndex < 0) {
437
- this.chatHitRegions = [];
438
451
  this.chatScrollbar = undefined;
452
+ this.scrollbarDrag = undefined;
439
453
  this.chatLayoutCache = undefined;
440
454
  this.chatUsesGutter = false;
441
455
  this.chatLaidOut = false;
@@ -453,31 +467,32 @@ export class TUI extends Container {
453
467
  this.chatLaidOut = true;
454
468
  let chatWidth = this.chatUsesGutter ? Math.max(1, width - 1) : width;
455
469
  let scrollLines;
470
+ let ranges;
456
471
  let fromCache = false;
457
472
  const cache = this.chatLayoutCache;
458
473
  if (cache && cache.epoch === this.contentEpoch && cache.chatWidth === chatWidth) {
459
474
  scrollLines = cache.scrollLines;
460
- this.chatHitRegions = this.copyHitRegions(cache.hitRegionsUnshifted);
475
+ ranges = cache.ranges;
461
476
  fromCache = true;
462
477
  }
463
478
  else {
464
- this.chatHitRegions = [];
465
- scrollLines = this.collectChildLines(0, pinIndex, chatWidth);
479
+ ranges = [];
480
+ scrollLines = this.collectChildLines(0, pinIndex, chatWidth, ranges);
466
481
  }
467
482
  if (this.chatUsesGutter) {
468
483
  if (scrollLines.length <= viewH) {
469
484
  this.chatUsesGutter = false;
470
485
  chatWidth = width;
471
- this.chatHitRegions = [];
472
- scrollLines = this.collectChildLines(0, pinIndex, chatWidth);
486
+ ranges = [];
487
+ scrollLines = this.collectChildLines(0, pinIndex, chatWidth, ranges);
473
488
  fromCache = false;
474
489
  }
475
490
  }
476
491
  else if (scrollLines.length > viewH) {
477
492
  this.chatUsesGutter = true;
478
493
  chatWidth = Math.max(1, width - 1);
479
- this.chatHitRegions = [];
480
- scrollLines = this.collectChildLines(0, pinIndex, chatWidth);
494
+ ranges = [];
495
+ scrollLines = this.collectChildLines(0, pinIndex, chatWidth, ranges);
481
496
  fromCache = false;
482
497
  }
483
498
  if (!fromCache) {
@@ -485,7 +500,7 @@ export class TUI extends Container {
485
500
  epoch: this.contentEpoch,
486
501
  chatWidth,
487
502
  scrollLines,
488
- hitRegionsUnshifted: this.copyHitRegions(this.chatHitRegions),
503
+ ranges,
489
504
  };
490
505
  }
491
506
  let chatSlice;
@@ -495,7 +510,9 @@ export class TUI extends Container {
495
510
  this.chatScrollOffset = 0;
496
511
  this.lastChatScrollMax = 0;
497
512
  this.chatScrollbar = undefined;
513
+ this.scrollbarDrag = undefined;
498
514
  chatSlice = scrollLines.slice();
515
+ this.lastChatStart = 0;
499
516
  }
500
517
  else {
501
518
  this.lastChatScrollMax = scrollLines.length - viewH;
@@ -503,46 +520,37 @@ export class TUI extends Container {
503
520
  start = scrollLines.length - viewH - this.chatScrollOffset;
504
521
  start = this.snapStartToKittyImageHeader(scrollLines, start);
505
522
  start = Math.max(0, start);
523
+ this.lastChatStart = start;
506
524
  chatSlice = scrollLines.slice(start, start + viewH);
507
525
  while (chatSlice.length < viewH) {
508
526
  chatSlice.push("");
509
527
  }
510
- this.chatScrollbar = { x: width, y0: 1, y1: viewH, viewH, scrollMax: this.lastChatScrollMax };
511
528
  const thumbH = Math.max(1, Math.round((viewH * viewH) / scrollLines.length));
512
529
  const travel = Math.max(0, viewH - thumbH);
513
530
  const thumbTop = this.lastChatScrollMax === 0
514
531
  ? viewH - thumbH
515
532
  : Math.round(travel * (1 - this.chatScrollOffset / this.lastChatScrollMax));
516
- const track = "\x1b[2m│\x1b[22m";
517
- const thumb = "\x1b[1m█\x1b[22m";
533
+ this.chatScrollbar = {
534
+ x: width,
535
+ y0: 1,
536
+ y1: viewH,
537
+ viewH,
538
+ scrollMax: this.lastChatScrollMax,
539
+ thumbTop,
540
+ thumbH,
541
+ };
542
+ // Reset SGR so error/thinking colors cannot bleed into the gutter.
543
+ const RESET = "\x1b[0m";
544
+ const track = `${RESET}\x1b[2m│\x1b[0m`;
545
+ const thumb = `${RESET}\x1b[1m█\x1b[0m`;
518
546
  for (let i = 0; i < chatSlice.length; i++) {
519
547
  const glyph = i >= thumbTop && i < thumbTop + thumbH ? thumb : track;
520
548
  chatSlice[i] = `${padToWidth(chatSlice[i] ?? "", chatWidth)}${glyph}`;
521
549
  }
522
550
  }
523
- this.shiftHitRegions(-start);
524
- if (this.selection) {
525
- const lo = Math.min(this.selection.anchorY, this.selection.focusY);
526
- const hi = Math.max(this.selection.anchorY, this.selection.focusY);
527
- const highlightWidth = this.chatUsesGutter ? chatWidth : width;
528
- for (let y = lo; y <= hi; y++) {
529
- const idx = y - 1;
530
- if (idx < 0 || idx >= chatSlice.length)
531
- continue;
532
- chatSlice[idx] = applySelectionHighlight(chatSlice[idx] ?? "", highlightWidth);
533
- }
534
- }
551
+ this.lastChatSliceHeight = chatSlice.length;
535
552
  return [...chatSlice, ...dockLines];
536
553
  }
537
- copyHitRegions(regions) {
538
- return regions.map((region) => ({ y0: region.y0, y1: region.y1, lines: region.lines }));
539
- }
540
- shiftHitRegions(delta) {
541
- for (const region of this.chatHitRegions) {
542
- region.y0 += delta;
543
- region.y1 += delta;
544
- }
545
- }
546
554
  /**
547
555
  * Show an overlay component with configurable positioning and sizing.
548
556
  * Returns a handle to control the overlay's visibility.
@@ -860,61 +868,98 @@ export class TUI extends Container {
860
868
  // Shift+drag stays with the terminal's native selection.
861
869
  if (mouse.shift)
862
870
  return;
863
- if (mouse.kind === "button" && !mouse.release && this.chatScrollbar && mouse.x === this.chatScrollbar.x) {
864
- const viewH = this.chatScrollbar.viewH;
865
- const y = Math.max(this.chatScrollbar.y0, Math.min(mouse.y, this.chatScrollbar.y1));
866
- const rel = (y - this.chatScrollbar.y0) / Math.max(1, viewH - 1);
867
- this.setChatScroll(Math.round((1 - rel) * this.chatScrollbar.scrollMax));
868
- this.selection = undefined;
871
+ if (this.scrollbarDrag) {
872
+ if (mouse.kind === "move") {
873
+ this.dragScrollbarTo(mouse.y);
874
+ return;
875
+ }
876
+ if (mouse.kind === "button" && mouse.release) {
877
+ this.scrollbarDrag = undefined;
878
+ }
869
879
  return;
870
880
  }
871
- if (mouse.kind === "button" && !mouse.release) {
872
- const region = this.hitSelectable(mouse.y);
873
- if (!region) {
874
- this.selection = undefined;
875
- return;
881
+ if (mouse.kind === "button" && !mouse.release && this.chatScrollbar && mouse.x >= this.chatScrollbar.x) {
882
+ const sb = this.chatScrollbar;
883
+ const y = Math.max(sb.y0, Math.min(mouse.y, sb.y1));
884
+ const row = y - sb.y0;
885
+ const onThumb = row >= sb.thumbTop && row < sb.thumbTop + sb.thumbH;
886
+ if (!onThumb) {
887
+ const rel = (y - sb.y0) / Math.max(1, sb.viewH - 1);
888
+ this.setChatScroll(Math.round((1 - rel) * sb.scrollMax));
876
889
  }
877
- this.selection = { anchorY: mouse.y, focusY: mouse.y };
878
- this.requestRender();
890
+ this.scrollbarDrag = { grabY: y, startOffset: this.chatScrollOffset };
891
+ this.pendingClick = undefined;
879
892
  return;
880
893
  }
881
- if (!this.selection)
894
+ if (mouse.kind === "button" && mouse.button === 0 && !mouse.release) {
895
+ this.pendingClick = { x: mouse.x, y: mouse.y };
882
896
  return;
883
- if (mouse.kind === "move") {
884
- this.selection.focusY = mouse.y;
885
- this.requestRender();
897
+ }
898
+ if (mouse.kind === "move" && this.pendingClick) {
899
+ const dx = Math.abs(mouse.x - this.pendingClick.x);
900
+ const dy = Math.abs(mouse.y - this.pendingClick.y);
901
+ if (dx + dy > 1) {
902
+ this.pendingClick = undefined;
903
+ }
886
904
  return;
887
905
  }
888
- if (mouse.kind === "button" && mouse.release) {
889
- this.selection.focusY = mouse.y;
890
- const text = this.selectedText();
891
- this.selection = undefined;
892
- this.requestRender();
893
- if (text)
894
- this.onTextSelected?.(text);
906
+ if (mouse.kind === "button" && mouse.button === 0 && mouse.release) {
907
+ const pending = this.pendingClick;
908
+ this.pendingClick = undefined;
909
+ if (!pending)
910
+ return;
911
+ if (this.dispatchChatClick(pending.y) || this.dispatchDockClick(pending.y)) {
912
+ this.requestRender();
913
+ }
895
914
  }
896
915
  }
897
- hitSelectable(y) {
898
- return this.chatHitRegions.find((region) => y >= region.y0 + 1 && y <= region.y1 + 1);
916
+ dispatchChatClick(mouseY) {
917
+ const cache = this.chatLayoutCache;
918
+ if (!cache || mouseY < 1 || mouseY > this.lastChatSliceHeight)
919
+ return false;
920
+ const lineIndex = this.lastChatStart + (mouseY - 1);
921
+ const hit = cache.ranges.find((range) => lineIndex >= range.start && lineIndex < range.end);
922
+ if (!hit || typeof hit.component.handleClick !== "function")
923
+ return false;
924
+ return hit.component.handleClick(lineIndex - hit.start, cache.chatWidth) === true;
899
925
  }
900
- selectedText() {
901
- if (!this.selection)
902
- return undefined;
903
- const lo = Math.min(this.selection.anchorY, this.selection.focusY);
904
- const hi = Math.max(this.selection.anchorY, this.selection.focusY);
905
- if (lo === hi)
906
- return undefined;
907
- const lines = [];
908
- for (const region of this.chatHitRegions) {
909
- for (let i = 0; i < region.lines.length; i++) {
910
- const y = region.y0 + i + 1;
911
- if (y < lo || y > hi)
912
- continue;
913
- lines.push(stripAnsiForCopy(region.lines[i] ?? ""));
926
+ dispatchDockClick(mouseY) {
927
+ if (mouseY <= this.lastChatSliceHeight)
928
+ return false;
929
+ const pin = this.getPinIndex();
930
+ if (pin < 0)
931
+ return false;
932
+ const width = this.terminal.columns;
933
+ let y = this.lastChatSliceHeight;
934
+ const local = mouseY - 1;
935
+ for (let i = pin; i < this.children.length; i++) {
936
+ const child = this.children[i];
937
+ if (!child)
938
+ continue;
939
+ const h = child.render(width).length;
940
+ if (local >= y && local < y + h) {
941
+ if (typeof child.handleClick === "function") {
942
+ return child.handleClick(local - y, width);
943
+ }
944
+ if (child instanceof Container) {
945
+ return child.handleClick(local - y, width);
946
+ }
947
+ return false;
914
948
  }
949
+ y += h;
915
950
  }
916
- const text = lines.join("\n").trim();
917
- return text.length > 0 ? text : undefined;
951
+ return false;
952
+ }
953
+ dragScrollbarTo(mouseY) {
954
+ if (!this.scrollbarDrag || !this.chatScrollbar)
955
+ return;
956
+ const { viewH, thumbH, scrollMax, y0, y1 } = this.chatScrollbar;
957
+ const travel = viewH - thumbH;
958
+ if (travel <= 0 || scrollMax <= 0)
959
+ return;
960
+ const y = Math.max(y0, Math.min(mouseY, y1));
961
+ const deltaY = y - this.scrollbarDrag.grabY;
962
+ this.setChatScroll(this.scrollbarDrag.startOffset - Math.round((deltaY * scrollMax) / travel));
918
963
  }
919
964
  handleInput(data) {
920
965
  if (this.consumeOsc11BackgroundResponse(data)) {
@@ -1791,22 +1836,4 @@ function padToWidth(line, width) {
1791
1836
  return line;
1792
1837
  return line + " ".repeat(width - vis);
1793
1838
  }
1794
- function applySelectionHighlight(line, width) {
1795
- const padded = padToWidth(line, width);
1796
- return `\x1b[7m${padded}\x1b[27m`;
1797
- }
1798
- function stripAnsiForCopy(line) {
1799
- let out = "";
1800
- let i = 0;
1801
- while (i < line.length) {
1802
- const ansi = extractAnsiCode(line, i);
1803
- if (ansi) {
1804
- i += ansi.length;
1805
- continue;
1806
- }
1807
- out += line[i];
1808
- i++;
1809
- }
1810
- return out.replace(/\s+$/, "");
1811
- }
1812
1839
  //# sourceMappingURL=tui.js.map