Release v1.01 Enhanced: Vi Control, TUI Gen5, Core Stability
This commit is contained in:
157
bin/terminal-theme-detect.mjs
Normal file
157
bin/terminal-theme-detect.mjs
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Terminal Theme Detection - OSC 11 Query
|
||||
*
|
||||
* Based on sst/opencode terminal detection
|
||||
* Credit: https://github.com/sst/opencode
|
||||
* Credit: https://github.com/MiniMax-AI/Mini-Agent (width utils)
|
||||
*
|
||||
* Probes terminal for dark/light mode using OSC 11
|
||||
*/
|
||||
|
||||
import { getCapabilities } from './terminal-profile.mjs';
|
||||
|
||||
// Theme modes
|
||||
export const THEME_MODES = {
|
||||
DARK: 'dark',
|
||||
LIGHT: 'light',
|
||||
AUTO: 'auto'
|
||||
};
|
||||
|
||||
// Cached result
|
||||
let cachedThemeMode = null;
|
||||
|
||||
/**
|
||||
* Parse RGB from OSC 11 response
|
||||
* Response format: ESC ] 11 ; rgb:RRRR/GGGG/BBBB ESC \
|
||||
*/
|
||||
function parseOSC11Response(response) {
|
||||
// Match rgb:RRRR/GGGG/BBBB pattern
|
||||
const match = response.match(/rgb:([0-9a-f]{4})\/([0-9a-f]{4})\/([0-9a-f]{4})/i);
|
||||
if (!match) return null;
|
||||
|
||||
// Convert to 0-255 range
|
||||
const r = parseInt(match[1], 16) >> 8;
|
||||
const g = parseInt(match[2], 16) >> 8;
|
||||
const b = parseInt(match[3], 16) >> 8;
|
||||
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate perceived brightness (0-255)
|
||||
* Using sRGB luminance formula
|
||||
*/
|
||||
function calculateBrightness(rgb) {
|
||||
if (!rgb) return 128; // default to middle
|
||||
return 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query terminal background color via OSC 11
|
||||
* Returns promise that resolves to 'dark' or 'light'
|
||||
*
|
||||
* Note: This is async and may timeout on some terminals
|
||||
*/
|
||||
export async function queryTerminalBackground(timeoutMs = 500) {
|
||||
// Return cached result if available
|
||||
if (cachedThemeMode) return cachedThemeMode;
|
||||
|
||||
// Skip if not a TTY
|
||||
if (!process.stdout.isTTY) {
|
||||
cachedThemeMode = THEME_MODES.DARK;
|
||||
return cachedThemeMode;
|
||||
}
|
||||
|
||||
// Skip on Windows PowerShell (doesn't support OSC 11)
|
||||
const caps = getCapabilities();
|
||||
if (caps.profile === 'SAFE_ASCII') {
|
||||
cachedThemeMode = THEME_MODES.DARK;
|
||||
return cachedThemeMode;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let response = '';
|
||||
let timeoutId;
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeoutId);
|
||||
process.stdin.setRawMode?.(false);
|
||||
process.stdin.removeListener('data', onData);
|
||||
};
|
||||
|
||||
const onData = (data) => {
|
||||
response += data.toString();
|
||||
|
||||
// Check for OSC response end (BEL or ST)
|
||||
if (response.includes('\x07') || response.includes('\x1b\\')) {
|
||||
cleanup();
|
||||
|
||||
const rgb = parseOSC11Response(response);
|
||||
const brightness = calculateBrightness(rgb);
|
||||
|
||||
// Threshold: < 128 = dark, >= 128 = light
|
||||
cachedThemeMode = brightness < 128 ? THEME_MODES.DARK : THEME_MODES.LIGHT;
|
||||
resolve(cachedThemeMode);
|
||||
}
|
||||
};
|
||||
|
||||
// Set timeout
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanup();
|
||||
// Default to dark on timeout
|
||||
cachedThemeMode = THEME_MODES.DARK;
|
||||
resolve(cachedThemeMode);
|
||||
}, timeoutMs);
|
||||
|
||||
try {
|
||||
// Enable raw mode to capture response
|
||||
process.stdin.setRawMode?.(true);
|
||||
process.stdin.resume();
|
||||
process.stdin.on('data', onData);
|
||||
|
||||
// Send OSC 11 query: ESC ] 11 ; ? ESC \
|
||||
process.stdout.write('\x1b]11;?\x1b\\');
|
||||
} catch (e) {
|
||||
cleanup();
|
||||
cachedThemeMode = THEME_MODES.DARK;
|
||||
resolve(cachedThemeMode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current theme mode (sync, returns cached or default)
|
||||
*/
|
||||
export function getThemeMode() {
|
||||
return cachedThemeMode || THEME_MODES.DARK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set theme mode manually
|
||||
*/
|
||||
export function setThemeMode(mode) {
|
||||
if (Object.values(THEME_MODES).includes(mode)) {
|
||||
cachedThemeMode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize theme detection
|
||||
* Call this at app startup
|
||||
*/
|
||||
export async function initThemeDetection() {
|
||||
try {
|
||||
await queryTerminalBackground();
|
||||
} catch (e) {
|
||||
cachedThemeMode = THEME_MODES.DARK;
|
||||
}
|
||||
return cachedThemeMode;
|
||||
}
|
||||
|
||||
export default {
|
||||
THEME_MODES,
|
||||
queryTerminalBackground,
|
||||
getThemeMode,
|
||||
setThemeMode,
|
||||
initThemeDetection
|
||||
};
|
||||
Reference in New Issue
Block a user