@ashx-j/lunr-tui 0.2.12 → 0.2.13-dev.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/components/box.d.ts +3 -8
  2. package/dist/components/box.d.ts.map +1 -1
  3. package/dist/components/box.js +8 -27
  4. package/dist/components/box.js.map +1 -1
  5. package/dist/components/editor.d.ts +1 -0
  6. package/dist/components/editor.d.ts.map +1 -1
  7. package/dist/components/editor.js +3 -2
  8. package/dist/components/editor.js.map +1 -1
  9. package/dist/components/input.d.ts.map +1 -1
  10. package/dist/components/input.js +8 -4
  11. package/dist/components/input.js.map +1 -1
  12. package/dist/components/markdown.d.ts.map +1 -1
  13. package/dist/components/markdown.js +2 -2
  14. package/dist/components/markdown.js.map +1 -1
  15. package/dist/components/text.d.ts.map +1 -1
  16. package/dist/components/text.js +2 -2
  17. package/dist/components/text.js.map +1 -1
  18. package/dist/components/truncated-text.d.ts.map +1 -1
  19. package/dist/components/truncated-text.js +5 -4
  20. package/dist/components/truncated-text.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/stdin-buffer.d.ts.map +1 -1
  26. package/dist/stdin-buffer.js +7 -4
  27. package/dist/stdin-buffer.js.map +1 -1
  28. package/dist/terminal.d.ts.map +1 -1
  29. package/dist/terminal.js +2 -1
  30. package/dist/terminal.js.map +1 -1
  31. package/dist/tui.d.ts +9 -2
  32. package/dist/tui.d.ts.map +1 -1
  33. package/dist/tui.js +98 -30
  34. package/dist/tui.js.map +1 -1
  35. package/dist/utils.d.ts +4 -0
  36. package/dist/utils.d.ts.map +1 -1
  37. package/dist/utils.js +112 -0
  38. package/dist/utils.js.map +1 -1
  39. package/package.json +1 -1
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 { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
12
+ import { extractSegments, normalizeTerminalOutput, sanitizeTerminalOutput, 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);
@@ -77,29 +77,57 @@ function isTermuxSession() {
77
77
  */
78
78
  export class Container {
79
79
  children = [];
80
+ revision = 0;
81
+ parents = new Set();
80
82
  addChild(component) {
81
83
  this.children.push(component);
84
+ if (component instanceof Container)
85
+ component.parents.add(this);
86
+ this.markDirty();
82
87
  }
83
88
  removeChild(component) {
84
89
  const index = this.children.indexOf(component);
85
90
  if (index !== -1) {
86
91
  this.children.splice(index, 1);
92
+ if (component instanceof Container && !this.children.includes(component))
93
+ component.parents.delete(this);
94
+ this.markDirty();
87
95
  }
88
96
  }
89
97
  clear() {
98
+ for (const child of this.children) {
99
+ if (child instanceof Container)
100
+ child.parents.delete(this);
101
+ }
90
102
  this.children = [];
103
+ this.markDirty();
91
104
  }
92
105
  invalidate() {
106
+ this.markDirty();
93
107
  for (const child of this.children) {
94
108
  child.invalidate?.();
95
109
  }
96
110
  }
111
+ getRenderRevision() {
112
+ return this.revision;
113
+ }
114
+ syncChildParents() {
115
+ for (const child of this.children) {
116
+ if (child instanceof Container)
117
+ child.parents.add(this);
118
+ }
119
+ }
120
+ markDirty() {
121
+ this.revision++;
122
+ for (const parent of this.parents)
123
+ parent.markDirty();
124
+ }
97
125
  render(width) {
98
126
  const lines = [];
99
127
  for (const child of this.children) {
100
128
  const childLines = child.render(width);
101
129
  for (const line of childLines) {
102
- lines.push(line);
130
+ lines.push(sanitizeTerminalOutput(line, isImageLine(line)));
103
131
  }
104
132
  }
105
133
  return lines;
@@ -162,8 +190,6 @@ export class TUI extends Container {
162
190
  lastChatScrollMax = 0;
163
191
  chatLaidOut = false;
164
192
  chatUsesGutter = false;
165
- /** Bumped on non-scroll `requestRender` so offset-only paints can reuse chat layout. */
166
- contentEpoch = 0;
167
193
  chatLayoutCache;
168
194
  alternateScreen = false;
169
195
  alternateScreenActive = false;
@@ -181,6 +207,11 @@ export class TUI extends Container {
181
207
  this.showHardwareCursor = showHardwareCursor;
182
208
  }
183
209
  }
210
+ markDirty() {
211
+ super.markDirty();
212
+ if (this.started)
213
+ this.queueRender(false);
214
+ }
184
215
  get fullRedraws() {
185
216
  return this.fullRedrawCount;
186
217
  }
@@ -403,6 +434,23 @@ export class TUI extends Container {
403
434
  return -1;
404
435
  return this.children.indexOf(this.pinComponent);
405
436
  }
437
+ captureChatComponentState(to) {
438
+ return this.children.slice(0, to).map((component) => ({
439
+ component,
440
+ revision: component instanceof Container ? component.getRenderRevision() : 0,
441
+ childCount: component instanceof Container ? component.children.length : undefined,
442
+ firstChild: component instanceof Container ? component.children[0] : undefined,
443
+ lastChild: component instanceof Container ? component.children[component.children.length - 1] : undefined,
444
+ }));
445
+ }
446
+ chatComponentStateMatches(cached, current) {
447
+ return (cached.length === current.length &&
448
+ cached.every((state, index) => state.component === current[index]?.component &&
449
+ state.revision === current[index]?.revision &&
450
+ state.childCount === current[index]?.childCount &&
451
+ state.firstChild === current[index]?.firstChild &&
452
+ state.lastChild === current[index]?.lastChild));
453
+ }
406
454
  collectChildLines(from, to, width, ranges) {
407
455
  const lines = [];
408
456
  for (let i = from; i < to; i++) {
@@ -423,7 +471,7 @@ export class TUI extends Container {
423
471
  }
424
472
  const start = lines.length;
425
473
  for (const line of child.render(width)) {
426
- lines.push(line);
474
+ lines.push(sanitizeTerminalOutput(line, isImageLine(line)));
427
475
  }
428
476
  ranges?.push({ component: child, start, end: lines.length });
429
477
  }
@@ -465,17 +513,25 @@ export class TUI extends Container {
465
513
  const viewH = Math.max(minView, rows - dockLines.length);
466
514
  this.lastChatViewportHeight = viewH;
467
515
  this.chatLaidOut = true;
468
- let chatWidth = this.chatUsesGutter ? Math.max(1, width - 1) : width;
516
+ const canUseGutter = width >= 2;
517
+ if (!canUseGutter)
518
+ this.chatUsesGutter = false;
519
+ let chatWidth = this.chatUsesGutter ? width - 1 : width;
469
520
  let scrollLines;
470
521
  let ranges;
471
522
  let fromCache = false;
472
523
  const cache = this.chatLayoutCache;
473
- if (cache && cache.epoch === this.contentEpoch && cache.chatWidth === chatWidth) {
524
+ const componentState = this.captureChatComponentState(pinIndex);
525
+ if (cache && cache.chatWidth === chatWidth && this.chatComponentStateMatches(cache.components, componentState)) {
474
526
  scrollLines = cache.scrollLines;
475
527
  ranges = cache.ranges;
476
528
  fromCache = true;
477
529
  }
478
530
  else {
531
+ for (const state of componentState) {
532
+ if (state.component instanceof Container)
533
+ state.component.syncChildParents();
534
+ }
479
535
  ranges = [];
480
536
  scrollLines = this.collectChildLines(0, pinIndex, chatWidth, ranges);
481
537
  }
@@ -488,17 +544,17 @@ export class TUI extends Container {
488
544
  fromCache = false;
489
545
  }
490
546
  }
491
- else if (scrollLines.length > viewH) {
547
+ else if (canUseGutter && scrollLines.length > viewH) {
492
548
  this.chatUsesGutter = true;
493
- chatWidth = Math.max(1, width - 1);
549
+ chatWidth = width - 1;
494
550
  ranges = [];
495
551
  scrollLines = this.collectChildLines(0, pinIndex, chatWidth, ranges);
496
552
  fromCache = false;
497
553
  }
498
554
  if (!fromCache) {
499
555
  this.chatLayoutCache = {
500
- epoch: this.contentEpoch,
501
556
  chatWidth,
557
+ components: this.captureChatComponentState(pinIndex),
502
558
  scrollLines,
503
559
  ranges,
504
560
  };
@@ -518,7 +574,8 @@ export class TUI extends Container {
518
574
  this.lastChatScrollMax = scrollLines.length - viewH;
519
575
  this.chatScrollOffset = Math.max(0, Math.min(this.chatScrollOffset, this.lastChatScrollMax));
520
576
  start = scrollLines.length - viewH - this.chatScrollOffset;
521
- start = this.snapStartToKittyImageHeader(scrollLines, start);
577
+ if (this.chatScrollOffset > 0)
578
+ start = this.snapStartToKittyImageHeader(scrollLines, start);
522
579
  start = Math.max(0, start);
523
580
  this.lastChatStart = start;
524
581
  chatSlice = scrollLines.slice(start, start + viewH);
@@ -530,26 +587,34 @@ export class TUI extends Container {
530
587
  const thumbTop = this.lastChatScrollMax === 0
531
588
  ? viewH - thumbH
532
589
  : Math.round(travel * (1 - this.chatScrollOffset / this.lastChatScrollMax));
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`;
546
- for (let i = 0; i < chatSlice.length; i++) {
547
- const glyph = i >= thumbTop && i < thumbTop + thumbH ? thumb : track;
548
- chatSlice[i] = `${padToWidth(chatSlice[i] ?? "", chatWidth)}${glyph}`;
590
+ if (this.chatUsesGutter) {
591
+ this.chatScrollbar = {
592
+ x: width,
593
+ y0: 1,
594
+ y1: viewH,
595
+ viewH,
596
+ scrollMax: this.lastChatScrollMax,
597
+ thumbTop,
598
+ thumbH,
599
+ };
600
+ const RESET = "\x1b[0m";
601
+ const track = `${RESET}\x1b[2m│\x1b[0m`;
602
+ const thumb = `${RESET}\x1b[1m█\x1b[0m`;
603
+ for (let i = 0; i < chatSlice.length; i++) {
604
+ const glyph = i >= thumbTop && i < thumbTop + thumbH ? thumb : track;
605
+ chatSlice[i] = `${padToWidth(chatSlice[i] ?? "", chatWidth)}${glyph}`;
606
+ }
607
+ }
608
+ else {
609
+ this.chatScrollbar = undefined;
610
+ this.scrollbarDrag = undefined;
549
611
  }
550
612
  }
551
613
  this.lastChatSliceHeight = chatSlice.length;
552
- return [...chatSlice, ...dockLines];
614
+ const frame = [...chatSlice, ...dockLines];
615
+ if (width > 1)
616
+ return frame;
617
+ return frame.map((line) => (visibleWidth(line) > width ? sliceByColumn(line, 0, width, true) : line));
553
618
  }
554
619
  /**
555
620
  * Show an overlay component with configurable positioning and sizing.
@@ -779,9 +844,12 @@ export class TUI extends Container {
779
844
  this.terminal.stop();
780
845
  }
781
846
  requestRender(force = false) {
782
- this.contentEpoch++;
847
+ this.chatLayoutCache = undefined;
783
848
  this.queueRender(force);
784
849
  }
850
+ requestPaint() {
851
+ this.queueRender(false);
852
+ }
785
853
  requestRenderFromScroll() {
786
854
  this.queueRender(false);
787
855
  }
@@ -1267,7 +1335,7 @@ export class TUI extends Container {
1267
1335
  for (let i = 0; i < lines.length; i++) {
1268
1336
  const line = lines[i];
1269
1337
  if (!isImageLine(line)) {
1270
- lines[i] = normalizeTerminalOutput(line) + reset;
1338
+ lines[i] = normalizeTerminalOutput(sanitizeTerminalOutput(line)) + reset;
1271
1339
  }
1272
1340
  }
1273
1341
  return lines;