@code-yeongyu/senpi-tui 2026.7.31 → 2026.8.3-3
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/README.md +77 -14
- package/dist/TuiAltScreen.d.ts +83 -0
- package/dist/TuiAltScreen.d.ts.map +1 -0
- package/dist/TuiAltScreen.js +687 -0
- package/dist/TuiAltScreen.js.map +1 -0
- package/dist/TuiMainScreen.d.ts +5 -0
- package/dist/TuiMainScreen.d.ts.map +1 -0
- package/dist/TuiMainScreen.js +5 -0
- package/dist/TuiMainScreen.js.map +1 -0
- package/dist/components/alt-screen-flash.d.ts +13 -0
- package/dist/components/alt-screen-flash.d.ts.map +1 -0
- package/dist/components/alt-screen-flash.js +36 -0
- package/dist/components/alt-screen-flash.js.map +1 -0
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +5 -1
- package/dist/components/editor.js.map +1 -1
- package/dist/components/h-stack.d.ts +7 -0
- package/dist/components/h-stack.d.ts.map +1 -0
- package/dist/components/h-stack.js +43 -0
- package/dist/components/h-stack.js.map +1 -0
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +4 -4
- package/dist/components/image.js.map +1 -1
- package/dist/components/markdown.d.ts +2 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +3 -2
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/scroll-view.d.ts +51 -0
- package/dist/components/scroll-view.d.ts.map +1 -0
- package/dist/components/scroll-view.js +163 -0
- package/dist/components/scroll-view.js.map +1 -0
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +3 -6
- package/dist/components/settings-list.js.map +1 -1
- package/dist/components/stack.d.ts +32 -0
- package/dist/components/stack.d.ts.map +1 -0
- package/dist/components/stack.js +109 -0
- package/dist/components/stack.js.map +1 -0
- package/dist/components/v-stack.d.ts +8 -0
- package/dist/components/v-stack.d.ts.map +1 -0
- package/dist/components/v-stack.js +21 -0
- package/dist/components/v-stack.js.map +1 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/keybindings.d.ts +30 -0
- package/dist/keybindings.d.ts.map +1 -1
- package/dist/keybindings.js +18 -0
- package/dist/keybindings.js.map +1 -1
- package/dist/layout-node.d.ts +40 -0
- package/dist/layout-node.d.ts.map +1 -0
- package/dist/layout-node.js +6 -0
- package/dist/layout-node.js.map +1 -0
- package/dist/layout.d.ts +40 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +315 -0
- package/dist/layout.js.map +1 -0
- package/dist/mux.js +1 -1
- package/dist/mux.js.map +1 -1
- package/dist/slash-command-autocomplete.d.ts.map +1 -1
- package/dist/slash-command-autocomplete.js +36 -8
- package/dist/slash-command-autocomplete.js.map +1 -1
- package/dist/terminal-image.d.ts +10 -0
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +61 -3
- package/dist/terminal-image.js.map +1 -1
- package/dist/terminal-text.d.ts +1 -0
- package/dist/terminal-text.d.ts.map +1 -1
- package/dist/terminal-text.js +12 -2
- package/dist/terminal-text.js.map +1 -1
- package/dist/tui.d.ts +39 -16
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +138 -54
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +95 -4
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
package/dist/tui.js
CHANGED
|
@@ -22,20 +22,17 @@ function parseKittyImageHeader(line) {
|
|
|
22
22
|
return undefined;
|
|
23
23
|
const ids = [];
|
|
24
24
|
let rows = 1;
|
|
25
|
-
const
|
|
26
|
-
for (const param of params.split(",")) {
|
|
25
|
+
for (const param of line.slice(paramsStart, paramsEnd).split(",")) {
|
|
27
26
|
const [key, value] = param.split("=", 2);
|
|
28
27
|
if (value === undefined)
|
|
29
28
|
continue;
|
|
30
29
|
const numberValue = Number(value);
|
|
31
30
|
if (!Number.isInteger(numberValue) || numberValue <= 0 || numberValue > 0xffffffff)
|
|
32
31
|
continue;
|
|
33
|
-
if (key === "i")
|
|
32
|
+
if (key === "i")
|
|
34
33
|
ids.push(numberValue);
|
|
35
|
-
|
|
36
|
-
else if (key === "r") {
|
|
34
|
+
else if (key === "r")
|
|
37
35
|
rows = numberValue;
|
|
38
|
-
}
|
|
39
36
|
}
|
|
40
37
|
return { ids, rows };
|
|
41
38
|
}
|
|
@@ -45,6 +42,9 @@ function extractKittyImageIds(line) {
|
|
|
45
42
|
function extractKittyImageRows(line) {
|
|
46
43
|
return parseKittyImageHeader(line)?.rows ?? 1;
|
|
47
44
|
}
|
|
45
|
+
function isTermuxSession() {
|
|
46
|
+
return Boolean(process.env.TERMUX_VERSION);
|
|
47
|
+
}
|
|
48
48
|
/** Type guard to check if a component implements Focusable */
|
|
49
49
|
export function isFocusable(component) {
|
|
50
50
|
return component !== null && "focused" in component;
|
|
@@ -56,6 +56,9 @@ export function isFocusable(component) {
|
|
|
56
56
|
* TUI finds and strips this marker, then positions the hardware cursor there.
|
|
57
57
|
*/
|
|
58
58
|
export const CURSOR_MARKER = "\x1b_pi:c\x07";
|
|
59
|
+
const FAKE_CURSOR_START = "\x1b[7m";
|
|
60
|
+
const FAKE_CURSOR_END = "\x1b[27m";
|
|
61
|
+
const FAKE_CURSOR_RESET = "\x1b[0m";
|
|
59
62
|
export { visibleWidth };
|
|
60
63
|
const renderErrorLoggedClasses = new Set();
|
|
61
64
|
let renderErrorLogWrites = 0;
|
|
@@ -186,9 +189,6 @@ function parseSizeValue(value, referenceSize) {
|
|
|
186
189
|
}
|
|
187
190
|
return undefined;
|
|
188
191
|
}
|
|
189
|
-
function isTermuxSession() {
|
|
190
|
-
return Boolean(process.env.TERMUX_VERSION);
|
|
191
|
-
}
|
|
192
192
|
/**
|
|
193
193
|
* Container - a component that contains other components
|
|
194
194
|
*/
|
|
@@ -258,15 +258,51 @@ export class Container {
|
|
|
258
258
|
/**
|
|
259
259
|
* TUI - Main class for managing terminal UI with differential rendering
|
|
260
260
|
*/
|
|
261
|
-
|
|
261
|
+
const SEGMENT_RESET = "\x1b[0m\x1b]8;;\x07";
|
|
262
|
+
/** Composite overlay content into a terminal line at a fixed column. */
|
|
263
|
+
export function compositeTuiLine(baseLine, overlayLine, startCol, overlayWidth, totalWidth) {
|
|
264
|
+
if (isImageLine(baseLine) && visibleWidth(baseLine) === 0)
|
|
265
|
+
return baseLine;
|
|
266
|
+
const placeholderIndex = baseLine.indexOf("\u{10eeee}");
|
|
267
|
+
const protocolEnd = placeholderIndex === -1 ? -1 : baseLine.lastIndexOf("\x1b\\", placeholderIndex);
|
|
268
|
+
const protocolPrefix = protocolEnd === -1 ? "" : baseLine.slice(0, protocolEnd + 2);
|
|
269
|
+
const afterStart = startCol + overlayWidth;
|
|
270
|
+
const base = extractSegments(baseLine, startCol, afterStart, totalWidth - afterStart, true);
|
|
271
|
+
const overlay = sliceWithWidth(overlayLine, 0, overlayWidth, true);
|
|
272
|
+
const beforePad = Math.max(0, startCol - base.beforeWidth);
|
|
273
|
+
const overlayPad = Math.max(0, overlayWidth - overlay.width);
|
|
274
|
+
const actualBeforeWidth = Math.max(startCol, base.beforeWidth);
|
|
275
|
+
const actualOverlayWidth = Math.max(overlayWidth, overlay.width);
|
|
276
|
+
const afterTarget = Math.max(0, totalWidth - actualBeforeWidth - actualOverlayWidth);
|
|
277
|
+
const afterPad = Math.max(0, afterTarget - base.afterWidth);
|
|
278
|
+
const result = base.before +
|
|
279
|
+
" ".repeat(beforePad) +
|
|
280
|
+
SEGMENT_RESET +
|
|
281
|
+
overlay.text +
|
|
282
|
+
" ".repeat(overlayPad) +
|
|
283
|
+
SEGMENT_RESET +
|
|
284
|
+
base.after +
|
|
285
|
+
" ".repeat(afterPad);
|
|
286
|
+
const composited = visibleWidth(result) <= totalWidth ? result : sliceByColumn(result, 0, totalWidth, true);
|
|
287
|
+
return protocolPrefix + composited;
|
|
288
|
+
}
|
|
289
|
+
export const VIEWPORT_TUI = Symbol.for("@earendil-works/pi-tui/viewport");
|
|
290
|
+
export function isViewportTUI(tui) {
|
|
291
|
+
return tui[VIEWPORT_TUI] === true;
|
|
292
|
+
}
|
|
293
|
+
export class TuiBase extends Container {
|
|
262
294
|
/** Minimum interval between scheduled renders. Default preserves the historic ~60fps cap. */
|
|
263
295
|
#minRenderIntervalMs;
|
|
264
296
|
#lastCursorVisibility;
|
|
297
|
+
get hasOverlayEntries() {
|
|
298
|
+
return this.overlayStack.length > 0;
|
|
299
|
+
}
|
|
265
300
|
#muxDetector;
|
|
266
|
-
constructor(terminal, options) {
|
|
301
|
+
constructor(terminal, options, logDirectory) {
|
|
267
302
|
super();
|
|
268
303
|
this.previousLines = [];
|
|
269
304
|
this.previousRawLines = [];
|
|
305
|
+
this.normalizeMemo = new Map();
|
|
270
306
|
this.previousKittyImageIds = new Set();
|
|
271
307
|
this.previousWidth = 0;
|
|
272
308
|
this.previousHeight = 0;
|
|
@@ -299,10 +335,16 @@ export class TUI extends Container {
|
|
|
299
335
|
// Preserve existing positional boolean callers while allowing explicit render-policy overrides.
|
|
300
336
|
const normalizedOptions = typeof options === "boolean" ? { showHardwareCursor: options } : (options ?? {});
|
|
301
337
|
this.#muxDetector = normalizedOptions.muxDetector ?? isMultiplexerSession;
|
|
338
|
+
this.logDirectory = logDirectory ?? path.join(os.homedir(), ".senpi", "agent");
|
|
302
339
|
if (normalizedOptions.showHardwareCursor !== undefined) {
|
|
303
340
|
this.showHardwareCursor = normalizedOptions.showHardwareCursor;
|
|
304
341
|
}
|
|
305
342
|
}
|
|
343
|
+
resetRenderState() { }
|
|
344
|
+
beforeTerminalStart() { }
|
|
345
|
+
afterTerminalStart() { }
|
|
346
|
+
beforeTerminalStop() { }
|
|
347
|
+
afterTerminalStop() { }
|
|
306
348
|
get fullRedraws() {
|
|
307
349
|
return this.fullRedrawCount;
|
|
308
350
|
}
|
|
@@ -316,9 +358,6 @@ export class TUI extends Container {
|
|
|
316
358
|
if (this.showHardwareCursor === enabled)
|
|
317
359
|
return;
|
|
318
360
|
this.showHardwareCursor = enabled;
|
|
319
|
-
if (!enabled) {
|
|
320
|
-
this.#setCursorVisibility(false);
|
|
321
|
-
}
|
|
322
361
|
this.requestRender();
|
|
323
362
|
}
|
|
324
363
|
getClearOnShrink() {
|
|
@@ -432,8 +471,11 @@ export class TUI extends Container {
|
|
|
432
471
|
}
|
|
433
472
|
}
|
|
434
473
|
}
|
|
474
|
+
getMountedRoots() {
|
|
475
|
+
return this.children;
|
|
476
|
+
}
|
|
435
477
|
isComponentMounted(component) {
|
|
436
|
-
return this.
|
|
478
|
+
return this.getMountedRoots().some((child) => this.containsComponent(child, component));
|
|
437
479
|
}
|
|
438
480
|
containsComponent(root, target) {
|
|
439
481
|
if (root === target)
|
|
@@ -595,7 +637,9 @@ export class TUI extends Container {
|
|
|
595
637
|
this.renderRequested = false;
|
|
596
638
|
this.inputRenderPending = false;
|
|
597
639
|
this.#lastCursorVisibility = undefined;
|
|
598
|
-
this.
|
|
640
|
+
this.beforeTerminalStart();
|
|
641
|
+
this.terminal.start((data) => this.handleTerminalInput(data), () => this.requestRender());
|
|
642
|
+
this.afterTerminalStart();
|
|
599
643
|
if (process.env.TMUX)
|
|
600
644
|
this.terminal.write(ENABLE_FOCUS_REPORTING);
|
|
601
645
|
this.#setCursorVisibility(false);
|
|
@@ -649,10 +693,14 @@ export class TUI extends Container {
|
|
|
649
693
|
if (this.terminalColorSchemeNotificationsEnabled) {
|
|
650
694
|
this.terminal.write("\x1b[?2031l");
|
|
651
695
|
}
|
|
696
|
+
this.beforeTerminalStop();
|
|
652
697
|
// Move cursor to the end of the content to prevent overwriting/artifacts on exit
|
|
653
698
|
if (this.previousLines.length > 0) {
|
|
654
|
-
//
|
|
655
|
-
|
|
699
|
+
// Only overwrite the cursor cell when the last published frame kept the hardware cursor hidden;
|
|
700
|
+
// a pending visibility change has not rendered yet and must not erase content.
|
|
701
|
+
if (this.#lastCursorVisibility === false) {
|
|
702
|
+
this.terminal.write(" ");
|
|
703
|
+
}
|
|
656
704
|
const targetRow = this.previousLines.length; // Line after the last content
|
|
657
705
|
const lineDiff = targetRow - this.hardwareCursorRow;
|
|
658
706
|
if (lineDiff > 0) {
|
|
@@ -668,6 +716,8 @@ export class TUI extends Container {
|
|
|
668
716
|
if (process.env.TMUX)
|
|
669
717
|
this.terminal.write(DISABLE_FOCUS_REPORTING);
|
|
670
718
|
this.terminal.stop();
|
|
719
|
+
this.afterTerminalStop();
|
|
720
|
+
this.resetRenderState();
|
|
671
721
|
this.#lastCursorVisibility = undefined;
|
|
672
722
|
this.previousLines = [];
|
|
673
723
|
this.previousRawLines = [];
|
|
@@ -693,6 +743,7 @@ export class TUI extends Container {
|
|
|
693
743
|
requestRender(force = false, source = "unknown") {
|
|
694
744
|
if (force) {
|
|
695
745
|
this.inputRenderPending = false;
|
|
746
|
+
this.resetRenderState();
|
|
696
747
|
this.previousLines = [];
|
|
697
748
|
this.previousRawLines = [];
|
|
698
749
|
this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
|
|
@@ -772,7 +823,7 @@ export class TUI extends Container {
|
|
|
772
823
|
this.lastRenderAt = performance.now();
|
|
773
824
|
this.doRender();
|
|
774
825
|
}
|
|
775
|
-
|
|
826
|
+
handleTerminalInput(data) {
|
|
776
827
|
const focus = consumeTmuxFocusEvent(data);
|
|
777
828
|
if (focus.event !== null) {
|
|
778
829
|
resetCapabilitiesCache();
|
|
@@ -1098,21 +1149,37 @@ export class TUI extends Container {
|
|
|
1098
1149
|
if (isImageLine(line)) {
|
|
1099
1150
|
return { line, normalized: false };
|
|
1100
1151
|
}
|
|
1101
|
-
|
|
1152
|
+
const cached = this.normalizeMemo.get(line);
|
|
1153
|
+
if (cached !== undefined) {
|
|
1154
|
+
return { line: cached, normalized: false };
|
|
1155
|
+
}
|
|
1156
|
+
const normalized = normalizeTerminalOutput(line) + TUI.SEGMENT_RESET;
|
|
1157
|
+
this.normalizeMemo.set(line, normalized);
|
|
1158
|
+
return { line: normalized, normalized: true };
|
|
1102
1159
|
}
|
|
1103
|
-
applyLineResets(lines
|
|
1160
|
+
applyLineResets(lines) {
|
|
1161
|
+
return this.applyLineResetResult(lines).lines;
|
|
1162
|
+
}
|
|
1163
|
+
applyLineResetResult(lines, mode = "full") {
|
|
1164
|
+
const previousMemo = this.normalizeMemo;
|
|
1165
|
+
const nextMemo = new Map();
|
|
1104
1166
|
const normalizedLines = [];
|
|
1105
1167
|
let normalizedCount = 0;
|
|
1106
1168
|
for (let i = 0; i < lines.length; i++) {
|
|
1107
1169
|
const line = lines[i];
|
|
1108
1170
|
if (isImageLine(line)) {
|
|
1109
1171
|
normalizedLines.push(line);
|
|
1172
|
+
continue;
|
|
1110
1173
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1174
|
+
let normalized = nextMemo.get(line) ?? previousMemo.get(line);
|
|
1175
|
+
if (normalized === undefined) {
|
|
1176
|
+
normalized = normalizeTerminalOutput(line) + TUI.SEGMENT_RESET;
|
|
1113
1177
|
normalizedCount += 1;
|
|
1114
1178
|
}
|
|
1179
|
+
nextMemo.set(line, normalized);
|
|
1180
|
+
normalizedLines.push(normalized);
|
|
1115
1181
|
}
|
|
1182
|
+
this.normalizeMemo = nextMemo;
|
|
1116
1183
|
recordViewportRenderStats(normalizedCount, mode);
|
|
1117
1184
|
return {
|
|
1118
1185
|
lines: normalizedLines,
|
|
@@ -1127,7 +1194,7 @@ export class TUI extends Container {
|
|
|
1127
1194
|
this.previousLines.length === 0 ||
|
|
1128
1195
|
this.previousLines.length !== rawLines.length ||
|
|
1129
1196
|
this.previousRawLines.length !== rawLines.length) {
|
|
1130
|
-
return this.
|
|
1197
|
+
return this.applyLineResetResult(rawLines);
|
|
1131
1198
|
}
|
|
1132
1199
|
const windowStart = Math.max(0, viewportTop - VIEWPORT_RENDER_OVERSCAN);
|
|
1133
1200
|
const windowEnd = Math.min(rawLines.length, viewportTop + height + VIEWPORT_RENDER_OVERSCAN);
|
|
@@ -1140,7 +1207,7 @@ export class TUI extends Container {
|
|
|
1140
1207
|
firstRawChanged = i;
|
|
1141
1208
|
}
|
|
1142
1209
|
if (i < windowStart || i >= windowEnd) {
|
|
1143
|
-
return this.
|
|
1210
|
+
return this.applyLineResetResult(rawLines, "escaped");
|
|
1144
1211
|
}
|
|
1145
1212
|
}
|
|
1146
1213
|
const lines = this.previousLines.slice();
|
|
@@ -1309,13 +1376,12 @@ export class TUI extends Container {
|
|
|
1309
1376
|
buffer += row.content;
|
|
1310
1377
|
finalPaintedScreenRow = row.screenRow;
|
|
1311
1378
|
}
|
|
1312
|
-
|
|
1379
|
+
const finalCursorRow = plan.viewportTop + finalPaintedScreenRow;
|
|
1380
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, finalCursorRow);
|
|
1313
1381
|
this.terminal.write(buffer);
|
|
1314
1382
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1315
|
-
this.hardwareCursorRow = plan.viewportTop + finalPaintedScreenRow;
|
|
1316
1383
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1317
1384
|
this.previousViewportTop = plan.viewportTop;
|
|
1318
|
-
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1319
1385
|
this.setPreviousLines(newLines, rawLines);
|
|
1320
1386
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1321
1387
|
this.previousWidth = width;
|
|
@@ -1338,13 +1404,11 @@ export class TUI extends Container {
|
|
|
1338
1404
|
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1339
1405
|
buffer += newLines[row] ?? "";
|
|
1340
1406
|
}
|
|
1341
|
-
buffer
|
|
1407
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, bufferLength - 1);
|
|
1342
1408
|
this.terminal.write(buffer);
|
|
1343
1409
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1344
|
-
this.hardwareCursorRow = bufferLength - 1;
|
|
1345
1410
|
this.maxLinesRendered = newLines.length;
|
|
1346
1411
|
this.previousViewportTop = Math.max(0, bufferLength - height);
|
|
1347
|
-
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1348
1412
|
this.setPreviousLines(newLines, rawLines);
|
|
1349
1413
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1350
1414
|
this.previousWidth = width;
|
|
@@ -1367,14 +1431,13 @@ export class TUI extends Container {
|
|
|
1367
1431
|
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1368
1432
|
buffer += newLines[viewportTop + row] ?? "";
|
|
1369
1433
|
}
|
|
1370
|
-
|
|
1434
|
+
const finalCursorRow = viewportTop + Math.max(0, height - 1);
|
|
1435
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, finalCursorRow);
|
|
1371
1436
|
this.terminal.write(buffer);
|
|
1372
1437
|
this.muxViewportRepaintCount += 1;
|
|
1373
1438
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1374
|
-
this.hardwareCursorRow = viewportTop + Math.max(0, height - 1);
|
|
1375
1439
|
this.maxLinesRendered = newLines.length;
|
|
1376
1440
|
this.previousViewportTop = viewportTop;
|
|
1377
|
-
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1378
1441
|
this.setPreviousLines(newLines, rawLines);
|
|
1379
1442
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1380
1443
|
this.previousWidth = width;
|
|
@@ -1441,8 +1504,22 @@ export class TUI extends Container {
|
|
|
1441
1504
|
// Calculate visual column (width of text before marker)
|
|
1442
1505
|
const beforeMarker = line.slice(0, markerIndex);
|
|
1443
1506
|
const col = visibleWidth(beforeMarker);
|
|
1444
|
-
|
|
1445
|
-
|
|
1507
|
+
let afterMarker = line.slice(markerIndex + CURSOR_MARKER.length);
|
|
1508
|
+
if (this.showHardwareCursor && afterMarker.startsWith(FAKE_CURSOR_START)) {
|
|
1509
|
+
const fakeCursorEnd = afterMarker.indexOf(FAKE_CURSOR_END, FAKE_CURSOR_START.length);
|
|
1510
|
+
const fakeCursorReset = afterMarker.indexOf(FAKE_CURSOR_RESET, FAKE_CURSOR_START.length);
|
|
1511
|
+
if (fakeCursorReset !== -1 && (fakeCursorEnd === -1 || fakeCursorReset < fakeCursorEnd)) {
|
|
1512
|
+
// Keep a full reset because it may also terminate styles surrounding the fake cursor.
|
|
1513
|
+
afterMarker = afterMarker.slice(FAKE_CURSOR_START.length);
|
|
1514
|
+
}
|
|
1515
|
+
else if (fakeCursorEnd !== -1) {
|
|
1516
|
+
afterMarker =
|
|
1517
|
+
afterMarker.slice(FAKE_CURSOR_START.length, fakeCursorEnd) +
|
|
1518
|
+
afterMarker.slice(fakeCursorEnd + FAKE_CURSOR_END.length);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
// Strip marker and any colocated fake cursor styling when the hardware cursor owns the position.
|
|
1522
|
+
lines[row] = beforeMarker + afterMarker;
|
|
1446
1523
|
return { row, col };
|
|
1447
1524
|
}
|
|
1448
1525
|
}
|
|
@@ -1508,10 +1585,10 @@ export class TUI extends Container {
|
|
|
1508
1585
|
}
|
|
1509
1586
|
buffer += line;
|
|
1510
1587
|
}
|
|
1511
|
-
|
|
1588
|
+
const finalCursorRow = Math.max(0, newLines.length - 1);
|
|
1589
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, finalCursorRow);
|
|
1512
1590
|
this.terminal.write(buffer);
|
|
1513
1591
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1514
|
-
this.hardwareCursorRow = this.cursorRow;
|
|
1515
1592
|
// Reset max lines when clearing, otherwise track growth
|
|
1516
1593
|
if (clear) {
|
|
1517
1594
|
this.maxLinesRendered = newLines.length;
|
|
@@ -1521,7 +1598,6 @@ export class TUI extends Container {
|
|
|
1521
1598
|
}
|
|
1522
1599
|
const bufferLength = Math.max(height, newLines.length);
|
|
1523
1600
|
this.previousViewportTop = Math.max(0, bufferLength - height);
|
|
1524
|
-
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1525
1601
|
this.setPreviousLines(newLines, rawLines);
|
|
1526
1602
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1527
1603
|
this.previousWidth = width;
|
|
@@ -1531,8 +1607,9 @@ export class TUI extends Container {
|
|
|
1531
1607
|
const logRedraw = (reason) => {
|
|
1532
1608
|
if (!debugRedraw)
|
|
1533
1609
|
return;
|
|
1534
|
-
const logPath = path.join(
|
|
1610
|
+
const logPath = path.join(this.logDirectory, "pi-debug.log");
|
|
1535
1611
|
const msg = `[${new Date().toISOString()}] fullRender: ${reason} (prev=${this.previousLines.length}, new=${newLines.length}, height=${height})\n`;
|
|
1612
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
1536
1613
|
fs.appendFileSync(logPath, msg, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
|
|
1537
1614
|
chmodDiagnosticLogBestEffort(logPath);
|
|
1538
1615
|
};
|
|
@@ -1653,12 +1730,13 @@ export class TUI extends Container {
|
|
|
1653
1730
|
if (moveBack > 0) {
|
|
1654
1731
|
buffer += `\x1b[${moveBack}A`;
|
|
1655
1732
|
}
|
|
1656
|
-
buffer
|
|
1733
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, targetRow);
|
|
1657
1734
|
this.terminal.write(buffer);
|
|
1658
1735
|
this.cursorRow = targetRow;
|
|
1659
|
-
this.hardwareCursorRow = targetRow;
|
|
1660
1736
|
}
|
|
1661
|
-
|
|
1737
|
+
else {
|
|
1738
|
+
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1739
|
+
}
|
|
1662
1740
|
this.setPreviousLines(newLines, rawLines);
|
|
1663
1741
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1664
1742
|
this.previousWidth = width;
|
|
@@ -1722,13 +1800,12 @@ export class TUI extends Container {
|
|
|
1722
1800
|
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1723
1801
|
buffer += newLines[viewportTop + row] ?? "";
|
|
1724
1802
|
}
|
|
1725
|
-
|
|
1803
|
+
const finalCursorRow = viewportTop + Math.max(0, height - 1);
|
|
1804
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, finalCursorRow);
|
|
1726
1805
|
this.terminal.write(buffer);
|
|
1727
1806
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1728
|
-
this.hardwareCursorRow = viewportTop + Math.max(0, height - 1);
|
|
1729
1807
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1730
1808
|
this.previousViewportTop = viewportTop;
|
|
1731
|
-
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1732
1809
|
this.setPreviousLines(newLines, rawLines);
|
|
1733
1810
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1734
1811
|
this.previousWidth = width;
|
|
@@ -1843,7 +1920,7 @@ export class TUI extends Container {
|
|
|
1843
1920
|
// Move cursor back to end of new content
|
|
1844
1921
|
buffer += `\x1b[${extraLines}A`;
|
|
1845
1922
|
}
|
|
1846
|
-
buffer
|
|
1923
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, finalCursorRow);
|
|
1847
1924
|
if (process.env.PI_TUI_DEBUG === "1") {
|
|
1848
1925
|
const debugDir = "/tmp/tui";
|
|
1849
1926
|
fs.mkdirSync(debugDir, { recursive: true });
|
|
@@ -1879,12 +1956,9 @@ export class TUI extends Container {
|
|
|
1879
1956
|
// cursorRow tracks end of content (for viewport calculation)
|
|
1880
1957
|
// hardwareCursorRow tracks actual terminal cursor position (for movement)
|
|
1881
1958
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1882
|
-
this.hardwareCursorRow = finalCursorRow;
|
|
1883
1959
|
// Track terminal's working area (grows but doesn't shrink unless cleared)
|
|
1884
1960
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1885
1961
|
this.previousViewportTop = Math.max(prevViewportTop, finalCursorRow - height + 1);
|
|
1886
|
-
// Position hardware cursor for IME
|
|
1887
|
-
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1888
1962
|
this.setPreviousLines(newLines, rawLines);
|
|
1889
1963
|
if (needsKittyImageExpansion) {
|
|
1890
1964
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
@@ -1897,7 +1971,7 @@ export class TUI extends Container {
|
|
|
1897
1971
|
* @param cursorPos The cursor position extracted from rendered output, or null
|
|
1898
1972
|
* @param totalLines Total number of rendered lines
|
|
1899
1973
|
*/
|
|
1900
|
-
|
|
1974
|
+
buildHardwareCursorSequence(cursorPos, totalLines) {
|
|
1901
1975
|
let buffer = "";
|
|
1902
1976
|
if (!cursorPos || totalLines <= 0) {
|
|
1903
1977
|
if (this.#lastCursorVisibility !== false) {
|
|
@@ -1925,9 +1999,16 @@ export class TUI extends Container {
|
|
|
1925
1999
|
this.#lastCursorVisibility = this.showHardwareCursor;
|
|
1926
2000
|
}
|
|
1927
2001
|
}
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2002
|
+
return buffer;
|
|
2003
|
+
}
|
|
2004
|
+
finishFrame(buffer, cursorPos, totalLines, hardwareCursorRow) {
|
|
2005
|
+
this.hardwareCursorRow = hardwareCursorRow;
|
|
2006
|
+
return buffer + this.buildHardwareCursorSequence(cursorPos, totalLines) + TUI.FRAME_END;
|
|
2007
|
+
}
|
|
2008
|
+
positionHardwareCursor(cursorPos, totalLines) {
|
|
2009
|
+
const buffer = this.buildHardwareCursorSequence(cursorPos, totalLines);
|
|
2010
|
+
if (buffer)
|
|
2011
|
+
this.terminal.write(TUI.FRAME_BEGIN + buffer + TUI.FRAME_END);
|
|
1931
2012
|
}
|
|
1932
2013
|
/**
|
|
1933
2014
|
* Query the terminal's default background color with OSC 11 (`ESC ] 11 ; ? BEL`).
|
|
@@ -1982,4 +2063,7 @@ export class TUI extends Container {
|
|
|
1982
2063
|
});
|
|
1983
2064
|
}
|
|
1984
2065
|
}
|
|
2066
|
+
/** Legacy main-screen renderer export. */
|
|
2067
|
+
export class TUI extends TuiBase {
|
|
2068
|
+
}
|
|
1985
2069
|
//# sourceMappingURL=tui.js.map
|