/** * Named UI themes: REPL ANSI tokens + LS_COLORS strings for Bare OS. */ import { bareDefaultLsColorsString, bareParseDircolorsDatabase, bareSerializeLsColors } from 'bare-os-lscolors' /** @typedef {{ colors: Record, lsColors?: string }} BareOsThemePreset */ /** @type {Record} */ export const BARE_OS_THEME_PRESETS = { default: { colors: { prompt: '\x1b[32m', command: '\x1b[36m', path: '\x1b[33m', envset: '\x1b[35m', envunset: '\x1b[31m', ghost: '\x1b[90m', search: '\x1b[36m' } }, dracula: { colors: { prompt: '\x1b[38;2;80;250;123m', command: '\x1b[38;2;139;233;253m', path: '\x1b[38;2;241;250;140m', envset: '\x1b[38;2;189;147;249m', envunset: '\x1b[38;2;255;85;85m', ghost: '\x1b[38;2;98;114;164m', search: '\x1b[38;2;139;233;253m' }, lsColors: 'no=00:fi=00:di=01;36:ln=01;35:pi=40;33:so=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:tw=30;42:ow=34;43:st=37;44:ex=01;33' }, nord: { colors: { prompt: '\x1b[38;2;163;190;140m', command: '\x1b[38;2;136;192;208m', path: '\x1b[38;2;235;203;139m', envset: '\x1b[38;2;180;142;173m', envunset: '\x1b[38;2;191;97;106m', ghost: '\x1b[38;2;76;86;106m', search: '\x1b[38;2;136;192;208m' }, lsColors: 'no=00:fi=00:di=01;34:ln=01;36:pi=33:so=01;35:bd=33;01:cd=33;01:or=31;01:mi=00:su=37;41:sg=30;43:tw=30;42:ow=34;43:st=37;44:ex=01;32' }, solarized_dark: { colors: { prompt: '\x1b[38;2;133;153;0m', command: '\x1b[38;2;38;139;210m', path: '\x1b[38;2;181;137;0m', envset: '\x1b[38;2;108;113;196m', envunset: '\x1b[38;2;220;50;47m', ghost: '\x1b[38;2;88;110;117m', search: '\x1b[38;2;38;139;210m' }, lsColors: 'no=00:fi=00:di=01;34:ln=01;36:pi=33:so=01;35:bd=33;01:cd=33;01:or=31;01:mi=00:su=37;41:sg=30;43:tw=30;42:ow=34;43:st=37;44:ex=01;32' }, gruvbox_dark: { colors: { prompt: '\x1b[38;2;184;187;38m', command: '\x1b[38;2;131;165;152m', path: '\x1b[38;2;250;189;47m', envset: '\x1b[38;2;211;134;155m', envunset: '\x1b[38;2;251;73;52m', ghost: '\x1b[38;2;102;92;84m', search: '\x1b[38;2;131;165;152m' }, lsColors: 'no=00:fi=00:di=01;34:ln=01;36:pi=33:so=01;35:bd=33;01:cd=33;01:or=31;01:mi=00:su=37;41:sg=30;43:tw=30;42:ow=34;43:st=37;44:ex=01;32' }, catppuccin_mocha: { colors: { prompt: '\x1b[38;2;166;227;161m', command: '\x1b[38;2;137;180;250m', path: '\x1b[38;2;249;226;175m', envset: '\x1b[38;2;203;166;247m', envunset: '\x1b[38;2;243;139;168m', ghost: '\x1b[38;2;108;112;134m', search: '\x1b[38;2;137;180;250m' }, lsColors: 'no=00:fi=00:di=01;36:ln=01;35:pi=40;33:so=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:tw=30;42:ow=34;43:st=37;44:ex=01;33' }, tokyo_night: { colors: { prompt: '\x1b[38;2;158;206;106m', command: '\x1b[38;2;122;162;247m', path: '\x1b[38;2;224;163;82m', envset: '\x1b[38;2;187;154;247m', envunset: '\x1b[38;2;247;118;142m', ghost: '\x1b[38;2;86;95;137m', search: '\x1b[38;2;122;162;247m' }, lsColors: 'no=00:fi=00:di=01;34:ln=01;36:pi=33:so=01;35:bd=33;01:cd=33;01:or=31;01:mi=00:su=37;41:sg=30;43:tw=30;42:ow=34;43:st=37;44:ex=01;32' }, github_dark: { colors: { prompt: '\x1b[38;2;63;185;80m', command: '\x1b[38;2;121;192;255m', path: '\x1b[38;2;210;153;34m', envset: '\x1b[38;2;188;140;255m', envunset: '\x1b[38;2;248;81;73m', ghost: '\x1b[38;2;110;118;129m', search: '\x1b[38;2;121;192;255m' }, lsColors: 'no=00:fi=00:di=01;34:ln=01;36:pi=33:so=01;35:bd=33;01:cd=33;01:or=31;01:mi=00:su=37;41:sg=30;43:tw=30;42:ow=34;43:st=37;44:ex=01;32' } } BARE_OS_THEME_PRESETS.default.lsColors = bareDefaultLsColorsString() /** * Downgrade truecolor REPL sequences in BARE_OS_COLOR_* for limited terminals. * @param {Record} env */ export function bareOsDowngradeReplColorsForDepth(env) { if (!env || typeof env !== 'object') return const depth = String(env.BARE_OS_COLOR_DEPTH ?? 'truecolor') .toLowerCase() .trim() if ( depth === '' || depth === 'truecolor' || depth === '24bit' || depth === 'rgb' ) return const keys = Object.keys(env).filter((k) => k.startsWith('BARE_OS_COLOR_')) for (const k of keys) { const raw = env[k] if (raw == null) continue let v = String(raw) if (depth === '256' || depth === '8bit') { v = v.replace( /\x1b\[38;2;(\d+);(\d+);(\d+)m/g, (_, rs, gs, bs) => { const r = Math.min(255, Math.max(0, Number(rs))) const g = Math.min(255, Math.max(0, Number(gs))) const b = Math.min(255, Math.max(0, Number(bs))) const ri = Math.min(5, Math.round((r / 255) * 5)) const gi = Math.min(5, Math.round((g / 255) * 5)) const bi = Math.min(5, Math.round((b / 255) * 5)) const idx = 16 + 36 * ri + 6 * gi + bi return '\x1b[38;5;' + idx + 'm' } ) } else if (depth === '16' || depth === '8' || depth === 'ansi') { v = v.replace( /\x1b\[38;2;(\d+);(\d+);(\d+)m/g, (_, rs, gs, bs) => { const r = Number(rs) const g = Number(gs) const b = Number(bs) const lum = 0.299 * r + 0.587 * g + 0.114 * b const bold = lum > 195 ? '1;' : '' const max = Math.max(r, g, b) if (max < 48) return '\x1b[90m' if (r > g * 1.25 && r > b * 1.25) return '\x1b[' + bold + '31m' if (g > r * 1.25 && g > b * 1.25) return '\x1b[' + bold + '32m' if (b > r * 1.25 && b > g * 1.25) return '\x1b[' + bold + '34m' if (r > 200 && g > 200 && b < 120) return '\x1b[' + bold + '33m' if (g > 180 && b > 180) return '\x1b[' + bold + '36m' if (r > 180 && b > 180) return '\x1b[' + bold + '35m' return '\x1b[' + bold + '37m' } ) } env[k] = v } } /** * @returns {string[]} */ export function bareOsListThemeNames() { return Object.keys(BARE_OS_THEME_PRESETS).sort() } /** * @param {string} name * @returns {BareOsThemePreset | null} */ export function bareOsGetThemePreset(name) { const k = String(name || 'default') .toLowerCase() .trim() .replace(/\s+/g, '_') return BARE_OS_THEME_PRESETS[k] || null } /** * Apply active theme to ctx.vfs.env (REPL colors + LS_COLORS when not user-locked). * @param {Record} ctx */ export async function applyBareOsThemeFromEnv(ctx) { const vfs = ctx.vfs const env = vfs && vfs.env if (!env || typeof env !== 'object') return const rawName = env.BARE_OS_THEME const themeKey = String(rawName != null && rawName !== '' ? rawName : 'default') .toLowerCase() .trim() .replace(/\s+/g, '_') const preset = BARE_OS_THEME_PRESETS[themeKey] || BARE_OS_THEME_PRESETS.default for (const [short, seq] of Object.entries(preset.colors || {})) { env['BARE_OS_COLOR_' + short.toUpperCase()] = seq } const locked = env.BARE_OS_LS_COLORS_LOCKED === '1' const hadLsColors = env.LS_COLORS != null && String(env.LS_COLORS).trim() !== '' const dcPath = env.BARE_OS_DIRCOLORS if (dcPath && String(dcPath).trim() && !locked) { try { const buf = await vfs.readFile(dcPath) if (buf && ctx.b4a) { const text = ctx.b4a.toString(buf) const term = env.TERM || '' const db = bareParseDircolorsDatabase(text, term) env.LS_COLORS = bareSerializeLsColors(db) } } catch { /* keep existing LS_COLORS */ } } else if (!hadLsColors && !locked) { env.LS_COLORS = preset.lsColors != null ? preset.lsColors : bareDefaultLsColorsString() } bareOsDowngradeReplColorsForDepth(env) }