@code-yeongyu/senpi-tui 2026.8.1 → 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/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 +38 -16
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +121 -51
- 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,12 +258,47 @@ 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 = [];
|
|
@@ -300,10 +335,16 @@ export class TUI extends Container {
|
|
|
300
335
|
// Preserve existing positional boolean callers while allowing explicit render-policy overrides.
|
|
301
336
|
const normalizedOptions = typeof options === "boolean" ? { showHardwareCursor: options } : (options ?? {});
|
|
302
337
|
this.#muxDetector = normalizedOptions.muxDetector ?? isMultiplexerSession;
|
|
338
|
+
this.logDirectory = logDirectory ?? path.join(os.homedir(), ".senpi", "agent");
|
|
303
339
|
if (normalizedOptions.showHardwareCursor !== undefined) {
|
|
304
340
|
this.showHardwareCursor = normalizedOptions.showHardwareCursor;
|
|
305
341
|
}
|
|
306
342
|
}
|
|
343
|
+
resetRenderState() { }
|
|
344
|
+
beforeTerminalStart() { }
|
|
345
|
+
afterTerminalStart() { }
|
|
346
|
+
beforeTerminalStop() { }
|
|
347
|
+
afterTerminalStop() { }
|
|
307
348
|
get fullRedraws() {
|
|
308
349
|
return this.fullRedrawCount;
|
|
309
350
|
}
|
|
@@ -317,9 +358,6 @@ export class TUI extends Container {
|
|
|
317
358
|
if (this.showHardwareCursor === enabled)
|
|
318
359
|
return;
|
|
319
360
|
this.showHardwareCursor = enabled;
|
|
320
|
-
if (!enabled) {
|
|
321
|
-
this.#setCursorVisibility(false);
|
|
322
|
-
}
|
|
323
361
|
this.requestRender();
|
|
324
362
|
}
|
|
325
363
|
getClearOnShrink() {
|
|
@@ -433,8 +471,11 @@ export class TUI extends Container {
|
|
|
433
471
|
}
|
|
434
472
|
}
|
|
435
473
|
}
|
|
474
|
+
getMountedRoots() {
|
|
475
|
+
return this.children;
|
|
476
|
+
}
|
|
436
477
|
isComponentMounted(component) {
|
|
437
|
-
return this.
|
|
478
|
+
return this.getMountedRoots().some((child) => this.containsComponent(child, component));
|
|
438
479
|
}
|
|
439
480
|
containsComponent(root, target) {
|
|
440
481
|
if (root === target)
|
|
@@ -596,7 +637,9 @@ export class TUI extends Container {
|
|
|
596
637
|
this.renderRequested = false;
|
|
597
638
|
this.inputRenderPending = false;
|
|
598
639
|
this.#lastCursorVisibility = undefined;
|
|
599
|
-
this.
|
|
640
|
+
this.beforeTerminalStart();
|
|
641
|
+
this.terminal.start((data) => this.handleTerminalInput(data), () => this.requestRender());
|
|
642
|
+
this.afterTerminalStart();
|
|
600
643
|
if (process.env.TMUX)
|
|
601
644
|
this.terminal.write(ENABLE_FOCUS_REPORTING);
|
|
602
645
|
this.#setCursorVisibility(false);
|
|
@@ -650,10 +693,14 @@ export class TUI extends Container {
|
|
|
650
693
|
if (this.terminalColorSchemeNotificationsEnabled) {
|
|
651
694
|
this.terminal.write("\x1b[?2031l");
|
|
652
695
|
}
|
|
696
|
+
this.beforeTerminalStop();
|
|
653
697
|
// Move cursor to the end of the content to prevent overwriting/artifacts on exit
|
|
654
698
|
if (this.previousLines.length > 0) {
|
|
655
|
-
//
|
|
656
|
-
|
|
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
|
+
}
|
|
657
704
|
const targetRow = this.previousLines.length; // Line after the last content
|
|
658
705
|
const lineDiff = targetRow - this.hardwareCursorRow;
|
|
659
706
|
if (lineDiff > 0) {
|
|
@@ -669,6 +716,8 @@ export class TUI extends Container {
|
|
|
669
716
|
if (process.env.TMUX)
|
|
670
717
|
this.terminal.write(DISABLE_FOCUS_REPORTING);
|
|
671
718
|
this.terminal.stop();
|
|
719
|
+
this.afterTerminalStop();
|
|
720
|
+
this.resetRenderState();
|
|
672
721
|
this.#lastCursorVisibility = undefined;
|
|
673
722
|
this.previousLines = [];
|
|
674
723
|
this.previousRawLines = [];
|
|
@@ -694,6 +743,7 @@ export class TUI extends Container {
|
|
|
694
743
|
requestRender(force = false, source = "unknown") {
|
|
695
744
|
if (force) {
|
|
696
745
|
this.inputRenderPending = false;
|
|
746
|
+
this.resetRenderState();
|
|
697
747
|
this.previousLines = [];
|
|
698
748
|
this.previousRawLines = [];
|
|
699
749
|
this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
|
|
@@ -773,7 +823,7 @@ export class TUI extends Container {
|
|
|
773
823
|
this.lastRenderAt = performance.now();
|
|
774
824
|
this.doRender();
|
|
775
825
|
}
|
|
776
|
-
|
|
826
|
+
handleTerminalInput(data) {
|
|
777
827
|
const focus = consumeTmuxFocusEvent(data);
|
|
778
828
|
if (focus.event !== null) {
|
|
779
829
|
resetCapabilitiesCache();
|
|
@@ -1107,7 +1157,10 @@ export class TUI extends Container {
|
|
|
1107
1157
|
this.normalizeMemo.set(line, normalized);
|
|
1108
1158
|
return { line: normalized, normalized: true };
|
|
1109
1159
|
}
|
|
1110
|
-
applyLineResets(lines
|
|
1160
|
+
applyLineResets(lines) {
|
|
1161
|
+
return this.applyLineResetResult(lines).lines;
|
|
1162
|
+
}
|
|
1163
|
+
applyLineResetResult(lines, mode = "full") {
|
|
1111
1164
|
const previousMemo = this.normalizeMemo;
|
|
1112
1165
|
const nextMemo = new Map();
|
|
1113
1166
|
const normalizedLines = [];
|
|
@@ -1141,7 +1194,7 @@ export class TUI extends Container {
|
|
|
1141
1194
|
this.previousLines.length === 0 ||
|
|
1142
1195
|
this.previousLines.length !== rawLines.length ||
|
|
1143
1196
|
this.previousRawLines.length !== rawLines.length) {
|
|
1144
|
-
return this.
|
|
1197
|
+
return this.applyLineResetResult(rawLines);
|
|
1145
1198
|
}
|
|
1146
1199
|
const windowStart = Math.max(0, viewportTop - VIEWPORT_RENDER_OVERSCAN);
|
|
1147
1200
|
const windowEnd = Math.min(rawLines.length, viewportTop + height + VIEWPORT_RENDER_OVERSCAN);
|
|
@@ -1154,7 +1207,7 @@ export class TUI extends Container {
|
|
|
1154
1207
|
firstRawChanged = i;
|
|
1155
1208
|
}
|
|
1156
1209
|
if (i < windowStart || i >= windowEnd) {
|
|
1157
|
-
return this.
|
|
1210
|
+
return this.applyLineResetResult(rawLines, "escaped");
|
|
1158
1211
|
}
|
|
1159
1212
|
}
|
|
1160
1213
|
const lines = this.previousLines.slice();
|
|
@@ -1323,13 +1376,12 @@ export class TUI extends Container {
|
|
|
1323
1376
|
buffer += row.content;
|
|
1324
1377
|
finalPaintedScreenRow = row.screenRow;
|
|
1325
1378
|
}
|
|
1326
|
-
|
|
1379
|
+
const finalCursorRow = plan.viewportTop + finalPaintedScreenRow;
|
|
1380
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, finalCursorRow);
|
|
1327
1381
|
this.terminal.write(buffer);
|
|
1328
1382
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1329
|
-
this.hardwareCursorRow = plan.viewportTop + finalPaintedScreenRow;
|
|
1330
1383
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1331
1384
|
this.previousViewportTop = plan.viewportTop;
|
|
1332
|
-
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1333
1385
|
this.setPreviousLines(newLines, rawLines);
|
|
1334
1386
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1335
1387
|
this.previousWidth = width;
|
|
@@ -1352,13 +1404,11 @@ export class TUI extends Container {
|
|
|
1352
1404
|
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1353
1405
|
buffer += newLines[row] ?? "";
|
|
1354
1406
|
}
|
|
1355
|
-
buffer
|
|
1407
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, bufferLength - 1);
|
|
1356
1408
|
this.terminal.write(buffer);
|
|
1357
1409
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1358
|
-
this.hardwareCursorRow = bufferLength - 1;
|
|
1359
1410
|
this.maxLinesRendered = newLines.length;
|
|
1360
1411
|
this.previousViewportTop = Math.max(0, bufferLength - height);
|
|
1361
|
-
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1362
1412
|
this.setPreviousLines(newLines, rawLines);
|
|
1363
1413
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1364
1414
|
this.previousWidth = width;
|
|
@@ -1381,14 +1431,13 @@ export class TUI extends Container {
|
|
|
1381
1431
|
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1382
1432
|
buffer += newLines[viewportTop + row] ?? "";
|
|
1383
1433
|
}
|
|
1384
|
-
|
|
1434
|
+
const finalCursorRow = viewportTop + Math.max(0, height - 1);
|
|
1435
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, finalCursorRow);
|
|
1385
1436
|
this.terminal.write(buffer);
|
|
1386
1437
|
this.muxViewportRepaintCount += 1;
|
|
1387
1438
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1388
|
-
this.hardwareCursorRow = viewportTop + Math.max(0, height - 1);
|
|
1389
1439
|
this.maxLinesRendered = newLines.length;
|
|
1390
1440
|
this.previousViewportTop = viewportTop;
|
|
1391
|
-
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1392
1441
|
this.setPreviousLines(newLines, rawLines);
|
|
1393
1442
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1394
1443
|
this.previousWidth = width;
|
|
@@ -1455,8 +1504,22 @@ export class TUI extends Container {
|
|
|
1455
1504
|
// Calculate visual column (width of text before marker)
|
|
1456
1505
|
const beforeMarker = line.slice(0, markerIndex);
|
|
1457
1506
|
const col = visibleWidth(beforeMarker);
|
|
1458
|
-
|
|
1459
|
-
|
|
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;
|
|
1460
1523
|
return { row, col };
|
|
1461
1524
|
}
|
|
1462
1525
|
}
|
|
@@ -1522,10 +1585,10 @@ export class TUI extends Container {
|
|
|
1522
1585
|
}
|
|
1523
1586
|
buffer += line;
|
|
1524
1587
|
}
|
|
1525
|
-
|
|
1588
|
+
const finalCursorRow = Math.max(0, newLines.length - 1);
|
|
1589
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, finalCursorRow);
|
|
1526
1590
|
this.terminal.write(buffer);
|
|
1527
1591
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1528
|
-
this.hardwareCursorRow = this.cursorRow;
|
|
1529
1592
|
// Reset max lines when clearing, otherwise track growth
|
|
1530
1593
|
if (clear) {
|
|
1531
1594
|
this.maxLinesRendered = newLines.length;
|
|
@@ -1535,7 +1598,6 @@ export class TUI extends Container {
|
|
|
1535
1598
|
}
|
|
1536
1599
|
const bufferLength = Math.max(height, newLines.length);
|
|
1537
1600
|
this.previousViewportTop = Math.max(0, bufferLength - height);
|
|
1538
|
-
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1539
1601
|
this.setPreviousLines(newLines, rawLines);
|
|
1540
1602
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1541
1603
|
this.previousWidth = width;
|
|
@@ -1545,8 +1607,9 @@ export class TUI extends Container {
|
|
|
1545
1607
|
const logRedraw = (reason) => {
|
|
1546
1608
|
if (!debugRedraw)
|
|
1547
1609
|
return;
|
|
1548
|
-
const logPath = path.join(
|
|
1610
|
+
const logPath = path.join(this.logDirectory, "pi-debug.log");
|
|
1549
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 });
|
|
1550
1613
|
fs.appendFileSync(logPath, msg, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
|
|
1551
1614
|
chmodDiagnosticLogBestEffort(logPath);
|
|
1552
1615
|
};
|
|
@@ -1667,12 +1730,13 @@ export class TUI extends Container {
|
|
|
1667
1730
|
if (moveBack > 0) {
|
|
1668
1731
|
buffer += `\x1b[${moveBack}A`;
|
|
1669
1732
|
}
|
|
1670
|
-
buffer
|
|
1733
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, targetRow);
|
|
1671
1734
|
this.terminal.write(buffer);
|
|
1672
1735
|
this.cursorRow = targetRow;
|
|
1673
|
-
this.hardwareCursorRow = targetRow;
|
|
1674
1736
|
}
|
|
1675
|
-
|
|
1737
|
+
else {
|
|
1738
|
+
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1739
|
+
}
|
|
1676
1740
|
this.setPreviousLines(newLines, rawLines);
|
|
1677
1741
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1678
1742
|
this.previousWidth = width;
|
|
@@ -1736,13 +1800,12 @@ export class TUI extends Container {
|
|
|
1736
1800
|
buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
|
|
1737
1801
|
buffer += newLines[viewportTop + row] ?? "";
|
|
1738
1802
|
}
|
|
1739
|
-
|
|
1803
|
+
const finalCursorRow = viewportTop + Math.max(0, height - 1);
|
|
1804
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, finalCursorRow);
|
|
1740
1805
|
this.terminal.write(buffer);
|
|
1741
1806
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1742
|
-
this.hardwareCursorRow = viewportTop + Math.max(0, height - 1);
|
|
1743
1807
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1744
1808
|
this.previousViewportTop = viewportTop;
|
|
1745
|
-
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1746
1809
|
this.setPreviousLines(newLines, rawLines);
|
|
1747
1810
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1748
1811
|
this.previousWidth = width;
|
|
@@ -1857,7 +1920,7 @@ export class TUI extends Container {
|
|
|
1857
1920
|
// Move cursor back to end of new content
|
|
1858
1921
|
buffer += `\x1b[${extraLines}A`;
|
|
1859
1922
|
}
|
|
1860
|
-
buffer
|
|
1923
|
+
buffer = this.finishFrame(buffer, cursorPos, newLines.length, finalCursorRow);
|
|
1861
1924
|
if (process.env.PI_TUI_DEBUG === "1") {
|
|
1862
1925
|
const debugDir = "/tmp/tui";
|
|
1863
1926
|
fs.mkdirSync(debugDir, { recursive: true });
|
|
@@ -1893,12 +1956,9 @@ export class TUI extends Container {
|
|
|
1893
1956
|
// cursorRow tracks end of content (for viewport calculation)
|
|
1894
1957
|
// hardwareCursorRow tracks actual terminal cursor position (for movement)
|
|
1895
1958
|
this.cursorRow = Math.max(0, newLines.length - 1);
|
|
1896
|
-
this.hardwareCursorRow = finalCursorRow;
|
|
1897
1959
|
// Track terminal's working area (grows but doesn't shrink unless cleared)
|
|
1898
1960
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1899
1961
|
this.previousViewportTop = Math.max(prevViewportTop, finalCursorRow - height + 1);
|
|
1900
|
-
// Position hardware cursor for IME
|
|
1901
|
-
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1902
1962
|
this.setPreviousLines(newLines, rawLines);
|
|
1903
1963
|
if (needsKittyImageExpansion) {
|
|
1904
1964
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
@@ -1911,7 +1971,7 @@ export class TUI extends Container {
|
|
|
1911
1971
|
* @param cursorPos The cursor position extracted from rendered output, or null
|
|
1912
1972
|
* @param totalLines Total number of rendered lines
|
|
1913
1973
|
*/
|
|
1914
|
-
|
|
1974
|
+
buildHardwareCursorSequence(cursorPos, totalLines) {
|
|
1915
1975
|
let buffer = "";
|
|
1916
1976
|
if (!cursorPos || totalLines <= 0) {
|
|
1917
1977
|
if (this.#lastCursorVisibility !== false) {
|
|
@@ -1939,9 +1999,16 @@ export class TUI extends Container {
|
|
|
1939
1999
|
this.#lastCursorVisibility = this.showHardwareCursor;
|
|
1940
2000
|
}
|
|
1941
2001
|
}
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
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);
|
|
1945
2012
|
}
|
|
1946
2013
|
/**
|
|
1947
2014
|
* Query the terminal's default background color with OSC 11 (`ESC ] 11 ; ? BEL`).
|
|
@@ -1996,4 +2063,7 @@ export class TUI extends Container {
|
|
|
1996
2063
|
});
|
|
1997
2064
|
}
|
|
1998
2065
|
}
|
|
2066
|
+
/** Legacy main-screen renderer export. */
|
|
2067
|
+
export class TUI extends TuiBase {
|
|
2068
|
+
}
|
|
1999
2069
|
//# sourceMappingURL=tui.js.map
|