@draht/tui 2026.5.12 → 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/autocomplete.d.ts +5 -1
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +16 -7
- package/dist/autocomplete.js.map +1 -1
- package/dist/components/box.d.ts +1 -1
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js.map +1 -1
- package/dist/components/cancellable-loader.d.ts +1 -1
- package/dist/components/cancellable-loader.d.ts.map +1 -1
- package/dist/components/cancellable-loader.js.map +1 -1
- package/dist/components/editor.d.ts +14 -3
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +234 -175
- package/dist/components/editor.js.map +1 -1
- package/dist/components/image.d.ts +2 -2
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +31 -11
- package/dist/components/image.js.map +1 -1
- package/dist/components/input.d.ts +1 -1
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +7 -55
- package/dist/components/input.js.map +1 -1
- package/dist/components/loader.d.ts +17 -7
- package/dist/components/loader.d.ts.map +1 -1
- package/dist/components/loader.js +34 -14
- package/dist/components/loader.js.map +1 -1
- package/dist/components/markdown.d.ts +11 -7
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +119 -102
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/select-list.d.ts +1 -1
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/settings-list.d.ts +1 -1
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js.map +1 -1
- package/dist/components/spacer.d.ts +1 -1
- package/dist/components/spacer.d.ts.map +1 -1
- package/dist/components/spacer.js.map +1 -1
- package/dist/components/text.d.ts +1 -1
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js.map +1 -1
- package/dist/components/truncated-text.d.ts +1 -1
- package/dist/components/truncated-text.d.ts.map +1 -1
- package/dist/components/truncated-text.js.map +1 -1
- package/dist/editor-component.d.ts +2 -2
- package/dist/editor-component.d.ts.map +1 -1
- package/dist/editor-component.js.map +1 -1
- package/dist/fuzzy.d.ts +1 -1
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js +5 -2
- package/dist/fuzzy.js.map +1 -1
- package/dist/index.d.ts +23 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/keybindings.d.ts +2 -2
- package/dist/keybindings.d.ts.map +1 -1
- package/dist/keybindings.js +1 -1
- package/dist/keybindings.js.map +1 -1
- package/dist/keys.d.ts +19 -5
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +82 -33
- package/dist/keys.js.map +1 -1
- package/dist/native-modifiers.d.ts +3 -0
- package/dist/native-modifiers.d.ts.map +1 -0
- package/dist/native-modifiers.js +53 -0
- package/dist/native-modifiers.js.map +1 -0
- package/dist/stdin-buffer.d.ts +2 -0
- package/dist/stdin-buffer.d.ts.map +1 -1
- package/dist/stdin-buffer.js +48 -4
- package/dist/stdin-buffer.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 +23 -1
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +98 -16
- package/dist/terminal-image.js.map +1 -1
- package/dist/terminal.d.ts +36 -10
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +212 -64
- package/dist/terminal.js.map +1 -1
- package/dist/tui.d.ts +51 -5
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +431 -32
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +8 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +143 -43
- package/dist/utils.js.map +1 -1
- package/dist/word-navigation.d.ts +25 -0
- package/dist/word-navigation.d.ts.map +1 -0
- package/dist/word-navigation.js +96 -0
- package/dist/word-navigation.js.map +1 -0
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -6,8 +6,43 @@ 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 {
|
|
10
|
-
import {
|
|
9
|
+
import { isOsc11BackgroundColorResponse, parseOsc11BackgroundColor, parseTerminalColorSchemeReport, } from "./terminal-colors.js";
|
|
10
|
+
import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
|
|
11
|
+
import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
|
|
12
|
+
const KITTY_SEQUENCE_PREFIX = "\x1b_G";
|
|
13
|
+
function parseKittyImageHeader(line) {
|
|
14
|
+
const sequenceStart = line.indexOf(KITTY_SEQUENCE_PREFIX);
|
|
15
|
+
if (sequenceStart === -1)
|
|
16
|
+
return undefined;
|
|
17
|
+
const paramsStart = sequenceStart + KITTY_SEQUENCE_PREFIX.length;
|
|
18
|
+
const paramsEnd = line.indexOf(";", paramsStart);
|
|
19
|
+
if (paramsEnd === -1)
|
|
20
|
+
return undefined;
|
|
21
|
+
const ids = [];
|
|
22
|
+
let rows = 1;
|
|
23
|
+
const params = line.slice(paramsStart, paramsEnd);
|
|
24
|
+
for (const param of params.split(",")) {
|
|
25
|
+
const [key, value] = param.split("=", 2);
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
const numberValue = Number(value);
|
|
29
|
+
if (!Number.isInteger(numberValue) || numberValue <= 0 || numberValue > 0xffffffff)
|
|
30
|
+
continue;
|
|
31
|
+
if (key === "i") {
|
|
32
|
+
ids.push(numberValue);
|
|
33
|
+
}
|
|
34
|
+
else if (key === "r") {
|
|
35
|
+
rows = numberValue;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
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;
|
|
45
|
+
}
|
|
11
46
|
/** Type guard to check if a component implements Focusable */
|
|
12
47
|
export function isFocusable(component) {
|
|
13
48
|
return component !== null && "focused" in component;
|
|
@@ -75,6 +110,7 @@ export class Container {
|
|
|
75
110
|
export class TUI extends Container {
|
|
76
111
|
terminal;
|
|
77
112
|
previousLines = [];
|
|
113
|
+
previousKittyImageIds = new Set();
|
|
78
114
|
previousWidth = 0;
|
|
79
115
|
previousHeight = 0;
|
|
80
116
|
focusedComponent = null;
|
|
@@ -93,9 +129,14 @@ export class TUI extends Container {
|
|
|
93
129
|
previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
|
|
94
130
|
fullRedrawCount = 0;
|
|
95
131
|
stopped = false;
|
|
132
|
+
pendingOsc11BackgroundReplies = 0;
|
|
133
|
+
pendingOsc11BackgroundQueries = [];
|
|
134
|
+
terminalColorSchemeListeners = new Set();
|
|
135
|
+
terminalColorSchemeNotificationsEnabled = false;
|
|
96
136
|
// Overlay stack for modal components rendered on top of base content
|
|
97
137
|
focusOrderCounter = 0;
|
|
98
138
|
overlayStack = [];
|
|
139
|
+
overlayFocusRestore = { status: "inactive" };
|
|
99
140
|
constructor(terminal, showHardwareCursor) {
|
|
100
141
|
super();
|
|
101
142
|
this.terminal = terminal;
|
|
@@ -130,16 +171,115 @@ export class TUI extends Container {
|
|
|
130
171
|
this.clearOnShrink = enabled;
|
|
131
172
|
}
|
|
132
173
|
setFocus(component) {
|
|
133
|
-
|
|
174
|
+
this.setFocusInternal({ component, overlayFocusRestore: "clear" });
|
|
175
|
+
}
|
|
176
|
+
setFocusInternal({ component, overlayFocusRestore, }) {
|
|
177
|
+
const previousFocus = this.focusedComponent;
|
|
178
|
+
let nextFocus = component;
|
|
179
|
+
const previousFocusedOverlay = previousFocus
|
|
180
|
+
? this.overlayStack.find((entry) => entry.component === previousFocus && this.isOverlayVisible(entry))
|
|
181
|
+
: undefined;
|
|
182
|
+
const nextFocusIsOverlay = nextFocus ? this.overlayStack.some((entry) => entry.component === nextFocus) : false;
|
|
183
|
+
const restoreState = this.getVisibleOverlayFocusRestore();
|
|
184
|
+
if (nextFocus && !nextFocusIsOverlay) {
|
|
185
|
+
if (restoreState.status === "blocked" && restoreState.blockedBy === previousFocus) {
|
|
186
|
+
if (restoreState.resume.status === "focus-target" || !this.isComponentMounted(restoreState.blockedBy)) {
|
|
187
|
+
nextFocus = this.resolveBlockedOverlayFocusResume(restoreState);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
this.overlayFocusRestore = {
|
|
191
|
+
status: "blocked",
|
|
192
|
+
overlay: restoreState.overlay,
|
|
193
|
+
blockedBy: nextFocus,
|
|
194
|
+
resume: restoreState.resume,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else if (previousFocusedOverlay &&
|
|
199
|
+
restoreState.status !== "inactive" &&
|
|
200
|
+
restoreState.overlay === previousFocusedOverlay &&
|
|
201
|
+
!this.isOverlayFocusAncestor(previousFocusedOverlay, nextFocus)) {
|
|
202
|
+
this.overlayFocusRestore = {
|
|
203
|
+
status: "blocked",
|
|
204
|
+
overlay: previousFocusedOverlay,
|
|
205
|
+
blockedBy: nextFocus,
|
|
206
|
+
resume: { status: "restore-overlay" },
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else if (nextFocus === null) {
|
|
211
|
+
if (restoreState.status === "blocked" && restoreState.blockedBy === previousFocus) {
|
|
212
|
+
nextFocus = this.resolveBlockedOverlayFocusResume(restoreState);
|
|
213
|
+
}
|
|
214
|
+
else if (overlayFocusRestore === "clear") {
|
|
215
|
+
this.clearOverlayFocusRestore();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
134
218
|
if (isFocusable(this.focusedComponent)) {
|
|
135
219
|
this.focusedComponent.focused = false;
|
|
136
220
|
}
|
|
137
|
-
this.focusedComponent =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
221
|
+
this.focusedComponent = nextFocus;
|
|
222
|
+
if (isFocusable(nextFocus)) {
|
|
223
|
+
nextFocus.focused = true;
|
|
224
|
+
}
|
|
225
|
+
const focusedOverlay = nextFocus
|
|
226
|
+
? this.overlayStack.find((entry) => entry.component === nextFocus && this.isOverlayVisible(entry))
|
|
227
|
+
: undefined;
|
|
228
|
+
if (focusedOverlay) {
|
|
229
|
+
this.overlayFocusRestore = { status: "eligible", overlay: focusedOverlay };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
clearOverlayFocusRestore() {
|
|
233
|
+
this.overlayFocusRestore = { status: "inactive" };
|
|
234
|
+
}
|
|
235
|
+
clearOverlayFocusRestoreFor(overlay) {
|
|
236
|
+
if (this.overlayFocusRestore.status !== "inactive" && this.overlayFocusRestore.overlay === overlay) {
|
|
237
|
+
this.clearOverlayFocusRestore();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
resolveBlockedOverlayFocusResume(restoreState) {
|
|
241
|
+
if (restoreState.resume.status === "restore-overlay")
|
|
242
|
+
return restoreState.overlay.component;
|
|
243
|
+
this.clearOverlayFocusRestore();
|
|
244
|
+
return restoreState.resume.target;
|
|
245
|
+
}
|
|
246
|
+
getVisibleOverlayFocusRestore() {
|
|
247
|
+
const restoreState = this.overlayFocusRestore;
|
|
248
|
+
if (restoreState.status === "inactive")
|
|
249
|
+
return restoreState;
|
|
250
|
+
if (!this.overlayStack.includes(restoreState.overlay) || !this.isOverlayVisible(restoreState.overlay)) {
|
|
251
|
+
return { status: "inactive" };
|
|
252
|
+
}
|
|
253
|
+
return restoreState;
|
|
254
|
+
}
|
|
255
|
+
isOverlayFocusAncestor(entry, component) {
|
|
256
|
+
const visited = new Set();
|
|
257
|
+
let current = entry.preFocus;
|
|
258
|
+
while (current && !visited.has(current)) {
|
|
259
|
+
visited.add(current);
|
|
260
|
+
if (current === component)
|
|
261
|
+
return true;
|
|
262
|
+
current = this.overlayStack.find((overlay) => overlay.component === current)?.preFocus ?? null;
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
retargetOverlayPreFocus(removed) {
|
|
267
|
+
for (const overlay of this.overlayStack) {
|
|
268
|
+
if (overlay !== removed && overlay.preFocus === removed.component) {
|
|
269
|
+
overlay.preFocus = removed.preFocus;
|
|
270
|
+
}
|
|
141
271
|
}
|
|
142
272
|
}
|
|
273
|
+
isComponentMounted(component) {
|
|
274
|
+
return this.children.some((child) => this.containsComponent(child, component));
|
|
275
|
+
}
|
|
276
|
+
containsComponent(root, target) {
|
|
277
|
+
if (root === target)
|
|
278
|
+
return true;
|
|
279
|
+
if (!(root instanceof Container))
|
|
280
|
+
return false;
|
|
281
|
+
return root.children.some((child) => this.containsComponent(child, target));
|
|
282
|
+
}
|
|
143
283
|
/**
|
|
144
284
|
* Show an overlay component with configurable positioning and sizing.
|
|
145
285
|
* Returns a handle to control the overlay's visibility.
|
|
@@ -147,7 +287,7 @@ export class TUI extends Container {
|
|
|
147
287
|
showOverlay(component, options) {
|
|
148
288
|
const entry = {
|
|
149
289
|
component,
|
|
150
|
-
options,
|
|
290
|
+
...(options === undefined ? {} : { options }),
|
|
151
291
|
preFocus: this.focusedComponent,
|
|
152
292
|
hidden: false,
|
|
153
293
|
focusOrder: ++this.focusOrderCounter,
|
|
@@ -164,6 +304,8 @@ export class TUI extends Container {
|
|
|
164
304
|
hide: () => {
|
|
165
305
|
const index = this.overlayStack.indexOf(entry);
|
|
166
306
|
if (index !== -1) {
|
|
307
|
+
this.clearOverlayFocusRestoreFor(entry);
|
|
308
|
+
this.retargetOverlayPreFocus(entry);
|
|
167
309
|
this.overlayStack.splice(index, 1);
|
|
168
310
|
// Restore focus if this overlay had focus
|
|
169
311
|
if (this.focusedComponent === component) {
|
|
@@ -181,6 +323,7 @@ export class TUI extends Container {
|
|
|
181
323
|
entry.hidden = hidden;
|
|
182
324
|
// Update focus when hiding/showing
|
|
183
325
|
if (hidden) {
|
|
326
|
+
this.clearOverlayFocusRestoreFor(entry);
|
|
184
327
|
// If this overlay had focus, move focus to next visible or preFocus
|
|
185
328
|
if (this.focusedComponent === component) {
|
|
186
329
|
const topVisible = this.getTopmostVisibleOverlay();
|
|
@@ -200,17 +343,39 @@ export class TUI extends Container {
|
|
|
200
343
|
focus: () => {
|
|
201
344
|
if (!this.overlayStack.includes(entry) || !this.isOverlayVisible(entry))
|
|
202
345
|
return;
|
|
203
|
-
if (this.focusedComponent !== component) {
|
|
204
|
-
this.setFocus(component);
|
|
205
|
-
}
|
|
206
346
|
entry.focusOrder = ++this.focusOrderCounter;
|
|
347
|
+
this.setFocus(component);
|
|
207
348
|
this.requestRender();
|
|
208
349
|
},
|
|
209
|
-
unfocus: () => {
|
|
210
|
-
|
|
350
|
+
unfocus: (unfocusOptions) => {
|
|
351
|
+
const isFocused = this.focusedComponent === component;
|
|
352
|
+
const restoreState = this.overlayFocusRestore;
|
|
353
|
+
const hasPendingRestore = restoreState.status !== "inactive" && restoreState.overlay === entry;
|
|
354
|
+
if (!isFocused && !hasPendingRestore)
|
|
211
355
|
return;
|
|
212
|
-
|
|
213
|
-
|
|
356
|
+
if (restoreState.status === "blocked" &&
|
|
357
|
+
restoreState.overlay === entry &&
|
|
358
|
+
this.focusedComponent === restoreState.blockedBy) {
|
|
359
|
+
if (unfocusOptions) {
|
|
360
|
+
this.overlayFocusRestore = {
|
|
361
|
+
status: "blocked",
|
|
362
|
+
overlay: entry,
|
|
363
|
+
blockedBy: restoreState.blockedBy,
|
|
364
|
+
resume: { status: "focus-target", target: unfocusOptions.target },
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
this.clearOverlayFocusRestore();
|
|
369
|
+
}
|
|
370
|
+
this.requestRender();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
this.clearOverlayFocusRestoreFor(entry);
|
|
374
|
+
if (isFocused || unfocusOptions) {
|
|
375
|
+
const topVisible = this.getTopmostVisibleOverlay();
|
|
376
|
+
const fallbackTarget = topVisible && topVisible !== entry ? topVisible.component : entry.preFocus;
|
|
377
|
+
this.setFocus(unfocusOptions ? unfocusOptions.target : fallbackTarget);
|
|
378
|
+
}
|
|
214
379
|
this.requestRender();
|
|
215
380
|
},
|
|
216
381
|
isFocused: () => this.focusedComponent === component,
|
|
@@ -218,9 +383,12 @@ export class TUI extends Container {
|
|
|
218
383
|
}
|
|
219
384
|
/** Hide the topmost overlay and restore previous focus. */
|
|
220
385
|
hideOverlay() {
|
|
221
|
-
const overlay = this.overlayStack.
|
|
386
|
+
const overlay = this.overlayStack[this.overlayStack.length - 1];
|
|
222
387
|
if (!overlay)
|
|
223
388
|
return;
|
|
389
|
+
this.clearOverlayFocusRestoreFor(overlay);
|
|
390
|
+
this.retargetOverlayPreFocus(overlay);
|
|
391
|
+
this.overlayStack.pop();
|
|
224
392
|
if (this.focusedComponent === overlay.component) {
|
|
225
393
|
// Find topmost visible overlay, or fall back to preFocus
|
|
226
394
|
const topVisible = this.getTopmostVisibleOverlay();
|
|
@@ -243,16 +411,17 @@ export class TUI extends Container {
|
|
|
243
411
|
}
|
|
244
412
|
return true;
|
|
245
413
|
}
|
|
246
|
-
/** Find the
|
|
414
|
+
/** Find the visual-frontmost visible capturing overlay, if any */
|
|
247
415
|
getTopmostVisibleOverlay() {
|
|
248
|
-
|
|
249
|
-
|
|
416
|
+
let topmost;
|
|
417
|
+
for (const overlay of this.overlayStack) {
|
|
418
|
+
if (overlay.options?.nonCapturing || !this.isOverlayVisible(overlay))
|
|
250
419
|
continue;
|
|
251
|
-
if (
|
|
252
|
-
|
|
420
|
+
if (!topmost || overlay.focusOrder > topmost.focusOrder) {
|
|
421
|
+
topmost = overlay;
|
|
253
422
|
}
|
|
254
423
|
}
|
|
255
|
-
return
|
|
424
|
+
return topmost;
|
|
256
425
|
}
|
|
257
426
|
invalidate() {
|
|
258
427
|
super.invalidate();
|
|
@@ -263,6 +432,9 @@ export class TUI extends Container {
|
|
|
263
432
|
this.stopped = false;
|
|
264
433
|
this.terminal.start((data) => this.handleInput(data), () => this.requestRender());
|
|
265
434
|
this.terminal.hideCursor();
|
|
435
|
+
if (this.terminalColorSchemeNotificationsEnabled) {
|
|
436
|
+
this.terminal.write("\x1b[?2031h");
|
|
437
|
+
}
|
|
266
438
|
this.queryCellSize();
|
|
267
439
|
this.requestRender();
|
|
268
440
|
}
|
|
@@ -275,6 +447,21 @@ export class TUI extends Container {
|
|
|
275
447
|
removeInputListener(listener) {
|
|
276
448
|
this.inputListeners.delete(listener);
|
|
277
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
|
+
}
|
|
278
465
|
queryCellSize() {
|
|
279
466
|
// Only query if terminal supports images (cell size is only used for image rendering)
|
|
280
467
|
if (!getCapabilities().images) {
|
|
@@ -290,6 +477,9 @@ export class TUI extends Container {
|
|
|
290
477
|
clearTimeout(this.renderTimer);
|
|
291
478
|
this.renderTimer = undefined;
|
|
292
479
|
}
|
|
480
|
+
if (this.terminalColorSchemeNotificationsEnabled) {
|
|
481
|
+
this.terminal.write("\x1b[?2031l");
|
|
482
|
+
}
|
|
293
483
|
// Move cursor to the end of the content to prevent overwriting/artifacts on exit
|
|
294
484
|
if (this.previousLines.length > 0) {
|
|
295
485
|
const targetRow = this.previousLines.length; // Line after the last content
|
|
@@ -354,6 +544,12 @@ export class TUI extends Container {
|
|
|
354
544
|
}, delay);
|
|
355
545
|
}
|
|
356
546
|
handleInput(data) {
|
|
547
|
+
if (this.consumeOsc11BackgroundResponse(data)) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (this.consumeTerminalColorSchemeReport(data)) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
357
553
|
if (this.inputListeners.size > 0) {
|
|
358
554
|
let current = data;
|
|
359
555
|
for (const listener of this.inputListeners) {
|
|
@@ -389,8 +585,23 @@ export class TUI extends Container {
|
|
|
389
585
|
this.setFocus(topVisible.component);
|
|
390
586
|
}
|
|
391
587
|
else {
|
|
392
|
-
|
|
393
|
-
|
|
588
|
+
this.setFocusInternal({ component: focusedOverlay.preFocus, overlayFocusRestore: "preserve" });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const focusIsOverlay = this.overlayStack.some((o) => o.component === this.focusedComponent);
|
|
592
|
+
if (!focusIsOverlay) {
|
|
593
|
+
const restoreState = this.getVisibleOverlayFocusRestore();
|
|
594
|
+
if (restoreState.status === "eligible") {
|
|
595
|
+
this.setFocus(restoreState.overlay.component);
|
|
596
|
+
}
|
|
597
|
+
else if (restoreState.status === "blocked" && restoreState.blockedBy !== this.focusedComponent) {
|
|
598
|
+
if (restoreState.resume.status === "restore-overlay") {
|
|
599
|
+
this.setFocus(restoreState.overlay.component);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
this.clearOverlayFocusRestore();
|
|
603
|
+
this.setFocus(restoreState.resume.target);
|
|
604
|
+
}
|
|
394
605
|
}
|
|
395
606
|
}
|
|
396
607
|
// Pass input to focused component (including Ctrl+C)
|
|
@@ -404,6 +615,37 @@ export class TUI extends Container {
|
|
|
404
615
|
this.requestRender();
|
|
405
616
|
}
|
|
406
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
|
+
}
|
|
407
649
|
consumeCellSizeResponse(data) {
|
|
408
650
|
// Response format: ESC [ 6 ; height ; width t
|
|
409
651
|
const match = data.match(/^\x1b\[6;(\d+);(\d+)t$/);
|
|
@@ -602,11 +844,71 @@ export class TUI extends Container {
|
|
|
602
844
|
for (let i = 0; i < lines.length; i++) {
|
|
603
845
|
const line = lines[i];
|
|
604
846
|
if (!isImageLine(line)) {
|
|
605
|
-
lines[i] = line + reset;
|
|
847
|
+
lines[i] = normalizeTerminalOutput(line) + reset;
|
|
606
848
|
}
|
|
607
849
|
}
|
|
608
850
|
return lines;
|
|
609
851
|
}
|
|
852
|
+
collectKittyImageIds(lines) {
|
|
853
|
+
const ids = new Set();
|
|
854
|
+
for (const line of lines) {
|
|
855
|
+
for (const id of extractKittyImageIds(line)) {
|
|
856
|
+
ids.add(id);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return ids;
|
|
860
|
+
}
|
|
861
|
+
deleteKittyImages(ids) {
|
|
862
|
+
let buffer = "";
|
|
863
|
+
for (const id of ids) {
|
|
864
|
+
buffer += deleteKittyImage(id);
|
|
865
|
+
}
|
|
866
|
+
return buffer;
|
|
867
|
+
}
|
|
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;
|
|
884
|
+
let expandedLastChanged = lastChanged;
|
|
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
|
+
}
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
expandForLines(this.previousLines);
|
|
897
|
+
expandForLines(newLines);
|
|
898
|
+
return { firstChanged: expandedFirstChanged, lastChanged: expandedLastChanged };
|
|
899
|
+
}
|
|
900
|
+
deleteChangedKittyImages(firstChanged, lastChanged) {
|
|
901
|
+
if (firstChanged < 0 || lastChanged < firstChanged)
|
|
902
|
+
return "";
|
|
903
|
+
const ids = new Set();
|
|
904
|
+
const maxLine = Math.min(lastChanged, this.previousLines.length - 1);
|
|
905
|
+
for (let i = firstChanged; i <= maxLine; i++) {
|
|
906
|
+
for (const id of extractKittyImageIds(this.previousLines[i] ?? "")) {
|
|
907
|
+
ids.add(id);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return this.deleteKittyImages(ids);
|
|
911
|
+
}
|
|
610
912
|
/** Splice overlay content into a base line at a specific column. Single-pass optimized. */
|
|
611
913
|
compositeLineAt(baseLine, overlayLine, startCol, overlayWidth, totalWidth) {
|
|
612
914
|
if (isImageLine(baseLine))
|
|
@@ -700,12 +1002,27 @@ export class TUI extends Container {
|
|
|
700
1002
|
const fullRender = (clear) => {
|
|
701
1003
|
this.fullRedrawCount += 1;
|
|
702
1004
|
let buffer = "\x1b[?2026h"; // Begin synchronized output
|
|
703
|
-
if (clear)
|
|
1005
|
+
if (clear) {
|
|
1006
|
+
buffer += this.deleteKittyImages(this.previousKittyImageIds);
|
|
704
1007
|
buffer += "\x1b[2J\x1b[H\x1b[3J"; // Clear screen, home, then clear scrollback
|
|
1008
|
+
}
|
|
705
1009
|
for (let i = 0; i < newLines.length; i++) {
|
|
706
1010
|
if (i > 0)
|
|
707
1011
|
buffer += "\r\n";
|
|
708
|
-
|
|
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;
|
|
709
1026
|
}
|
|
710
1027
|
buffer += "\x1b[?2026l"; // End synchronized output
|
|
711
1028
|
this.terminal.write(buffer);
|
|
@@ -722,6 +1039,7 @@ export class TUI extends Container {
|
|
|
722
1039
|
this.previousViewportTop = Math.max(0, bufferLength - height);
|
|
723
1040
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
724
1041
|
this.previousLines = newLines;
|
|
1042
|
+
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
725
1043
|
this.previousWidth = width;
|
|
726
1044
|
this.previousHeight = height;
|
|
727
1045
|
};
|
|
@@ -782,6 +1100,11 @@ export class TUI extends Container {
|
|
|
782
1100
|
}
|
|
783
1101
|
lastChanged = newLines.length - 1;
|
|
784
1102
|
}
|
|
1103
|
+
if (firstChanged !== -1) {
|
|
1104
|
+
const expandedRange = this.expandChangedRangeForKittyImages(firstChanged, lastChanged, newLines);
|
|
1105
|
+
firstChanged = expandedRange.firstChanged;
|
|
1106
|
+
lastChanged = expandedRange.lastChanged;
|
|
1107
|
+
}
|
|
785
1108
|
const appendStart = appendedLines && firstChanged === this.previousLines.length && firstChanged > 0;
|
|
786
1109
|
// No changes - but still need to update hardware cursor position if it moved
|
|
787
1110
|
if (firstChanged === -1) {
|
|
@@ -794,6 +1117,7 @@ export class TUI extends Container {
|
|
|
794
1117
|
if (firstChanged >= newLines.length) {
|
|
795
1118
|
if (this.previousLines.length > newLines.length) {
|
|
796
1119
|
let buffer = "\x1b[?2026h";
|
|
1120
|
+
buffer += this.deleteChangedKittyImages(firstChanged, lastChanged);
|
|
797
1121
|
// Move to end of new content (clamp to 0 for empty content)
|
|
798
1122
|
const targetRow = Math.max(0, newLines.length - 1);
|
|
799
1123
|
if (targetRow < prevViewportTop) {
|
|
@@ -814,16 +1138,18 @@ export class TUI extends Container {
|
|
|
814
1138
|
fullRender(true);
|
|
815
1139
|
return;
|
|
816
1140
|
}
|
|
817
|
-
|
|
818
|
-
|
|
1141
|
+
const clearStartOffset = newLines.length === 0 ? 0 : 1;
|
|
1142
|
+
if (extraLines > 0 && clearStartOffset > 0) {
|
|
1143
|
+
buffer += `\x1b[${clearStartOffset}B`;
|
|
819
1144
|
}
|
|
820
1145
|
for (let i = 0; i < extraLines; i++) {
|
|
821
1146
|
buffer += "\r\x1b[2K";
|
|
822
1147
|
if (i < extraLines - 1)
|
|
823
1148
|
buffer += "\x1b[1B";
|
|
824
1149
|
}
|
|
825
|
-
|
|
826
|
-
|
|
1150
|
+
const moveBack = Math.max(0, extraLines - 1 + clearStartOffset);
|
|
1151
|
+
if (moveBack > 0) {
|
|
1152
|
+
buffer += `\x1b[${moveBack}A`;
|
|
827
1153
|
}
|
|
828
1154
|
buffer += "\x1b[?2026l";
|
|
829
1155
|
this.terminal.write(buffer);
|
|
@@ -832,6 +1158,7 @@ export class TUI extends Container {
|
|
|
832
1158
|
}
|
|
833
1159
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
834
1160
|
this.previousLines = newLines;
|
|
1161
|
+
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
835
1162
|
this.previousWidth = width;
|
|
836
1163
|
this.previousHeight = height;
|
|
837
1164
|
this.previousViewportTop = prevViewportTop;
|
|
@@ -847,6 +1174,7 @@ export class TUI extends Container {
|
|
|
847
1174
|
// Render from first changed line to end
|
|
848
1175
|
// Build buffer with all updates wrapped in synchronized output
|
|
849
1176
|
let buffer = "\x1b[?2026h"; // Begin synchronized output
|
|
1177
|
+
buffer += this.deleteChangedKittyImages(firstChanged, lastChanged);
|
|
850
1178
|
const prevViewportBottom = prevViewportTop + height - 1;
|
|
851
1179
|
const moveTargetRow = appendStart ? firstChanged - 1 : firstChanged;
|
|
852
1180
|
if (moveTargetRow > prevViewportBottom) {
|
|
@@ -876,9 +1204,27 @@ export class TUI extends Container {
|
|
|
876
1204
|
for (let i = firstChanged; i <= renderEnd; i++) {
|
|
877
1205
|
if (i > firstChanged)
|
|
878
1206
|
buffer += "\r\n";
|
|
879
|
-
buffer += "\x1b[2K"; // Clear current line
|
|
880
1207
|
const line = newLines[i];
|
|
881
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
|
|
882
1228
|
if (!isImage && visibleWidth(line) > width) {
|
|
883
1229
|
// Log all lines to crash file for debugging
|
|
884
1230
|
const crashLogPath = path.join(os.homedir(), ".pi", "agent", "pi-crash.log");
|
|
@@ -966,6 +1312,7 @@ export class TUI extends Container {
|
|
|
966
1312
|
// Position hardware cursor for IME
|
|
967
1313
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
968
1314
|
this.previousLines = newLines;
|
|
1315
|
+
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
969
1316
|
this.previousWidth = width;
|
|
970
1317
|
this.previousHeight = height;
|
|
971
1318
|
}
|
|
@@ -1004,5 +1351,57 @@ export class TUI extends Container {
|
|
|
1004
1351
|
this.terminal.hideCursor();
|
|
1005
1352
|
}
|
|
1006
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
|
+
}
|
|
1007
1406
|
}
|
|
1008
1407
|
//# sourceMappingURL=tui.js.map
|