@kolisachint/hoocode-tui 0.5.73 → 0.5.75
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 +2 -2
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js +14 -14
- package/dist/components/box.js.map +1 -1
- package/dist/tui.d.ts +31 -5
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +107 -9
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -8,7 +8,7 @@ import { performance } from "node:perf_hooks";
|
|
|
8
8
|
import { stripVTControlCharacters } from "node:util";
|
|
9
9
|
import { isKeyRelease, matchesKey } from "./keys.js";
|
|
10
10
|
import { mouseSequenceLength, parseMouseEvent } from "./mouse.js";
|
|
11
|
-
import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
|
|
11
|
+
import { allocateImageId, deleteKittyImage, getCapabilities, isImageLine, setCellDimensions, } from "./terminal-image.js";
|
|
12
12
|
import { extractSegments, hyperlinkAt, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, truncateToWidth, visibleWidth, } from "./utils.js";
|
|
13
13
|
const KITTY_SEQUENCE_PREFIX = "\x1b_G";
|
|
14
14
|
function extractKittyImageIds(line) {
|
|
@@ -31,6 +31,39 @@ function extractKittyImageIds(line) {
|
|
|
31
31
|
}
|
|
32
32
|
return [];
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Transmit the same image under a different id.
|
|
36
|
+
*
|
|
37
|
+
* Only the first chunk of a chunked transmission carries the parameter list,
|
|
38
|
+
* and that is the one `extractKittyImageIds` reads, so rewriting it is enough
|
|
39
|
+
* to make the whole sequence a second, independently deletable copy.
|
|
40
|
+
*/
|
|
41
|
+
function retagKittyImageId(line, id) {
|
|
42
|
+
const sequenceStart = line.indexOf(KITTY_SEQUENCE_PREFIX);
|
|
43
|
+
if (sequenceStart === -1)
|
|
44
|
+
return null;
|
|
45
|
+
const paramsStart = sequenceStart + KITTY_SEQUENCE_PREFIX.length;
|
|
46
|
+
const paramsEnd = line.indexOf(";", paramsStart);
|
|
47
|
+
if (paramsEnd === -1)
|
|
48
|
+
return null;
|
|
49
|
+
const params = line.slice(paramsStart, paramsEnd);
|
|
50
|
+
const retagged = params.replace(/(^|,)i=\d+/, `$1i=${id}`);
|
|
51
|
+
if (retagged === params)
|
|
52
|
+
return null;
|
|
53
|
+
return line.slice(0, paramsStart) + retagged + line.slice(paramsEnd);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* How far above its own row an image line's picture reaches.
|
|
57
|
+
*
|
|
58
|
+
* `Image` renders an n-row picture as n-1 blank lines and one line that moves
|
|
59
|
+
* the cursor back up and draws, so the leading `CSI <n> A` is the whole block's
|
|
60
|
+
* height minus one — and the top edge of the picture is that many rows above
|
|
61
|
+
* wherever the line itself is painted.
|
|
62
|
+
*/
|
|
63
|
+
function imageRowOffset(line) {
|
|
64
|
+
const match = /^\x1b\[(\d+)A/.exec(line);
|
|
65
|
+
return match ? Number(match[1]) : 0;
|
|
66
|
+
}
|
|
34
67
|
/** Type guard to check if a component implements Focusable */
|
|
35
68
|
export function isFocusable(component) {
|
|
36
69
|
return component !== null && "focused" in component;
|
|
@@ -265,6 +298,20 @@ export class TUI extends Container {
|
|
|
265
298
|
* per-frame full-buffer scan (collectKittyImageIds) is skipped entirely —
|
|
266
299
|
* the common case for a pure-text session. */
|
|
267
300
|
sawImageLine = false;
|
|
301
|
+
/**
|
|
302
|
+
* Live kitty image id -> the id the pinned window transmits its own copy
|
|
303
|
+
* under, and the copies currently placed on the alternate screen.
|
|
304
|
+
*
|
|
305
|
+
* The pinned window repaints whole, so a placement from the previous scroll
|
|
306
|
+
* position has to be deleted or it hangs over the new one. Deleting a kitty
|
|
307
|
+
* image deletes *every* placement of it, though, including the one on the
|
|
308
|
+
* normal screen — which the differential frame on the way back out has no
|
|
309
|
+
* reason to repaint, so the picture would simply be gone. The window
|
|
310
|
+
* therefore transmits a second copy under an id of its own and only ever
|
|
311
|
+
* deletes that one.
|
|
312
|
+
*/
|
|
313
|
+
scrollImageIds = new Map();
|
|
314
|
+
scrollPlacedImages = new Set();
|
|
268
315
|
static EMPTY_KITTY_IDS = new Set();
|
|
269
316
|
previousWidth = 0;
|
|
270
317
|
previousHeight = 0;
|
|
@@ -517,6 +564,7 @@ export class TUI extends Container {
|
|
|
517
564
|
return false;
|
|
518
565
|
this.scrollOffset = null;
|
|
519
566
|
this.scrollSearch = null;
|
|
567
|
+
this.releaseScrollImages();
|
|
520
568
|
this.terminal.setAlternateScreen(false);
|
|
521
569
|
// `?1049l` restores the normal screen, its scrollback and the cursor
|
|
522
570
|
// exactly as they were at `?1049h`, and the snapshot taken on the way in
|
|
@@ -941,6 +989,7 @@ export class TUI extends Container {
|
|
|
941
989
|
// the cursor relative to content that lives on the normal screen.
|
|
942
990
|
if (this.scrollOffset !== null) {
|
|
943
991
|
this.scrollOffset = null;
|
|
992
|
+
this.releaseScrollImages();
|
|
944
993
|
this.terminal.setAlternateScreen(false);
|
|
945
994
|
}
|
|
946
995
|
this.stopped = true;
|
|
@@ -1660,13 +1709,16 @@ export class TUI extends Container {
|
|
|
1660
1709
|
// Autowrap off for the paint: a full-width row would otherwise wrap into
|
|
1661
1710
|
// the row below it and shift the rest of the window down by one.
|
|
1662
1711
|
buffer += "\x1b[?7l";
|
|
1712
|
+
// A kitty placement is not text and `CSI 2 K` does not touch it, so last
|
|
1713
|
+
// frame's pictures come off before this frame's rows go down.
|
|
1714
|
+
buffer += this.clearScrollImages();
|
|
1663
1715
|
this.refreshScrollSearch(lines.length);
|
|
1664
1716
|
const query = this.scrollSearch?.query ?? "";
|
|
1665
1717
|
for (let row = 0; row < viewHeight; row++) {
|
|
1666
1718
|
buffer += `\x1b[${row + 1};1H\x1b[2K`;
|
|
1667
1719
|
const line = lines[top + row];
|
|
1668
1720
|
if (line !== undefined)
|
|
1669
|
-
buffer += this.emitScrollLine(line, query);
|
|
1721
|
+
buffer += this.emitScrollLine(line, row, query);
|
|
1670
1722
|
}
|
|
1671
1723
|
buffer += `\x1b[${height};1H\x1b[2K`;
|
|
1672
1724
|
buffer += this.scrollStatusFormatter({
|
|
@@ -1688,24 +1740,70 @@ export class TUI extends Container {
|
|
|
1688
1740
|
// its full-redraw branch — which is exactly right, because the normal
|
|
1689
1741
|
// screen `?1049l` restored was drawn at the old size.
|
|
1690
1742
|
}
|
|
1743
|
+
/** Take the pinned window's own copies of the images off the screen. */
|
|
1744
|
+
clearScrollImages() {
|
|
1745
|
+
if (this.scrollPlacedImages.size === 0)
|
|
1746
|
+
return "";
|
|
1747
|
+
const buffer = this.deleteKittyImages(this.scrollPlacedImages);
|
|
1748
|
+
this.scrollPlacedImages.clear();
|
|
1749
|
+
return buffer;
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* On the way off the alternate screen: free the copies and the ids with
|
|
1753
|
+
* them, while there is still a screen to write to. The live ids are never
|
|
1754
|
+
* touched, so the normal screen comes back with its pictures intact.
|
|
1755
|
+
*/
|
|
1756
|
+
releaseScrollImages() {
|
|
1757
|
+
const buffer = this.clearScrollImages();
|
|
1758
|
+
if (buffer)
|
|
1759
|
+
this.terminal.write(buffer);
|
|
1760
|
+
this.scrollImageIds.clear();
|
|
1761
|
+
}
|
|
1691
1762
|
/**
|
|
1692
1763
|
* One transcript row, ready for the pinned window.
|
|
1693
1764
|
*
|
|
1694
|
-
*
|
|
1695
|
-
*
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1698
|
-
*
|
|
1765
|
+
* `row` is where in the window the line lands, which decides whether an image
|
|
1766
|
+
* on it can be drawn at all: a picture reaches `imageRowOffset` rows *above*
|
|
1767
|
+
* its line, and a terminal will not draw above the first row — it clamps,
|
|
1768
|
+
* putting the picture over rows that are not its own and leaving it there.
|
|
1769
|
+
* A block hanging off the top of the window is named instead of drawn, so
|
|
1770
|
+
* scrolling one into view shows a placeholder until all of it is on screen.
|
|
1699
1771
|
*/
|
|
1700
|
-
emitScrollLine(line, query = "") {
|
|
1772
|
+
emitScrollLine(line, row, query = "") {
|
|
1701
1773
|
if (isImageLine(line))
|
|
1702
|
-
return
|
|
1774
|
+
return this.emitScrollImage(line, row);
|
|
1703
1775
|
const marker = line.indexOf(CURSOR_MARKER);
|
|
1704
1776
|
let text = marker === -1 ? line : line.slice(0, marker) + line.slice(marker + CURSOR_MARKER.length);
|
|
1705
1777
|
if (query.length > 0)
|
|
1706
1778
|
text = this.highlightScrollMatches(text, query);
|
|
1707
1779
|
return normalizeTerminalOutput(text) + TUI.SEGMENT_RESET;
|
|
1708
1780
|
}
|
|
1781
|
+
static IMAGE_PLACEHOLDER = "\x1b[2m[image]\x1b[0m";
|
|
1782
|
+
/** An image line in the pinned window: drawn if all of it fits, named if not. */
|
|
1783
|
+
emitScrollImage(line, row) {
|
|
1784
|
+
if (imageRowOffset(line) > row)
|
|
1785
|
+
return TUI.IMAGE_PLACEHOLDER;
|
|
1786
|
+
// iTerm2 paints into the cells it covers, so the next frame's `CSI 2 K`
|
|
1787
|
+
// clears it like any other row and there is nothing to track.
|
|
1788
|
+
if (!line.includes(KITTY_SEQUENCE_PREFIX))
|
|
1789
|
+
return line;
|
|
1790
|
+
const [liveId] = extractKittyImageIds(line);
|
|
1791
|
+
// A kitty placement with no id can never be deleted on its own, and the
|
|
1792
|
+
// only alternative deletes the live screen's pictures with it.
|
|
1793
|
+
if (liveId === undefined)
|
|
1794
|
+
return TUI.IMAGE_PLACEHOLDER;
|
|
1795
|
+
let pinnedId = this.scrollImageIds.get(liveId);
|
|
1796
|
+
if (pinnedId === undefined) {
|
|
1797
|
+
pinnedId = allocateImageId();
|
|
1798
|
+
this.scrollImageIds.set(liveId, pinnedId);
|
|
1799
|
+
}
|
|
1800
|
+
const retagged = retagKittyImageId(line, pinnedId);
|
|
1801
|
+
if (retagged === null)
|
|
1802
|
+
return TUI.IMAGE_PLACEHOLDER;
|
|
1803
|
+
this.scrollPlacedImages.add(pinnedId);
|
|
1804
|
+
this.sawImageLine = true;
|
|
1805
|
+
return retagged;
|
|
1806
|
+
}
|
|
1709
1807
|
doRender() {
|
|
1710
1808
|
if (this.stopped)
|
|
1711
1809
|
return;
|