@draht/tui 2026.6.11 → 2026.7.7
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/editor.d.ts +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/markdown.d.ts +4 -1
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +33 -1
- package/dist/components/markdown.js.map +1 -1
- package/dist/fuzzy.d.ts +1 -1
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js +2 -2
- package/dist/fuzzy.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/keybindings.d.ts +1 -1
- package/dist/keybindings.d.ts.map +1 -1
- package/dist/keybindings.js +1 -1
- package/dist/keybindings.js.map +1 -1
- package/dist/terminal-colors.d.ts +10 -0
- package/dist/terminal-colors.d.ts.map +1 -0
- package/dist/terminal-colors.js +59 -0
- package/dist/terminal-colors.js.map +1 -0
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +4 -0
- package/dist/terminal-image.js.map +1 -1
- package/dist/tui.d.ts +27 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +201 -17
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -1
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -6,28 +6,42 @@ import * as os from "node:os";
|
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { performance } from "node:perf_hooks";
|
|
8
8
|
import { isKeyRelease, matchesKey } from "./keys.js";
|
|
9
|
+
import { isOsc11BackgroundColorResponse, parseOsc11BackgroundColor, parseTerminalColorSchemeReport, } from "./terminal-colors.js";
|
|
9
10
|
import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
|
|
10
11
|
import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
|
|
11
12
|
const KITTY_SEQUENCE_PREFIX = "\x1b_G";
|
|
12
|
-
function
|
|
13
|
+
function parseKittyImageHeader(line) {
|
|
13
14
|
const sequenceStart = line.indexOf(KITTY_SEQUENCE_PREFIX);
|
|
14
15
|
if (sequenceStart === -1)
|
|
15
|
-
return
|
|
16
|
+
return undefined;
|
|
16
17
|
const paramsStart = sequenceStart + KITTY_SEQUENCE_PREFIX.length;
|
|
17
18
|
const paramsEnd = line.indexOf(";", paramsStart);
|
|
18
19
|
if (paramsEnd === -1)
|
|
19
|
-
return
|
|
20
|
+
return undefined;
|
|
21
|
+
const ids = [];
|
|
22
|
+
let rows = 1;
|
|
20
23
|
const params = line.slice(paramsStart, paramsEnd);
|
|
21
24
|
for (const param of params.split(",")) {
|
|
22
25
|
const [key, value] = param.split("=", 2);
|
|
23
|
-
if (
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
const numberValue = Number(value);
|
|
29
|
+
if (!Number.isInteger(numberValue) || numberValue <= 0 || numberValue > 0xffffffff)
|
|
24
30
|
continue;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
if (key === "i") {
|
|
32
|
+
ids.push(numberValue);
|
|
33
|
+
}
|
|
34
|
+
else if (key === "r") {
|
|
35
|
+
rows = numberValue;
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
|
-
return
|
|
38
|
+
return { ids, rows };
|
|
39
|
+
}
|
|
40
|
+
function extractKittyImageIds(line) {
|
|
41
|
+
return parseKittyImageHeader(line)?.ids ?? [];
|
|
42
|
+
}
|
|
43
|
+
function extractKittyImageRows(line) {
|
|
44
|
+
return parseKittyImageHeader(line)?.rows ?? 1;
|
|
31
45
|
}
|
|
32
46
|
/** Type guard to check if a component implements Focusable */
|
|
33
47
|
export function isFocusable(component) {
|
|
@@ -115,6 +129,10 @@ export class TUI extends Container {
|
|
|
115
129
|
previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
|
|
116
130
|
fullRedrawCount = 0;
|
|
117
131
|
stopped = false;
|
|
132
|
+
pendingOsc11BackgroundReplies = 0;
|
|
133
|
+
pendingOsc11BackgroundQueries = [];
|
|
134
|
+
terminalColorSchemeListeners = new Set();
|
|
135
|
+
terminalColorSchemeNotificationsEnabled = false;
|
|
118
136
|
// Overlay stack for modal components rendered on top of base content
|
|
119
137
|
focusOrderCounter = 0;
|
|
120
138
|
overlayStack = [];
|
|
@@ -414,6 +432,9 @@ export class TUI extends Container {
|
|
|
414
432
|
this.stopped = false;
|
|
415
433
|
this.terminal.start((data) => this.handleInput(data), () => this.requestRender());
|
|
416
434
|
this.terminal.hideCursor();
|
|
435
|
+
if (this.terminalColorSchemeNotificationsEnabled) {
|
|
436
|
+
this.terminal.write("\x1b[?2031h");
|
|
437
|
+
}
|
|
417
438
|
this.queryCellSize();
|
|
418
439
|
this.requestRender();
|
|
419
440
|
}
|
|
@@ -426,6 +447,21 @@ export class TUI extends Container {
|
|
|
426
447
|
removeInputListener(listener) {
|
|
427
448
|
this.inputListeners.delete(listener);
|
|
428
449
|
}
|
|
450
|
+
onTerminalColorSchemeChange(listener) {
|
|
451
|
+
this.terminalColorSchemeListeners.add(listener);
|
|
452
|
+
return () => {
|
|
453
|
+
this.terminalColorSchemeListeners.delete(listener);
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
setTerminalColorSchemeNotifications(enabled) {
|
|
457
|
+
if (this.terminalColorSchemeNotificationsEnabled === enabled) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
this.terminalColorSchemeNotificationsEnabled = enabled;
|
|
461
|
+
if (!this.stopped) {
|
|
462
|
+
this.terminal.write(enabled ? "\x1b[?2031h" : "\x1b[?2031l");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
429
465
|
queryCellSize() {
|
|
430
466
|
// Only query if terminal supports images (cell size is only used for image rendering)
|
|
431
467
|
if (!getCapabilities().images) {
|
|
@@ -441,6 +477,9 @@ export class TUI extends Container {
|
|
|
441
477
|
clearTimeout(this.renderTimer);
|
|
442
478
|
this.renderTimer = undefined;
|
|
443
479
|
}
|
|
480
|
+
if (this.terminalColorSchemeNotificationsEnabled) {
|
|
481
|
+
this.terminal.write("\x1b[?2031l");
|
|
482
|
+
}
|
|
444
483
|
// Move cursor to the end of the content to prevent overwriting/artifacts on exit
|
|
445
484
|
if (this.previousLines.length > 0) {
|
|
446
485
|
const targetRow = this.previousLines.length; // Line after the last content
|
|
@@ -505,6 +544,12 @@ export class TUI extends Container {
|
|
|
505
544
|
}, delay);
|
|
506
545
|
}
|
|
507
546
|
handleInput(data) {
|
|
547
|
+
if (this.consumeOsc11BackgroundResponse(data)) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (this.consumeTerminalColorSchemeReport(data)) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
508
553
|
if (this.inputListeners.size > 0) {
|
|
509
554
|
let current = data;
|
|
510
555
|
for (const listener of this.inputListeners) {
|
|
@@ -570,6 +615,37 @@ export class TUI extends Container {
|
|
|
570
615
|
this.requestRender();
|
|
571
616
|
}
|
|
572
617
|
}
|
|
618
|
+
consumeOsc11BackgroundResponse(data) {
|
|
619
|
+
if (this.pendingOsc11BackgroundReplies <= 0) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
if (!isOsc11BackgroundColorResponse(data)) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
const rgb = parseOsc11BackgroundColor(data);
|
|
626
|
+
this.pendingOsc11BackgroundReplies -= 1;
|
|
627
|
+
const query = this.pendingOsc11BackgroundQueries.shift();
|
|
628
|
+
if (query && !query.settled) {
|
|
629
|
+
query.settled = true;
|
|
630
|
+
if (query.timer) {
|
|
631
|
+
clearTimeout(query.timer);
|
|
632
|
+
query.timer = undefined;
|
|
633
|
+
}
|
|
634
|
+
query.resolve?.(rgb);
|
|
635
|
+
query.resolve = undefined;
|
|
636
|
+
}
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
consumeTerminalColorSchemeReport(data) {
|
|
640
|
+
const scheme = parseTerminalColorSchemeReport(data);
|
|
641
|
+
if (!scheme) {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
for (const listener of this.terminalColorSchemeListeners) {
|
|
645
|
+
listener(scheme);
|
|
646
|
+
}
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
573
649
|
consumeCellSizeResponse(data) {
|
|
574
650
|
// Response format: ESC [ 6 ; height ; width t
|
|
575
651
|
const match = data.match(/^\x1b\[6;(\d+);(\d+)t$/);
|
|
@@ -789,14 +865,37 @@ export class TUI extends Container {
|
|
|
789
865
|
}
|
|
790
866
|
return buffer;
|
|
791
867
|
}
|
|
792
|
-
|
|
868
|
+
getKittyImageReservedRows(lines, index, maxIndex = lines.length - 1) {
|
|
869
|
+
const rows = extractKittyImageRows(lines[index] ?? "");
|
|
870
|
+
if (rows <= 1)
|
|
871
|
+
return 1;
|
|
872
|
+
const maxRows = Math.min(rows, maxIndex - index + 1, lines.length - index);
|
|
873
|
+
let reservedRows = 1;
|
|
874
|
+
while (reservedRows < maxRows) {
|
|
875
|
+
const line = lines[index + reservedRows] ?? "";
|
|
876
|
+
if (isImageLine(line) || visibleWidth(line) > 0)
|
|
877
|
+
break;
|
|
878
|
+
reservedRows++;
|
|
879
|
+
}
|
|
880
|
+
return reservedRows;
|
|
881
|
+
}
|
|
882
|
+
expandChangedRangeForKittyImages(firstChanged, lastChanged, newLines) {
|
|
883
|
+
let expandedFirstChanged = firstChanged;
|
|
793
884
|
let expandedLastChanged = lastChanged;
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
885
|
+
const expandForLines = (lines) => {
|
|
886
|
+
for (let i = 0; i < lines.length; i++) {
|
|
887
|
+
if (extractKittyImageIds(lines[i]).length === 0)
|
|
888
|
+
continue;
|
|
889
|
+
const blockEnd = i + this.getKittyImageReservedRows(lines, i) - 1;
|
|
890
|
+
if (i >= firstChanged || (i <= lastChanged && blockEnd >= firstChanged)) {
|
|
891
|
+
expandedFirstChanged = Math.min(expandedFirstChanged, i);
|
|
892
|
+
expandedLastChanged = Math.max(expandedLastChanged, blockEnd);
|
|
893
|
+
}
|
|
797
894
|
}
|
|
798
|
-
}
|
|
799
|
-
|
|
895
|
+
};
|
|
896
|
+
expandForLines(this.previousLines);
|
|
897
|
+
expandForLines(newLines);
|
|
898
|
+
return { firstChanged: expandedFirstChanged, lastChanged: expandedLastChanged };
|
|
800
899
|
}
|
|
801
900
|
deleteChangedKittyImages(firstChanged, lastChanged) {
|
|
802
901
|
if (firstChanged < 0 || lastChanged < firstChanged)
|
|
@@ -910,7 +1009,20 @@ export class TUI extends Container {
|
|
|
910
1009
|
for (let i = 0; i < newLines.length; i++) {
|
|
911
1010
|
if (i > 0)
|
|
912
1011
|
buffer += "\r\n";
|
|
913
|
-
|
|
1012
|
+
const line = newLines[i];
|
|
1013
|
+
const isImage = isImageLine(line);
|
|
1014
|
+
const imageReservedRows = isImage ? this.getKittyImageReservedRows(newLines, i) : 1;
|
|
1015
|
+
if (imageReservedRows > 1 && imageReservedRows <= height) {
|
|
1016
|
+
for (let row = 1; row < imageReservedRows; row++) {
|
|
1017
|
+
buffer += "\r\n";
|
|
1018
|
+
}
|
|
1019
|
+
buffer += `\x1b[${imageReservedRows - 1}A`;
|
|
1020
|
+
buffer += line;
|
|
1021
|
+
buffer += `\x1b[${imageReservedRows - 1}B`;
|
|
1022
|
+
i += imageReservedRows - 1;
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
buffer += line;
|
|
914
1026
|
}
|
|
915
1027
|
buffer += "\x1b[?2026l"; // End synchronized output
|
|
916
1028
|
this.terminal.write(buffer);
|
|
@@ -989,7 +1101,9 @@ export class TUI extends Container {
|
|
|
989
1101
|
lastChanged = newLines.length - 1;
|
|
990
1102
|
}
|
|
991
1103
|
if (firstChanged !== -1) {
|
|
992
|
-
|
|
1104
|
+
const expandedRange = this.expandChangedRangeForKittyImages(firstChanged, lastChanged, newLines);
|
|
1105
|
+
firstChanged = expandedRange.firstChanged;
|
|
1106
|
+
lastChanged = expandedRange.lastChanged;
|
|
993
1107
|
}
|
|
994
1108
|
const appendStart = appendedLines && firstChanged === this.previousLines.length && firstChanged > 0;
|
|
995
1109
|
// No changes - but still need to update hardware cursor position if it moved
|
|
@@ -1090,9 +1204,27 @@ export class TUI extends Container {
|
|
|
1090
1204
|
for (let i = firstChanged; i <= renderEnd; i++) {
|
|
1091
1205
|
if (i > firstChanged)
|
|
1092
1206
|
buffer += "\r\n";
|
|
1093
|
-
buffer += "\x1b[2K"; // Clear current line
|
|
1094
1207
|
const line = newLines[i];
|
|
1095
1208
|
const isImage = isImageLine(line);
|
|
1209
|
+
const imageReservedRows = isImage ? this.getKittyImageReservedRows(newLines, i, renderEnd) : 1;
|
|
1210
|
+
if (imageReservedRows > 1) {
|
|
1211
|
+
const imageStartScreenRow = i - viewportTop;
|
|
1212
|
+
if (imageStartScreenRow < 0 || imageStartScreenRow + imageReservedRows > height) {
|
|
1213
|
+
logRedraw(`kitty image pre-clear would scroll (${imageStartScreenRow} + ${imageReservedRows} > ${height})`);
|
|
1214
|
+
fullRender(true);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
buffer += "\x1b[2K";
|
|
1218
|
+
for (let row = 1; row < imageReservedRows; row++) {
|
|
1219
|
+
buffer += "\r\n\x1b[2K";
|
|
1220
|
+
}
|
|
1221
|
+
buffer += `\x1b[${imageReservedRows - 1}A`;
|
|
1222
|
+
buffer += line;
|
|
1223
|
+
buffer += `\x1b[${imageReservedRows - 1}B`;
|
|
1224
|
+
i += imageReservedRows - 1;
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
buffer += "\x1b[2K"; // Clear current line
|
|
1096
1228
|
if (!isImage && visibleWidth(line) > width) {
|
|
1097
1229
|
// Log all lines to crash file for debugging
|
|
1098
1230
|
const crashLogPath = path.join(os.homedir(), ".pi", "agent", "pi-crash.log");
|
|
@@ -1219,5 +1351,57 @@ export class TUI extends Container {
|
|
|
1219
1351
|
this.terminal.hideCursor();
|
|
1220
1352
|
}
|
|
1221
1353
|
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Query the terminal's default background color with OSC 11 (`ESC ] 11 ; ? BEL`).
|
|
1356
|
+
* @param timeoutMs Query timeout in milliseconds.
|
|
1357
|
+
* @returns Promise containing the parsed RGB color, or undefined if it times out or fails to parse.
|
|
1358
|
+
*/
|
|
1359
|
+
queryTerminalBackgroundColor({ timeoutMs }) {
|
|
1360
|
+
return new Promise((resolve) => {
|
|
1361
|
+
const query = {
|
|
1362
|
+
settled: false,
|
|
1363
|
+
resolve,
|
|
1364
|
+
timer: undefined,
|
|
1365
|
+
};
|
|
1366
|
+
query.timer = setTimeout(() => {
|
|
1367
|
+
if (query.settled) {
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
query.settled = true;
|
|
1371
|
+
query.timer = undefined;
|
|
1372
|
+
query.resolve?.(undefined);
|
|
1373
|
+
query.resolve = undefined;
|
|
1374
|
+
}, timeoutMs);
|
|
1375
|
+
this.pendingOsc11BackgroundQueries.push(query);
|
|
1376
|
+
this.pendingOsc11BackgroundReplies += 1;
|
|
1377
|
+
this.terminal.write("\x1b]11;?\x07");
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Query the terminal's color-scheme preference with DSR (`CSI ? 996 n`).
|
|
1382
|
+
* Terminals that support the color palette notification protocol reply with
|
|
1383
|
+
* `CSI ? 997 ; 1 n` for dark or `CSI ? 997 ; 2 n` for light.
|
|
1384
|
+
*/
|
|
1385
|
+
queryTerminalColorScheme({ timeoutMs }) {
|
|
1386
|
+
return new Promise((resolve) => {
|
|
1387
|
+
let settled = false;
|
|
1388
|
+
let timer;
|
|
1389
|
+
let unsubscribe = () => { };
|
|
1390
|
+
const settle = (scheme) => {
|
|
1391
|
+
if (settled)
|
|
1392
|
+
return;
|
|
1393
|
+
settled = true;
|
|
1394
|
+
if (timer) {
|
|
1395
|
+
clearTimeout(timer);
|
|
1396
|
+
timer = undefined;
|
|
1397
|
+
}
|
|
1398
|
+
unsubscribe();
|
|
1399
|
+
resolve(scheme);
|
|
1400
|
+
};
|
|
1401
|
+
unsubscribe = this.onTerminalColorSchemeChange(settle);
|
|
1402
|
+
timer = setTimeout(() => settle(undefined), timeoutMs);
|
|
1403
|
+
this.terminal.write("\x1b[?996n");
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1222
1406
|
}
|
|
1223
1407
|
//# sourceMappingURL=tui.js.map
|