@ashx-j/lunr-tui 0.1.7 → 0.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.
- package/dist/components/box.d.ts +5 -1
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js +20 -4
- package/dist/components/box.js.map +1 -1
- package/dist/mouse.d.ts +2 -2
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +2 -2
- package/dist/mouse.js.map +1 -1
- package/dist/tui.d.ts +22 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +217 -17
- package/dist/tui.js.map +1 -1
- 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 { extractAnsiCode, 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);
|
|
@@ -118,6 +118,8 @@ export class TUI extends Container {
|
|
|
118
118
|
inputListeners = new Set();
|
|
119
119
|
/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
|
|
120
120
|
onDebug;
|
|
121
|
+
/** Called with selected user/assistant text after an in-app drag-release. */
|
|
122
|
+
onTextSelected;
|
|
121
123
|
renderRequested = false;
|
|
122
124
|
renderTimer;
|
|
123
125
|
lastRenderAt = 0;
|
|
@@ -144,10 +146,18 @@ export class TUI extends Container {
|
|
|
144
146
|
chatScrollOffset = 0;
|
|
145
147
|
lastChatViewportHeight = 1;
|
|
146
148
|
lastChatScrollMax = 0;
|
|
149
|
+
chatLaidOut = false;
|
|
150
|
+
chatUsesGutter = false;
|
|
151
|
+
/** Bumped on non-scroll `requestRender` so offset-only paints can reuse chat layout. */
|
|
152
|
+
contentEpoch = 0;
|
|
153
|
+
chatLayoutCache;
|
|
147
154
|
alternateScreen = false;
|
|
148
155
|
alternateScreenActive = false;
|
|
149
156
|
mouseTrackingActive = false;
|
|
150
157
|
static WHEEL_LINES = 3;
|
|
158
|
+
chatHitRegions = [];
|
|
159
|
+
chatScrollbar;
|
|
160
|
+
selection;
|
|
151
161
|
constructor(terminal, showHardwareCursor) {
|
|
152
162
|
super();
|
|
153
163
|
this.terminal = terminal;
|
|
@@ -187,6 +197,9 @@ export class TUI extends Container {
|
|
|
187
197
|
*/
|
|
188
198
|
pinFrom(component) {
|
|
189
199
|
this.pinComponent = component;
|
|
200
|
+
this.chatLayoutCache = undefined;
|
|
201
|
+
this.chatUsesGutter = false;
|
|
202
|
+
this.chatLaidOut = false;
|
|
190
203
|
if (!component) {
|
|
191
204
|
this.chatScrollOffset = 0;
|
|
192
205
|
this.lastChatViewportHeight = Math.max(1, this.terminal.rows);
|
|
@@ -202,16 +215,20 @@ export class TUI extends Container {
|
|
|
202
215
|
/** `0` follows the latest chat. Larger values scroll up into history. */
|
|
203
216
|
setChatScroll(offset) {
|
|
204
217
|
const next = Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0;
|
|
205
|
-
this.
|
|
206
|
-
|
|
207
|
-
this.render(this.terminal.columns);
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
218
|
+
if (this.getPinIndex() < 0) {
|
|
219
|
+
const changed = this.chatScrollOffset !== 0 || this.lastChatScrollMax !== 0;
|
|
210
220
|
this.chatScrollOffset = 0;
|
|
211
221
|
this.lastChatScrollMax = 0;
|
|
222
|
+
if (changed && this.started)
|
|
223
|
+
this.requestRender();
|
|
224
|
+
return;
|
|
212
225
|
}
|
|
226
|
+
const clamped = this.chatLaidOut ? Math.min(next, this.lastChatScrollMax) : next;
|
|
227
|
+
if (clamped === this.chatScrollOffset)
|
|
228
|
+
return;
|
|
229
|
+
this.chatScrollOffset = clamped;
|
|
213
230
|
if (this.started)
|
|
214
|
-
this.
|
|
231
|
+
this.requestRenderFromScroll();
|
|
215
232
|
}
|
|
216
233
|
/** Positive delta scrolls up (older). Negative scrolls toward the latest. */
|
|
217
234
|
scrollChat(deltaLines) {
|
|
@@ -376,12 +393,26 @@ export class TUI extends Container {
|
|
|
376
393
|
const child = this.children[i];
|
|
377
394
|
if (!child)
|
|
378
395
|
continue;
|
|
379
|
-
|
|
380
|
-
lines.push(line);
|
|
381
|
-
}
|
|
396
|
+
this.appendRenderedChild(lines, child, width);
|
|
382
397
|
}
|
|
383
398
|
return lines;
|
|
384
399
|
}
|
|
400
|
+
appendRenderedChild(lines, child, width) {
|
|
401
|
+
const nested = "children" in child && Array.isArray(child.children);
|
|
402
|
+
if (nested && child.children.length > 0 && child.render === Container.prototype.render) {
|
|
403
|
+
for (const nestedChild of child.children) {
|
|
404
|
+
this.appendRenderedChild(lines, nestedChild, width);
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const start = lines.length;
|
|
409
|
+
for (const line of child.render(width)) {
|
|
410
|
+
lines.push(line);
|
|
411
|
+
}
|
|
412
|
+
if (child.selectable && lines.length > start) {
|
|
413
|
+
this.chatHitRegions.push({ y0: start, y1: lines.length - 1, lines: lines.slice(start) });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
385
416
|
snapStartToKittyImageHeader(lines, start) {
|
|
386
417
|
if (start <= 0 || start >= lines.length)
|
|
387
418
|
return start;
|
|
@@ -403,11 +434,15 @@ export class TUI extends Container {
|
|
|
403
434
|
render(width) {
|
|
404
435
|
const pinIndex = this.getPinIndex();
|
|
405
436
|
if (pinIndex < 0) {
|
|
437
|
+
this.chatHitRegions = [];
|
|
438
|
+
this.chatScrollbar = undefined;
|
|
439
|
+
this.chatLayoutCache = undefined;
|
|
440
|
+
this.chatUsesGutter = false;
|
|
441
|
+
this.chatLaidOut = false;
|
|
406
442
|
return super.render(width);
|
|
407
443
|
}
|
|
408
444
|
const rows = Math.max(1, this.terminal.rows);
|
|
409
445
|
const minView = 1;
|
|
410
|
-
const scrollLines = this.collectChildLines(0, pinIndex, width);
|
|
411
446
|
let dockLines = this.collectChildLines(pinIndex, this.children.length, width);
|
|
412
447
|
const maxDock = rows - minView;
|
|
413
448
|
if (dockLines.length > maxDock) {
|
|
@@ -415,26 +450,99 @@ export class TUI extends Container {
|
|
|
415
450
|
}
|
|
416
451
|
const viewH = Math.max(minView, rows - dockLines.length);
|
|
417
452
|
this.lastChatViewportHeight = viewH;
|
|
453
|
+
this.chatLaidOut = true;
|
|
454
|
+
let chatWidth = this.chatUsesGutter ? Math.max(1, width - 1) : width;
|
|
455
|
+
let scrollLines;
|
|
456
|
+
let fromCache = false;
|
|
457
|
+
const cache = this.chatLayoutCache;
|
|
458
|
+
if (cache && cache.epoch === this.contentEpoch && cache.chatWidth === chatWidth) {
|
|
459
|
+
scrollLines = cache.scrollLines;
|
|
460
|
+
this.chatHitRegions = this.copyHitRegions(cache.hitRegionsUnshifted);
|
|
461
|
+
fromCache = true;
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
this.chatHitRegions = [];
|
|
465
|
+
scrollLines = this.collectChildLines(0, pinIndex, chatWidth);
|
|
466
|
+
}
|
|
467
|
+
if (this.chatUsesGutter) {
|
|
468
|
+
if (scrollLines.length <= viewH) {
|
|
469
|
+
this.chatUsesGutter = false;
|
|
470
|
+
chatWidth = width;
|
|
471
|
+
this.chatHitRegions = [];
|
|
472
|
+
scrollLines = this.collectChildLines(0, pinIndex, chatWidth);
|
|
473
|
+
fromCache = false;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
else if (scrollLines.length > viewH) {
|
|
477
|
+
this.chatUsesGutter = true;
|
|
478
|
+
chatWidth = Math.max(1, width - 1);
|
|
479
|
+
this.chatHitRegions = [];
|
|
480
|
+
scrollLines = this.collectChildLines(0, pinIndex, chatWidth);
|
|
481
|
+
fromCache = false;
|
|
482
|
+
}
|
|
483
|
+
if (!fromCache) {
|
|
484
|
+
this.chatLayoutCache = {
|
|
485
|
+
epoch: this.contentEpoch,
|
|
486
|
+
chatWidth,
|
|
487
|
+
scrollLines,
|
|
488
|
+
hitRegionsUnshifted: this.copyHitRegions(this.chatHitRegions),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
418
491
|
let chatSlice;
|
|
492
|
+
let start = 0;
|
|
419
493
|
if (scrollLines.length <= viewH) {
|
|
420
494
|
// Content-hug: do not pin the dock to the bottom of an unfilled screen.
|
|
421
495
|
this.chatScrollOffset = 0;
|
|
422
496
|
this.lastChatScrollMax = 0;
|
|
423
|
-
|
|
497
|
+
this.chatScrollbar = undefined;
|
|
498
|
+
chatSlice = scrollLines.slice();
|
|
424
499
|
}
|
|
425
500
|
else {
|
|
426
501
|
this.lastChatScrollMax = scrollLines.length - viewH;
|
|
427
502
|
this.chatScrollOffset = Math.max(0, Math.min(this.chatScrollOffset, this.lastChatScrollMax));
|
|
428
|
-
|
|
503
|
+
start = scrollLines.length - viewH - this.chatScrollOffset;
|
|
429
504
|
start = this.snapStartToKittyImageHeader(scrollLines, start);
|
|
430
505
|
start = Math.max(0, start);
|
|
431
506
|
chatSlice = scrollLines.slice(start, start + viewH);
|
|
432
507
|
while (chatSlice.length < viewH) {
|
|
433
508
|
chatSlice.push("");
|
|
434
509
|
}
|
|
510
|
+
this.chatScrollbar = { x: width, y0: 1, y1: viewH, viewH, scrollMax: this.lastChatScrollMax };
|
|
511
|
+
const thumbH = Math.max(1, Math.round((viewH * viewH) / scrollLines.length));
|
|
512
|
+
const travel = Math.max(0, viewH - thumbH);
|
|
513
|
+
const thumbTop = this.lastChatScrollMax === 0
|
|
514
|
+
? viewH - thumbH
|
|
515
|
+
: Math.round(travel * (1 - this.chatScrollOffset / this.lastChatScrollMax));
|
|
516
|
+
const track = "\x1b[2m│\x1b[22m";
|
|
517
|
+
const thumb = "\x1b[1m█\x1b[22m";
|
|
518
|
+
for (let i = 0; i < chatSlice.length; i++) {
|
|
519
|
+
const glyph = i >= thumbTop && i < thumbTop + thumbH ? thumb : track;
|
|
520
|
+
chatSlice[i] = `${padToWidth(chatSlice[i] ?? "", chatWidth)}${glyph}`;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
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
|
+
}
|
|
435
534
|
}
|
|
436
535
|
return [...chatSlice, ...dockLines];
|
|
437
536
|
}
|
|
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
|
+
}
|
|
438
546
|
/**
|
|
439
547
|
* Show an overlay component with configurable positioning and sizing.
|
|
440
548
|
* Returns a handle to control the overlay's visibility.
|
|
@@ -663,6 +771,13 @@ export class TUI extends Container {
|
|
|
663
771
|
this.terminal.stop();
|
|
664
772
|
}
|
|
665
773
|
requestRender(force = false) {
|
|
774
|
+
this.contentEpoch++;
|
|
775
|
+
this.queueRender(force);
|
|
776
|
+
}
|
|
777
|
+
requestRenderFromScroll() {
|
|
778
|
+
this.queueRender(false);
|
|
779
|
+
}
|
|
780
|
+
queueRender(force) {
|
|
666
781
|
if (force) {
|
|
667
782
|
this.previousLines = [];
|
|
668
783
|
this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
|
|
@@ -731,14 +846,75 @@ export class TUI extends Container {
|
|
|
731
846
|
this.mouseTrackingActive = false;
|
|
732
847
|
}
|
|
733
848
|
handleMouseEvent(mouse) {
|
|
734
|
-
if (mouse.kind !== "wheel" || mouse.delta === 0)
|
|
735
|
-
return;
|
|
736
849
|
if (!this.pinComponent || this.getPinIndex() < 0)
|
|
737
850
|
return;
|
|
738
851
|
if (this.hasCapturingOverlayFocus())
|
|
739
852
|
return;
|
|
740
|
-
|
|
741
|
-
|
|
853
|
+
if (mouse.kind === "wheel") {
|
|
854
|
+
if (mouse.delta === 0)
|
|
855
|
+
return;
|
|
856
|
+
const amount = mouse.ctrl ? this.getChatViewportHeight() : TUI.WHEEL_LINES;
|
|
857
|
+
this.scrollChat(mouse.delta * amount);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
// Shift+drag stays with the terminal's native selection.
|
|
861
|
+
if (mouse.shift)
|
|
862
|
+
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;
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (mouse.kind === "button" && !mouse.release) {
|
|
872
|
+
const region = this.hitSelectable(mouse.y);
|
|
873
|
+
if (!region) {
|
|
874
|
+
this.selection = undefined;
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
this.selection = { anchorY: mouse.y, focusY: mouse.y };
|
|
878
|
+
this.requestRender();
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (!this.selection)
|
|
882
|
+
return;
|
|
883
|
+
if (mouse.kind === "move") {
|
|
884
|
+
this.selection.focusY = mouse.y;
|
|
885
|
+
this.requestRender();
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
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);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
hitSelectable(y) {
|
|
898
|
+
return this.chatHitRegions.find((region) => y >= region.y0 + 1 && y <= region.y1 + 1);
|
|
899
|
+
}
|
|
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] ?? ""));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const text = lines.join("\n").trim();
|
|
917
|
+
return text.length > 0 ? text : undefined;
|
|
742
918
|
}
|
|
743
919
|
handleInput(data) {
|
|
744
920
|
if (this.consumeOsc11BackgroundResponse(data)) {
|
|
@@ -1609,4 +1785,28 @@ export class TUI extends Container {
|
|
|
1609
1785
|
});
|
|
1610
1786
|
}
|
|
1611
1787
|
}
|
|
1788
|
+
function padToWidth(line, width) {
|
|
1789
|
+
const vis = visibleWidth(line);
|
|
1790
|
+
if (vis >= width)
|
|
1791
|
+
return line;
|
|
1792
|
+
return line + " ".repeat(width - vis);
|
|
1793
|
+
}
|
|
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
|
+
}
|
|
1612
1812
|
//# sourceMappingURL=tui.js.map
|