@sayknow-cli/tui 0.3.13 → 0.3.16
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/types/animation-scheduler.d.ts +13 -0
- package/dist/types/autocomplete.d.ts +83 -0
- package/dist/types/bracketed-paste.d.ts +26 -0
- package/dist/types/components/box.d.ts +20 -0
- package/dist/types/components/cancellable-loader.d.ts +21 -0
- package/dist/types/components/editor.d.ts +126 -0
- package/dist/types/components/image.d.ts +18 -0
- package/dist/types/components/input.d.ts +16 -0
- package/dist/types/components/loader.d.ts +23 -0
- package/dist/types/components/markdown.d.ts +87 -0
- package/dist/types/components/sayknow-pet.d.ts +128 -0
- package/dist/types/components/select-list.d.ts +46 -0
- package/dist/types/components/settings-list.d.ts +39 -0
- package/dist/types/components/spacer.d.ts +11 -0
- package/dist/types/components/tab-bar.d.ts +56 -0
- package/dist/types/components/text.d.ts +22 -0
- package/dist/types/components/truncated-text.d.ts +10 -0
- package/dist/types/editor-component.d.ts +36 -0
- package/dist/types/fuzzy.d.ts +15 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/keybindings.d.ts +201 -0
- package/dist/types/keys.d.ts +208 -0
- package/dist/types/kill-ring.d.ts +27 -0
- package/dist/types/metrics.d.ts +85 -0
- package/dist/types/stdin-buffer.d.ts +50 -0
- package/dist/types/symbols.d.ts +23 -0
- package/dist/types/terminal-capabilities.d.ts +187 -0
- package/dist/types/terminal.d.ts +90 -0
- package/dist/types/ttyid.d.ts +9 -0
- package/dist/types/tui.d.ts +269 -0
- package/dist/types/utils.d.ts +110 -0
- package/package.json +9 -8
- package/src/terminal-capabilities.ts +77 -4
- package/src/tui.ts +76 -29
package/src/tui.ts
CHANGED
|
@@ -9,7 +9,14 @@ import { getKeybindings } from "./keybindings";
|
|
|
9
9
|
import { isKeyRelease } from "./keys";
|
|
10
10
|
import { renderMetrics } from "./metrics";
|
|
11
11
|
import type { Terminal } from "./terminal";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
ImageProtocol,
|
|
14
|
+
isImageProtocolForced,
|
|
15
|
+
isUnderTerminalMultiplexer,
|
|
16
|
+
setCellDimensions,
|
|
17
|
+
setTerminalImageProtocol,
|
|
18
|
+
TERMINAL,
|
|
19
|
+
} from "./terminal-capabilities";
|
|
13
20
|
import {
|
|
14
21
|
Ellipsis,
|
|
15
22
|
extractSegments,
|
|
@@ -217,7 +224,6 @@ function isTermuxSession(env: Record<string, string | undefined> = Bun.env): boo
|
|
|
217
224
|
return Boolean(env.TERMUX_VERSION);
|
|
218
225
|
}
|
|
219
226
|
|
|
220
|
-
const SKC_TMUX_LAUNCHED_ENV = "SKC_TMUX_LAUNCHED";
|
|
221
227
|
const DISABLED_ENV_VALUES = new Set(["0", "false", "off", "no"]);
|
|
222
228
|
const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on", "y"]);
|
|
223
229
|
|
|
@@ -231,25 +237,38 @@ function envFlagEnabled(value: string | undefined): boolean {
|
|
|
231
237
|
return normalized !== undefined && TRUTHY_ENV_VALUES.has(normalized);
|
|
232
238
|
}
|
|
233
239
|
|
|
234
|
-
function termLooksMultiplexed(value: string | undefined): boolean {
|
|
235
|
-
const term = value?.trim().toLowerCase() ?? "";
|
|
236
|
-
return term.startsWith("tmux") || term.startsWith("screen");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
240
|
function isWindowsTerminalSession(env: Record<string, string | undefined> = Bun.env): boolean {
|
|
240
241
|
return envIsEnabled(env.WT_SESSION) || env.TERM_PROGRAM === "Windows_Terminal";
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
/**
|
|
244
|
+
/**
|
|
245
|
+
* Detect terminal multiplexers where scrollback clearing and height-change
|
|
246
|
+
* redraws are hostile. Delegates to the shared capability predicate so the
|
|
247
|
+
* renderer and graphics-protocol selection agree on what counts as a
|
|
248
|
+
* multiplexed host.
|
|
249
|
+
*/
|
|
244
250
|
function isMultiplexerSession(env: Record<string, string | undefined> = Bun.env): boolean {
|
|
245
|
-
return
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
return isUnderTerminalMultiplexer(env as NodeJS.ProcessEnv);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Startup sixel capability probe policy (pure; exported for tests):
|
|
256
|
+
* - Never probe when PI_FORCE_IMAGE_PROTOCOL is set — an explicit
|
|
257
|
+
* configuration (including "off") is authoritative.
|
|
258
|
+
* - Never probe inside a terminal multiplexer: tmux advertises DA1 ";4"
|
|
259
|
+
* whenever it was compiled with sixel support, regardless of whether the
|
|
260
|
+
* attached client terminal can render sixel, so a positive reply is not
|
|
261
|
+
* end-to-end evidence. Graphics under a multiplexer are strictly opt-in
|
|
262
|
+
* via PI_FORCE_IMAGE_PROTOCOL=sixel.
|
|
263
|
+
* - Probe Windows Terminal (>=1.22 renders sixel but exposes no env marker).
|
|
264
|
+
*/
|
|
265
|
+
export function shouldProbeSixelCapability(
|
|
266
|
+
env: NodeJS.ProcessEnv = Bun.env,
|
|
267
|
+
platform: NodeJS.Platform = process.platform,
|
|
268
|
+
): boolean {
|
|
269
|
+
if (isImageProtocolForced()) return false;
|
|
270
|
+
if (isUnderTerminalMultiplexer(env)) return false;
|
|
271
|
+
return platform === "win32" && Boolean(env.WT_SESSION?.trim());
|
|
253
272
|
}
|
|
254
273
|
|
|
255
274
|
function useLegacyMultiplexerFullRender(env: Record<string, string | undefined> = Bun.env): boolean {
|
|
@@ -1008,8 +1027,7 @@ export class TUI extends Container {
|
|
|
1008
1027
|
|
|
1009
1028
|
#querySixelSupport(): void {
|
|
1010
1029
|
if (TERMINAL.imageProtocol) return;
|
|
1011
|
-
if (
|
|
1012
|
-
if (!Bun.env.WT_SESSION) return;
|
|
1030
|
+
if (!this.#isSixelProbeCandidate()) return;
|
|
1013
1031
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
1014
1032
|
|
|
1015
1033
|
this.#clearSixelProbeState();
|
|
@@ -1023,6 +1041,10 @@ export class TUI extends Container {
|
|
|
1023
1041
|
}, 250);
|
|
1024
1042
|
}
|
|
1025
1043
|
|
|
1044
|
+
#isSixelProbeCandidate(): boolean {
|
|
1045
|
+
return shouldProbeSixelCapability();
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1026
1048
|
#handleSixelProbeInput(data: string): InputListenerResult {
|
|
1027
1049
|
if (!this.#sixelProbePendingDa && !this.#sixelProbePendingGraphics) {
|
|
1028
1050
|
return undefined;
|
|
@@ -1049,11 +1071,15 @@ export class TUI extends Container {
|
|
|
1049
1071
|
|
|
1050
1072
|
if (useDa && this.#sixelProbePendingDa) {
|
|
1051
1073
|
this.#sixelProbePendingDa = false;
|
|
1052
|
-
const
|
|
1074
|
+
const params = (match[1] ?? "")
|
|
1053
1075
|
.split(";")
|
|
1054
1076
|
.map(value => Number.parseInt(value, 10))
|
|
1055
1077
|
.filter(value => Number.isFinite(value));
|
|
1056
|
-
|
|
1078
|
+
// The first DA1 parameter is the device/operating class (e.g. 1,
|
|
1079
|
+
// 62, 64), not an extension attribute: `CSI ?4;6c` identifies a
|
|
1080
|
+
// VT132, it does not advertise sixel. Only the parameters after
|
|
1081
|
+
// the class carry attributes like 4 (sixel graphics).
|
|
1082
|
+
const hasSixelAttribute = params.slice(1).includes(4);
|
|
1057
1083
|
if (hasSixelAttribute) {
|
|
1058
1084
|
this.#sixelProbePendingGraphics = false;
|
|
1059
1085
|
probeOutcome = true;
|
|
@@ -1062,8 +1088,11 @@ export class TUI extends Container {
|
|
|
1062
1088
|
}
|
|
1063
1089
|
} else if (!useDa && this.#sixelProbePendingGraphics) {
|
|
1064
1090
|
this.#sixelProbePendingGraphics = false;
|
|
1091
|
+
// XTSMGRAPHICS reply is `CSI ? 2 ; Ps ; ... S` where Ps=0 means
|
|
1092
|
+
// success and 1/2/3 are errors (tmux answers our unsupported
|
|
1093
|
+
// read with `CSI ?2;3;0S`). Only a success reply proves sixel.
|
|
1065
1094
|
const status = Number.parseInt(match[1] ?? "", 10);
|
|
1066
|
-
const supportsSixel =
|
|
1095
|
+
const supportsSixel = status === 0;
|
|
1067
1096
|
if (supportsSixel) {
|
|
1068
1097
|
this.#sixelProbePendingDa = false;
|
|
1069
1098
|
probeOutcome = true;
|
|
@@ -2153,16 +2182,25 @@ export class TUI extends Container {
|
|
|
2153
2182
|
this.#previousHeight = height;
|
|
2154
2183
|
};
|
|
2155
2184
|
|
|
2156
|
-
const viewportRepaint = (reason: string): void => {
|
|
2185
|
+
const viewportRepaint = (reason: string, absoluteClear = false): void => {
|
|
2157
2186
|
this.#fullRedrawCount += 1;
|
|
2158
2187
|
if (renderMetrics.enabled) renderMetrics.recordFullRedraw(reason);
|
|
2159
2188
|
const nextViewportTop = Math.max(0, newLines.length - height);
|
|
2160
|
-
const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
|
|
2161
2189
|
let buffer = "\x1b[?2026h";
|
|
2162
|
-
if (
|
|
2163
|
-
|
|
2190
|
+
if (absoluteClear) {
|
|
2191
|
+
// A width reflow under a terminal multiplexer re-wraps the on-screen rows to
|
|
2192
|
+
// the new width before SIGWINCH fires, so cursor-relative row math (from the
|
|
2193
|
+
// old width) lands on the wrong physical row and corrupts the repaint. Clear
|
|
2194
|
+
// the visible screen and repaint from absolute home; skip 3J so scrollback
|
|
2195
|
+
// history is preserved.
|
|
2196
|
+
buffer += "\x1b[2J\x1b[H";
|
|
2197
|
+
} else {
|
|
2198
|
+
const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
|
|
2199
|
+
if (currentScreenRow > 0) {
|
|
2200
|
+
buffer += `\x1b[${currentScreenRow}A`;
|
|
2201
|
+
}
|
|
2202
|
+
buffer += "\r";
|
|
2164
2203
|
}
|
|
2165
|
-
buffer += "\r";
|
|
2166
2204
|
for (let screenRow = 0; screenRow < height; screenRow++) {
|
|
2167
2205
|
if (screenRow > 0) buffer += "\r\n";
|
|
2168
2206
|
buffer += "\x1b[2K";
|
|
@@ -2224,13 +2262,22 @@ export class TUI extends Container {
|
|
|
2224
2262
|
|
|
2225
2263
|
// Width changes always need a full re-render because wrapping changes.
|
|
2226
2264
|
if (widthChanged) {
|
|
2265
|
+
// A forced render (requestRender(true)) resets #previousWidth to -1: that is a
|
|
2266
|
+
// *fake* width change (the terminal never reflowed), so it keeps the cheap
|
|
2267
|
+
// cursor-relative repaint. A *real* width change (a valid prior width that
|
|
2268
|
+
// differs) means the multiplexer already re-wrapped the on-screen rows,
|
|
2269
|
+
// invalidating cursor-relative row math, so it needs the absolute clear.
|
|
2270
|
+
const realWidthChange = this.#previousWidth > 0 && this.#previousWidth !== width;
|
|
2227
2271
|
logRedraw(`terminal width changed (${this.#previousWidth} -> ${width})`);
|
|
2228
2272
|
if (useViewportRepaintPath(this.terminal)) {
|
|
2229
2273
|
// In viewport-repaint sessions a full replay can either pile the transcript
|
|
2230
2274
|
// back onto scrollback (tmux/screen) or visibly jump to the transcript top
|
|
2231
|
-
// (Windows Terminal).
|
|
2232
|
-
//
|
|
2233
|
-
|
|
2275
|
+
// (Windows Terminal). For a real width change the reflow invalidates
|
|
2276
|
+
// cursor-relative math, so clear the visible screen and repaint the viewport
|
|
2277
|
+
// from absolute home (absoluteClear) — fixing the corruption without replaying
|
|
2278
|
+
// the whole transcript (no scrollback storm). A fake (force) width change
|
|
2279
|
+
// keeps the in-place relative repaint.
|
|
2280
|
+
viewportRepaint(`terminal width changed (${this.#previousWidth} -> ${width})`, realWidthChange);
|
|
2234
2281
|
} else {
|
|
2235
2282
|
fullRender(true, "terminal width changed");
|
|
2236
2283
|
}
|