{"version":3,"file":"terminal-detect.d.ts","sourceRoot":"","sources":["../src/terminal-detect.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,MAAM,MAAM,eAAe,GACxB,SAAS,GACT,QAAQ,GACR,gBAAgB,GAChB,OAAO,GACP,SAAS,GACT,WAAW,GACX,KAAK,GACL,MAAM,GACN,QAAQ,GACR,eAAe,GACf,MAAM,GACN,kBAAkB,GAClB,QAAQ,GACR,SAAS,CAAC;AAEb,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AACrD,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,OAAO,CAAC;AAExD,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,eAAe,CAAC;IACzB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;IACzB,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,kBAAkB;IAClC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,wBAAwB,CAAC;IACzC,MAAM,EAAE,OAAO,GAAG,WAAW,GAAG,UAAU,CAAC;CAC3C;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACtC,+EAA6E;IAC7E,cAAc,EAAE,wBAAwB,CAAC;CACzC;AA+CD,wBAAgB,sBAAsB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,gBAAgB,CAwC7F;AAED,8DAA8D;AAC9D,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAMzE;AAsDD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC5C,QAAQ,EAAE,eAAe,GAAG,IAAI,EAChC,SAAS,SAAM,EACf,GAAG,GAAE,MAAM,CAAC,UAAwB,GAClC,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA8CpC;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,SAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA4DxF;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC5C,QAAQ,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,gHAAgH;IAChH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,OAAO,CAAC,WAAW,CAAC,CA0CvB","sourcesContent":["/**\n * Terminal identity and background detection.\n *\n * Covers cavekit-terminal-blend R1 (background luminance detection via OSC 11,\n * COLORFGBG, and CAVE_TERM_BG override) and R2 (terminal program identity).\n */\n\nimport type { ProcessTerminal } from \"./terminal.js\";\n\nexport type TerminalProgram =\n\t| \"ghostty\"\n\t| \"iterm2\"\n\t| \"apple-terminal\"\n\t| \"kitty\"\n\t| \"wezterm\"\n\t| \"alacritty\"\n\t| \"vte\"\n\t| \"tmux\"\n\t| \"screen\"\n\t| \"linux-console\"\n\t| \"cmux\"\n\t| \"windows-terminal\"\n\t| \"vscode\"\n\t| \"unknown\";\n\nexport type Multiplexer = \"tmux\" | \"screen\" | \"none\";\nexport type BackgroundClassification = \"dark\" | \"light\";\n\nexport interface TerminalIdentity {\n\tprogram: TerminalProgram;\n\t/** Outer host terminal when running under a multiplexer. */\n\thostProgram?: TerminalProgram;\n\tversion?: string;\n\tmultiplexer: Multiplexer;\n\tisSsh: boolean;\n\traw: Record<string, string | undefined>;\n}\n\nexport interface TerminalBackground {\n\tr: number;\n\tg: number;\n\tb: number;\n\t/** Relative luminance per WCAG 2.x (0-1). */\n\tluminance: number;\n\tclassification: BackgroundClassification;\n\tsource: \"osc11\" | \"colorfgbg\" | \"override\";\n}\n\nexport interface ProbeResult {\n\tidentity: TerminalIdentity;\n\tbackground: TerminalBackground | null;\n\t/** Final classification — always a concrete value (falls back to \"dark\"). */\n\tclassification: BackgroundClassification;\n}\n\nfunction _getEnv(name: string): string | undefined {\n\tconst v = process.env[name];\n\treturn v === undefined || v === \"\" ? undefined : v;\n}\n\nfunction classifyProgram(env: NodeJS.ProcessEnv): { program: TerminalProgram; version?: string } {\n\tconst termProgram = env.TERM_PROGRAM?.toLowerCase();\n\tconst termProgramVersion = env.TERM_PROGRAM_VERSION;\n\n\tif (env.GHOSTTY_RESOURCES_DIR || termProgram === \"ghostty\") {\n\t\treturn { program: \"ghostty\", version: termProgramVersion };\n\t}\n\tif (env.KITTY_WINDOW_ID || env.TERM?.toLowerCase().includes(\"kitty\")) {\n\t\treturn { program: \"kitty\", version: termProgramVersion };\n\t}\n\tif (env.WEZTERM_EXECUTABLE || termProgram === \"wezterm\") {\n\t\treturn { program: \"wezterm\", version: termProgramVersion };\n\t}\n\tif (env.ALACRITTY_LOG || termProgram === \"alacritty\") {\n\t\treturn { program: \"alacritty\", version: termProgramVersion };\n\t}\n\tif (termProgram === \"iterm.app\" || env.ITERM_SESSION_ID) {\n\t\treturn { program: \"iterm2\", version: termProgramVersion };\n\t}\n\tif (termProgram === \"apple_terminal\") {\n\t\treturn { program: \"apple-terminal\", version: termProgramVersion };\n\t}\n\tif (env.VTE_VERSION) {\n\t\treturn { program: \"vte\", version: env.VTE_VERSION };\n\t}\n\tif (env.WT_SESSION) {\n\t\treturn { program: \"windows-terminal\" };\n\t}\n\tif (env.VSCODE_INJECTION || termProgram === \"vscode\") {\n\t\treturn { program: \"vscode\", version: termProgramVersion };\n\t}\n\tif (env.CMUX_SESSION || env.CMUX || termProgram === \"cmux\") {\n\t\treturn { program: \"cmux\", version: termProgramVersion };\n\t}\n\tif (env.TERM === \"linux\") {\n\t\treturn { program: \"linux-console\" };\n\t}\n\treturn { program: \"unknown\" };\n}\n\nexport function detectTerminalIdentity(env: NodeJS.ProcessEnv = process.env): TerminalIdentity {\n\tconst tmux = Boolean(env.TMUX);\n\tconst screen = Boolean(env.STY);\n\tconst multiplexer: Multiplexer = tmux ? \"tmux\" : screen ? \"screen\" : \"none\";\n\n\tconst host = classifyProgram(env);\n\tlet program: TerminalProgram = host.program;\n\tlet hostProgram: TerminalProgram | undefined;\n\n\tif (tmux) {\n\t\thostProgram = host.program !== \"unknown\" ? host.program : undefined;\n\t\tprogram = \"tmux\";\n\t} else if (screen) {\n\t\thostProgram = host.program !== \"unknown\" ? host.program : undefined;\n\t\tprogram = \"screen\";\n\t}\n\n\treturn {\n\t\tprogram,\n\t\thostProgram,\n\t\tversion: host.version,\n\t\tmultiplexer,\n\t\tisSsh: Boolean(env.SSH_TTY || env.SSH_CONNECTION),\n\t\traw: {\n\t\t\tTERM_PROGRAM: env.TERM_PROGRAM,\n\t\t\tTERM_PROGRAM_VERSION: env.TERM_PROGRAM_VERSION,\n\t\t\tTERM: env.TERM,\n\t\t\tCOLORTERM: env.COLORTERM,\n\t\t\tCOLORFGBG: env.COLORFGBG,\n\t\t\tTMUX: env.TMUX,\n\t\t\tSTY: env.STY,\n\t\t\tSSH_TTY: env.SSH_TTY,\n\t\t\tGHOSTTY_RESOURCES_DIR: env.GHOSTTY_RESOURCES_DIR,\n\t\t\tKITTY_WINDOW_ID: env.KITTY_WINDOW_ID,\n\t\t\tWEZTERM_EXECUTABLE: env.WEZTERM_EXECUTABLE,\n\t\t\tITERM_SESSION_ID: env.ITERM_SESSION_ID,\n\t\t\tVTE_VERSION: env.VTE_VERSION,\n\t\t\tWT_SESSION: env.WT_SESSION,\n\t\t},\n\t};\n}\n\n/** WCAG 2.x relative luminance from sRGB 0-255 components. */\nexport function relativeLuminance(r: number, g: number, b: number): number {\n\tconst toLinear = (c: number): number => {\n\t\tconst s = c / 255;\n\t\treturn s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n\t};\n\treturn 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);\n}\n\nfunction classifyLuminance(lum: number): BackgroundClassification {\n\treturn lum >= 0.5 ? \"light\" : \"dark\";\n}\n\nfunction parseOsc11Response(data: string): { r: number; g: number; b: number } | null {\n\t// Match: ESC ]11;rgb:RRRR/GGGG/BBBB (BEL | ESC \\)\n\t// Components may be 1-4 hex digits (commonly 4).\n\tconst match = data.match(/\\x1b\\]11;rgb:([0-9a-fA-F]{1,4})\\/([0-9a-fA-F]{1,4})\\/([0-9a-fA-F]{1,4})(?:\\x07|\\x1b\\\\)/);\n\tif (!match) return null;\n\tconst normalize = (hex: string): number => {\n\t\tconst v = parseInt(hex, 16);\n\t\tconst max = (1 << (hex.length * 4)) - 1;\n\t\treturn Math.round((v / max) * 255);\n\t};\n\treturn { r: normalize(match[1]), g: normalize(match[2]), b: normalize(match[3]) };\n}\n\nfunction parseColorFgBg(raw: string | undefined): BackgroundClassification | null {\n\tif (!raw) return null;\n\tconst parts = raw.split(\";\");\n\tconst bg = parts[parts.length - 1];\n\tconst n = parseInt(bg, 10);\n\tif (!Number.isFinite(n)) return null;\n\t// 0-6 and 8 = dark variants; 7 and 9-15 = light variants (per xterm convention).\n\tif (n >= 0 && n <= 6) return \"dark\";\n\tif (n === 8) return \"dark\";\n\tif (n === 7) return \"light\";\n\tif (n >= 9 && n <= 15) return \"light\";\n\treturn null;\n}\n\nfunction parseOverride(raw: string | undefined): TerminalBackground | null {\n\tif (!raw) return null;\n\tconst v = raw.trim().toLowerCase();\n\tif (v === \"dark\") {\n\t\treturn { r: 0, g: 0, b: 0, luminance: 0, classification: \"dark\", source: \"override\" };\n\t}\n\tif (v === \"light\") {\n\t\treturn { r: 255, g: 255, b: 255, luminance: 1, classification: \"light\", source: \"override\" };\n\t}\n\t// Accept #rrggbb\n\tconst hex = v.startsWith(\"#\") ? v.slice(1) : v;\n\tif (/^[0-9a-f]{6}$/.test(hex)) {\n\t\tconst r = parseInt(hex.slice(0, 2), 16);\n\t\tconst g = parseInt(hex.slice(2, 4), 16);\n\t\tconst b = parseInt(hex.slice(4, 6), 16);\n\t\tconst lum = relativeLuminance(r, g, b);\n\t\treturn { r, g, b, luminance: lum, classification: classifyLuminance(lum), source: \"override\" };\n\t}\n\treturn null;\n}\n\n/**\n * Query the terminal's background color. Tries CAVE_TERM_BG override, then OSC 11,\n * then COLORFGBG. Returns null when no signal is available. Total wall time capped\n * by timeoutMs (default 200ms for the OSC 11 part; env lookups are instant).\n */\nexport async function queryTerminalBackground(\n\tterminal: ProcessTerminal | null,\n\ttimeoutMs = 200,\n\tenv: NodeJS.ProcessEnv = process.env,\n): Promise<TerminalBackground | null> {\n\t// 1. CAVE_TERM_BG override wins unconditionally.\n\tconst override = parseOverride(env.CAVE_TERM_BG);\n\tif (override) return override;\n\n\t// 2. OSC 11 query (when we have a terminal and it's a TTY)\n\tif (terminal && typeof (terminal as unknown as { queryOsc?: unknown }).queryOsc === \"function\") {\n\t\ttry {\n\t\t\tconst response = await (\n\t\t\t\tterminal as unknown as {\n\t\t\t\t\tqueryOsc: (seq: string, prefix: string, ms: number) => Promise<string | null>;\n\t\t\t\t}\n\t\t\t).queryOsc(\"\\x1b]11;?\\x07\", \"\\x1b]11;\", Math.min(timeoutMs, 150));\n\t\t\tif (response) {\n\t\t\t\tconst rgb = parseOsc11Response(response);\n\t\t\t\tif (rgb) {\n\t\t\t\t\tconst lum = relativeLuminance(rgb.r, rgb.g, rgb.b);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tr: rgb.r,\n\t\t\t\t\t\tg: rgb.g,\n\t\t\t\t\t\tb: rgb.b,\n\t\t\t\t\t\tluminance: lum,\n\t\t\t\t\t\tclassification: classifyLuminance(lum),\n\t\t\t\t\t\tsource: \"osc11\",\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Fall through to COLORFGBG.\n\t\t}\n\t}\n\n\t// 3. COLORFGBG parse\n\tconst fgbg = parseColorFgBg(env.COLORFGBG);\n\tif (fgbg) {\n\t\treturn {\n\t\t\tr: fgbg === \"dark\" ? 0 : 255,\n\t\t\tg: fgbg === \"dark\" ? 0 : 255,\n\t\t\tb: fgbg === \"dark\" ? 0 : 255,\n\t\t\tluminance: fgbg === \"dark\" ? 0 : 1,\n\t\t\tclassification: fgbg,\n\t\t\tsource: \"colorfgbg\",\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Standalone OSC 11 query that works before the TUI has started.\n *\n * Briefly installs its own stdin listener in raw mode, writes the OSC 11\n * query, waits up to `timeoutMs` for the response, then restores stdin.\n * Returns null on timeout / non-TTY / parse failure.\n *\n * Use this during startup when we need the background classification before\n * theme init but haven't yet constructed a ProcessTerminal.\n */\nexport function queryOsc11Standalone(timeoutMs = 150): Promise<TerminalBackground | null> {\n\treturn new Promise((resolve) => {\n\t\tif (!process.stdout.isTTY || !process.stdin.isTTY) {\n\t\t\tresolve(null);\n\t\t\treturn;\n\t\t}\n\n\t\tconst wasRaw = process.stdin.isRaw;\n\t\tlet settled = false;\n\t\tlet buffer = \"\";\n\t\tlet timer: NodeJS.Timeout | undefined;\n\n\t\tconst cleanup = () => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tif (timer) clearTimeout(timer);\n\t\t\ttry {\n\t\t\t\tprocess.stdin.removeListener(\"data\", onData);\n\t\t\t\tif (process.stdin.setRawMode && !wasRaw) {\n\t\t\t\t\tprocess.stdin.setRawMode(false);\n\t\t\t\t}\n\t\t\t\tprocess.stdin.pause();\n\t\t\t} catch {\n\t\t\t\t// swallow\n\t\t\t}\n\t\t};\n\n\t\tconst onData = (chunk: Buffer | string) => {\n\t\t\tbuffer += typeof chunk === \"string\" ? chunk : chunk.toString(\"utf8\");\n\t\t\tconst rgb = parseOsc11Response(buffer);\n\t\t\tif (rgb) {\n\t\t\t\tcleanup();\n\t\t\t\tconst lum = relativeLuminance(rgb.r, rgb.g, rgb.b);\n\t\t\t\tresolve({\n\t\t\t\t\tr: rgb.r,\n\t\t\t\t\tg: rgb.g,\n\t\t\t\t\tb: rgb.b,\n\t\t\t\t\tluminance: lum,\n\t\t\t\t\tclassification: classifyLuminance(lum),\n\t\t\t\t\tsource: \"osc11\",\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\n\t\ttry {\n\t\t\tif (process.stdin.setRawMode) process.stdin.setRawMode(true);\n\t\t\tprocess.stdin.resume();\n\t\t\tprocess.stdin.on(\"data\", onData);\n\t\t\tprocess.stdout.write(\"\\x1b]11;?\\x07\");\n\t\t} catch {\n\t\t\tcleanup();\n\t\t\tresolve(null);\n\t\t\treturn;\n\t\t}\n\n\t\ttimer = setTimeout(() => {\n\t\t\tcleanup();\n\t\t\tresolve(null);\n\t\t}, timeoutMs);\n\t});\n}\n\n/**\n * One-call probe: identity + background + final classification (with \"dark\" fallback).\n * The classification field is always concrete per R1.\n */\nexport async function probeTerminal(options: {\n\tterminal?: ProcessTerminal | null;\n\ttimeoutMs?: number;\n\tenv?: NodeJS.ProcessEnv;\n\t/** Whether to run the standalone OSC 11 query (needs raw stdin). Default: true when no terminal is supplied. */\n\tuseStandaloneOsc?: boolean;\n}): Promise<ProbeResult> {\n\tconst env = options.env ?? process.env;\n\tconst identity = detectTerminalIdentity(env);\n\tconst timeoutMs = options.timeoutMs ?? 200;\n\tconst useStandalone = options.useStandaloneOsc ?? !options.terminal;\n\n\t// Override first (R1 AC7)\n\tconst override = parseOverride(env.CAVE_TERM_BG);\n\tif (override) {\n\t\treturn { identity, background: override, classification: override.classification };\n\t}\n\n\t// Pre-TUI OSC 11 (R1 AC1+2)\n\tif (useStandalone) {\n\t\tconst osc = await queryOsc11Standalone(Math.min(timeoutMs, 150));\n\t\tif (osc) {\n\t\t\treturn { identity, background: osc, classification: osc.classification };\n\t\t}\n\t} else if (options.terminal) {\n\t\tconst bg = await queryTerminalBackground(options.terminal, timeoutMs, env);\n\t\tif (bg) return { identity, background: bg, classification: bg.classification };\n\t}\n\n\t// COLORFGBG (R1 AC3)\n\tconst fgbg = parseColorFgBg(env.COLORFGBG);\n\tif (fgbg) {\n\t\treturn {\n\t\t\tidentity,\n\t\t\tbackground: {\n\t\t\t\tr: fgbg === \"dark\" ? 0 : 255,\n\t\t\t\tg: fgbg === \"dark\" ? 0 : 255,\n\t\t\t\tb: fgbg === \"dark\" ? 0 : 255,\n\t\t\t\tluminance: fgbg === \"dark\" ? 0 : 1,\n\t\t\t\tclassification: fgbg,\n\t\t\t\tsource: \"colorfgbg\",\n\t\t\t},\n\t\t\tclassification: fgbg,\n\t\t};\n\t}\n\n\t// Fallback (R1 AC4)\n\treturn { identity, background: null, classification: \"dark\" };\n}\n"]}