@code-yeongyu/senpi-tui 2026.10.18 → 2026.10.19
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/escape-window.d.ts +10 -0
- package/dist/escape-window.js +12 -0
- package/dist/stdin-buffer.js +7 -2
- package/dist/terminal-capabilities.d.ts +2 -0
- package/dist/terminal-capabilities.js +4 -1
- package/dist/terminal-image.js +16 -5
- package/dist/terminal.d.ts +3 -2
- package/dist/terminal.js +8 -6
- package/dist/tui-main-screen.d.ts +8 -0
- package/dist/tui-main-screen.js +37 -2
- package/dist/tui.d.ts +16 -5
- package/dist/tui.js +20 -48
- package/dist/viewport-insert-scroll.d.ts +22 -0
- package/dist/viewport-insert-scroll.js +60 -0
- package/dist/viewport-shift.d.ts +24 -0
- package/dist/viewport-shift.js +65 -0
- package/dist/wezterm-windows-images.d.ts +17 -0
- package/dist/wezterm-windows-images.js +26 -0
- package/dist/wsl-session.d.ts +3 -0
- package/dist/wsl-session.js +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pending lone ESC whose next input is delivered after the escape window is
|
|
3
|
+
* released as a standalone Escape press before the new input is processed. The
|
|
4
|
+
* decision uses the arrival-clock gap observed at delivery time, so an
|
|
5
|
+
* over-window gap is always treated as a lone Escape regardless of whether the
|
|
6
|
+
* flush timer callback ran first (#3628). Within the window the pair still
|
|
7
|
+
* merges into one Alt-key sequence.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isStaleEscape(pending: string, gapMs: number | undefined, escapeWindowMs: number): boolean;
|
|
10
|
+
//# sourceMappingURL=escape-window.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pending lone ESC whose next input is delivered after the escape window is
|
|
3
|
+
* released as a standalone Escape press before the new input is processed. The
|
|
4
|
+
* decision uses the arrival-clock gap observed at delivery time, so an
|
|
5
|
+
* over-window gap is always treated as a lone Escape regardless of whether the
|
|
6
|
+
* flush timer callback ran first (#3628). Within the window the pair still
|
|
7
|
+
* merges into one Alt-key sequence.
|
|
8
|
+
*/
|
|
9
|
+
export function isStaleEscape(pending, gapMs, escapeWindowMs) {
|
|
10
|
+
return pending === "\x1b" && gapMs !== undefined && gapMs >= escapeWindowMs;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=escape-window.js.map
|
package/dist/stdin-buffer.js
CHANGED
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { StringDecoder } from "node:string_decoder";
|
|
20
20
|
import { EventEmitter } from "events";
|
|
21
|
+
import { isStaleEscape } from "./escape-window.js";
|
|
21
22
|
const ESC = "\x1b";
|
|
22
23
|
const DEFAULT_SEQUENCE_TIMEOUT_MS = 50;
|
|
23
|
-
const DEFAULT_ESCAPE_TIMEOUT_MS =
|
|
24
|
+
const DEFAULT_ESCAPE_TIMEOUT_MS = 50;
|
|
24
25
|
const DEFAULT_BURST_WINDOW_MS = 20;
|
|
25
26
|
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
26
27
|
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
@@ -299,12 +300,16 @@ export class StdinBuffer extends EventEmitter {
|
|
|
299
300
|
clearTimeout(this.burstTimer);
|
|
300
301
|
this.burstTimer = null;
|
|
301
302
|
}
|
|
302
|
-
this.buffer += str;
|
|
303
303
|
const chunkAt = str.length > 0 ? this.clock() : undefined;
|
|
304
304
|
const burstGap = chunkAt !== undefined && this.lastInputAt !== undefined ? chunkAt - this.lastInputAt : undefined;
|
|
305
305
|
if (chunkAt !== undefined) {
|
|
306
306
|
this.lastInputAt = chunkAt;
|
|
307
307
|
}
|
|
308
|
+
if (isStaleEscape(this.buffer, burstGap, this.escapeTimeoutMs)) {
|
|
309
|
+
this.buffer = "";
|
|
310
|
+
this.emitDataSequence(ESC);
|
|
311
|
+
}
|
|
312
|
+
this.buffer += str;
|
|
308
313
|
if (this.heldNewline.length > 0) {
|
|
309
314
|
// The clock decides, not timer delivery: a read outside the window means the held line
|
|
310
315
|
// break was an Enter, even when its release timer has not fired yet (a stalled loop).
|
|
@@ -6,6 +6,8 @@ export interface TerminalCapabilities {
|
|
|
6
6
|
hyperlinks: boolean;
|
|
7
7
|
tmuxPassthrough?: boolean;
|
|
8
8
|
kittyUnicodePlaceholders?: boolean;
|
|
9
|
+
/** Shown next to the image placeholder when the terminal could show images after a user action. */
|
|
10
|
+
imageHint?: string;
|
|
9
11
|
}
|
|
10
12
|
export interface DetectedTerminalCapabilities extends TerminalCapabilities {
|
|
11
13
|
cellDimensions?: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { decideTmuxImageCapability, parseTmuxKittyTerminalOverride, } from "./tmux-image-capability.js";
|
|
2
2
|
import { probeTmuxImageState } from "./tmux-image-probe.js";
|
|
3
|
+
import { isOldWindowsWezTerm, WEZTERM_WINDOWS_IMAGES_HINT } from "./wezterm-windows-images.js";
|
|
3
4
|
export function outerKittyGraphicsMode(clientTermname) {
|
|
4
5
|
const term = clientTermname.trim().toLowerCase();
|
|
5
6
|
return term.includes("kitty") || term.includes("ghostty") || term.includes("warp") ? "placeholder" : null;
|
|
@@ -33,7 +34,9 @@ export function detectTerminalCapabilities(env = process.env, platform = process
|
|
|
33
34
|
return { images: "kitty", trueColor: true, hyperlinks: true };
|
|
34
35
|
}
|
|
35
36
|
if (env.WEZTERM_PANE || termProgram === "wezterm") {
|
|
36
|
-
return
|
|
37
|
+
return isOldWindowsWezTerm(env.TERM_PROGRAM_VERSION, platform)
|
|
38
|
+
? { images: null, trueColor: true, hyperlinks: true, imageHint: WEZTERM_WINDOWS_IMAGES_HINT }
|
|
39
|
+
: { images: "kitty", trueColor: true, hyperlinks: true };
|
|
37
40
|
}
|
|
38
41
|
if (termProgram === "iterm.app" || env.ITERM_SESSION_ID) {
|
|
39
42
|
return { images: "iterm2", trueColor: true, hyperlinks: true };
|
package/dist/terminal-image.js
CHANGED
|
@@ -3,6 +3,7 @@ import { isAbsolute } from "node:path";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { detectTerminalCapabilities, } from "./terminal-capabilities.js";
|
|
5
5
|
import { sanitizeTerminalLabel, shortenImagePath } from "./terminal-text.js";
|
|
6
|
+
import { isOldWindowsWezTerm, WEZTERM_WINDOWS_IMAGES_HINT } from "./wezterm-windows-images.js";
|
|
6
7
|
export { outerKittyGraphicsMode } from "./terminal-capabilities.js";
|
|
7
8
|
let cachedCapabilities = null;
|
|
8
9
|
let capabilityOverrides = {};
|
|
@@ -53,7 +54,9 @@ function detectCapabilitiesFromEnvironment(tmuxForwardsHyperlink) {
|
|
|
53
54
|
return { images: "kitty", trueColor: true, hyperlinks: true };
|
|
54
55
|
}
|
|
55
56
|
if (process.env.WEZTERM_PANE || termProgram === "wezterm") {
|
|
56
|
-
return
|
|
57
|
+
return isOldWindowsWezTerm(process.env.TERM_PROGRAM_VERSION, process.platform)
|
|
58
|
+
? { images: null, trueColor: true, hyperlinks: true, imageHint: WEZTERM_WINDOWS_IMAGES_HINT }
|
|
59
|
+
: { images: "kitty", trueColor: true, hyperlinks: true };
|
|
57
60
|
}
|
|
58
61
|
// Warp supports the Kitty graphics protocol and OSC 8 hyperlinks.
|
|
59
62
|
if (termProgram === "warpterminal" || process.env.WARP_SESSION_ID || process.env.WARP_TERMINAL_SESSION_UUID) {
|
|
@@ -86,6 +89,11 @@ function detectCapabilitiesFromEnvironment(tmuxForwardsHyperlink) {
|
|
|
86
89
|
function parseBooleanCapabilityOverride(value) {
|
|
87
90
|
return value === "1" ? true : value === "0" ? false : undefined;
|
|
88
91
|
}
|
|
92
|
+
// The hint explains why detection turned images off; an explicit image choice makes it wrong.
|
|
93
|
+
function withoutImageHint(caps) {
|
|
94
|
+
const { imageHint: _imageHint, ...rest } = caps;
|
|
95
|
+
return rest;
|
|
96
|
+
}
|
|
89
97
|
export function detectCapabilities(tmuxForwardsHyperlink, tmuxPassthroughState) {
|
|
90
98
|
if (!tmuxForwardsHyperlink || !tmuxPassthroughState) {
|
|
91
99
|
const hyperlinks = parseBooleanCapabilityOverride(process.env.PI_HYPERLINKS);
|
|
@@ -98,7 +106,7 @@ export function detectCapabilities(tmuxForwardsHyperlink, tmuxPassthroughState)
|
|
|
98
106
|
: undefined;
|
|
99
107
|
const trueColor = parseBooleanCapabilityOverride(process.env.PI_TRUE_COLOR);
|
|
100
108
|
return {
|
|
101
|
-
...detected,
|
|
109
|
+
...(images !== undefined ? withoutImageHint(detected) : detected),
|
|
102
110
|
...(images !== undefined ? { images } : {}),
|
|
103
111
|
...(trueColor !== undefined ? { trueColor } : {}),
|
|
104
112
|
...(hyperlinks !== undefined ? { hyperlinks } : {}),
|
|
@@ -125,8 +133,9 @@ export function detectCapabilities(tmuxForwardsHyperlink, tmuxPassthroughState)
|
|
|
125
133
|
export function getCapabilities() {
|
|
126
134
|
if (!cachedCapabilities) {
|
|
127
135
|
const hyperlinks = capabilityOverrides.hyperlinks;
|
|
136
|
+
const detected = detectCapabilities(hyperlinks === undefined ? undefined : () => hyperlinks);
|
|
128
137
|
cachedCapabilities = {
|
|
129
|
-
...
|
|
138
|
+
...(capabilityOverrides.images !== undefined ? withoutImageHint(detected) : detected),
|
|
130
139
|
...capabilityOverrides,
|
|
131
140
|
};
|
|
132
141
|
}
|
|
@@ -142,7 +151,8 @@ export function resetCapabilitiesCache() {
|
|
|
142
151
|
export function setCapabilityOverrides(overrides) {
|
|
143
152
|
if (capabilityOverrides.images === overrides.images &&
|
|
144
153
|
capabilityOverrides.trueColor === overrides.trueColor &&
|
|
145
|
-
capabilityOverrides.hyperlinks === overrides.hyperlinks
|
|
154
|
+
capabilityOverrides.hyperlinks === overrides.hyperlinks &&
|
|
155
|
+
capabilityOverrides.imageHint === overrides.imageHint) {
|
|
146
156
|
return;
|
|
147
157
|
}
|
|
148
158
|
capabilityOverrides = { ...overrides };
|
|
@@ -657,6 +667,7 @@ export function imageFallback(mimeType, dimensions, filename) {
|
|
|
657
667
|
parts.push(`[${sanitizeTerminalLabel(mimeType)}]`);
|
|
658
668
|
if (dimensions)
|
|
659
669
|
parts.push(`${dimensions.widthPx}x${dimensions.heightPx}`);
|
|
660
|
-
|
|
670
|
+
const hint = getCapabilities().imageHint;
|
|
671
|
+
return hint ? `[Image: ${parts.join(" ")}] (${hint})` : `[Image: ${parts.join(" ")}]`;
|
|
661
672
|
}
|
|
662
673
|
//# sourceMappingURL=terminal-image.js.map
|
package/dist/terminal.d.ts
CHANGED
|
@@ -74,9 +74,10 @@ export interface ProcessTerminalOptions {
|
|
|
74
74
|
/**
|
|
75
75
|
* Resolve how long to wait for the rest of an escape sequence before
|
|
76
76
|
* dispatching a lone ESC as the Escape key. Legacy Alt+key input is ESC plus
|
|
77
|
-
* another byte, so high-latency transports
|
|
77
|
+
* another byte, so high-latency transports (SSH, the WSL interop pipe) need a
|
|
78
|
+
* longer reassembly window.
|
|
78
79
|
*/
|
|
79
|
-
export declare function resolveEscapeTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
80
|
+
export declare function resolveEscapeTimeoutMs(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): number;
|
|
80
81
|
/**
|
|
81
82
|
* Resolve how long a line break that ends a read with text is held as a possible paste fragment
|
|
82
83
|
* when the terminal sends no bracketed-paste markers. Paste chunks over SSH arrive further apart,
|
package/dist/terminal.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getNativePlatformHelper } from "./native-platform.js";
|
|
|
7
7
|
import { observeProcessStderrWrites } from "./stderr-observer.js";
|
|
8
8
|
import { StdinBuffer } from "./stdin-buffer.js";
|
|
9
9
|
import { queryTmuxCursorPosition } from "./tmux-cursor-query.js";
|
|
10
|
+
import { isWslSession } from "./wsl-session.js";
|
|
10
11
|
const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
|
|
11
12
|
const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
|
|
12
13
|
const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0\x07";
|
|
@@ -161,22 +162,23 @@ function unsubscribeFromStdinErrors(subscriber) {
|
|
|
161
162
|
if (stdinErrorSubscribers.size === 0)
|
|
162
163
|
process.stdin.removeListener("error", dispatchStdinError);
|
|
163
164
|
}
|
|
164
|
-
const DEFAULT_ESCAPE_TIMEOUT_MS =
|
|
165
|
-
const
|
|
165
|
+
const DEFAULT_ESCAPE_TIMEOUT_MS = 50;
|
|
166
|
+
const DEFAULT_REMOTE_ESCAPE_TIMEOUT_MS = 100;
|
|
166
167
|
const DEFAULT_BURST_WINDOW_MS = 20;
|
|
167
168
|
const DEFAULT_SSH_BURST_WINDOW_MS = 100;
|
|
168
169
|
/**
|
|
169
170
|
* Resolve how long to wait for the rest of an escape sequence before
|
|
170
171
|
* dispatching a lone ESC as the Escape key. Legacy Alt+key input is ESC plus
|
|
171
|
-
* another byte, so high-latency transports
|
|
172
|
+
* another byte, so high-latency transports (SSH, the WSL interop pipe) need a
|
|
173
|
+
* longer reassembly window.
|
|
172
174
|
*/
|
|
173
|
-
export function resolveEscapeTimeoutMs(env = process.env) {
|
|
175
|
+
export function resolveEscapeTimeoutMs(env = process.env, platform = process.platform) {
|
|
174
176
|
const configured = Number(env.PI_TUI_ESC_TIMEOUT);
|
|
175
177
|
if (Number.isFinite(configured) && configured > 0) {
|
|
176
178
|
return configured;
|
|
177
179
|
}
|
|
178
|
-
if (env.SSH_CONNECTION || env.SSH_TTY) {
|
|
179
|
-
return
|
|
180
|
+
if (env.SSH_CONNECTION || env.SSH_TTY || isWslSession(env, platform)) {
|
|
181
|
+
return DEFAULT_REMOTE_ESCAPE_TIMEOUT_MS;
|
|
180
182
|
}
|
|
181
183
|
return DEFAULT_ESCAPE_TIMEOUT_MS;
|
|
182
184
|
}
|
|
@@ -12,6 +12,11 @@ export interface TuiMainScreenRenderState {
|
|
|
12
12
|
export declare class TuiMainScreen extends TuiBase {
|
|
13
13
|
readonly mode: "regular";
|
|
14
14
|
private trackingEnabled;
|
|
15
|
+
/**
|
|
16
|
+
* The app cannot scroll the terminal's scrollback, so the first wheel report while armed hands the
|
|
17
|
+
* mouse back to the terminal; the next key press arms it again (senpi#3604).
|
|
18
|
+
*/
|
|
19
|
+
private yielded;
|
|
15
20
|
private readonly clicks;
|
|
16
21
|
/**
|
|
17
22
|
* A press stays clickable only while the committed frame and component tree it hit are unchanged.
|
|
@@ -19,6 +24,7 @@ export declare class TuiMainScreen extends TuiBase {
|
|
|
19
24
|
*/
|
|
20
25
|
private mousePress?;
|
|
21
26
|
constructor(...args: ConstructorParameters<typeof TuiBase>);
|
|
27
|
+
mouseCaptureState(): "off" | "armed" | "yielded";
|
|
22
28
|
protected applyMouseTracking(enabled: boolean): void;
|
|
23
29
|
protected beforeTerminalStop(): void;
|
|
24
30
|
protected doRender(): void;
|
|
@@ -26,6 +32,8 @@ export declare class TuiMainScreen extends TuiBase {
|
|
|
26
32
|
private pressLayoutUnchanged;
|
|
27
33
|
private applyMouseResult;
|
|
28
34
|
private handleMouseInput;
|
|
35
|
+
private yieldToTerminal;
|
|
36
|
+
private rearmAfterYield;
|
|
29
37
|
captureRenderState(): TuiMainScreenRenderState;
|
|
30
38
|
restoreRenderState(state: TuiMainScreenRenderState): void;
|
|
31
39
|
}
|
package/dist/tui-main-screen.js
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { isMouseSequence, MOUSE_TRACKING, MouseClickSynthesizer, parseSgrMouseEvent, toTuiMouseEvent, } from "./mouse-input.js";
|
|
2
2
|
import { isImageLine } from "./terminal-image.js";
|
|
3
|
-
import { Container, dispatchMouseEvent, retargetMouseEvent, TuiBase, } from "./tui.js";
|
|
3
|
+
import { Container, dispatchMouseEvent, isTerminalReport, retargetMouseEvent, TuiBase, } from "./tui.js";
|
|
4
4
|
/** TUI implementation that renders into the terminal's main screen and scrollback. */
|
|
5
5
|
export class TuiMainScreen extends TuiBase {
|
|
6
6
|
constructor(...args) {
|
|
7
7
|
super(...args);
|
|
8
8
|
this.mode = "regular";
|
|
9
9
|
this.trackingEnabled = false;
|
|
10
|
+
/**
|
|
11
|
+
* The app cannot scroll the terminal's scrollback, so the first wheel report while armed hands the
|
|
12
|
+
* mouse back to the terminal; the next key press arms it again (senpi#3604).
|
|
13
|
+
*/
|
|
14
|
+
this.yielded = false;
|
|
10
15
|
this.clicks = new MouseClickSynthesizer();
|
|
11
16
|
this.addInputListener((data) => this.handleMouseInput(data));
|
|
12
17
|
}
|
|
18
|
+
mouseCaptureState() {
|
|
19
|
+
if (this.yielded)
|
|
20
|
+
return "yielded";
|
|
21
|
+
return this.trackingEnabled ? "armed" : "off";
|
|
22
|
+
}
|
|
13
23
|
applyMouseTracking(enabled) {
|
|
24
|
+
this.yielded = false;
|
|
14
25
|
const supported = Boolean(process.stdout.isTTY) &&
|
|
15
26
|
!process.env.TERMUX_VERSION &&
|
|
16
27
|
(process.platform !== "win32" || Boolean(process.env.WT_SESSION));
|
|
@@ -25,6 +36,7 @@ export class TuiMainScreen extends TuiBase {
|
|
|
25
36
|
beforeTerminalStop() {
|
|
26
37
|
this.terminal.write(MOUSE_TRACKING.disable);
|
|
27
38
|
this.trackingEnabled = false;
|
|
39
|
+
this.yielded = false;
|
|
28
40
|
this.clicks.cancel();
|
|
29
41
|
this.mousePress = undefined;
|
|
30
42
|
}
|
|
@@ -83,9 +95,16 @@ export class TuiMainScreen extends TuiBase {
|
|
|
83
95
|
this.setFocus(target);
|
|
84
96
|
}
|
|
85
97
|
handleMouseInput(data) {
|
|
86
|
-
if (!isMouseSequence(data))
|
|
98
|
+
if (!isMouseSequence(data)) {
|
|
99
|
+
if (this.yielded && !isTerminalReport(data))
|
|
100
|
+
this.rearmAfterYield();
|
|
87
101
|
return { consume: false };
|
|
102
|
+
}
|
|
88
103
|
const raw = parseSgrMouseEvent(data);
|
|
104
|
+
if (this.trackingEnabled && raw && (raw.button & 64) !== 0) {
|
|
105
|
+
this.yieldToTerminal();
|
|
106
|
+
return { consume: true };
|
|
107
|
+
}
|
|
89
108
|
if (!this.trackingEnabled || !raw || raw.button !== 0) {
|
|
90
109
|
this.clicks.cancel();
|
|
91
110
|
this.mousePress = undefined;
|
|
@@ -146,6 +165,22 @@ export class TuiMainScreen extends TuiBase {
|
|
|
146
165
|
}
|
|
147
166
|
return { consume: true };
|
|
148
167
|
}
|
|
168
|
+
yieldToTerminal() {
|
|
169
|
+
this.terminal.write(MOUSE_TRACKING.disable);
|
|
170
|
+
this.trackingEnabled = false;
|
|
171
|
+
this.yielded = true;
|
|
172
|
+
this.clicks.cancel();
|
|
173
|
+
this.mousePress = undefined;
|
|
174
|
+
this.requestRender();
|
|
175
|
+
}
|
|
176
|
+
rearmAfterYield() {
|
|
177
|
+
if (!this.mouseCaptureEnabled) {
|
|
178
|
+
this.yielded = false;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.applyMouseTracking(true);
|
|
182
|
+
this.requestRender();
|
|
183
|
+
}
|
|
149
184
|
captureRenderState() {
|
|
150
185
|
return {
|
|
151
186
|
previousLines: [...this.previousLines],
|
package/dist/tui.d.ts
CHANGED
|
@@ -391,6 +391,11 @@ export declare abstract class TuiBase extends Container {
|
|
|
391
391
|
acquireMouseCapture(reason: string): () => void;
|
|
392
392
|
private resetMouseCaptureState;
|
|
393
393
|
protected get mouseCaptureEnabled(): boolean;
|
|
394
|
+
/**
|
|
395
|
+
* Whether mouse reports reach the app right now: `"off"` without an enabled lease, `"armed"` while
|
|
396
|
+
* they do, `"yielded"` while a renderer has handed the mouse back to the terminal (main-screen wheel).
|
|
397
|
+
*/
|
|
398
|
+
mouseCaptureState(): "off" | "armed" | "yielded";
|
|
394
399
|
protected applyMouseTracking(_enabled: boolean): void;
|
|
395
400
|
protected getMouseLayoutRoots(): readonly Component[];
|
|
396
401
|
protected setMouseBlocker(name: "suspended" | "external-editor" | "shutting-down", on: boolean): void;
|
|
@@ -546,15 +551,15 @@ export declare abstract class TuiBase extends Container {
|
|
|
546
551
|
private deleteChangedKittyImages;
|
|
547
552
|
private getViewportRows;
|
|
548
553
|
private shouldPreserveMuxScrollback;
|
|
549
|
-
private createViewportInsertScrollPlan;
|
|
550
554
|
private renderViewportInsertScroll;
|
|
551
555
|
private renderScrollbackReplay;
|
|
552
556
|
/**
|
|
553
557
|
* The held frame of `setScrollbackReplayHold` (#2836): rows above the old viewport stay as the terminal
|
|
554
|
-
* shows them (possibly stale), and the frame is rewritten from the old viewport top down
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
+
* shows them (possibly stale), and the frame is rewritten from the old viewport top down, or from where the
|
|
559
|
+
* old viewport's rows moved to when rows were inserted above it (senpi#3649), so rows already in the
|
|
560
|
+
* scrollback are not written into it again. Rows the document grew by scroll naturally into scrollback in
|
|
561
|
+
* their current form, so nothing is lost there and nothing is cleared, and a reader who scrolled up keeps
|
|
562
|
+
* their place. Returns false when an image row is involved, so the caller replays as before.
|
|
558
563
|
*/
|
|
559
564
|
private renderHeldRepaint;
|
|
560
565
|
private renderMuxViewportRepaint;
|
|
@@ -594,6 +599,12 @@ export declare abstract class TuiBase extends Container {
|
|
|
594
599
|
onLateReply?: (colors: TerminalColors) => void;
|
|
595
600
|
}): Promise<TerminalColors>;
|
|
596
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* Input that the terminal sends on its own rather than the user typing: mouse reports, OSC/DCS/APC
|
|
604
|
+
* replies, DEC private reports (`ESC[?...`) and window/cell-size reports (`ESC[...t`). OSC/DCS/APC need
|
|
605
|
+
* a body after the introducer, so a legacy Alt+] / Alt+Shift+P / Alt+_ key press is still a key.
|
|
606
|
+
*/
|
|
607
|
+
export declare function isTerminalReport(data: string): boolean;
|
|
597
608
|
/** Legacy main-screen renderer export. */
|
|
598
609
|
export declare class TUI extends TuiBase {
|
|
599
610
|
readonly mode: TuiMode;
|
package/dist/tui.js
CHANGED
|
@@ -12,6 +12,8 @@ import { parseOscColorResponse, parseTerminalColorSchemeReport, } from "./termin
|
|
|
12
12
|
import { deleteKittyImage, getCapabilities, isImageLine, resetCapabilitiesCache, setCellDimensions, } from "./terminal-image.js";
|
|
13
13
|
import { consumeTmuxFocusEvent, DISABLE_FOCUS_REPORTING, ENABLE_FOCUS_REPORTING } from "./tmux-focus.js";
|
|
14
14
|
import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
|
|
15
|
+
import { planViewportInsertScroll } from "./viewport-insert-scroll.js";
|
|
16
|
+
import { insertedAboveViewport } from "./viewport-shift.js";
|
|
15
17
|
const KITTY_SEQUENCE_PREFIX = "\x1b_G";
|
|
16
18
|
const MAX_RENDER_WRITE_CHARS = 1024 * 1024;
|
|
17
19
|
function writeBounded(terminal, data) {
|
|
@@ -148,8 +150,6 @@ function defaultDiagnosticLogDirectory() {
|
|
|
148
150
|
*/
|
|
149
151
|
let renderErrorLogDirectory;
|
|
150
152
|
const VIEWPORT_RENDER_OVERSCAN = 16;
|
|
151
|
-
// Keep scroll-region wins cheap when a few visible rows mutate during append streaming.
|
|
152
|
-
const MAX_SCROLL_DIFF_ROWS = 4;
|
|
153
153
|
const viewportRenderStats = {
|
|
154
154
|
lastNormalizedLines: 0,
|
|
155
155
|
lastKittyImageScannedLines: 0,
|
|
@@ -728,6 +728,13 @@ export class TuiBase extends Container {
|
|
|
728
728
|
get mouseCaptureEnabled() {
|
|
729
729
|
return this.mouseLeases.size > 0 && this.mouseBlockers.size === 0;
|
|
730
730
|
}
|
|
731
|
+
/**
|
|
732
|
+
* Whether mouse reports reach the app right now: `"off"` without an enabled lease, `"armed"` while
|
|
733
|
+
* they do, `"yielded"` while a renderer has handed the mouse back to the terminal (main-screen wheel).
|
|
734
|
+
*/
|
|
735
|
+
mouseCaptureState() {
|
|
736
|
+
return this.mouseCaptureEnabled ? "armed" : "off";
|
|
737
|
+
}
|
|
731
738
|
applyMouseTracking(_enabled) { }
|
|
732
739
|
getMouseLayoutRoots() {
|
|
733
740
|
return [...this.getMountedRoots(), ...this.renderedOverlayLayouts.map((layout) => layout.entry.component)];
|
|
@@ -2097,45 +2104,6 @@ export class TuiBase extends Container {
|
|
|
2097
2104
|
shouldPreserveMuxScrollback() {
|
|
2098
2105
|
return this.#muxDetector() && !useLegacyMuxRender();
|
|
2099
2106
|
}
|
|
2100
|
-
createViewportInsertScrollPlan(newLines, prevViewportTop, height, lineCountDelta) {
|
|
2101
|
-
if (lineCountDelta <= 0 || lineCountDelta >= height || this.overlayStack.length > 0) {
|
|
2102
|
-
return undefined;
|
|
2103
|
-
}
|
|
2104
|
-
const maxViewportTop = Math.max(0, newLines.length - height);
|
|
2105
|
-
const viewportTop = Math.min(maxViewportTop, prevViewportTop + lineCountDelta);
|
|
2106
|
-
if (viewportTop <= prevViewportTop) {
|
|
2107
|
-
return undefined;
|
|
2108
|
-
}
|
|
2109
|
-
const previousVisible = this.getViewportRows(this.previousLines, prevViewportTop, height);
|
|
2110
|
-
const nextVisible = this.getViewportRows(newLines, viewportTop, height);
|
|
2111
|
-
if (previousVisible.some(isImageLine) || nextVisible.some(isImageLine)) {
|
|
2112
|
-
return undefined;
|
|
2113
|
-
}
|
|
2114
|
-
let stableSuffixRows = 0;
|
|
2115
|
-
while (stableSuffixRows < height &&
|
|
2116
|
-
previousVisible[height - stableSuffixRows - 1] === nextVisible[height - stableSuffixRows - 1]) {
|
|
2117
|
-
stableSuffixRows += 1;
|
|
2118
|
-
}
|
|
2119
|
-
const regionHeight = height - stableSuffixRows;
|
|
2120
|
-
if (regionHeight <= 0 || regionHeight < lineCountDelta) {
|
|
2121
|
-
return undefined;
|
|
2122
|
-
}
|
|
2123
|
-
const mutatedRows = [];
|
|
2124
|
-
for (let row = 0; row < regionHeight - lineCountDelta; row++) {
|
|
2125
|
-
if (previousVisible[row + lineCountDelta] !== nextVisible[row]) {
|
|
2126
|
-
mutatedRows.push({ screenRow: row, content: nextVisible[row] });
|
|
2127
|
-
if (mutatedRows.length > MAX_SCROLL_DIFF_ROWS) {
|
|
2128
|
-
return undefined;
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
return {
|
|
2133
|
-
viewportTop,
|
|
2134
|
-
regionBottom: regionHeight - 1,
|
|
2135
|
-
insertedRows: nextVisible.slice(regionHeight - lineCountDelta, regionHeight),
|
|
2136
|
-
mutatedRows,
|
|
2137
|
-
};
|
|
2138
|
-
}
|
|
2139
2107
|
renderViewportInsertScroll(plan, newLines, rawLines, cursorPos, width, height) {
|
|
2140
2108
|
let buffer = TUI.FRAME_BEGIN;
|
|
2141
2109
|
const regionTop = 0;
|
|
@@ -2199,13 +2167,15 @@ export class TuiBase extends Container {
|
|
|
2199
2167
|
}
|
|
2200
2168
|
/**
|
|
2201
2169
|
* The held frame of `setScrollbackReplayHold` (#2836): rows above the old viewport stay as the terminal
|
|
2202
|
-
* shows them (possibly stale), and the frame is rewritten from the old viewport top down
|
|
2203
|
-
*
|
|
2204
|
-
*
|
|
2205
|
-
*
|
|
2170
|
+
* shows them (possibly stale), and the frame is rewritten from the old viewport top down, or from where the
|
|
2171
|
+
* old viewport's rows moved to when rows were inserted above it (senpi#3649), so rows already in the
|
|
2172
|
+
* scrollback are not written into it again. Rows the document grew by scroll naturally into scrollback in
|
|
2173
|
+
* their current form, so nothing is lost there and nothing is cleared, and a reader who scrolled up keeps
|
|
2174
|
+
* their place. Returns false when an image row is involved, so the caller replays as before.
|
|
2206
2175
|
*/
|
|
2207
2176
|
renderHeldRepaint(newLines, rawLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow) {
|
|
2208
|
-
const
|
|
2177
|
+
const shift = insertedAboveViewport(this.previousLines, newLines, prevViewportTop, height, newLines.length - this.previousLines.length);
|
|
2178
|
+
const start = Math.min(prevViewportTop + shift, Math.max(0, newLines.length - height));
|
|
2209
2179
|
const rowCount = Math.max(height, newLines.length - start);
|
|
2210
2180
|
const rows = Array.from({ length: rowCount }, (_, index) => newLines[start + index] ?? "");
|
|
2211
2181
|
const previousVisible = this.getViewportRows(this.previousLines, prevViewportTop, height);
|
|
@@ -2563,7 +2533,9 @@ export class TuiBase extends Container {
|
|
|
2563
2533
|
lastChanged = expandedRange.lastChanged;
|
|
2564
2534
|
}
|
|
2565
2535
|
const appendStart = appendedLines && firstChanged === this.previousLines.length && firstChanged > 0;
|
|
2566
|
-
const insertScrollPlan = this.
|
|
2536
|
+
const insertScrollPlan = this.overlayStack.length > 0
|
|
2537
|
+
? undefined
|
|
2538
|
+
: planViewportInsertScroll(this.previousLines, newLines, prevViewportTop, height, lineCountDelta);
|
|
2567
2539
|
// No changes - but still need to update hardware cursor position if it moved
|
|
2568
2540
|
if (firstChanged === -1) {
|
|
2569
2541
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
@@ -2948,7 +2920,7 @@ export class TuiBase extends Container {
|
|
|
2948
2920
|
* replies, DEC private reports (`ESC[?...`) and window/cell-size reports (`ESC[...t`). OSC/DCS/APC need
|
|
2949
2921
|
* a body after the introducer, so a legacy Alt+] / Alt+Shift+P / Alt+_ key press is still a key.
|
|
2950
2922
|
*/
|
|
2951
|
-
function isTerminalReport(data) {
|
|
2923
|
+
export function isTerminalReport(data) {
|
|
2952
2924
|
return (data.startsWith("\x1b[<") ||
|
|
2953
2925
|
data.startsWith("\x1b[M") ||
|
|
2954
2926
|
(data.length > 2 && (data.startsWith("\x1b]") || data.startsWith("\x1bP") || data.startsWith("\x1b_"))) ||
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ViewportInsertScrollPlan {
|
|
2
|
+
viewportTop: number;
|
|
3
|
+
regionBottom: number;
|
|
4
|
+
insertedRows: string[];
|
|
5
|
+
mutatedRows: Array<{
|
|
6
|
+
screenRow: number;
|
|
7
|
+
content: string;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Plan a scroll-region update for a frame that grew by `lineCountDelta` rows below a stable bottom suffix.
|
|
12
|
+
*
|
|
13
|
+
* Scrolling the region pushes the old top `lineCountDelta` visible rows into the terminal's scrollback as
|
|
14
|
+
* they are, so the plan only holds when the new frame keeps exactly those rows there. Rows inserted at or
|
|
15
|
+
* above the viewport top (a live tool card growing above a tall live area such as the /btw panel) break that:
|
|
16
|
+
* the scroll would lose the inserted rows and send stale copies of the live area into the scrollback on
|
|
17
|
+
* every frame (senpi#3649), so such a frame takes the general repaint paths instead. Identical rows can make the
|
|
18
|
+
* departing rows equal the new ones by coincidence, so a frame that reads as rows moved down from above
|
|
19
|
+
* (`shiftedBlockMoved`) is refused too.
|
|
20
|
+
*/
|
|
21
|
+
export declare function planViewportInsertScroll(previousLines: readonly string[], newLines: readonly string[], prevViewportTop: number, height: number, lineCountDelta: number): ViewportInsertScrollPlan | undefined;
|
|
22
|
+
//# sourceMappingURL=viewport-insert-scroll.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isImageLine } from "./terminal-image.js";
|
|
2
|
+
import { shiftedBlockMoved } from "./viewport-shift.js";
|
|
3
|
+
// Keep scroll-region wins cheap when a few visible rows mutate during append streaming.
|
|
4
|
+
const MAX_SCROLL_DIFF_ROWS = 4;
|
|
5
|
+
function viewportRows(lines, viewportTop, height) {
|
|
6
|
+
return Array.from({ length: height }, (_, row) => lines[viewportTop + row] ?? "");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Plan a scroll-region update for a frame that grew by `lineCountDelta` rows below a stable bottom suffix.
|
|
10
|
+
*
|
|
11
|
+
* Scrolling the region pushes the old top `lineCountDelta` visible rows into the terminal's scrollback as
|
|
12
|
+
* they are, so the plan only holds when the new frame keeps exactly those rows there. Rows inserted at or
|
|
13
|
+
* above the viewport top (a live tool card growing above a tall live area such as the /btw panel) break that:
|
|
14
|
+
* the scroll would lose the inserted rows and send stale copies of the live area into the scrollback on
|
|
15
|
+
* every frame (senpi#3649), so such a frame takes the general repaint paths instead. Identical rows can make the
|
|
16
|
+
* departing rows equal the new ones by coincidence, so a frame that reads as rows moved down from above
|
|
17
|
+
* (`shiftedBlockMoved`) is refused too.
|
|
18
|
+
*/
|
|
19
|
+
export function planViewportInsertScroll(previousLines, newLines, prevViewportTop, height, lineCountDelta) {
|
|
20
|
+
if (lineCountDelta <= 0 || lineCountDelta >= height)
|
|
21
|
+
return undefined;
|
|
22
|
+
const maxViewportTop = Math.max(0, newLines.length - height);
|
|
23
|
+
const viewportTop = Math.min(maxViewportTop, prevViewportTop + lineCountDelta);
|
|
24
|
+
if (viewportTop <= prevViewportTop)
|
|
25
|
+
return undefined;
|
|
26
|
+
const previousVisible = viewportRows(previousLines, prevViewportTop, height);
|
|
27
|
+
const nextVisible = viewportRows(newLines, viewportTop, height);
|
|
28
|
+
if (previousVisible.some(isImageLine) || nextVisible.some(isImageLine))
|
|
29
|
+
return undefined;
|
|
30
|
+
const scrolledOut = viewportTop - prevViewportTop;
|
|
31
|
+
for (let row = 0; row < scrolledOut; row++) {
|
|
32
|
+
if (previousVisible[row] !== newLines[prevViewportTop + row])
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
if (shiftedBlockMoved(previousLines, newLines, prevViewportTop, scrolledOut, height))
|
|
36
|
+
return undefined;
|
|
37
|
+
let stableSuffixRows = 0;
|
|
38
|
+
while (stableSuffixRows < height &&
|
|
39
|
+
previousVisible[height - stableSuffixRows - 1] === nextVisible[height - stableSuffixRows - 1]) {
|
|
40
|
+
stableSuffixRows += 1;
|
|
41
|
+
}
|
|
42
|
+
const regionHeight = height - stableSuffixRows;
|
|
43
|
+
if (regionHeight <= 0 || regionHeight < lineCountDelta)
|
|
44
|
+
return undefined;
|
|
45
|
+
const mutatedRows = [];
|
|
46
|
+
for (let row = 0; row < regionHeight - lineCountDelta; row++) {
|
|
47
|
+
if (previousVisible[row + lineCountDelta] !== nextVisible[row]) {
|
|
48
|
+
mutatedRows.push({ screenRow: row, content: nextVisible[row] ?? "" });
|
|
49
|
+
if (mutatedRows.length > MAX_SCROLL_DIFF_ROWS)
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
viewportTop,
|
|
55
|
+
regionBottom: regionHeight - 1,
|
|
56
|
+
insertedRows: nextVisible.slice(regionHeight - lineCountDelta, regionHeight),
|
|
57
|
+
mutatedRows,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=viewport-insert-scroll.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the new frame reads as the old one with `shift` rows inserted above `prevViewportTop`: the rows at
|
|
3
|
+
* `prevViewportTop..+shift` are the old rows right above it, and walking up through the block of rows that keeps
|
|
4
|
+
* matching that reading finds at least one row that really moved. Identical rows (blank or repeated lines) match
|
|
5
|
+
* both readings and prove nothing; the walk stops at the first row the shift does not explain.
|
|
6
|
+
*/
|
|
7
|
+
export declare function shiftedBlockMoved(previousLines: readonly string[], newLines: readonly string[], prevViewportTop: number, shift: number, height: number): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* How far the last frame's visible rows moved down in the new frame because rows were inserted above them.
|
|
10
|
+
*
|
|
11
|
+
* A held repaint writes the frame from a start row and lets the rows above the screen scroll into the
|
|
12
|
+
* terminal's scrollback. Rows that sat above the old viewport are already there, so after an insertion
|
|
13
|
+
* above the viewport (a live tool card growing over a tall /btw panel, senpi#3649) the repaint must start
|
|
14
|
+
* where the old viewport's rows went, or every frame pushes another copy of them.
|
|
15
|
+
*
|
|
16
|
+
* A shift counts only when the rows it would skip are exactly the rows already in the scrollback right above
|
|
17
|
+
* the old viewport, the contiguous block of rows above the viewport that matches the shift holds at least one
|
|
18
|
+
* row that really moved (identical rows, such as blank or repeated lines, match both readings and prove
|
|
19
|
+
* nothing), and most of the old visible rows reappear at it. A majority alone is not enough: a tall
|
|
20
|
+
* stable live area matches at any offset after an append, while rows re-laid out in place (a streamed table,
|
|
21
|
+
* #2836) are new rows that must still scroll into the scrollback, so the start stays at the old viewport top.
|
|
22
|
+
*/
|
|
23
|
+
export declare function insertedAboveViewport(previousLines: readonly string[], newLines: readonly string[], prevViewportTop: number, height: number, lineCountDelta: number): number;
|
|
24
|
+
//# sourceMappingURL=viewport-shift.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the new frame reads as the old one with `shift` rows inserted above `prevViewportTop`: the rows at
|
|
3
|
+
* `prevViewportTop..+shift` are the old rows right above it, and walking up through the block of rows that keeps
|
|
4
|
+
* matching that reading finds at least one row that really moved. Identical rows (blank or repeated lines) match
|
|
5
|
+
* both readings and prove nothing; the walk stops at the first row the shift does not explain.
|
|
6
|
+
*/
|
|
7
|
+
export function shiftedBlockMoved(previousLines, newLines, prevViewportTop, shift, height) {
|
|
8
|
+
if (shift <= 0 || shift > prevViewportTop)
|
|
9
|
+
return false;
|
|
10
|
+
for (let row = prevViewportTop; row < prevViewportTop + shift; row++) {
|
|
11
|
+
const scrolled = previousLines[row - shift];
|
|
12
|
+
if (scrolled === undefined || scrolled !== newLines[row])
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const scanFloor = Math.max(shift, prevViewportTop - 2 * height);
|
|
16
|
+
for (let row = prevViewportTop + shift - 1; row >= scanFloor; row--) {
|
|
17
|
+
const line = newLines[row];
|
|
18
|
+
if (line !== previousLines[row - shift])
|
|
19
|
+
return false;
|
|
20
|
+
if (line !== previousLines[row])
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* How far the last frame's visible rows moved down in the new frame because rows were inserted above them.
|
|
27
|
+
*
|
|
28
|
+
* A held repaint writes the frame from a start row and lets the rows above the screen scroll into the
|
|
29
|
+
* terminal's scrollback. Rows that sat above the old viewport are already there, so after an insertion
|
|
30
|
+
* above the viewport (a live tool card growing over a tall /btw panel, senpi#3649) the repaint must start
|
|
31
|
+
* where the old viewport's rows went, or every frame pushes another copy of them.
|
|
32
|
+
*
|
|
33
|
+
* A shift counts only when the rows it would skip are exactly the rows already in the scrollback right above
|
|
34
|
+
* the old viewport, the contiguous block of rows above the viewport that matches the shift holds at least one
|
|
35
|
+
* row that really moved (identical rows, such as blank or repeated lines, match both readings and prove
|
|
36
|
+
* nothing), and most of the old visible rows reappear at it. A majority alone is not enough: a tall
|
|
37
|
+
* stable live area matches at any offset after an append, while rows re-laid out in place (a streamed table,
|
|
38
|
+
* #2836) are new rows that must still scroll into the scrollback, so the start stays at the old viewport top.
|
|
39
|
+
*/
|
|
40
|
+
export function insertedAboveViewport(previousLines, newLines, prevViewportTop, height, lineCountDelta) {
|
|
41
|
+
if (lineCountDelta <= 0)
|
|
42
|
+
return 0;
|
|
43
|
+
const visibleMatchesAt = (shift) => {
|
|
44
|
+
let matches = 0;
|
|
45
|
+
for (let row = 0; row < height; row++) {
|
|
46
|
+
const previous = previousLines[prevViewportTop + row];
|
|
47
|
+
if (previous !== undefined && previous === newLines[prevViewportTop + shift + row])
|
|
48
|
+
matches += 1;
|
|
49
|
+
}
|
|
50
|
+
return matches;
|
|
51
|
+
};
|
|
52
|
+
let best = 0;
|
|
53
|
+
let bestMatches = visibleMatchesAt(0);
|
|
54
|
+
for (let shift = 1; shift <= Math.min(lineCountDelta, prevViewportTop); shift++) {
|
|
55
|
+
if (!shiftedBlockMoved(previousLines, newLines, prevViewportTop, shift, height))
|
|
56
|
+
continue;
|
|
57
|
+
const matches = visibleMatchesAt(shift);
|
|
58
|
+
if (matches > bestMatches) {
|
|
59
|
+
best = shift;
|
|
60
|
+
bestMatches = matches;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return bestMatches * 2 > height ? best : 0;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=viewport-shift.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WezTerm on Windows runs its shell through the ConPTY it bundles. Bundled
|
|
3
|
+
* ConPTY builds before 1.22 drop the Kitty graphics APC (`ESC _G ... ESC \`)
|
|
4
|
+
* before it reaches WezTerm, so an image placement leaves a blank gap. WezTerm
|
|
5
|
+
* bundled ConPTY 1.22.250204002 in wezterm/wezterm@4accc376f, whose version
|
|
6
|
+
* string is below (committer date in its own zone, see wezterm-version/build.rs);
|
|
7
|
+
* that build and later ones deliver the APC. No stable release has it yet.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MIN_WEZTERM_WINDOWS_IMAGES_VERSION = "20250208-160106";
|
|
10
|
+
export declare const WEZTERM_WINDOWS_IMAGES_HINT = "update WezTerm to a nightly build to show images inline";
|
|
11
|
+
/**
|
|
12
|
+
* True only when WezTerm on Windows positively reports a build older than the
|
|
13
|
+
* first one whose bundled ConPTY passes Kitty graphics through. A missing or
|
|
14
|
+
* unparsable version keeps images on: there is no evidence of an old build.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isOldWindowsWezTerm(version: string | undefined, platform: NodeJS.Platform): boolean;
|
|
17
|
+
//# sourceMappingURL=wezterm-windows-images.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WezTerm on Windows runs its shell through the ConPTY it bundles. Bundled
|
|
3
|
+
* ConPTY builds before 1.22 drop the Kitty graphics APC (`ESC _G ... ESC \`)
|
|
4
|
+
* before it reaches WezTerm, so an image placement leaves a blank gap. WezTerm
|
|
5
|
+
* bundled ConPTY 1.22.250204002 in wezterm/wezterm@4accc376f, whose version
|
|
6
|
+
* string is below (committer date in its own zone, see wezterm-version/build.rs);
|
|
7
|
+
* that build and later ones deliver the APC. No stable release has it yet.
|
|
8
|
+
*/
|
|
9
|
+
export const MIN_WEZTERM_WINDOWS_IMAGES_VERSION = "20250208-160106";
|
|
10
|
+
export const WEZTERM_WINDOWS_IMAGES_HINT = "update WezTerm to a nightly build to show images inline";
|
|
11
|
+
// WezTerm sets TERM_PROGRAM_VERSION to `wezterm_version()`: YYYYMMDD-HHMMSS-<commit>.
|
|
12
|
+
const WEZTERM_VERSION = /^(\d{8})-(\d{6})-[0-9a-f]+/i;
|
|
13
|
+
/**
|
|
14
|
+
* True only when WezTerm on Windows positively reports a build older than the
|
|
15
|
+
* first one whose bundled ConPTY passes Kitty graphics through. A missing or
|
|
16
|
+
* unparsable version keeps images on: there is no evidence of an old build.
|
|
17
|
+
*/
|
|
18
|
+
export function isOldWindowsWezTerm(version, platform) {
|
|
19
|
+
if (platform !== "win32" || version === undefined)
|
|
20
|
+
return false;
|
|
21
|
+
const match = WEZTERM_VERSION.exec(version);
|
|
22
|
+
if (!match)
|
|
23
|
+
return false;
|
|
24
|
+
return `${match[1]}-${match[2]}` < MIN_WEZTERM_WINDOWS_IMAGES_VERSION;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=wezterm-windows-images.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
const WSL_ENV_MARKERS = ["WSL_DISTRO_NAME", "WSL_INTEROP", "WSLENV"];
|
|
3
|
+
/** Windows Subsystem for Linux session detection that survives sanitized environments (#3628). */
|
|
4
|
+
export function isWslSession(env, platform = process.platform, procVersionPath = "/proc/version") {
|
|
5
|
+
if (platform !== "linux") {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
if (WSL_ENV_MARKERS.some((marker) => Boolean(env[marker]))) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return /microsoft|wsl/i.test(readFileSync(procVersionPath, "utf-8"));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=wsl-session.js.map
|
package/package.json
CHANGED