// Global themes. Each one is a set of CSS variables applied to , a matching xterm // palette, and a data-theme attribute the stylesheet can hang extra flourishes on. import type { ITheme } from '@xterm/xterm' export interface Theme { id: string name: string blurb: string vars: Record xterm: ITheme } const base = { '--ok': '#3fb950', '--bad': '#ff7b72', '--s1': '#3987e5', '--s2': '#c98500', '--track': 'rgba(57, 135, 229, .18)', '--btn-fg': '#111', '--term-bg': '#0d1117', '--font': 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif', '--mono': 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace', } // A palette in one line: surfaces, text, the two accents, status colours, and the sixteen // terminal colours (black red green yellow blue magenta cyan white, then the bright eight). function mk(id: string, name: string, blurb: string, ui: string[], term: string[], extra: Record = {}): Theme { const [bg, bg2, bg3, border, fg, muted, accent, accent2, ok, bad, s1, s2, termBg] = ui const k = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'brightBlack', 'brightRed', 'brightGreen', 'brightYellow', 'brightBlue', 'brightMagenta', 'brightCyan', 'brightWhite'] const light = parseInt(bg.slice(1, 3), 16) > 128 return { id, name, blurb, vars: { ...base, '--bg': bg, '--bg2': bg2, '--bg3': bg3, '--border': border, '--fg': fg, '--muted': muted, '--accent': accent, '--accent2': accent2, '--ok': ok, '--bad': bad, '--s1': s1, '--s2': s2, '--track': light ? 'rgba(0, 0, 0, .10)' : 'rgba(255, 255, 255, .09)', '--term-bg': termBg, '--btn-fg': light ? '#fff' : '#111', ...extra }, xterm: { background: termBg, foreground: fg, cursor: accent, selectionBackground: light ? 'rgba(0,0,0,.18)' : 'rgba(255,255,255,.22)', ...Object.fromEntries(k.map((name, i) => [name, term[i]])) }, } } export const THEMES: Theme[] = [ { id: 'dark', name: 'Dark', blurb: 'The default. Graphite surfaces, amber brand, blue accents.', vars: { ...base, '--bg': '#111418', '--bg2': '#171b21', '--bg3': '#1e242c', '--border': '#2a313b', '--fg': '#d0d7de', '--muted': '#7d8590', '--accent': '#e3a33b', '--accent2': '#58a6ff' }, xterm: { background: '#0d1117', foreground: '#d0d7de', cursor: '#d0d7de', selectionBackground: '#264f78', black: '#0d1117', red: '#ff7b72', green: '#3fb950', yellow: '#d29922', blue: '#58a6ff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#b1bac4', brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364', brightYellow: '#e3b341', brightBlue: '#79c0ff', brightMagenta: '#d2a8ff', brightCyan: '#56d4dd', brightWhite: '#f0f6fc', }, }, { id: 'light', name: 'Light', blurb: 'Paper white with the same layout. For daylight and bright rooms.', vars: { ...base, '--bg': '#f6f8fa', '--bg2': '#ffffff', '--bg3': '#eaeef2', '--border': '#d0d7de', '--fg': '#1f2328', '--muted': '#59636e', '--accent': '#b35c00', '--accent2': '#0969da', '--ok': '#1a7f37', '--bad': '#cf222e', '--s1': '#2a78d6', '--s2': '#9a6700', '--track': 'rgba(42, 120, 214, .16)', '--btn-fg': '#ffffff', '--term-bg': '#ffffff' }, xterm: { background: '#ffffff', foreground: '#1f2328', cursor: '#1f2328', selectionBackground: '#b6d6ff', black: '#24292f', red: '#cf222e', green: '#116329', yellow: '#4d2d00', blue: '#0969da', magenta: '#8250df', cyan: '#1b7c83', white: '#6e7781', brightBlack: '#57606a', brightRed: '#a40e26', brightGreen: '#1a7f37', brightYellow: '#633c01', brightBlue: '#218bff', brightMagenta: '#a475f9', brightCyan: '#3192aa', brightWhite: '#8c959f', }, }, { id: 'cyberpunk', name: 'Cyberpunk', blurb: 'Deep violet, magenta brand, cyan accents, a little neon glow.', vars: { ...base, '--bg': '#0b0014', '--bg2': '#140026', '--bg3': '#1f0a3a', '--border': '#3b1f6b', '--fg': '#e8e0ff', '--muted': '#9b8bd0', '--accent': '#ff2bd6', '--accent2': '#00e5ff', '--ok': '#39ff14', '--bad': '#ff3860', '--s1': '#00e5ff', '--s2': '#ffe600', '--track': 'rgba(0, 229, 255, .16)', '--btn-fg': '#0b0014', '--term-bg': '#07000f' }, xterm: { background: '#07000f', foreground: '#e8e0ff', cursor: '#ff2bd6', selectionBackground: '#3b1f6b', black: '#0b0014', red: '#ff3860', green: '#39ff14', yellow: '#ffe600', blue: '#00e5ff', magenta: '#ff2bd6', cyan: '#5cf6ff', white: '#c8bff0', brightBlack: '#6a55a3', brightRed: '#ff6b8a', brightGreen: '#7dff5c', brightYellow: '#fff27a', brightBlue: '#66eeff', brightMagenta: '#ff7de8', brightCyan: '#a5fbff', brightWhite: '#ffffff', }, }, { id: 'retro', name: 'Retro', blurb: 'Amber phosphor terminal. Monospace everywhere, faint scanlines.', vars: { ...base, '--bg': '#0e0b05', '--bg2': '#171106', '--bg3': '#221909', '--border': '#3a2c10', '--fg': '#ffb000', '--muted': '#a8781c', '--accent': '#ffcc33', '--accent2': '#ffb000', '--ok': '#ffd166', '--bad': '#ff6b35', '--s1': '#ffb000', '--s2': '#ff8c00', '--track': 'rgba(255, 176, 0, .16)', '--btn-fg': '#0e0b05', '--term-bg': '#0a0803', '--font': 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace' }, xterm: { background: '#0a0803', foreground: '#ffb000', cursor: '#ffcc33', selectionBackground: '#3a2c10', black: '#0a0803', red: '#ff6b35', green: '#ffd166', yellow: '#ffcc33', blue: '#e0a020', magenta: '#ff9f43', cyan: '#f5c76b', white: '#ffb000', brightBlack: '#6b4e14', brightRed: '#ff8c5a', brightGreen: '#ffe08a', brightYellow: '#ffe066', brightBlue: '#f0b040', brightMagenta: '#ffb870', brightCyan: '#ffdd99', brightWhite: '#fff3cc', }, }, mk('hud', 'HUD', 'Hairlines of cyan light on near-black. Thin type, no fills. At your service.', ['#03070c', '#050c14', '#0a1a28', '#123047', '#bfe9ff', '#5b8ba6', '#35d6ff', '#7df9ff', '#4dffb5', '#ff5470', '#35d6ff', '#ffb547', '#02050a'], ['#02050a', '#ff5470', '#4dffb5', '#ffb547', '#35a8ff', '#b28dff', '#35d6ff', '#bfe9ff', '#3a5a70', '#ff8398', '#8dffd0', '#ffd08a', '#7cc6ff', '#cdb5ff', '#7df9ff', '#ffffff']), mk('nord', 'Nord', 'Arctic blue-greys. Calm, low contrast, easy at night.', ['#2e3440', '#3b4252', '#434c5e', '#4c566a', '#d8dee9', '#8f9bb3', '#88c0d0', '#81a1c1', '#a3be8c', '#d9828a', '#81a1c1', '#ebcb8b', '#2b303b'], ['#3b4252', '#bf616a', '#a3be8c', '#ebcb8b', '#81a1c1', '#b48ead', '#88c0d0', '#e5e9f0', '#4c566a', '#bf616a', '#a3be8c', '#ebcb8b', '#81a1c1', '#b48ead', '#8fbcbb', '#eceff4']), mk('gruvbox', 'Gruvbox', 'Warm retro earth tones. Cream on charcoal.', ['#282828', '#32302f', '#3c3836', '#504945', '#ebdbb2', '#a89984', '#fabd2f', '#83a598', '#b8bb26', '#fb4934', '#83a598', '#fe8019', '#1d2021'], ['#282828', '#cc241d', '#98971a', '#d79921', '#458588', '#b16286', '#689d6a', '#a89984', '#928374', '#fb4934', '#b8bb26', '#fabd2f', '#83a598', '#d3869b', '#8ec07c', '#ebdbb2']), mk('catppuccin', 'Catppuccin', 'Mocha: soft pastels on deep mauve-grey.', ['#1e1e2e', '#181825', '#313244', '#45475a', '#cdd6f4', '#9399b2', '#cba6f7', '#89b4fa', '#a6e3a1', '#f38ba8', '#89b4fa', '#fab387', '#11111b'], ['#45475a', '#f38ba8', '#a6e3a1', '#f9e2af', '#89b4fa', '#f5c2e7', '#94e2d5', '#bac2de', '#585b70', '#f38ba8', '#a6e3a1', '#f9e2af', '#89b4fa', '#f5c2e7', '#94e2d5', '#a6adc8']), mk('latte', 'Catppuccin Latte', 'The same pastels, by daylight.', ['#eff1f5', '#e6e9ef', '#dce0e8', '#bcc0cc', '#4c4f69', '#6c6f85', '#8839ef', '#1e66f5', '#35891f', '#d20f39', '#1e66f5', '#fe640b', '#e6e9ef'], ['#5c5f77', '#d20f39', '#40a02b', '#df8e1d', '#1e66f5', '#ea76cb', '#179299', '#acb0be', '#6c6f85', '#d20f39', '#40a02b', '#df8e1d', '#1e66f5', '#ea76cb', '#179299', '#bcc0cc']), mk('dracula', 'Dracula', 'Purple, pink and green on midnight.', ['#282a36', '#21222c', '#343746', '#44475a', '#f8f8f2', '#8b93b8', '#bd93f9', '#8be9fd', '#50fa7b', '#ff5555', '#8be9fd', '#ffb86c', '#1e1f29'], ['#21222c', '#ff5555', '#50fa7b', '#f1fa8c', '#bd93f9', '#ff79c6', '#8be9fd', '#f8f8f2', '#6272a4', '#ff6e6e', '#69ff94', '#ffffa5', '#d6acff', '#ff92df', '#a4ffff', '#ffffff']), mk('tokyo', 'Tokyo Night', 'Neon signs over a dark blue city.', ['#1a1b26', '#16161e', '#24283b', '#343b58', '#c0caf5', '#737aa2', '#7aa2f7', '#bb9af7', '#9ece6a', '#f7768e', '#7aa2f7', '#ff9e64', '#13141c'], ['#15161e', '#f7768e', '#9ece6a', '#e0af68', '#7aa2f7', '#bb9af7', '#7dcfff', '#a9b1d6', '#414868', '#f7768e', '#9ece6a', '#e0af68', '#7aa2f7', '#bb9af7', '#7dcfff', '#c0caf5']), mk('solarized', 'Solarized Dark', 'The precise, low-glare classic.', ['#002b36', '#073642', '#0a4352', '#1a5666', '#a9b5b5', '#7b9199', '#b58900', '#268bd2', '#859900', '#ea4b48', '#268bd2', '#cb4b16', '#00212b'], ['#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', '#586e75', '#cb4b16', '#93a1a1', '#b58900', '#839496', '#6c71c4', '#2aa198', '#fdf6e3']), mk('solarized-light', 'Solarized Light', 'The same sixteen colours on warm paper.', ['#fdf6e3', '#eee8d5', '#e4ddc8', '#d3cbb4', '#40555c', '#64777a', '#946f00', '#268bd2', '#6a7a00', '#dc322f', '#268bd2', '#cb4b16', '#eee8d5'], ['#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', '#002b36', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', '#fdf6e3']), ] export const DEFAULT_THEME = 'dark' const KEY = 'hamlab.theme' export function themeById(id: string | null | undefined): Theme { return THEMES.find((t) => t.id === id) ?? THEMES[0] } export function currentThemeId(): string { return localStorage.getItem(KEY) || DEFAULT_THEME } // applyTheme sets the variables on , remembers the choice, and tells open // terminals to repaint. Cheap enough to call on every load before first render. export function applyTheme(id: string) { const t = themeById(id) // unknown ids fall back to the default const root = document.documentElement for (const [k, v] of Object.entries(t.vars)) root.style.setProperty(k, v) root.dataset.theme = t.id localStorage.setItem(KEY, t.id) document.querySelector('meta[name=theme-color]')?.setAttribute('content', t.vars['--bg2']) window.dispatchEvent(new CustomEvent('hamlab:theme', { detail: t })) }