@@ -8,9 +8,26 @@ import * as path from "node:path";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { isKeyRelease, matchesKey } from "./keys.js";
|
||||
import type { Terminal } from "./terminal.js";
|
||||
import { getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
|
||||
import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
|
||||
import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
|
||||
|
||||
const KITTY_SEQUENCE_PATTERN = /\x1b_G([^;\x1b]*);/g;
|
||||
|
||||
function extractKittyImageIds(line: string): number[] {
|
||||
const ids: number[] = [];
|
||||
for (const match of line.matchAll(KITTY_SEQUENCE_PATTERN)) {
|
||||
for (const param of match[1].split(",")) {
|
||||
const [key, value] = param.split("=", 2);
|
||||
if (key !== "i" || value === undefined) continue;
|
||||
const id = Number(value);
|
||||
if (Number.isInteger(id) && id > 0 && id <= 0xffffffff && !ids.includes(id)) {
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component interface - all components must implement this
|
||||
*/
|
||||
@@ -217,6 +234,7 @@ export class Container implements Component {
|
||||
export class TUI extends Container {
|
||||
public terminal: Terminal;
|
||||
private previousLines: string[] = [];
|
||||
private previousKittyImageIds = new Set<number>();
|
||||
private previousWidth = 0;
|
||||
private previousHeight = 0;
|
||||
private focusedComponent: Component | null = null;
|
||||
@@ -806,6 +824,48 @@ export class TUI extends Container {
|
||||
return lines;
|
||||
}
|
||||
|
||||
private collectKittyImageIds(lines: string[]): Set<number> {
|
||||
const ids = new Set<number>();
|
||||
for (const line of lines) {
|
||||
for (const id of extractKittyImageIds(line)) {
|
||||
ids.add(id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private deleteKittyImages(ids: Iterable<number>): string {
|
||||
let buffer = "";
|
||||
for (const id of ids) {
|
||||
buffer += deleteKittyImage(id);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private expandLastChangedForKittyImages(firstChanged: number, lastChanged: number): number {
|
||||
let expandedLastChanged = lastChanged;
|
||||
for (let i = firstChanged; i < this.previousLines.length; i++) {
|
||||
if (extractKittyImageIds(this.previousLines[i]).length > 0) {
|
||||
expandedLastChanged = Math.max(expandedLastChanged, i);
|
||||
}
|
||||
}
|
||||
return expandedLastChanged;
|
||||
}
|
||||
|
||||
private deleteChangedKittyImages(firstChanged: number, lastChanged: number): string {
|
||||
if (firstChanged < 0 || lastChanged < firstChanged) return "";
|
||||
|
||||
const ids = new Set<number>();
|
||||
const maxLine = Math.min(lastChanged, this.previousLines.length - 1);
|
||||
for (let i = firstChanged; i <= maxLine; i++) {
|
||||
for (const id of extractKittyImageIds(this.previousLines[i] ?? "")) {
|
||||
ids.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return this.deleteKittyImages(ids);
|
||||
}
|
||||
|
||||
/** Splice overlay content into a base line at a specific column. Single-pass optimized. */
|
||||
private compositeLineAt(
|
||||
baseLine: string,
|
||||
@@ -918,7 +978,10 @@ export class TUI extends Container {
|
||||
const fullRender = (clear: boolean): void => {
|
||||
this.fullRedrawCount += 1;
|
||||
let buffer = "\x1b[?2026h"; // Begin synchronized output
|
||||
if (clear) buffer += "\x1b[2J\x1b[H\x1b[3J"; // Clear screen, home, then clear scrollback
|
||||
if (clear) {
|
||||
buffer += this.deleteKittyImages(this.previousKittyImageIds);
|
||||
buffer += "\x1b[2J\x1b[H\x1b[3J"; // Clear screen, home, then clear scrollback
|
||||
}
|
||||
for (let i = 0; i < newLines.length; i++) {
|
||||
if (i > 0) buffer += "\r\n";
|
||||
buffer += newLines[i];
|
||||
@@ -937,6 +1000,7 @@ export class TUI extends Container {
|
||||
this.previousViewportTop = Math.max(0, bufferLength - height);
|
||||
this.positionHardwareCursor(cursorPos, newLines.length);
|
||||
this.previousLines = newLines;
|
||||
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
||||
this.previousWidth = width;
|
||||
this.previousHeight = height;
|
||||
};
|
||||
@@ -1003,6 +1067,9 @@ export class TUI extends Container {
|
||||
}
|
||||
lastChanged = newLines.length - 1;
|
||||
}
|
||||
if (firstChanged !== -1) {
|
||||
lastChanged = this.expandLastChangedForKittyImages(firstChanged, lastChanged);
|
||||
}
|
||||
const appendStart = appendedLines && firstChanged === this.previousLines.length && firstChanged > 0;
|
||||
|
||||
// No changes - but still need to update hardware cursor position if it moved
|
||||
@@ -1017,6 +1084,7 @@ export class TUI extends Container {
|
||||
if (firstChanged >= newLines.length) {
|
||||
if (this.previousLines.length > newLines.length) {
|
||||
let buffer = "\x1b[?2026h";
|
||||
buffer += this.deleteChangedKittyImages(firstChanged, lastChanged);
|
||||
// Move to end of new content (clamp to 0 for empty content)
|
||||
const targetRow = Math.max(0, newLines.length - 1);
|
||||
if (targetRow < prevViewportTop) {
|
||||
@@ -1052,6 +1120,7 @@ export class TUI extends Container {
|
||||
}
|
||||
this.positionHardwareCursor(cursorPos, newLines.length);
|
||||
this.previousLines = newLines;
|
||||
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
||||
this.previousWidth = width;
|
||||
this.previousHeight = height;
|
||||
this.previousViewportTop = prevViewportTop;
|
||||
@@ -1069,6 +1138,7 @@ export class TUI extends Container {
|
||||
// Render from first changed line to end
|
||||
// Build buffer with all updates wrapped in synchronized output
|
||||
let buffer = "\x1b[?2026h"; // Begin synchronized output
|
||||
buffer += this.deleteChangedKittyImages(firstChanged, lastChanged);
|
||||
const prevViewportBottom = prevViewportTop + height - 1;
|
||||
const moveTargetRow = appendStart ? firstChanged - 1 : firstChanged;
|
||||
if (moveTargetRow > prevViewportBottom) {
|
||||
@@ -1199,6 +1269,7 @@ export class TUI extends Container {
|
||||
this.positionHardwareCursor(cursorPos, newLines.length);
|
||||
|
||||
this.previousLines = newLines;
|
||||
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
||||
this.previousWidth = width;
|
||||
this.previousHeight = height;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user