@ashx-j/lunr-tui 0.1.7 → 0.2.0
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/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 +14 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +155 -10
- 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;
|
|
@@ -148,6 +150,9 @@ export class TUI extends Container {
|
|
|
148
150
|
alternateScreenActive = false;
|
|
149
151
|
mouseTrackingActive = false;
|
|
150
152
|
static WHEEL_LINES = 3;
|
|
153
|
+
chatHitRegions = [];
|
|
154
|
+
chatScrollbar;
|
|
155
|
+
selection;
|
|
151
156
|
constructor(terminal, showHardwareCursor) {
|
|
152
157
|
super();
|
|
153
158
|
this.terminal = terminal;
|
|
@@ -376,12 +381,26 @@ export class TUI extends Container {
|
|
|
376
381
|
const child = this.children[i];
|
|
377
382
|
if (!child)
|
|
378
383
|
continue;
|
|
379
|
-
|
|
380
|
-
lines.push(line);
|
|
381
|
-
}
|
|
384
|
+
this.appendRenderedChild(lines, child, width);
|
|
382
385
|
}
|
|
383
386
|
return lines;
|
|
384
387
|
}
|
|
388
|
+
appendRenderedChild(lines, child, width) {
|
|
389
|
+
const nested = "children" in child && Array.isArray(child.children);
|
|
390
|
+
if (nested && child.children.length > 0 && child.render === Container.prototype.render) {
|
|
391
|
+
for (const nestedChild of child.children) {
|
|
392
|
+
this.appendRenderedChild(lines, nestedChild, width);
|
|
393
|
+
}
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const start = lines.length;
|
|
397
|
+
for (const line of child.render(width)) {
|
|
398
|
+
lines.push(line);
|
|
399
|
+
}
|
|
400
|
+
if (child.selectable && lines.length > start) {
|
|
401
|
+
this.chatHitRegions.push({ y0: start, y1: lines.length - 1, lines: lines.slice(start) });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
385
404
|
snapStartToKittyImageHeader(lines, start) {
|
|
386
405
|
if (start <= 0 || start >= lines.length)
|
|
387
406
|
return start;
|
|
@@ -403,11 +422,12 @@ export class TUI extends Container {
|
|
|
403
422
|
render(width) {
|
|
404
423
|
const pinIndex = this.getPinIndex();
|
|
405
424
|
if (pinIndex < 0) {
|
|
425
|
+
this.chatHitRegions = [];
|
|
426
|
+
this.chatScrollbar = undefined;
|
|
406
427
|
return super.render(width);
|
|
407
428
|
}
|
|
408
429
|
const rows = Math.max(1, this.terminal.rows);
|
|
409
430
|
const minView = 1;
|
|
410
|
-
const scrollLines = this.collectChildLines(0, pinIndex, width);
|
|
411
431
|
let dockLines = this.collectChildLines(pinIndex, this.children.length, width);
|
|
412
432
|
const maxDock = rows - minView;
|
|
413
433
|
if (dockLines.length > maxDock) {
|
|
@@ -415,26 +435,66 @@ export class TUI extends Container {
|
|
|
415
435
|
}
|
|
416
436
|
const viewH = Math.max(minView, rows - dockLines.length);
|
|
417
437
|
this.lastChatViewportHeight = viewH;
|
|
438
|
+
this.chatHitRegions = [];
|
|
439
|
+
const chatWidthProbe = this.collectChildLines(0, pinIndex, width);
|
|
440
|
+
const showScrollbar = chatWidthProbe.length > viewH;
|
|
441
|
+
const chatWidth = showScrollbar ? Math.max(1, width - 1) : width;
|
|
442
|
+
let scrollLines = chatWidthProbe;
|
|
443
|
+
if (showScrollbar) {
|
|
444
|
+
this.chatHitRegions = [];
|
|
445
|
+
scrollLines = this.collectChildLines(0, pinIndex, chatWidth);
|
|
446
|
+
}
|
|
418
447
|
let chatSlice;
|
|
448
|
+
let start = 0;
|
|
419
449
|
if (scrollLines.length <= viewH) {
|
|
420
450
|
// Content-hug: do not pin the dock to the bottom of an unfilled screen.
|
|
421
451
|
this.chatScrollOffset = 0;
|
|
422
452
|
this.lastChatScrollMax = 0;
|
|
453
|
+
this.chatScrollbar = undefined;
|
|
423
454
|
chatSlice = scrollLines;
|
|
424
455
|
}
|
|
425
456
|
else {
|
|
426
457
|
this.lastChatScrollMax = scrollLines.length - viewH;
|
|
427
458
|
this.chatScrollOffset = Math.max(0, Math.min(this.chatScrollOffset, this.lastChatScrollMax));
|
|
428
|
-
|
|
459
|
+
start = scrollLines.length - viewH - this.chatScrollOffset;
|
|
429
460
|
start = this.snapStartToKittyImageHeader(scrollLines, start);
|
|
430
461
|
start = Math.max(0, start);
|
|
431
462
|
chatSlice = scrollLines.slice(start, start + viewH);
|
|
432
463
|
while (chatSlice.length < viewH) {
|
|
433
464
|
chatSlice.push("");
|
|
434
465
|
}
|
|
466
|
+
this.chatScrollbar = { x: width, y0: 1, y1: viewH, viewH, scrollMax: this.lastChatScrollMax };
|
|
467
|
+
const thumbH = Math.max(1, Math.round((viewH * viewH) / scrollLines.length));
|
|
468
|
+
const travel = Math.max(0, viewH - thumbH);
|
|
469
|
+
const thumbTop = this.lastChatScrollMax === 0
|
|
470
|
+
? viewH - thumbH
|
|
471
|
+
: Math.round(travel * (1 - this.chatScrollOffset / this.lastChatScrollMax));
|
|
472
|
+
const track = "\x1b[2m│\x1b[22m";
|
|
473
|
+
const thumb = "\x1b[1m█\x1b[22m";
|
|
474
|
+
for (let i = 0; i < chatSlice.length; i++) {
|
|
475
|
+
const glyph = i >= thumbTop && i < thumbTop + thumbH ? thumb : track;
|
|
476
|
+
chatSlice[i] = `${padToWidth(chatSlice[i] ?? "", chatWidth)}${glyph}`;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
this.shiftHitRegions(-start);
|
|
480
|
+
if (this.selection) {
|
|
481
|
+
const lo = Math.min(this.selection.anchorY, this.selection.focusY);
|
|
482
|
+
const hi = Math.max(this.selection.anchorY, this.selection.focusY);
|
|
483
|
+
for (let y = lo; y <= hi; y++) {
|
|
484
|
+
const idx = y - 1;
|
|
485
|
+
if (idx < 0 || idx >= chatSlice.length)
|
|
486
|
+
continue;
|
|
487
|
+
chatSlice[idx] = applySelectionHighlight(chatSlice[idx] ?? "", showScrollbar ? chatWidth : width);
|
|
488
|
+
}
|
|
435
489
|
}
|
|
436
490
|
return [...chatSlice, ...dockLines];
|
|
437
491
|
}
|
|
492
|
+
shiftHitRegions(delta) {
|
|
493
|
+
for (const region of this.chatHitRegions) {
|
|
494
|
+
region.y0 += delta;
|
|
495
|
+
region.y1 += delta;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
438
498
|
/**
|
|
439
499
|
* Show an overlay component with configurable positioning and sizing.
|
|
440
500
|
* Returns a handle to control the overlay's visibility.
|
|
@@ -731,14 +791,75 @@ export class TUI extends Container {
|
|
|
731
791
|
this.mouseTrackingActive = false;
|
|
732
792
|
}
|
|
733
793
|
handleMouseEvent(mouse) {
|
|
734
|
-
if (mouse.kind !== "wheel" || mouse.delta === 0)
|
|
735
|
-
return;
|
|
736
794
|
if (!this.pinComponent || this.getPinIndex() < 0)
|
|
737
795
|
return;
|
|
738
796
|
if (this.hasCapturingOverlayFocus())
|
|
739
797
|
return;
|
|
740
|
-
|
|
741
|
-
|
|
798
|
+
if (mouse.kind === "wheel") {
|
|
799
|
+
if (mouse.delta === 0)
|
|
800
|
+
return;
|
|
801
|
+
const amount = mouse.ctrl ? this.getChatViewportHeight() : TUI.WHEEL_LINES;
|
|
802
|
+
this.scrollChat(mouse.delta * amount);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
// Shift+drag stays with the terminal's native selection.
|
|
806
|
+
if (mouse.shift)
|
|
807
|
+
return;
|
|
808
|
+
if (mouse.kind === "button" && !mouse.release && this.chatScrollbar && mouse.x === this.chatScrollbar.x) {
|
|
809
|
+
const viewH = this.chatScrollbar.viewH;
|
|
810
|
+
const y = Math.max(this.chatScrollbar.y0, Math.min(mouse.y, this.chatScrollbar.y1));
|
|
811
|
+
const rel = (y - this.chatScrollbar.y0) / Math.max(1, viewH - 1);
|
|
812
|
+
this.setChatScroll(Math.round((1 - rel) * this.chatScrollbar.scrollMax));
|
|
813
|
+
this.selection = undefined;
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (mouse.kind === "button" && !mouse.release) {
|
|
817
|
+
const region = this.hitSelectable(mouse.y);
|
|
818
|
+
if (!region) {
|
|
819
|
+
this.selection = undefined;
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
this.selection = { anchorY: mouse.y, focusY: mouse.y };
|
|
823
|
+
this.requestRender();
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (!this.selection)
|
|
827
|
+
return;
|
|
828
|
+
if (mouse.kind === "move") {
|
|
829
|
+
this.selection.focusY = mouse.y;
|
|
830
|
+
this.requestRender();
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (mouse.kind === "button" && mouse.release) {
|
|
834
|
+
this.selection.focusY = mouse.y;
|
|
835
|
+
const text = this.selectedText();
|
|
836
|
+
this.selection = undefined;
|
|
837
|
+
this.requestRender();
|
|
838
|
+
if (text)
|
|
839
|
+
this.onTextSelected?.(text);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
hitSelectable(y) {
|
|
843
|
+
return this.chatHitRegions.find((region) => y >= region.y0 + 1 && y <= region.y1 + 1);
|
|
844
|
+
}
|
|
845
|
+
selectedText() {
|
|
846
|
+
if (!this.selection)
|
|
847
|
+
return undefined;
|
|
848
|
+
const lo = Math.min(this.selection.anchorY, this.selection.focusY);
|
|
849
|
+
const hi = Math.max(this.selection.anchorY, this.selection.focusY);
|
|
850
|
+
if (lo === hi)
|
|
851
|
+
return undefined;
|
|
852
|
+
const lines = [];
|
|
853
|
+
for (const region of this.chatHitRegions) {
|
|
854
|
+
for (let i = 0; i < region.lines.length; i++) {
|
|
855
|
+
const y = region.y0 + i + 1;
|
|
856
|
+
if (y < lo || y > hi)
|
|
857
|
+
continue;
|
|
858
|
+
lines.push(stripAnsiForCopy(region.lines[i] ?? ""));
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const text = lines.join("\n").trim();
|
|
862
|
+
return text.length > 0 ? text : undefined;
|
|
742
863
|
}
|
|
743
864
|
handleInput(data) {
|
|
744
865
|
if (this.consumeOsc11BackgroundResponse(data)) {
|
|
@@ -1609,4 +1730,28 @@ export class TUI extends Container {
|
|
|
1609
1730
|
});
|
|
1610
1731
|
}
|
|
1611
1732
|
}
|
|
1733
|
+
function padToWidth(line, width) {
|
|
1734
|
+
const vis = visibleWidth(line);
|
|
1735
|
+
if (vis >= width)
|
|
1736
|
+
return line;
|
|
1737
|
+
return line + " ".repeat(width - vis);
|
|
1738
|
+
}
|
|
1739
|
+
function applySelectionHighlight(line, width) {
|
|
1740
|
+
const padded = padToWidth(line, width);
|
|
1741
|
+
return `\x1b[7m${padded}\x1b[27m`;
|
|
1742
|
+
}
|
|
1743
|
+
function stripAnsiForCopy(line) {
|
|
1744
|
+
let out = "";
|
|
1745
|
+
let i = 0;
|
|
1746
|
+
while (i < line.length) {
|
|
1747
|
+
const ansi = extractAnsiCode(line, i);
|
|
1748
|
+
if (ansi) {
|
|
1749
|
+
i += ansi.length;
|
|
1750
|
+
continue;
|
|
1751
|
+
}
|
|
1752
|
+
out += line[i];
|
|
1753
|
+
i++;
|
|
1754
|
+
}
|
|
1755
|
+
return out.replace(/\s+$/, "");
|
|
1756
|
+
}
|
|
1612
1757
|
//# sourceMappingURL=tui.js.map
|