tformula 0.1.0

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.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * MathJax dimensions are expressed in ex units. A terminal glyph's x-height is
3
+ * approximately 45% of its cell height. This keeps ordinary symbols aligned
4
+ * with terminal text while allowing fractions to use their natural extra height.
5
+ */
6
+ export function calculateFormulaGeometry(input) {
7
+ const canvasWidth = Math.max(1, Math.round(input.columns * input.cell.width));
8
+ const canvasHeight = Math.max(1, Math.round(input.rows * input.cell.height));
9
+ const exPx = input.cell.height * 0.45 * input.scale;
10
+ const naturalHeight = Math.max(1, input.naturalHeightEx * exPx);
11
+ const naturalWidth = Math.max(1, naturalHeight * input.aspectRatio);
12
+ const horizontalPadding = input.display ? input.cell.width : Math.min(2, input.cell.width * 0.15);
13
+ // A multi-row display region already has whitespace reserved by the TUI.
14
+ // Use its full height so tall fractions keep the same glyph scale as simple
15
+ // equations. Inline formulas retain padding for adjacent terminal text.
16
+ const verticalPadding = input.display && input.rows > 1
17
+ ? 0
18
+ : Math.max(1, input.cell.height * 0.08);
19
+ const availableWidth = Math.max(1, canvasWidth - horizontalPadding * 2);
20
+ const availableHeight = Math.max(1, canvasHeight - verticalPadding * 2);
21
+ // Never enlarge merely to fill the source rectangle; only shrink to fit.
22
+ const fit = Math.min(1, availableWidth / naturalWidth, availableHeight / naturalHeight);
23
+ const formulaWidth = Math.max(1, Math.round(naturalWidth * fit));
24
+ const formulaHeight = Math.max(1, Math.round(naturalHeight * fit));
25
+ const depthRatio = Math.max(0, Math.min(1, input.depthEx / input.naturalHeightEx));
26
+ const scaledDepth = formulaHeight * depthRatio;
27
+ // Terminal protocols expose cell dimensions, but not font ascent. A baseline
28
+ // near 78% of the cell height matches the usual terminal font metrics. Honor
29
+ // MathJax's depth below that baseline so rho, J, and subscripted symbols line
30
+ // up with neighboring terminal text instead of centering unlike bounding boxes.
31
+ const inlineBaseline = input.cell.height * 0.78;
32
+ const baselineOffsetY = Math.round(inlineBaseline - (formulaHeight - scaledDepth));
33
+ const maxOffsetY = Math.max(0, canvasHeight - formulaHeight);
34
+ return {
35
+ canvasWidth,
36
+ canvasHeight,
37
+ formulaWidth,
38
+ formulaHeight,
39
+ offsetX: input.leftAlign
40
+ ? Math.round(horizontalPadding)
41
+ : Math.round((canvasWidth - formulaWidth) / 2),
42
+ offsetY: input.display
43
+ ? Math.round((canvasHeight - formulaHeight) / 2)
44
+ : Math.max(0, Math.min(maxOffsetY, baselineOffsetY))
45
+ };
46
+ }
47
+ //# sourceMappingURL=geometry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.js","sourceRoot":"","sources":["../src/geometry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export declare const TFORMULA_Z_INDEX = 20260713;
2
+ export declare const TFORMULA_IMAGE_ID_MIN = 1400000000;
3
+ export declare const TFORMULA_IMAGE_ID_MAX = 1999999999;
4
+ export declare function kittyDeleteImage(imageId: number): string;
5
+ /** Delete one placement while retaining the shared image data in the terminal. */
6
+ export declare function kittyDeletePlacement(imageId: number, placementId: number): string;
7
+ export declare function kittyDeleteByZIndex(): string;
8
+ export declare function kittyDeleteRange(firstImageId?: number, lastImageId?: number): string;
9
+ export declare function kittyTransmitAndPlace(png: Uint8Array, imageId: number, columns: number, rows: number): string;
10
+ /** Upload PNG data without creating a placement. */
11
+ export declare function kittyTransmitImage(png: Uint8Array, imageId: number): string;
12
+ /** Place image data that has already been uploaded to the terminal. */
13
+ export declare function kittyPlaceImage(imageId: number, placementId: number, columns: number, rows: number): string;
14
+ export declare function cursorPosition(row: number, column: number): string;
15
+ export declare function synchronizedOutput(content: string): string;
package/dist/kitty.js ADDED
@@ -0,0 +1,61 @@
1
+ const ESC = "\x1b";
2
+ const APC_START = `${ESC}_G`;
3
+ const ST = `${ESC}\\`;
4
+ export const TFORMULA_Z_INDEX = 20_260_713;
5
+ export const TFORMULA_IMAGE_ID_MIN = 1_400_000_000;
6
+ export const TFORMULA_IMAGE_ID_MAX = 1_999_999_999;
7
+ export function kittyDeleteImage(imageId) {
8
+ return `${APC_START}a=d,d=I,i=${imageId},q=2${ST}`;
9
+ }
10
+ /** Delete one placement while retaining the shared image data in the terminal. */
11
+ export function kittyDeletePlacement(imageId, placementId) {
12
+ return `${APC_START}a=d,d=i,i=${imageId},p=${placementId},q=2${ST}`;
13
+ }
14
+ export function kittyDeleteByZIndex() {
15
+ return `${APC_START}a=d,d=Z,z=${TFORMULA_Z_INDEX},q=2${ST}`;
16
+ }
17
+ export function kittyDeleteRange(firstImageId = TFORMULA_IMAGE_ID_MIN, lastImageId = TFORMULA_IMAGE_ID_MAX) {
18
+ return `${APC_START}a=d,d=R,x=${firstImageId},y=${lastImageId},q=2${ST}`;
19
+ }
20
+ export function kittyTransmitAndPlace(png, imageId, columns, rows) {
21
+ const base64 = Buffer.from(png).toString("base64");
22
+ const chunkSize = 4096;
23
+ const chunks = [];
24
+ for (let offset = 0; offset < base64.length; offset += chunkSize) {
25
+ chunks.push(base64.slice(offset, offset + chunkSize));
26
+ }
27
+ if (chunks.length === 0)
28
+ chunks.push("");
29
+ return chunks.map((chunk, index) => {
30
+ const first = index === 0;
31
+ const more = index < chunks.length - 1 ? 1 : 0;
32
+ const controls = first
33
+ ? `a=T,f=100,t=d,i=${imageId},q=2,c=${columns},r=${rows},C=1,z=${TFORMULA_Z_INDEX},m=${more}`
34
+ : `m=${more},q=2`;
35
+ return `${APC_START}${controls};${chunk}${ST}`;
36
+ }).join("");
37
+ }
38
+ /** Upload PNG data without creating a placement. */
39
+ export function kittyTransmitImage(png, imageId) {
40
+ const base64 = Buffer.from(png).toString("base64");
41
+ const chunks = base64.match(/.{1,4096}/gu) ?? [""];
42
+ return chunks.map((chunk, index) => {
43
+ const first = index === 0;
44
+ const more = index < chunks.length - 1 ? 1 : 0;
45
+ const controls = first
46
+ ? `a=t,f=100,t=d,i=${imageId},q=2,m=${more}`
47
+ : `m=${more},q=2`;
48
+ return `${APC_START}${controls};${chunk}${ST}`;
49
+ }).join("");
50
+ }
51
+ /** Place image data that has already been uploaded to the terminal. */
52
+ export function kittyPlaceImage(imageId, placementId, columns, rows) {
53
+ return `${APC_START}a=p,i=${imageId},p=${placementId},q=2,c=${columns},r=${rows},C=1,z=${TFORMULA_Z_INDEX}${ST}`;
54
+ }
55
+ export function cursorPosition(row, column) {
56
+ return `${ESC}[${Math.max(1, row)};${Math.max(1, column)}H`;
57
+ }
58
+ export function synchronizedOutput(content) {
59
+ return `${ESC}[?2026h${content}${ESC}[?2026l`;
60
+ }
61
+ //# sourceMappingURL=kitty.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kitty.js","sourceRoot":"","sources":["../src/kitty.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { FormulaRegion, RenderedFormula, TerminalCapabilities } from "./types.js";
2
+ import { FormulaCache } from "./formula-cache.js";
3
+ interface SvgDimensions {
4
+ aspectRatio: number;
5
+ heightEx: number;
6
+ depthEx: number;
7
+ }
8
+ export declare function readSvgDimensions(svg: string): SvgDimensions;
9
+ export declare function normalizeLatexForRendering(latex: string): string;
10
+ export declare function renderMathJaxSvg(latex: string, display: boolean, _containerWidth: number, cache?: FormulaCache): Promise<string>;
11
+ export declare class MathRenderer {
12
+ #private;
13
+ readonly persistentCache: FormulaCache;
14
+ constructor(persistentCache?: FormulaCache);
15
+ render(region: FormulaRegion, columns: number, rows: number, capabilities: TerminalCapabilities, scale: number, foreground?: string, background?: string): Promise<RenderedFormula>;
16
+ clear(): void;
17
+ }
18
+ export {};
@@ -0,0 +1,204 @@
1
+ import { Resvg } from "@resvg/resvg-js";
2
+ import { calculateFormulaGeometry } from "./geometry.js";
3
+ import { formulaCacheKey, sharedFormulaCache } from "./formula-cache.js";
4
+ let mathJaxPromise;
5
+ const MATHJAX_CACHE_VERSION = "mathjax-4.1.3-svg-v1";
6
+ const PNG_CACHE_VERSION = "resvg-2.6.2-terminal-canvas-v1";
7
+ const CANONICAL_CONTAINER_WIDTH = 100_000;
8
+ function mathJaxCacheRequest(latex, display) {
9
+ const normalized = safeLatex(normalizeLatexForRendering(latex).trim());
10
+ const source = display ? normalized : `\\textstyle{${normalized}}`;
11
+ return {
12
+ source,
13
+ svgKey: formulaCacheKey({
14
+ version: MATHJAX_CACHE_VERSION,
15
+ source,
16
+ display: true,
17
+ em: 16,
18
+ ex: 8,
19
+ containerWidth: CANONICAL_CONTAINER_WIDTH,
20
+ fontCache: "local"
21
+ })
22
+ };
23
+ }
24
+ async function getMathJax() {
25
+ if (!mathJaxPromise) {
26
+ mathJaxPromise = import("@mathjax/src").then(async (module) => {
27
+ const mathJax = module.default;
28
+ await mathJax.init({
29
+ loader: { load: ["input/tex", "output/svg"] },
30
+ tex: { maxBuffer: 8192 },
31
+ svg: { fontCache: "local" }
32
+ });
33
+ return mathJax;
34
+ });
35
+ }
36
+ return mathJaxPromise;
37
+ }
38
+ function parseLength(value) {
39
+ const match = value?.match(/^([\d.]+)(ex|em|px)?$/u);
40
+ return match ? { value: Number(match[1]), unit: match[2] ?? "px" } : undefined;
41
+ }
42
+ export function readSvgDimensions(svg) {
43
+ const width = parseLength(svg.match(/\bwidth="([^"]+)"/u)?.[1]);
44
+ const height = parseLength(svg.match(/\bheight="([^"]+)"/u)?.[1]);
45
+ const viewBox = svg.match(/\bviewBox="[^\s]+\s+[^\s]+\s+([\d.]+)\s+([\d.]+)"/u);
46
+ const fallbackRatio = viewBox ? Number(viewBox[1]) / Number(viewBox[2]) : 1;
47
+ if (!width || !height)
48
+ return { aspectRatio: fallbackRatio, heightEx: 1.8, depthEx: 0 };
49
+ const unitToEx = (length) => {
50
+ if (length.unit === "ex")
51
+ return length.value;
52
+ if (length.unit === "em")
53
+ return length.value * 2;
54
+ return length.value / 8;
55
+ };
56
+ const widthEx = unitToEx(width);
57
+ const heightEx = unitToEx(height);
58
+ const verticalAlign = svg.match(/vertical-align:\s*(-?[\d.]+)(ex|em|px)?/u);
59
+ const verticalAlignEx = verticalAlign
60
+ ? unitToEx({ value: Number(verticalAlign[1]), unit: verticalAlign[2] ?? "px" })
61
+ : 0;
62
+ return {
63
+ aspectRatio: Number.isFinite(widthEx / heightEx) ? widthEx / heightEx : fallbackRatio,
64
+ heightEx,
65
+ depthEx: Math.max(0, -verticalAlignEx)
66
+ };
67
+ }
68
+ function safeLatex(latex) {
69
+ if (latex.length > 8192)
70
+ throw new Error("formula exceeds the 8192 character limit");
71
+ if (/\\(?:require|href|url|html|class|cssId|style)\b/iu.test(latex)) {
72
+ throw new Error("formula contains a disabled command");
73
+ }
74
+ return latex;
75
+ }
76
+ export function normalizeLatexForRendering(latex) {
77
+ const marker = /(?<![A-Za-z0-9.])1\s*\/\s*\\sqrt\s*\{/gu;
78
+ let normalized = latex;
79
+ let searchFrom = 0;
80
+ while (searchFrom < normalized.length) {
81
+ marker.lastIndex = searchFrom;
82
+ const match = marker.exec(normalized);
83
+ if (!match || match.index === undefined)
84
+ break;
85
+ const openBrace = marker.lastIndex - 1;
86
+ let depth = 1;
87
+ let closeBrace = -1;
88
+ for (let index = openBrace + 1; index < normalized.length; index += 1) {
89
+ if (normalized[index] === "{" && normalized[index - 1] !== "\\")
90
+ depth += 1;
91
+ if (normalized[index] === "}" && normalized[index - 1] !== "\\")
92
+ depth -= 1;
93
+ if (depth === 0) {
94
+ closeBrace = index;
95
+ break;
96
+ }
97
+ }
98
+ if (closeBrace < 0)
99
+ break;
100
+ const sqrtOffset = match[0].indexOf("\\sqrt");
101
+ const sqrt = normalized.slice(match.index + sqrtOffset, closeBrace + 1);
102
+ const replacement = `\\frac{1}{${sqrt}}`;
103
+ normalized = normalized.slice(0, match.index) + replacement + normalized.slice(closeBrace + 1);
104
+ searchFrom = match.index + replacement.length;
105
+ }
106
+ return normalized;
107
+ }
108
+ function escapeAttribute(value) {
109
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;");
110
+ }
111
+ function resizeNestedSvg(svg, x, y, width, height) {
112
+ return svg.replace(/^<svg\b([^>]*)>/u, (_all, attributes) => {
113
+ const cleaned = attributes
114
+ .replace(/\s(?:width|height|x|y|style)="[^"]*"/gu, "")
115
+ .replace(/\sxmlns="[^"]*"/u, "");
116
+ return `<svg x="${x}" y="${y}" width="${width}" height="${height}"${cleaned}>`;
117
+ });
118
+ }
119
+ export async function renderMathJaxSvg(latex, display, _containerWidth, cache = sharedFormulaCache) {
120
+ // MathJax 4.1 can treat top-level relation and binary operators as inline
121
+ // line-break opportunities and return only the first fragment when called
122
+ // with display:false. Convert in display mode and force textstyle instead;
123
+ // this preserves complete inline expressions without enlarging their glyphs.
124
+ const { source, svgKey } = mathJaxCacheRequest(latex, display);
125
+ return cache.getOrCreateSvg(svgKey, async () => {
126
+ const mathJax = await getMathJax();
127
+ const node = await mathJax.tex2svgPromise(source, {
128
+ display: true,
129
+ em: 16,
130
+ ex: 8,
131
+ containerWidth: CANONICAL_CONTAINER_WIDTH
132
+ });
133
+ const adaptor = mathJax.startup.adaptor;
134
+ const svgNode = adaptor.tags(node, "svg")[0];
135
+ if (!svgNode)
136
+ throw new Error("MathJax produced no SVG");
137
+ return adaptor.serializeXML(svgNode);
138
+ });
139
+ }
140
+ export class MathRenderer {
141
+ persistentCache;
142
+ #cache = new Map();
143
+ constructor(persistentCache = sharedFormulaCache) {
144
+ this.persistentCache = persistentCache;
145
+ }
146
+ async render(region, columns, rows, capabilities, scale, foreground = capabilities.foreground, background = capabilities.background) {
147
+ const { svgKey } = mathJaxCacheRequest(region.latex, region.display);
148
+ const cacheKey = formulaCacheKey({
149
+ version: PNG_CACHE_VERSION,
150
+ svgKey,
151
+ display: region.display,
152
+ compact: Boolean(region.compact),
153
+ columns,
154
+ rows,
155
+ cell: { width: capabilities.cell.width, height: capabilities.cell.height },
156
+ scale,
157
+ foreground,
158
+ background
159
+ });
160
+ const cached = this.#cache.get(cacheKey);
161
+ if (cached)
162
+ return cached;
163
+ const formulaSvg = await renderMathJaxSvg(region.latex, region.display, Math.max(80, columns * 16), this.persistentCache);
164
+ const dimensions = readSvgDimensions(formulaSvg);
165
+ const geometry = calculateFormulaGeometry({
166
+ aspectRatio: dimensions.aspectRatio,
167
+ naturalHeightEx: dimensions.heightEx,
168
+ depthEx: dimensions.depthEx,
169
+ columns,
170
+ rows,
171
+ cell: capabilities.cell,
172
+ scale,
173
+ display: region.display,
174
+ leftAlign: region.compact
175
+ });
176
+ const nested = resizeNestedSvg(formulaSvg, geometry.offsetX, geometry.offsetY, geometry.formulaWidth, geometry.formulaHeight);
177
+ const wrapper = [
178
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${geometry.canvasWidth}" height="${geometry.canvasHeight}" viewBox="0 0 ${geometry.canvasWidth} ${geometry.canvasHeight}">`,
179
+ `<rect width="100%" height="100%" fill="${escapeAttribute(background)}"/>`,
180
+ `<g color="${escapeAttribute(foreground)}" fill="${escapeAttribute(foreground)}">${nested}</g>`,
181
+ "</svg>"
182
+ ].join("");
183
+ const png = await this.persistentCache.getOrCreatePng(cacheKey, async () => new Resvg(wrapper, {
184
+ background: background,
185
+ fitTo: { mode: "original" }
186
+ }).render().asPng());
187
+ const rendered = {
188
+ png,
189
+ cacheKey,
190
+ columns,
191
+ rows,
192
+ widthPx: geometry.canvasWidth,
193
+ heightPx: geometry.canvasHeight
194
+ };
195
+ this.#cache.set(cacheKey, rendered);
196
+ if (this.#cache.size > 256)
197
+ this.#cache.delete(this.#cache.keys().next().value);
198
+ return rendered;
199
+ }
200
+ clear() {
201
+ this.#cache.clear();
202
+ }
203
+ }
204
+ //# sourceMappingURL=math-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"math-renderer.js","sourceRoot":"","sources":["../src/math-renderer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export interface OutputSlice {
2
+ data: string;
3
+ checkpoint: boolean;
4
+ }
5
+ /**
6
+ * Splits a streaming PTY transcript only at line boundaries. A checkpoint lets
7
+ * the renderer place formulas before a large burst scrolls them out of view.
8
+ * The line count is retained across PTY chunks.
9
+ */
10
+ export declare class OutputCheckpointSplitter {
11
+ #private;
12
+ constructor(lineInterval: number);
13
+ setLineInterval(lineInterval: number): void;
14
+ push(data: string): OutputSlice[];
15
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Splits a streaming PTY transcript only at line boundaries. A checkpoint lets
3
+ * the renderer place formulas before a large burst scrolls them out of view.
4
+ * The line count is retained across PTY chunks.
5
+ */
6
+ export class OutputCheckpointSplitter {
7
+ #lineInterval;
8
+ #linesSinceCheckpoint = 0;
9
+ constructor(lineInterval) {
10
+ this.#lineInterval = 1;
11
+ this.setLineInterval(lineInterval);
12
+ }
13
+ setLineInterval(lineInterval) {
14
+ this.#lineInterval = Math.max(1, Math.floor(lineInterval));
15
+ }
16
+ push(data) {
17
+ if (!data)
18
+ return [];
19
+ const slices = [];
20
+ let sliceStart = 0;
21
+ for (let index = 0; index < data.length; index += 1) {
22
+ if (data[index] !== "\n")
23
+ continue;
24
+ this.#linesSinceCheckpoint += 1;
25
+ if (this.#linesSinceCheckpoint < this.#lineInterval)
26
+ continue;
27
+ slices.push({ data: data.slice(sliceStart, index + 1), checkpoint: true });
28
+ sliceStart = index + 1;
29
+ this.#linesSinceCheckpoint = 0;
30
+ }
31
+ if (sliceStart < data.length)
32
+ slices.push({ data: data.slice(sliceStart), checkpoint: false });
33
+ return slices;
34
+ }
35
+ }
36
+ //# sourceMappingURL=output-checkpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output-checkpoints.js","sourceRoot":"","sources":["../src/output-checkpoints.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,MAAM,OAAO,wBAAwB;IACnC,aAAa,CAAS;IACtB,qBAAqB,GAAG,CAAC,CAAC;IAE1B,YAAY,YAAoB;QAC9B,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;QACvB,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC;IACrC,CAAC;IAED,eAAe,CAAC,YAAoB;QAClC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;IAC7D,CAAC;IAED,IAAI,CAAC,IAAY;QACf,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,CAAC;QACrB,MAAM,MAAM,GAAkB,EAAE,CAAC;QACjC,IAAI,UAAU,GAAG,CAAC,CAAC;QACnB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;YACpD,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI;gBAAE,SAAS;YACnC,IAAI,CAAC,qBAAqB,IAAI,CAAC,CAAC;YAChC,IAAI,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAC,aAAa;gBAAE,SAAS;YAC9D,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3E,UAAU,GAAG,KAAK,GAAG,CAAC,CAAC;YACvB,IAAI,CAAC,qBAAqB,GAAG,CAAC,CAAC;QACjC,CAAC;QACD,IAAI,UAAU,GAAG,IAAI,CAAC,MAAM;YAAE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;QAC/F,OAAO,MAAM,CAAC;IAChB,CAAC;CACF"}
@@ -0,0 +1,30 @@
1
+ import type { TerminalCapabilities } from "./types.js";
2
+ interface ParsedResponses {
3
+ cell?: {
4
+ width: number;
5
+ height: number;
6
+ };
7
+ windowPixels?: {
8
+ width: number;
9
+ height: number;
10
+ };
11
+ foreground?: string;
12
+ background?: string;
13
+ residual: string;
14
+ }
15
+ declare function parseRgb(value: string): string | undefined;
16
+ export declare function parseTerminalResponses(input: string): ParsedResponses;
17
+ declare function supportsKittyGraphics(env: NodeJS.ProcessEnv): boolean;
18
+ export interface ProbeResult {
19
+ capabilities: TerminalCapabilities;
20
+ pendingInput: string;
21
+ }
22
+ export declare function probeTerminal(cellOverride?: {
23
+ width: number;
24
+ height: number;
25
+ }, timeoutMs?: number): Promise<ProbeResult>;
26
+ export declare const probeInternals: {
27
+ parseRgb: typeof parseRgb;
28
+ supportsKittyGraphics: typeof supportsKittyGraphics;
29
+ };
30
+ export {};
package/dist/probe.js ADDED
@@ -0,0 +1,97 @@
1
+ const DEFAULT_CELL = { width: 9, height: 18, source: "fallback" };
2
+ const DEFAULT_FOREGROUND = "#d8dee9";
3
+ const DEFAULT_BACKGROUND = "#1e1e2e";
4
+ function normalizeRgbComponent(value) {
5
+ const parsed = Number.parseInt(value, 16);
6
+ const maximum = (16 ** value.length) - 1;
7
+ return Math.round((parsed / maximum) * 255);
8
+ }
9
+ function parseRgb(value) {
10
+ const match = value.match(/^rgb:([0-9a-f]{1,4})\/([0-9a-f]{1,4})\/([0-9a-f]{1,4})$/iu);
11
+ if (!match)
12
+ return undefined;
13
+ return `#${match.slice(1, 4).map((part) => normalizeRgbComponent(part).toString(16).padStart(2, "0")).join("")}`;
14
+ }
15
+ export function parseTerminalResponses(input) {
16
+ let residual = input;
17
+ let cell;
18
+ let windowPixels;
19
+ let foreground;
20
+ let background;
21
+ residual = residual.replace(/\x1b\[6;(\d+);(\d+)t/gu, (_all, height, width) => {
22
+ cell = { width: Number(width), height: Number(height) };
23
+ return "";
24
+ });
25
+ residual = residual.replace(/\x1b\[4;(\d+);(\d+)t/gu, (_all, height, width) => {
26
+ windowPixels = { width: Number(width), height: Number(height) };
27
+ return "";
28
+ });
29
+ residual = residual.replace(/\x1b\](10|11);([^\x07\x1b]+)(?:\x07|\x1b\\)/gu, (_all, slot, value) => {
30
+ const color = parseRgb(value);
31
+ if (slot === "10")
32
+ foreground = color;
33
+ if (slot === "11")
34
+ background = color;
35
+ return "";
36
+ });
37
+ return { cell, windowPixels, foreground, background, residual };
38
+ }
39
+ function supportsKittyGraphics(env) {
40
+ const identity = `${env.TERM ?? ""} ${env.TERM_PROGRAM ?? ""}`.toLowerCase();
41
+ return /ghostty|kitty|wezterm/u.test(identity);
42
+ }
43
+ export async function probeTerminal(cellOverride, timeoutMs = 180) {
44
+ const tty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
45
+ const fallback = {
46
+ kittyGraphics: supportsKittyGraphics(process.env),
47
+ foreground: DEFAULT_FOREGROUND,
48
+ background: DEFAULT_BACKGROUND,
49
+ cell: cellOverride
50
+ ? { ...cellOverride, source: "override" }
51
+ : DEFAULT_CELL
52
+ };
53
+ if (!tty || typeof process.stdin.setRawMode !== "function") {
54
+ return { capabilities: fallback, pendingInput: "" };
55
+ }
56
+ const wasRaw = process.stdin.isRaw;
57
+ const chunks = [];
58
+ const onData = (chunk) => {
59
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
60
+ };
61
+ process.stdin.setRawMode(true);
62
+ process.stdin.resume();
63
+ process.stdin.on("data", onData);
64
+ // Cell pixels, text-area pixels, foreground, and background.
65
+ process.stdout.write("\x1b[16t\x1b[14t\x1b]10;?\x1b\\\x1b]11;?\x1b\\");
66
+ await new Promise((resolve) => setTimeout(resolve, timeoutMs));
67
+ process.stdin.off("data", onData);
68
+ if (!wasRaw)
69
+ process.stdin.setRawMode(false);
70
+ process.stdin.pause();
71
+ const parsed = parseTerminalResponses(Buffer.concat(chunks).toString("utf8"));
72
+ let cell = fallback.cell;
73
+ if (!cellOverride && parsed.cell && parsed.cell.width > 0 && parsed.cell.height > 0) {
74
+ cell = { ...parsed.cell, source: "cell-query" };
75
+ }
76
+ else if (!cellOverride && parsed.windowPixels) {
77
+ const cols = Math.max(1, process.stdout.columns ?? 80);
78
+ const rows = Math.max(1, process.stdout.rows ?? 24);
79
+ cell = {
80
+ width: parsed.windowPixels.width / cols,
81
+ height: parsed.windowPixels.height / rows,
82
+ source: "window-query"
83
+ };
84
+ }
85
+ return {
86
+ capabilities: {
87
+ ...fallback,
88
+ foreground: parsed.foreground ?? fallback.foreground,
89
+ background: parsed.background ?? fallback.background,
90
+ cell,
91
+ windowPixels: parsed.windowPixels
92
+ },
93
+ pendingInput: parsed.residual
94
+ };
95
+ }
96
+ export const probeInternals = { parseRgb, supportsKittyGraphics };
97
+ //# sourceMappingURL=probe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"probe.js","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { CliOptions, TerminalCapabilities } from "./types.js";
2
+ export declare function runProxy(options: CliOptions, initialCapabilities: TerminalCapabilities, pendingInput?: string): Promise<number>;