@dungle-scrubs/tallow 0.9.4 → 0.9.6
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/cli.js +7 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/interactive-mode-patch.d.ts +24 -12
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +229 -146
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/interactive-reset.d.ts +49 -0
- package/dist/interactive-reset.d.ts.map +1 -0
- package/dist/interactive-reset.js +40 -0
- package/dist/interactive-reset.js.map +1 -0
- package/dist/pi-tui-editor-patch.d.ts +10 -0
- package/dist/pi-tui-editor-patch.d.ts.map +1 -0
- package/dist/pi-tui-editor-patch.js +159 -0
- package/dist/pi-tui-editor-patch.js.map +1 -0
- package/dist/pi-tui-patch.d.ts +2 -0
- package/dist/pi-tui-patch.d.ts.map +1 -0
- package/dist/pi-tui-patch.js +563 -0
- package/dist/pi-tui-patch.js.map +1 -0
- package/dist/pi-tui-settings-list-patch.d.ts +11 -0
- package/dist/pi-tui-settings-list-patch.d.ts.map +1 -0
- package/dist/pi-tui-settings-list-patch.js +38 -0
- package/dist/pi-tui-settings-list-patch.js.map +1 -0
- package/dist/reset-diagnostics.d.ts +69 -0
- package/dist/reset-diagnostics.d.ts.map +1 -0
- package/dist/reset-diagnostics.js +41 -0
- package/dist/reset-diagnostics.js.map +1 -0
- package/dist/sdk.d.ts +5 -21
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +180 -149
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-interactive.d.ts +1 -0
- package/dist/workspace-transition-interactive.d.ts.map +1 -1
- package/dist/workspace-transition-interactive.js +7 -17
- package/dist/workspace-transition-interactive.js.map +1 -1
- package/extensions/__integration__/audit-findings.test.ts +4 -5
- package/extensions/_icons/index.ts +2 -4
- package/extensions/_shared/__tests__/image-metadata.test.ts +33 -0
- package/extensions/_shared/__tests__/terminal-links.test.ts +18 -0
- package/extensions/_shared/image-metadata.ts +99 -0
- package/extensions/_shared/inline-preview.ts +1 -1
- package/extensions/_shared/terminal-links.ts +22 -0
- package/extensions/ask-user-question-tool/index.ts +0 -3
- package/extensions/clear/__tests__/clear.test.ts +269 -2
- package/extensions/command-expansion/index.ts +1 -1
- package/extensions/context-files/index.ts +5 -1
- package/extensions/context-fork/__tests__/context-fork.test.ts +94 -1
- package/extensions/context-fork/extension.json +1 -1
- package/extensions/context-fork/index.ts +32 -0
- package/extensions/edit-tool-enhanced/index.ts +2 -1
- package/extensions/hooks/index.ts +33 -11
- package/extensions/loop/index.ts +14 -1
- package/extensions/lsp/index.ts +64 -13
- package/extensions/lsp/package.json +2 -2
- package/extensions/random-spinner/index.ts +7 -642
- package/extensions/read-tool-enhanced/index.ts +6 -8
- package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +2 -3
- package/extensions/render-stabilizer/index.ts +6 -6
- package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +26 -0
- package/extensions/slash-command-bridge/index.ts +14 -2
- package/extensions/subagent-tool/model-resolver.ts +274 -7
- package/extensions/tasks/commands/register-tasks-extension.ts +9 -9
- package/extensions/teams-tool/tools/register-extension.ts +1 -3
- package/extensions/web-search-tool/index.ts +2 -1
- package/extensions/write-tool-enhanced/index.ts +2 -1
- package/node_modules/@mariozechner/pi-tui/README.md +56 -34
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts +18 -13
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js +182 -113
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js +3 -3
- package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts +45 -36
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.js +489 -325
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts +1 -99
- package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/image.js +17 -192
- package/node_modules/@mariozechner/pi-tui/dist/components/image.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/input.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/input.js +57 -60
- package/node_modules/@mariozechner/pi-tui/dist/components/input.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts +2 -69
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.js +5 -102
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js +111 -53
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts +19 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js +78 -67
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts +1 -25
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js +13 -50
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +8 -10
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.js +6 -9
- package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +108 -238
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +108 -365
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +33 -48
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.js +239 -155
- package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts +14 -94
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js +44 -186
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +13 -58
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js +78 -111
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +24 -110
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js +188 -435
- package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts +0 -18
- package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/utils.js +251 -119
- package/node_modules/@mariozechner/pi-tui/dist/utils.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/package.json +6 -6
- package/node_modules/@mariozechner/pi-tui/src/__tests__/__snapshots__/render.test.ts.snap +3 -40
- package/node_modules/@mariozechner/pi-tui/src/__tests__/image-component.test.ts +71 -81
- package/node_modules/@mariozechner/pi-tui/src/__tests__/render.test.ts +0 -33
- package/node_modules/@mariozechner/pi-tui/src/__tests__/terminal-image.test.ts +93 -334
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +1 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/utils.test.ts +11 -196
- package/node_modules/@mariozechner/pi-tui/src/autocomplete.ts +228 -142
- package/node_modules/@mariozechner/pi-tui/src/components/cancellable-loader.ts +3 -3
- package/node_modules/@mariozechner/pi-tui/src/components/editor.ts +624 -390
- package/node_modules/@mariozechner/pi-tui/src/components/image.ts +17 -227
- package/node_modules/@mariozechner/pi-tui/src/components/input.ts +71 -63
- package/node_modules/@mariozechner/pi-tui/src/components/loader.ts +5 -137
- package/node_modules/@mariozechner/pi-tui/src/components/markdown.ts +143 -52
- package/node_modules/@mariozechner/pi-tui/src/components/select-list.ts +136 -70
- package/node_modules/@mariozechner/pi-tui/src/components/settings-list.ts +12 -51
- package/node_modules/@mariozechner/pi-tui/src/index.ts +17 -36
- package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +148 -421
- package/node_modules/@mariozechner/pi-tui/src/keys.ts +253 -181
- package/node_modules/@mariozechner/pi-tui/src/terminal-image.ts +51 -252
- package/node_modules/@mariozechner/pi-tui/src/terminal.ts +78 -133
- package/node_modules/@mariozechner/pi-tui/src/tui.ts +202 -478
- package/node_modules/@mariozechner/pi-tui/src/utils.ts +289 -125
- package/node_modules/@mariozechner/pi-tui/tsconfig.build.json +1 -0
- package/package.json +13 -13
- package/packages/tallow-tui/node_modules/@types/mime-types/README.md +8 -2
- package/packages/tallow-tui/node_modules/@types/mime-types/index.d.ts +6 -0
- package/packages/tallow-tui/node_modules/@types/mime-types/package.json +9 -3
- package/packages/tallow-tui/node_modules/get-east-asian-width/lookup-data.js +18 -0
- package/packages/tallow-tui/node_modules/get-east-asian-width/lookup.js +116 -384
- package/packages/tallow-tui/node_modules/get-east-asian-width/package.json +5 -4
- package/packages/tallow-tui/node_modules/get-east-asian-width/utilities.js +24 -0
- package/packages/tallow-tui/node_modules/marked/README.md +5 -4
- package/packages/tallow-tui/node_modules/marked/bin/main.js +10 -8
- package/packages/tallow-tui/node_modules/marked/bin/marked.js +2 -1
- package/packages/tallow-tui/node_modules/marked/lib/marked.d.ts +156 -125
- package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js +67 -2179
- package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js.map +3 -3
- package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js +67 -2201
- package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js.map +3 -3
- package/packages/tallow-tui/node_modules/marked/man/marked.1 +4 -2
- package/packages/tallow-tui/node_modules/marked/man/marked.1.md +2 -1
- package/packages/tallow-tui/node_modules/marked/package.json +26 -34
- package/skills/tallow-expert/SKILL.md +1 -3
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts +0 -32
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.js +0 -46
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts +0 -52
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js +0 -89
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts +0 -14
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js +0 -55
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-change-listener.test.ts +0 -121
- package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-ghost-text.test.ts +0 -112
- package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +0 -134
- package/node_modules/@mariozechner/pi-tui/src/__tests__/settings-list.test.ts +0 -81
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +0 -555
- package/node_modules/@mariozechner/pi-tui/src/border-styles.ts +0 -60
- package/node_modules/@mariozechner/pi-tui/src/components/bordered-box.ts +0 -113
- package/node_modules/@mariozechner/pi-tui/src/test-utils/capability-env.ts +0 -56
- package/packages/tallow-tui/node_modules/marked/lib/marked.cjs +0 -2211
- package/packages/tallow-tui/node_modules/marked/lib/marked.cjs.map +0 -7
- package/packages/tallow-tui/node_modules/marked/lib/marked.d.cts +0 -728
- package/packages/tallow-tui/node_modules/marked/marked.min.js +0 -69
|
@@ -34,9 +34,7 @@ export function getCellDimensions(): CellDimensions {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function setCellDimensions(dims: CellDimensions): void {
|
|
37
|
-
|
|
38
|
-
cellDimensions = dims;
|
|
39
|
-
}
|
|
37
|
+
cellDimensions = dims;
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
export function detectCapabilities(): TerminalCapabilities {
|
|
@@ -44,6 +42,16 @@ export function detectCapabilities(): TerminalCapabilities {
|
|
|
44
42
|
const term = process.env.TERM?.toLowerCase() || "";
|
|
45
43
|
const colorTerm = process.env.COLORTERM?.toLowerCase() || "";
|
|
46
44
|
|
|
45
|
+
// tmux and screen swallow OSC 8 by default (passthrough is opt-in and wraps
|
|
46
|
+
// sequences differently). Force hyperlinks off whenever we detect them, even
|
|
47
|
+
// when the outer terminal would otherwise support OSC 8. Image protocols are
|
|
48
|
+
// also unreliable under tmux/screen, so leave `images: null` for safety.
|
|
49
|
+
const inTmuxOrScreen = !!process.env.TMUX || term.startsWith("tmux") || term.startsWith("screen");
|
|
50
|
+
if (inTmuxOrScreen) {
|
|
51
|
+
const trueColor = colorTerm === "truecolor" || colorTerm === "24bit";
|
|
52
|
+
return { images: null, trueColor, hyperlinks: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
if (process.env.KITTY_WINDOW_ID || termProgram === "kitty") {
|
|
48
56
|
return { images: "kitty", trueColor: true, hyperlinks: true };
|
|
49
57
|
}
|
|
@@ -68,8 +76,12 @@ export function detectCapabilities(): TerminalCapabilities {
|
|
|
68
76
|
return { images: null, trueColor: true, hyperlinks: true };
|
|
69
77
|
}
|
|
70
78
|
|
|
79
|
+
// Unknown terminal: be conservative. OSC 8 is rendered invisibly as "just
|
|
80
|
+
// text" on terminals that swallow it, which means the URL disappears from
|
|
81
|
+
// the rendered output. Default to the legacy `text (url)` behavior unless we
|
|
82
|
+
// have positively identified a hyperlink-capable terminal above.
|
|
71
83
|
const trueColor = colorTerm === "truecolor" || colorTerm === "24bit";
|
|
72
|
-
return { images: null, trueColor, hyperlinks:
|
|
84
|
+
return { images: null, trueColor, hyperlinks: false };
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
export function getCapabilities(): TerminalCapabilities {
|
|
@@ -83,6 +95,11 @@ export function resetCapabilitiesCache(): void {
|
|
|
83
95
|
cachedCapabilities = null;
|
|
84
96
|
}
|
|
85
97
|
|
|
98
|
+
/** Override the cached capabilities. Useful in tests to exercise both code paths. */
|
|
99
|
+
export function setCapabilities(caps: TerminalCapabilities): void {
|
|
100
|
+
cachedCapabilities = caps;
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
const KITTY_PREFIX = "\x1b_G";
|
|
87
104
|
const ITERM2_PREFIX = "\x1b]1337;File=";
|
|
88
105
|
|
|
@@ -189,79 +206,16 @@ export function encodeITerm2(
|
|
|
189
206
|
return `\x1b]1337;File=${params.join(";")}:${base64Data}\x07`;
|
|
190
207
|
}
|
|
191
208
|
|
|
192
|
-
|
|
193
|
-
export interface ImageLayout {
|
|
194
|
-
/** Number of terminal rows the image occupies. */
|
|
195
|
-
rows: number;
|
|
196
|
-
/** Number of terminal columns the image occupies. */
|
|
197
|
-
columns: number;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Pick the closest valid integer terminal cell count for a fractional target.
|
|
202
|
-
*
|
|
203
|
-
* @param target - Ideal (fractional) cell count
|
|
204
|
-
* @param min - Minimum allowed cell count (inclusive)
|
|
205
|
-
* @param max - Maximum allowed cell count (inclusive)
|
|
206
|
-
* @returns Closest in-range integer cell count
|
|
207
|
-
*/
|
|
208
|
-
function closestCellCount(target: number, min: number, max: number): number {
|
|
209
|
-
const lower = Math.max(min, Math.min(max, Math.floor(target)));
|
|
210
|
-
const upper = Math.max(min, Math.min(max, Math.ceil(target)));
|
|
211
|
-
if (lower === upper) {
|
|
212
|
-
return lower;
|
|
213
|
-
}
|
|
214
|
-
return Math.abs(target - upper) < Math.abs(target - lower) ? upper : lower;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Calculate the cell layout for an image at a given max width.
|
|
219
|
-
* Clamps to the image's natural pixel width (prevents upscaling),
|
|
220
|
-
* then optionally clamps height and back-calculates width to preserve aspect ratio.
|
|
221
|
-
*
|
|
222
|
-
* Uses nearest-integer quantization instead of always rounding up to reduce
|
|
223
|
-
* narrow-width aspect distortion.
|
|
224
|
-
*
|
|
225
|
-
* @param imageDimensions - Native pixel dimensions of the image
|
|
226
|
-
* @param maxWidthCells - Maximum column count for the image
|
|
227
|
-
* @param cellDims - Terminal cell pixel dimensions
|
|
228
|
-
* @param maxHeightCells - Optional row cap (portrait images)
|
|
229
|
-
* @returns Layout with rows and columns the image will occupy
|
|
230
|
-
*/
|
|
231
|
-
export function calculateImageLayout(
|
|
209
|
+
export function calculateImageRows(
|
|
232
210
|
imageDimensions: ImageDimensions,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
const safeMaxWidthCells = Math.max(1, Math.floor(maxWidthCells));
|
|
242
|
-
const safeMaxHeightCells =
|
|
243
|
-
maxHeightCells === undefined ? undefined : Math.max(1, Math.floor(maxHeightCells));
|
|
244
|
-
|
|
245
|
-
// Clamp to natural width — prevents upscaling small images.
|
|
246
|
-
const naturalCols = Math.max(1, Math.ceil(safeImageWidthPx / safeCellWidthPx));
|
|
247
|
-
const maxColumns = Math.min(safeMaxWidthCells, naturalCols);
|
|
248
|
-
let columns = maxColumns;
|
|
249
|
-
|
|
250
|
-
const idealRows =
|
|
251
|
-
(columns * safeCellWidthPx * safeImageHeightPx) / (safeImageWidthPx * safeCellHeightPx);
|
|
252
|
-
let rows = closestCellCount(idealRows, 1, Number.MAX_SAFE_INTEGER);
|
|
253
|
-
|
|
254
|
-
// When height-clamped, reduce columns proportionally to preserve aspect ratio.
|
|
255
|
-
// Without this, the terminal receives a wide column count but the text layer
|
|
256
|
-
// only reserves maxHeightCells rows, causing the image to overflow or squish.
|
|
257
|
-
if (safeMaxHeightCells !== undefined && rows > safeMaxHeightCells) {
|
|
258
|
-
rows = safeMaxHeightCells;
|
|
259
|
-
const idealColumns =
|
|
260
|
-
(rows * safeCellHeightPx * safeImageWidthPx) / (safeImageHeightPx * safeCellWidthPx);
|
|
261
|
-
columns = closestCellCount(idealColumns, 1, maxColumns);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return { columns: Math.max(1, columns), rows: Math.max(1, rows) };
|
|
211
|
+
targetWidthCells: number,
|
|
212
|
+
cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 }
|
|
213
|
+
): number {
|
|
214
|
+
const targetWidthPx = targetWidthCells * cellDimensions.widthPx;
|
|
215
|
+
const scale = targetWidthPx / imageDimensions.widthPx;
|
|
216
|
+
const scaledHeightPx = imageDimensions.heightPx * scale;
|
|
217
|
+
const rows = Math.ceil(scaledHeightPx / cellDimensions.heightPx);
|
|
218
|
+
return Math.max(1, rows);
|
|
265
219
|
}
|
|
266
220
|
|
|
267
221
|
export function getPngDimensions(base64Data: string): ImageDimensions | null {
|
|
@@ -389,156 +343,6 @@ export function getWebpDimensions(base64Data: string): ImageDimensions | null {
|
|
|
389
343
|
}
|
|
390
344
|
}
|
|
391
345
|
|
|
392
|
-
/** Supported image format identifiers returned by `detectImageFormat`. */
|
|
393
|
-
export type ImageFormat = "png" | "jpeg" | "gif" | "webp";
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Detect image format from file header bytes (magic numbers).
|
|
397
|
-
*
|
|
398
|
-
* Checks the first few bytes of a buffer for known image signatures:
|
|
399
|
-
* - PNG: `89 50 4E 47 0D 0A 1A 0A` (8 bytes)
|
|
400
|
-
* - JPEG: `FF D8 FF` (3 bytes)
|
|
401
|
-
* - GIF: `47 49 46 38` + `37`/`39` + `61` (GIF87a / GIF89a)
|
|
402
|
-
* - WebP: `52 49 46 46 ... 57 45 42 50` (RIFF at 0-3, WEBP at 8-11)
|
|
403
|
-
*
|
|
404
|
-
* @param buffer - Raw file bytes (only first 12 bytes are inspected)
|
|
405
|
-
* @returns Detected format string, or null if not a recognized image
|
|
406
|
-
*/
|
|
407
|
-
export function detectImageFormat(buffer: Buffer): ImageFormat | null {
|
|
408
|
-
if (buffer.length < 3) return null;
|
|
409
|
-
|
|
410
|
-
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
|
411
|
-
if (
|
|
412
|
-
buffer.length >= 8 &&
|
|
413
|
-
buffer[0] === 0x89 &&
|
|
414
|
-
buffer[1] === 0x50 &&
|
|
415
|
-
buffer[2] === 0x4e &&
|
|
416
|
-
buffer[3] === 0x47 &&
|
|
417
|
-
buffer[4] === 0x0d &&
|
|
418
|
-
buffer[5] === 0x0a &&
|
|
419
|
-
buffer[6] === 0x1a &&
|
|
420
|
-
buffer[7] === 0x0a
|
|
421
|
-
) {
|
|
422
|
-
return "png";
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// JPEG: FF D8 FF
|
|
426
|
-
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
|
|
427
|
-
return "jpeg";
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// GIF: 47 49 46 38 (37|39) 61
|
|
431
|
-
if (
|
|
432
|
-
buffer.length >= 6 &&
|
|
433
|
-
buffer[0] === 0x47 &&
|
|
434
|
-
buffer[1] === 0x49 &&
|
|
435
|
-
buffer[2] === 0x46 &&
|
|
436
|
-
buffer[3] === 0x38 &&
|
|
437
|
-
(buffer[4] === 0x37 || buffer[4] === 0x39) &&
|
|
438
|
-
buffer[5] === 0x61
|
|
439
|
-
) {
|
|
440
|
-
return "gif";
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// WebP: RIFF....WEBP (bytes 0-3 = "RIFF", bytes 8-11 = "WEBP")
|
|
444
|
-
if (
|
|
445
|
-
buffer.length >= 12 &&
|
|
446
|
-
buffer[0] === 0x52 &&
|
|
447
|
-
buffer[1] === 0x49 &&
|
|
448
|
-
buffer[2] === 0x46 &&
|
|
449
|
-
buffer[3] === 0x46 &&
|
|
450
|
-
buffer[8] === 0x57 &&
|
|
451
|
-
buffer[9] === 0x45 &&
|
|
452
|
-
buffer[10] === 0x42 &&
|
|
453
|
-
buffer[11] === 0x50
|
|
454
|
-
) {
|
|
455
|
-
return "webp";
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return null;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Map an `ImageFormat` to its standard MIME type string.
|
|
463
|
-
*
|
|
464
|
-
* @param format - Detected image format
|
|
465
|
-
* @returns MIME type (e.g., `"image/png"`)
|
|
466
|
-
*/
|
|
467
|
-
export function imageFormatToMime(format: ImageFormat): string {
|
|
468
|
-
switch (format) {
|
|
469
|
-
case "png":
|
|
470
|
-
return "image/png";
|
|
471
|
-
case "jpeg":
|
|
472
|
-
return "image/jpeg";
|
|
473
|
-
case "gif":
|
|
474
|
-
return "image/gif";
|
|
475
|
-
case "webp":
|
|
476
|
-
return "image/webp";
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/** Metadata about an image's original and display dimensions after processing. */
|
|
481
|
-
export interface ImageMetadata {
|
|
482
|
-
/** Original pixel width before any resizing. */
|
|
483
|
-
readonly originalWidth: number;
|
|
484
|
-
/** Original pixel height before any resizing. */
|
|
485
|
-
readonly originalHeight: number;
|
|
486
|
-
/** Display pixel width after resizing (equals original if not resized). */
|
|
487
|
-
readonly displayWidth: number;
|
|
488
|
-
/** Display pixel height after resizing (equals original if not resized). */
|
|
489
|
-
readonly displayHeight: number;
|
|
490
|
-
/** Whether the image was resized from its original dimensions. */
|
|
491
|
-
readonly resized: boolean;
|
|
492
|
-
/** Detected image format (e.g., "png", "jpeg"). */
|
|
493
|
-
readonly format: ImageFormat | null;
|
|
494
|
-
/** File size in bytes, if known. */
|
|
495
|
-
readonly sizeBytes?: number;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Create an `ImageMetadata` object from original and display dimensions.
|
|
500
|
-
*
|
|
501
|
-
* Sets `resized` automatically by comparing original and display dimensions.
|
|
502
|
-
*
|
|
503
|
-
* @param original - Original pixel dimensions before resizing
|
|
504
|
-
* @param display - Display pixel dimensions after resizing
|
|
505
|
-
* @param format - Detected image format, or null if unknown
|
|
506
|
-
* @param sizeBytes - File size in bytes (optional)
|
|
507
|
-
* @returns Populated ImageMetadata
|
|
508
|
-
*/
|
|
509
|
-
export function createImageMetadata(
|
|
510
|
-
original: ImageDimensions,
|
|
511
|
-
display: ImageDimensions,
|
|
512
|
-
format: ImageFormat | null,
|
|
513
|
-
sizeBytes?: number
|
|
514
|
-
): ImageMetadata {
|
|
515
|
-
return {
|
|
516
|
-
originalWidth: original.widthPx,
|
|
517
|
-
originalHeight: original.heightPx,
|
|
518
|
-
displayWidth: display.widthPx,
|
|
519
|
-
displayHeight: display.heightPx,
|
|
520
|
-
resized: original.widthPx !== display.widthPx || original.heightPx !== display.heightPx,
|
|
521
|
-
format,
|
|
522
|
-
sizeBytes,
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Format a compact dimension string for display.
|
|
528
|
-
*
|
|
529
|
-
* - Not resized: `"1920×1080"`
|
|
530
|
-
* - Resized: `"3840×2160 → 1920×1080"`
|
|
531
|
-
*
|
|
532
|
-
* @param meta - Image metadata with dimension info
|
|
533
|
-
* @returns Formatted dimension string
|
|
534
|
-
*/
|
|
535
|
-
export function formatImageDimensions(meta: ImageMetadata): string {
|
|
536
|
-
if (meta.resized) {
|
|
537
|
-
return `${meta.originalWidth}×${meta.originalHeight} → ${meta.displayWidth}×${meta.displayHeight}`;
|
|
538
|
-
}
|
|
539
|
-
return `${meta.originalWidth}×${meta.originalHeight}`;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
346
|
export function getImageDimensions(base64Data: string, mimeType: string): ImageDimensions | null {
|
|
543
347
|
if (mimeType === "image/png") {
|
|
544
348
|
return getPngDimensions(base64Data);
|
|
@@ -555,21 +359,11 @@ export function getImageDimensions(base64Data: string, mimeType: string): ImageD
|
|
|
555
359
|
return null;
|
|
556
360
|
}
|
|
557
361
|
|
|
558
|
-
/**
|
|
559
|
-
* Render an image as a terminal escape sequence (Kitty or iTerm2 protocol).
|
|
560
|
-
* Returns the escape sequence, the number of rows/columns it occupies, and
|
|
561
|
-
* an optional Kitty image ID for later cleanup.
|
|
562
|
-
*
|
|
563
|
-
* @param base64Data - Base64-encoded image data
|
|
564
|
-
* @param imageDimensions - Native pixel dimensions
|
|
565
|
-
* @param options - Width/height caps, aspect ratio, image ID
|
|
566
|
-
* @returns Rendered sequence with layout info, or null if unsupported
|
|
567
|
-
*/
|
|
568
362
|
export function renderImage(
|
|
569
363
|
base64Data: string,
|
|
570
364
|
imageDimensions: ImageDimensions,
|
|
571
365
|
options: ImageRenderOptions = {}
|
|
572
|
-
): { sequence: string; rows: number;
|
|
366
|
+
): { sequence: string; rows: number; imageId?: number } | null {
|
|
573
367
|
const caps = getCapabilities();
|
|
574
368
|
|
|
575
369
|
if (!caps.images) {
|
|
@@ -577,35 +371,40 @@ export function renderImage(
|
|
|
577
371
|
}
|
|
578
372
|
|
|
579
373
|
const maxWidth = options.maxWidthCells ?? 80;
|
|
580
|
-
const
|
|
581
|
-
imageDimensions,
|
|
582
|
-
maxWidth,
|
|
583
|
-
getCellDimensions(),
|
|
584
|
-
options.maxHeightCells
|
|
585
|
-
);
|
|
374
|
+
const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions());
|
|
586
375
|
|
|
587
376
|
if (caps.images === "kitty") {
|
|
588
|
-
//
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
columns: layout.columns,
|
|
592
|
-
imageId: options.imageId,
|
|
593
|
-
});
|
|
594
|
-
return { sequence, rows: layout.rows, columns: layout.columns, imageId: options.imageId };
|
|
377
|
+
// Only use imageId if explicitly provided - static images don't need IDs
|
|
378
|
+
const sequence = encodeKitty(base64Data, { columns: maxWidth, rows, imageId: options.imageId });
|
|
379
|
+
return { sequence, rows, imageId: options.imageId };
|
|
595
380
|
}
|
|
596
381
|
|
|
597
382
|
if (caps.images === "iterm2") {
|
|
598
383
|
const sequence = encodeITerm2(base64Data, {
|
|
599
|
-
width:
|
|
384
|
+
width: maxWidth,
|
|
600
385
|
height: "auto",
|
|
601
386
|
preserveAspectRatio: options.preserveAspectRatio ?? true,
|
|
602
387
|
});
|
|
603
|
-
return { sequence, rows
|
|
388
|
+
return { sequence, rows };
|
|
604
389
|
}
|
|
605
390
|
|
|
606
391
|
return null;
|
|
607
392
|
}
|
|
608
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Wrap text in an OSC 8 hyperlink sequence.
|
|
396
|
+
* The text is rendered as a clickable hyperlink in terminals that support OSC 8
|
|
397
|
+
* (Ghostty, Kitty, WezTerm, iTerm2, VSCode, and others).
|
|
398
|
+
* In terminals that do not support OSC 8, the escape sequences are ignored
|
|
399
|
+
* and only the plain text is displayed.
|
|
400
|
+
*
|
|
401
|
+
* @param text - The visible text to display
|
|
402
|
+
* @param url - The URL to link to
|
|
403
|
+
*/
|
|
404
|
+
export function hyperlink(text: string, url: string): string {
|
|
405
|
+
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
406
|
+
}
|
|
407
|
+
|
|
609
408
|
export function imageFallback(
|
|
610
409
|
mimeType: string,
|
|
611
410
|
dimensions?: ImageDimensions,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import * as path from "node:path";
|
|
2
4
|
import { setKittyProtocolActive } from "./keys.js";
|
|
3
5
|
import { StdinBuffer } from "./stdin-buffer.js";
|
|
4
6
|
|
|
7
|
+
const cjsRequire = createRequire(import.meta.url);
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* Minimal terminal interface for TUI
|
|
7
11
|
*/
|
|
@@ -30,9 +34,6 @@ export interface Terminal {
|
|
|
30
34
|
// Whether Kitty keyboard protocol is active
|
|
31
35
|
get kittyProtocolActive(): boolean;
|
|
32
36
|
|
|
33
|
-
// Whether running inside tmux (using modifyOtherKeys instead of Kitty protocol)
|
|
34
|
-
get isTmux(): boolean;
|
|
35
|
-
|
|
36
37
|
// Cursor positioning (relative to current position)
|
|
37
38
|
moveBy(lines: number): void; // Move cursor up (negative) or down (positive) by N lines
|
|
38
39
|
|
|
@@ -45,21 +46,8 @@ export interface Terminal {
|
|
|
45
46
|
clearFromCursor(): void; // Clear from cursor to end of screen
|
|
46
47
|
clearScreen(): void; // Clear entire screen and move cursor to (0,0)
|
|
47
48
|
|
|
48
|
-
// Mouse reporting
|
|
49
|
-
enableMouse(): void; // Enable SGR mouse tracking (scroll, click)
|
|
50
|
-
disableMouse(): void; // Disable mouse tracking
|
|
51
|
-
|
|
52
|
-
// Screen buffer mode
|
|
53
|
-
enterAlternateScreen(): void; // Switch to alternate screen buffer (no scrollback)
|
|
54
|
-
leaveAlternateScreen(): void; // Restore normal screen buffer and scrollback
|
|
55
|
-
|
|
56
49
|
// Title operations
|
|
57
50
|
setTitle(title: string): void; // Set terminal window title
|
|
58
|
-
|
|
59
|
-
/** Set terminal progress bar via OSC 9;4. Supported by Windows Terminal, iTerm2, ConEmu. */
|
|
60
|
-
setProgress(percent: number): void;
|
|
61
|
-
/** Clear terminal progress bar via OSC 9;4. */
|
|
62
|
-
clearProgress(): void;
|
|
63
51
|
}
|
|
64
52
|
|
|
65
53
|
/**
|
|
@@ -70,24 +58,31 @@ export class ProcessTerminal implements Terminal {
|
|
|
70
58
|
private inputHandler?: (data: string) => void;
|
|
71
59
|
private resizeHandler?: () => void;
|
|
72
60
|
private _kittyProtocolActive = false;
|
|
61
|
+
private _modifyOtherKeysActive = false;
|
|
73
62
|
private stdinBuffer?: StdinBuffer;
|
|
74
63
|
private stdinDataHandler?: (data: string) => void;
|
|
75
|
-
private writeLogPath =
|
|
76
|
-
|
|
77
|
-
|
|
64
|
+
private writeLogPath = (() => {
|
|
65
|
+
const env = process.env.PI_TUI_WRITE_LOG || "";
|
|
66
|
+
if (!env) return "";
|
|
67
|
+
try {
|
|
68
|
+
if (fs.statSync(env).isDirectory()) {
|
|
69
|
+
const now = new Date();
|
|
70
|
+
const ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}-${String(now.getMinutes()).padStart(2, "0")}-${String(now.getSeconds()).padStart(2, "0")}`;
|
|
71
|
+
return path.join(env, `tui-${ts}-${process.pid}.log`);
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Not an existing directory - use as-is (file path)
|
|
75
|
+
}
|
|
76
|
+
return env;
|
|
77
|
+
})();
|
|
78
78
|
|
|
79
79
|
get kittyProtocolActive(): boolean {
|
|
80
80
|
return this._kittyProtocolActive;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
get isTmux(): boolean {
|
|
84
|
-
return !!process.env.TMUX;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
83
|
start(onInput: (data: string) => void, onResize: () => void): void {
|
|
88
84
|
this.inputHandler = onInput;
|
|
89
85
|
this.resizeHandler = onResize;
|
|
90
|
-
this.alternateScreenActive = false;
|
|
91
86
|
|
|
92
87
|
// Save previous state and enable raw mode
|
|
93
88
|
this.wasRaw = process.stdin.isRaw || false;
|
|
@@ -109,24 +104,16 @@ export class ProcessTerminal implements Terminal {
|
|
|
109
104
|
process.kill(process.pid, "SIGWINCH");
|
|
110
105
|
}
|
|
111
106
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
if (process.env.TMUX) {
|
|
123
|
-
this.setupTmuxInput();
|
|
124
|
-
} else {
|
|
125
|
-
// Query and enable Kitty keyboard protocol
|
|
126
|
-
// The query handler intercepts input temporarily, then installs the user's handler
|
|
127
|
-
// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
128
|
-
this.queryAndEnableKittyProtocol();
|
|
129
|
-
}
|
|
107
|
+
// On Windows, enable ENABLE_VIRTUAL_TERMINAL_INPUT so the console sends
|
|
108
|
+
// VT escape sequences (e.g. \x1b[Z for Shift+Tab) instead of raw console
|
|
109
|
+
// events that lose modifier information. Must run AFTER setRawMode(true)
|
|
110
|
+
// since that resets console mode flags.
|
|
111
|
+
this.enableWindowsVTInput();
|
|
112
|
+
|
|
113
|
+
// Query and enable Kitty keyboard protocol
|
|
114
|
+
// The query handler intercepts input temporarily, then installs the user's handler
|
|
115
|
+
// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
116
|
+
this.queryAndEnableKittyProtocol();
|
|
130
117
|
}
|
|
131
118
|
|
|
132
119
|
/**
|
|
@@ -176,32 +163,21 @@ export class ProcessTerminal implements Terminal {
|
|
|
176
163
|
|
|
177
164
|
// Handler that pipes stdin data through the buffer
|
|
178
165
|
this.stdinDataHandler = (data: string) => {
|
|
179
|
-
this.stdinBuffer
|
|
166
|
+
this.stdinBuffer!.process(data);
|
|
180
167
|
};
|
|
181
168
|
}
|
|
182
169
|
|
|
183
|
-
/**
|
|
184
|
-
* Set up stdin handling for tmux without Kitty keyboard protocol.
|
|
185
|
-
*
|
|
186
|
-
* tmux doesn't support the Kitty keyboard protocol. Escape and Ctrl+C
|
|
187
|
-
* arrive as raw bytes (\x1b and \x03) which the key matching handles natively.
|
|
188
|
-
*
|
|
189
|
-
* For Shift+Enter, tmux needs `extended-keys on` and `extended-keys-format csi-u`
|
|
190
|
-
* in tmux.conf. With that config, we request modifyOtherKeys mode 1 so tmux
|
|
191
|
-
* encodes modified keys (Shift+Enter → CSI 13;2 u) while leaving standard
|
|
192
|
-
* keys (Escape, Ctrl+C, regular typing) as raw bytes.
|
|
193
|
-
*/
|
|
194
|
-
private setupTmuxInput(): void {
|
|
195
|
-
this.setupStdinBuffer();
|
|
196
|
-
process.stdin.on("data", this.stdinDataHandler!);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
170
|
/**
|
|
200
171
|
* Query terminal for Kitty keyboard protocol support and enable if available.
|
|
201
172
|
*
|
|
202
173
|
* Sends CSI ? u to query current flags. If terminal responds with CSI ? <flags> u,
|
|
203
174
|
* it supports the protocol and we enable it with CSI > 1 u.
|
|
204
175
|
*
|
|
176
|
+
* If no Kitty response arrives shortly after startup, fall back to enabling
|
|
177
|
+
* xterm modifyOtherKeys mode 2. This is needed for tmux, which can forward
|
|
178
|
+
* modified enter keys as CSI-u when extended-keys is enabled, but may not
|
|
179
|
+
* answer the Kitty protocol query.
|
|
180
|
+
*
|
|
205
181
|
* The response is detected in setupStdinBuffer's data handler, which properly
|
|
206
182
|
* handles the case where the response arrives split across multiple stdin events.
|
|
207
183
|
*/
|
|
@@ -209,6 +185,41 @@ export class ProcessTerminal implements Terminal {
|
|
|
209
185
|
this.setupStdinBuffer();
|
|
210
186
|
process.stdin.on("data", this.stdinDataHandler!);
|
|
211
187
|
process.stdout.write("\x1b[?u");
|
|
188
|
+
setTimeout(() => {
|
|
189
|
+
if (!this._kittyProtocolActive && !this._modifyOtherKeysActive) {
|
|
190
|
+
process.stdout.write("\x1b[>4;2m");
|
|
191
|
+
this._modifyOtherKeysActive = true;
|
|
192
|
+
}
|
|
193
|
+
}, 150);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* On Windows, add ENABLE_VIRTUAL_TERMINAL_INPUT (0x0200) to the stdin
|
|
198
|
+
* console handle so the terminal sends VT sequences for modified keys
|
|
199
|
+
* (e.g. \x1b[Z for Shift+Tab). Without this, libuv's ReadConsoleInputW
|
|
200
|
+
* discards modifier state and Shift+Tab arrives as plain \t.
|
|
201
|
+
*/
|
|
202
|
+
private enableWindowsVTInput(): void {
|
|
203
|
+
if (process.platform !== "win32") return;
|
|
204
|
+
try {
|
|
205
|
+
// Dynamic require to avoid bundling koffi's 74MB of cross-platform
|
|
206
|
+
// native binaries into every compiled binary. Koffi is only needed
|
|
207
|
+
// on Windows for VT input support.
|
|
208
|
+
const koffi = cjsRequire("koffi");
|
|
209
|
+
const k32 = koffi.load("kernel32.dll");
|
|
210
|
+
const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
|
|
211
|
+
const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
|
|
212
|
+
const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
|
|
213
|
+
|
|
214
|
+
const STD_INPUT_HANDLE = -10;
|
|
215
|
+
const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
216
|
+
const handle = GetStdHandle(STD_INPUT_HANDLE);
|
|
217
|
+
const mode = new Uint32Array(1);
|
|
218
|
+
GetConsoleMode(handle, mode);
|
|
219
|
+
SetConsoleMode(handle, mode[0]! | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
|
220
|
+
} catch {
|
|
221
|
+
// koffi not available — Shift+Tab won't be distinguishable from Tab
|
|
222
|
+
}
|
|
212
223
|
}
|
|
213
224
|
|
|
214
225
|
async drainInput(maxMs = 1000, idleMs = 50): Promise<void> {
|
|
@@ -219,6 +230,10 @@ export class ProcessTerminal implements Terminal {
|
|
|
219
230
|
this._kittyProtocolActive = false;
|
|
220
231
|
setKittyProtocolActive(false);
|
|
221
232
|
}
|
|
233
|
+
if (this._modifyOtherKeysActive) {
|
|
234
|
+
process.stdout.write("\x1b[>4;0m");
|
|
235
|
+
this._modifyOtherKeysActive = false;
|
|
236
|
+
}
|
|
222
237
|
|
|
223
238
|
const previousHandler = this.inputHandler;
|
|
224
239
|
this.inputHandler = undefined;
|
|
@@ -246,14 +261,6 @@ export class ProcessTerminal implements Terminal {
|
|
|
246
261
|
}
|
|
247
262
|
|
|
248
263
|
stop(): void {
|
|
249
|
-
// Always restore normal screen buffer before exiting.
|
|
250
|
-
if (this.alternateScreenActive) {
|
|
251
|
-
this.leaveAlternateScreen();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Disable mouse tracking
|
|
255
|
-
this.disableMouse();
|
|
256
|
-
|
|
257
264
|
// Disable bracketed paste mode
|
|
258
265
|
process.stdout.write("\x1b[?2004l");
|
|
259
266
|
|
|
@@ -263,6 +270,10 @@ export class ProcessTerminal implements Terminal {
|
|
|
263
270
|
this._kittyProtocolActive = false;
|
|
264
271
|
setKittyProtocolActive(false);
|
|
265
272
|
}
|
|
273
|
+
if (this._modifyOtherKeysActive) {
|
|
274
|
+
process.stdout.write("\x1b[>4;0m");
|
|
275
|
+
this._modifyOtherKeysActive = false;
|
|
276
|
+
}
|
|
266
277
|
|
|
267
278
|
// Clean up StdinBuffer
|
|
268
279
|
if (this.stdinBuffer) {
|
|
@@ -342,74 +353,8 @@ export class ProcessTerminal implements Terminal {
|
|
|
342
353
|
process.stdout.write("\x1b[2J\x1b[H"); // Clear screen and move to home (1,1)
|
|
343
354
|
}
|
|
344
355
|
|
|
345
|
-
/**
|
|
346
|
-
* Enable SGR mouse tracking.
|
|
347
|
-
* Mode 1000 = button press/release (includes scroll wheel).
|
|
348
|
-
* Mode 1006 = SGR extended format (avoids 223-column limit).
|
|
349
|
-
*/
|
|
350
|
-
enableMouse(): void {
|
|
351
|
-
if (this.mouseActive) return;
|
|
352
|
-
process.stdout.write("\x1b[?1000h\x1b[?1006h");
|
|
353
|
-
this.mouseActive = true;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Disable mouse tracking and restore default terminal mouse handling.
|
|
358
|
-
*/
|
|
359
|
-
disableMouse(): void {
|
|
360
|
-
if (!this.mouseActive) return;
|
|
361
|
-
process.stdout.write("\x1b[?1006l\x1b[?1000l");
|
|
362
|
-
this.mouseActive = false;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Switch to alternate screen buffer and clear it.
|
|
367
|
-
* @returns void
|
|
368
|
-
*/
|
|
369
|
-
enterAlternateScreen(): void {
|
|
370
|
-
if (this.alternateScreenActive) return;
|
|
371
|
-
process.stdout.write("\x1b[?1049h");
|
|
372
|
-
this.alternateScreenActive = true;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Restore normal screen buffer and previous scrollback.
|
|
377
|
-
* @returns void
|
|
378
|
-
*/
|
|
379
|
-
leaveAlternateScreen(): void {
|
|
380
|
-
if (!this.alternateScreenActive) return;
|
|
381
|
-
process.stdout.write("\x1b[?1049l");
|
|
382
|
-
this.alternateScreenActive = false;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
356
|
setTitle(title: string): void {
|
|
386
357
|
// OSC 0;title BEL - set terminal window title
|
|
387
358
|
process.stdout.write(`\x1b]0;${title}\x07`);
|
|
388
359
|
}
|
|
389
|
-
|
|
390
|
-
private lastProgressWrite = 0;
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Set terminal progress bar via OSC 9;4.
|
|
394
|
-
* Throttled to max 1 update per 100ms (percent=100 always passes through).
|
|
395
|
-
* Supported by Windows Terminal (tab indicator), iTerm2 (title bar), ConEmu (taskbar).
|
|
396
|
-
* Unsupported terminals gracefully ignore these sequences.
|
|
397
|
-
* @param percent - Progress percentage, clamped to 0-100
|
|
398
|
-
*/
|
|
399
|
-
setProgress(percent: number): void {
|
|
400
|
-
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
401
|
-
const now = Date.now();
|
|
402
|
-
if (clamped !== 100 && now - this.lastProgressWrite < 100) return;
|
|
403
|
-
this.lastProgressWrite = now;
|
|
404
|
-
process.stdout.write(`\x1b]9;4;1;${clamped}\x07`);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Clear terminal progress bar via OSC 9;4.
|
|
409
|
-
* Resets the throttle timestamp so a subsequent setProgress fires immediately.
|
|
410
|
-
*/
|
|
411
|
-
clearProgress(): void {
|
|
412
|
-
this.lastProgressWrite = 0;
|
|
413
|
-
process.stdout.write("\x1b]9;4;0;0\x07");
|
|
414
|
-
}
|
|
415
360
|
}
|