@sayknow-cli/tui 0.4.3 → 0.4.5
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.
|
@@ -124,5 +124,10 @@ export declare function buildSayknowPixelFrames(options: {
|
|
|
124
124
|
kittyImageId?: number;
|
|
125
125
|
/** Color skin for the sprite palette (default "red"). */
|
|
126
126
|
skin?: PetSkinId;
|
|
127
|
+
/**
|
|
128
|
+
* Optional wrapper applied to each encoded sixel frame (e.g. tmux DCS
|
|
129
|
+
* passthrough). Identity when omitted. Never applied to kitty frames.
|
|
130
|
+
*/
|
|
131
|
+
wrapSixel?: (frame: string) => string;
|
|
127
132
|
}): SayknowPixelFrames;
|
|
128
133
|
export {};
|
|
@@ -58,6 +58,26 @@ export declare function isCursorNeutralImagePermittedInFallback(): boolean;
|
|
|
58
58
|
* authoritative: runtime capability probes must not override it.
|
|
59
59
|
*/
|
|
60
60
|
export declare function isImageProtocolForced(): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Returns whether the sixel-through-multiplexer path (passthrough-wrapped probe
|
|
63
|
+
* + DCS passthrough render) is enabled. On by default; set SKC_SIXEL_MULTIPLEXER=0
|
|
64
|
+
* to force the pre-0.4.5 behavior (graphics unconditionally off under tmux).
|
|
65
|
+
*/
|
|
66
|
+
export declare function isSixelMultiplexerEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Returns whether the process runs under tmux specifically. Only tmux implements
|
|
69
|
+
* the DCS passthrough envelope (`\ePtmux;…\e\\`) that forwards graphics escapes to
|
|
70
|
+
* the outer terminal, so sixel graphics stay suppressed under screen/zellij.
|
|
71
|
+
*/
|
|
72
|
+
export declare function isUnderTmux(env?: NodeJS.ProcessEnv): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Wrap raw terminal escape bytes in tmux's DCS passthrough envelope so tmux
|
|
75
|
+
* forwards them verbatim to the outer terminal (requires `allow-passthrough on`,
|
|
76
|
+
* which SKC-launched tmux sessions set automatically). Each ESC in the payload is
|
|
77
|
+
* doubled per the tmux protocol. Returns the payload unchanged when not under
|
|
78
|
+
* tmux, so non-multiplexed, screen, and zellij paths are untouched.
|
|
79
|
+
*/
|
|
80
|
+
export declare function wrapTmuxPassthrough(payload: string, env?: NodeJS.ProcessEnv): string;
|
|
61
81
|
/**
|
|
62
82
|
* Returns true when running in Windows Terminal with known SIXEL support.
|
|
63
83
|
*
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -125,11 +125,10 @@ export type SizeValue = number | `${number}%`;
|
|
|
125
125
|
* Startup sixel capability probe policy (pure; exported for tests):
|
|
126
126
|
* - Never probe when PI_FORCE_IMAGE_PROTOCOL is set — an explicit
|
|
127
127
|
* configuration (including "off") is authoritative.
|
|
128
|
-
* -
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* via PI_FORCE_IMAGE_PROTOCOL=sixel.
|
|
128
|
+
* - Under tmux, probe when the sixel-multiplexer path is enabled (default): the
|
|
129
|
+
* query is DCS-passthrough wrapped so the OUTER terminal answers, not tmux's
|
|
130
|
+
* unreliable compile-time DA1, making a positive reply genuine end-to-end
|
|
131
|
+
* evidence. screen/zellij have no passthrough envelope, so graphics stay off.
|
|
133
132
|
* - Probe Windows Terminal (>=1.22 renders sixel but exposes no env marker).
|
|
134
133
|
*/
|
|
135
134
|
export declare function shouldProbeSixelCapability(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@sayknow-cli/tui",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.5",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://sayknow-cli.com",
|
|
7
7
|
"author": "jaybeyond",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"fmt": "biome format --write ."
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@sayknow-cli/natives": "0.4.
|
|
42
|
-
"@sayknow-cli/utils": "0.4.
|
|
41
|
+
"@sayknow-cli/natives": "0.4.5",
|
|
42
|
+
"@sayknow-cli/utils": "0.4.5",
|
|
43
43
|
"lru-cache": "11.3.6",
|
|
44
44
|
"marked": "^18.0.3"
|
|
45
45
|
},
|
|
@@ -392,6 +392,11 @@ export function buildSayknowPixelFrames(options: {
|
|
|
392
392
|
kittyImageId?: number;
|
|
393
393
|
/** Color skin for the sprite palette (default "red"). */
|
|
394
394
|
skin?: PetSkinId;
|
|
395
|
+
/**
|
|
396
|
+
* Optional wrapper applied to each encoded sixel frame (e.g. tmux DCS
|
|
397
|
+
* passthrough). Identity when omitted. Never applied to kitty frames.
|
|
398
|
+
*/
|
|
399
|
+
wrapSixel?: (frame: string) => string;
|
|
395
400
|
}): SayknowPixelFrames {
|
|
396
401
|
const targetRows = options.targetRows ?? 2;
|
|
397
402
|
const gridSize = 16;
|
|
@@ -416,7 +421,7 @@ export function buildSayknowPixelFrames(options: {
|
|
|
416
421
|
for (const name of Object.keys(PIXEL_GRIDS) as SayknowPixelFrameName[]) {
|
|
417
422
|
frames[name] =
|
|
418
423
|
options.protocol === "sixel"
|
|
419
|
-
? encodeGridSixel(PIXEL_GRIDS[name], scale, topPaddingPx, palette)
|
|
424
|
+
? (options.wrapSixel ?? (frame => frame))(encodeGridSixel(PIXEL_GRIDS[name], scale, topPaddingPx, palette))
|
|
420
425
|
: encodeGridKitty(
|
|
421
426
|
PIXEL_GRIDS[name],
|
|
422
427
|
scale,
|
|
@@ -143,6 +143,45 @@ export function isImageProtocolForced(): boolean {
|
|
|
143
143
|
return getForcedImageProtocol() !== undefined;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
const SIXEL_MULTIPLEXER_DISABLED_ENV_VALUES = new Set(["0", "off", "false", "no"]);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Returns whether the sixel-through-multiplexer path (passthrough-wrapped probe
|
|
150
|
+
* + DCS passthrough render) is enabled. On by default; set SKC_SIXEL_MULTIPLEXER=0
|
|
151
|
+
* to force the pre-0.4.5 behavior (graphics unconditionally off under tmux).
|
|
152
|
+
*/
|
|
153
|
+
export function isSixelMultiplexerEnabled(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
154
|
+
const raw = env.SKC_SIXEL_MULTIPLEXER?.trim().toLowerCase();
|
|
155
|
+
return raw === undefined || raw === "" || !SIXEL_MULTIPLEXER_DISABLED_ENV_VALUES.has(raw);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns whether the process runs under tmux specifically. Only tmux implements
|
|
160
|
+
* the DCS passthrough envelope (`\ePtmux;…\e\\`) that forwards graphics escapes to
|
|
161
|
+
* the outer terminal, so sixel graphics stay suppressed under screen/zellij.
|
|
162
|
+
*/
|
|
163
|
+
export function isUnderTmux(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
164
|
+
if (
|
|
165
|
+
multiplexerEnvEnabled(env.TMUX) ||
|
|
166
|
+
multiplexerEnvEnabled(env.TMUX_PANE) ||
|
|
167
|
+
multiplexerEnvEnabled(env.SKC_TMUX_LAUNCHED)
|
|
168
|
+
)
|
|
169
|
+
return true;
|
|
170
|
+
return (env.TERM?.trim().toLowerCase() ?? "").startsWith("tmux");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Wrap raw terminal escape bytes in tmux's DCS passthrough envelope so tmux
|
|
175
|
+
* forwards them verbatim to the outer terminal (requires `allow-passthrough on`,
|
|
176
|
+
* which SKC-launched tmux sessions set automatically). Each ESC in the payload is
|
|
177
|
+
* doubled per the tmux protocol. Returns the payload unchanged when not under
|
|
178
|
+
* tmux, so non-multiplexed, screen, and zellij paths are untouched.
|
|
179
|
+
*/
|
|
180
|
+
export function wrapTmuxPassthrough(payload: string, env: NodeJS.ProcessEnv = Bun.env): string {
|
|
181
|
+
if (!payload || !isUnderTmux(env)) return payload;
|
|
182
|
+
return `\x1bPtmux;${payload.replaceAll("\x1b", "\x1b\x1b")}\x1b\\`;
|
|
183
|
+
}
|
|
184
|
+
|
|
146
185
|
function parseMajorMinorVersion(versionRaw?: string): { major: number; minor: number } | null {
|
|
147
186
|
if (!versionRaw) return null;
|
|
148
187
|
const match = /^(\d+)\.(\d+)/u.exec(versionRaw.trim());
|
|
@@ -767,8 +806,10 @@ export function renderImage(
|
|
|
767
806
|
const targetWidthPx = Math.max(1, fit.columns * cellDims.widthPx);
|
|
768
807
|
const targetHeightPx = Math.max(1, fit.rows * cellDims.heightPx);
|
|
769
808
|
const decoded = new Uint8Array(Buffer.from(base64Data, "base64"));
|
|
770
|
-
const
|
|
771
|
-
|
|
809
|
+
const raster = encodeSixel(decoded, targetWidthPx, targetHeightPx);
|
|
810
|
+
// Under tmux, forward the raster to the outer terminal via the DCS
|
|
811
|
+
// passthrough envelope (no-op when not under tmux).
|
|
812
|
+
return { sequence: wrapTmuxPassthrough(raster), rows: fit.rows };
|
|
772
813
|
} catch {
|
|
773
814
|
return null;
|
|
774
815
|
}
|
package/src/tui.ts
CHANGED
|
@@ -12,10 +12,13 @@ import type { Terminal } from "./terminal";
|
|
|
12
12
|
import {
|
|
13
13
|
ImageProtocol,
|
|
14
14
|
isImageProtocolForced,
|
|
15
|
+
isSixelMultiplexerEnabled,
|
|
15
16
|
isUnderTerminalMultiplexer,
|
|
17
|
+
isUnderTmux,
|
|
16
18
|
setCellDimensions,
|
|
17
19
|
setTerminalImageProtocol,
|
|
18
20
|
TERMINAL,
|
|
21
|
+
wrapTmuxPassthrough,
|
|
19
22
|
} from "./terminal-capabilities";
|
|
20
23
|
import {
|
|
21
24
|
Ellipsis,
|
|
@@ -296,11 +299,10 @@ function isMultiplexerSession(env: Record<string, string | undefined> = Bun.env)
|
|
|
296
299
|
* Startup sixel capability probe policy (pure; exported for tests):
|
|
297
300
|
* - Never probe when PI_FORCE_IMAGE_PROTOCOL is set — an explicit
|
|
298
301
|
* configuration (including "off") is authoritative.
|
|
299
|
-
* -
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* via PI_FORCE_IMAGE_PROTOCOL=sixel.
|
|
302
|
+
* - Under tmux, probe when the sixel-multiplexer path is enabled (default): the
|
|
303
|
+
* query is DCS-passthrough wrapped so the OUTER terminal answers, not tmux's
|
|
304
|
+
* unreliable compile-time DA1, making a positive reply genuine end-to-end
|
|
305
|
+
* evidence. screen/zellij have no passthrough envelope, so graphics stay off.
|
|
304
306
|
* - Probe Windows Terminal (>=1.22 renders sixel but exposes no env marker).
|
|
305
307
|
*/
|
|
306
308
|
export function shouldProbeSixelCapability(
|
|
@@ -308,7 +310,7 @@ export function shouldProbeSixelCapability(
|
|
|
308
310
|
platform: NodeJS.Platform = process.platform,
|
|
309
311
|
): boolean {
|
|
310
312
|
if (isImageProtocolForced()) return false;
|
|
311
|
-
if (isUnderTerminalMultiplexer(env)) return
|
|
313
|
+
if (isUnderTerminalMultiplexer(env)) return isUnderTmux(env) && isSixelMultiplexerEnabled(env);
|
|
312
314
|
return platform === "win32" && Boolean(env.WT_SESSION?.trim());
|
|
313
315
|
}
|
|
314
316
|
|
|
@@ -1145,11 +1147,16 @@ export class TUI extends Container {
|
|
|
1145
1147
|
this.#sixelProbePendingDa = true;
|
|
1146
1148
|
this.#sixelProbePendingGraphics = true;
|
|
1147
1149
|
this.#sixelProbeUnsubscribe = this.addInputListener(data => this.#handleSixelProbeInput(data));
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1150
|
+
// Under tmux, wrap the DA1 + XTSMGRAPHICS queries in the DCS passthrough
|
|
1151
|
+
// envelope so the outer terminal answers instead of tmux itself. Round-trip
|
|
1152
|
+
// through the outer terminal is slower, so allow a longer settle window.
|
|
1153
|
+
if (!this.#writeTerminal(wrapTmuxPassthrough("\x1b[c\x1b[?2;1;0S"))) return;
|
|
1154
|
+
this.#sixelProbeTimeout = setTimeout(
|
|
1155
|
+
() => {
|
|
1156
|
+
this.#finishSixelProbe(false);
|
|
1157
|
+
},
|
|
1158
|
+
isUnderTmux() ? 600 : 250,
|
|
1159
|
+
);
|
|
1153
1160
|
}
|
|
1154
1161
|
|
|
1155
1162
|
#isSixelProbeCandidate(): boolean {
|