feat(web): add terminal theme preference

This commit is contained in:
Peter Steinberger 2025-07-12 11:52:17 +02:00
parent 93ba0064bd
commit d019559f2b
6 changed files with 222 additions and 58 deletions

View file

@ -32,6 +32,7 @@ import {
COMMON_TERMINAL_WIDTHS,
TerminalPreferencesManager,
} from '../utils/terminal-preferences.js';
import type { TerminalThemeId } from '../utils/terminal-themes.js';
import { ConnectionManager } from './session-view/connection-manager.js';
import {
type DirectKeyboardCallbacks,
@ -87,6 +88,7 @@ export class SessionView extends LitElement {
@state() private showImagePicker = false;
@state() private isDragOver = false;
@state() private terminalFontSize = 14;
@state() private terminalTheme: TerminalThemeId = 'auto';
@state() private terminalContainerHeight = '100%';
@state() private isLandscape = false;
@ -369,8 +371,10 @@ export class SessionView extends LitElement {
// Load terminal preferences
this.terminalMaxCols = this.preferencesManager.getMaxCols();
this.terminalFontSize = this.preferencesManager.getFontSize();
this.terminalTheme = this.preferencesManager.getTheme();
this.terminalLifecycleManager.setTerminalFontSize(this.terminalFontSize);
this.terminalLifecycleManager.setTerminalMaxCols(this.terminalMaxCols);
this.terminalLifecycleManager.setTerminalTheme(this.terminalTheme);
// Initialize lifecycle event manager
this.lifecycleEventManager = new LifecycleEventManager();
@ -869,6 +873,18 @@ export class SessionView extends LitElement {
}
}
private handleThemeChange(newTheme: TerminalThemeId) {
this.terminalTheme = newTheme;
this.preferencesManager.setTheme(newTheme);
this.terminalLifecycleManager.setTerminalTheme(newTheme);
const terminal = this.querySelector('vibe-terminal') as Terminal;
if (terminal) {
terminal.theme = newTheme;
terminal.requestUpdate();
}
}
private handleOpenFileBrowser() {
this.showFileBrowser = true;
}
@ -1285,6 +1301,7 @@ export class SessionView extends LitElement {
.fontSize=${this.terminalFontSize}
.fitHorizontally=${false}
.maxCols=${this.terminalMaxCols}
.theme=${this.terminalTheme}
.initialCols=${this.session?.initialCols || 0}
.initialRows=${this.session?.initialRows || 0}
.disableClick=${this.isMobile && this.useDirectKeyboard}
@ -1478,10 +1495,12 @@ export class SessionView extends LitElement {
.visible=${this.showWidthSelector}
.terminalMaxCols=${this.terminalMaxCols}
.terminalFontSize=${this.terminalFontSize}
.terminalTheme=${this.terminalTheme}
.customWidth=${this.customWidth}
.isMobile=${this.isMobile}
.onWidthSelect=${(width: number) => this.handleWidthSelect(width)}
.onFontSizeChange=${(size: number) => this.handleFontSizeChange(size)}
.onThemeChange=${(theme: TerminalThemeId) => this.handleThemeChange(theme)}
.onClose=${() => {
this.showWidthSelector = false;
this.customWidth = '';

View file

@ -7,6 +7,7 @@
import { authClient } from '../../services/auth-client.js';
import { createLogger } from '../../utils/logger.js';
import type { TerminalThemeId } from '../../utils/terminal-themes.js';
import type { Session } from '../session-list.js';
import type { Terminal } from '../terminal.js';
import type { ConnectionManager } from './connection-manager.js';
@ -32,6 +33,7 @@ export class TerminalLifecycleManager {
private connected = false;
private terminalFontSize = 14;
private terminalMaxCols = 0;
private terminalTheme: TerminalThemeId = 'auto';
private resizeTimeout: number | null = null;
private lastResizeWidth = 0;
private lastResizeHeight = 0;
@ -67,6 +69,10 @@ export class TerminalLifecycleManager {
this.terminalMaxCols = maxCols;
}
setTerminalTheme(theme: TerminalThemeId) {
this.terminalTheme = theme;
}
getTerminal(): Terminal | null {
return this.terminal;
}
@ -114,6 +120,7 @@ export class TerminalLifecycleManager {
this.terminal.fontSize = this.terminalFontSize; // Apply saved font size preference
this.terminal.fitHorizontally = false; // Allow natural terminal sizing
this.terminal.maxCols = this.terminalMaxCols; // Apply saved max width preference
this.terminal.theme = this.terminalTheme;
if (this.eventHandlers) {
// Listen for session exit events

View file

@ -8,6 +8,7 @@ import { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { Z_INDEX } from '../../utils/constants.js';
import { COMMON_TERMINAL_WIDTHS } from '../../utils/terminal-preferences.js';
import { TERMINAL_THEMES, type TerminalThemeId } from '../../utils/terminal-themes.js';
@customElement('width-selector')
export class WidthSelector extends LitElement {
@ -19,9 +20,11 @@ export class WidthSelector extends LitElement {
@property({ type: Boolean }) visible = false;
@property({ type: Number }) terminalMaxCols = 0;
@property({ type: Number }) terminalFontSize = 14;
@property({ type: String }) terminalTheme: TerminalThemeId = 'auto';
@property({ type: String }) customWidth = '';
@property({ type: Function }) onWidthSelect?: (width: number) => void;
@property({ type: Function }) onFontSizeChange?: (size: number) => void;
@property({ type: Function }) onThemeChange?: (theme: TerminalThemeId) => void;
@property({ type: Function }) onClose?: () => void;
@property({ type: Boolean }) isMobile = false;
@ -128,8 +131,8 @@ export class WidthSelector extends LitElement {
</div>
</div>
<div class="border-t border-border mt-3 pt-3">
<div class="text-sm font-semibold text-text-bright mb-3">Font Size</div>
<div class="flex items-center gap-3">
<div class="text-sm font-semibold text-text-bright mb-3">Font Size</div>
<div class="flex items-center gap-3">
<button
class="w-10 h-10 rounded-md border transition-all duration-200 flex items-center justify-center
${
@ -175,6 +178,19 @@ export class WidthSelector extends LitElement {
</button>
</div>
</div>
<div class="border-t border-border mt-3 pt-3">
<div class="text-sm font-semibold text-text-bright mb-3">Theme</div>
<select
class="w-full bg-bg-secondary border border-border rounded-md p-2 text-sm font-mono text-text focus:border-primary focus:shadow-glow-sm"
.value=${this.terminalTheme}
@change=${(e: Event) => this.onThemeChange?.((e.target as HTMLSelectElement).value as TerminalThemeId)}
>
${TERMINAL_THEMES.map(
(t) =>
html`<option value=${t.id} ?selected=${this.terminalTheme === t.id}>${t.name}</option>`
)}
</select>
</div>
</div>
</div>
`;

View file

@ -16,6 +16,8 @@ import { customElement, property, state } from 'lit/decorators.js';
import { processKeyboardShortcuts } from '../utils/keyboard-shortcut-highlighter.js';
import { createLogger } from '../utils/logger.js';
import { detectMobile } from '../utils/mobile-utils.js';
import { TerminalPreferencesManager } from '../utils/terminal-preferences.js';
import { TERMINAL_THEMES, type TerminalThemeId } from '../utils/terminal-themes.js';
import { UrlHighlighter } from '../utils/url-highlighter';
const logger = createLogger('terminal');
@ -34,6 +36,7 @@ export class Terminal extends LitElement {
@property({ type: Number }) fontSize = 14;
@property({ type: Boolean }) fitHorizontally = false;
@property({ type: Number }) maxCols = 0; // 0 means no limit
@property({ type: String }) theme: TerminalThemeId = 'auto';
@property({ type: Boolean }) disableClick = false; // Disable click handling (for mobile direct keyboard)
@property({ type: Boolean }) hideScrollButton = false; // Hide scroll-to-bottom button
@property({ type: Number }) initialCols = 0; // Initial terminal width from session creation
@ -138,6 +141,8 @@ export class Terminal extends LitElement {
private themeObserver?: MutationObserver;
connectedCallback() {
const prefs = TerminalPreferencesManager.getInstance();
this.theme = prefs.getTheme();
super.connectedCallback();
// Check for debug mode
@ -217,6 +222,12 @@ export class Terminal extends LitElement {
this.requestResize('property-change');
}
}
if (changedProperties.has('theme')) {
if (this.terminal) {
this.terminal.options.theme = this.getTerminalTheme();
}
}
}
disconnectedCallback() {
@ -353,64 +364,14 @@ export class Terminal extends LitElement {
}
private getTerminalTheme() {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
let themeId = this.theme;
if (isDark) {
// Dark theme (original colors)
return {
background: '#1e1e1e',
foreground: '#d4d4d4',
cursor: 'rgb(var(--color-primary))',
cursorAccent: '#1e1e1e',
// Standard 16 colors (0-15) - using proper xterm colors
black: '#000000',
red: '#cd0000',
green: '#00cd00',
yellow: '#cdcd00',
blue: '#0000ee',
magenta: '#cd00cd',
cyan: '#00cdcd',
white: '#e5e5e5',
brightBlack: '#7f7f7f',
brightRed: '#ff0000',
brightGreen: '#00ff00',
brightYellow: '#ffff00',
brightBlue: '#5c5cff',
brightMagenta: '#ff00ff',
brightCyan: '#00ffff',
brightWhite: '#ffffff',
};
} else {
// Light theme - optimized for readability with softer contrast
return {
background: '#f8f9fa', // Slightly off-white for less eye strain
foreground: '#1f2328', // Dark gray for better readability than pure black
cursor: 'rgb(var(--color-primary))',
cursorAccent: '#f8f9fa',
// Standard 16 colors optimized for light backgrounds
// Based on GitHub light theme for proven readability
black: '#24292f',
red: '#cf222e',
green: '#1a7f37',
yellow: '#9a6700',
blue: '#0969da',
magenta: '#8250df',
cyan: '#1b7c83',
white: '#6e7781',
brightBlack: '#57606a',
brightRed: '#da3633',
brightGreen: '#2da44e',
brightYellow: '#bf8700',
brightBlue: '#218bff',
brightMagenta: '#a475f9',
brightCyan: '#3192aa',
brightWhite: '#8c959f',
// Selection colors for better visibility
selectionBackground: '#0969da',
selectionForeground: '#ffffff',
selectionInactiveBackground: '#e1e4e8',
};
if (themeId === 'auto') {
themeId = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
}
const preset = TERMINAL_THEMES.find((t) => t.id === themeId) || TERMINAL_THEMES[0];
return { ...preset.colors };
}
private async initializeTerminal() {

View file

@ -5,6 +5,7 @@
import { createLogger } from './logger.js';
import { detectMobile } from './mobile-utils.js';
import type { TerminalThemeId } from './terminal-themes.js';
const logger = createLogger('terminal-preferences');
@ -12,6 +13,7 @@ export interface TerminalPreferences {
maxCols: number; // 0 means no limit, positive numbers set max width
fontSize: number;
fitHorizontally: boolean;
theme: TerminalThemeId;
}
// Common terminal widths
@ -28,6 +30,7 @@ const DEFAULT_PREFERENCES: TerminalPreferences = {
maxCols: 0, // No limit by default - take as much as possible
fontSize: detectMobile() ? 12 : 14, // 12px on mobile, 14px on desktop
fitHorizontally: false,
theme: 'auto',
};
const STORAGE_KEY_TERMINAL_PREFS = 'vibetunnel_terminal_preferences';
@ -96,6 +99,15 @@ export class TerminalPreferencesManager {
this.savePreferences();
}
getTheme(): TerminalThemeId {
return this.preferences.theme;
}
setTheme(theme: TerminalThemeId) {
this.preferences.theme = theme;
this.savePreferences();
}
getPreferences(): TerminalPreferences {
return { ...this.preferences };
}

View file

@ -0,0 +1,149 @@
export type TerminalThemeId = 'auto' | 'light' | 'dark' | 'vscode-dark' | 'dracula' | 'nord';
export interface TerminalTheme {
id: TerminalThemeId;
name: string;
description: string;
colors: Record<string, string>;
}
export const TERMINAL_THEMES: TerminalTheme[] = [
{
id: 'dark',
name: 'Dark',
description: 'VibeTunnel default dark',
colors: {
background: '#1e1e1e',
foreground: '#d4d4d4',
cursor: 'rgb(var(--color-primary))',
cursorAccent: '#1e1e1e',
black: '#000000',
red: '#cd0000',
green: '#00cd00',
yellow: '#cdcd00',
blue: '#0000ee',
magenta: '#cd00cd',
cyan: '#00cdcd',
white: '#e5e5e5',
brightBlack: '#7f7f7f',
brightRed: '#ff0000',
brightGreen: '#00ff00',
brightYellow: '#ffff00',
brightBlue: '#5c5cff',
brightMagenta: '#ff00ff',
brightCyan: '#00ffff',
brightWhite: '#ffffff',
},
},
{
id: 'light',
name: 'Light',
description: 'Soft light theme',
colors: {
background: '#f8f9fa',
foreground: '#1f2328',
cursor: 'rgb(var(--color-primary))',
cursorAccent: '#f8f9fa',
black: '#24292f',
red: '#cf222e',
green: '#1a7f37',
yellow: '#9a6700',
blue: '#0969da',
magenta: '#8250df',
cyan: '#1b7c83',
white: '#6e7781',
brightBlack: '#57606a',
brightRed: '#da3633',
brightGreen: '#2da44e',
brightYellow: '#bf8700',
brightBlue: '#218bff',
brightMagenta: '#a475f9',
brightCyan: '#3192aa',
brightWhite: '#8c959f',
selectionBackground: '#0969da',
selectionForeground: '#ffffff',
selectionInactiveBackground: '#e1e4e8',
},
},
{
id: 'vscode-dark',
name: 'VS Code Dark',
description: 'Popular theme from Visual Studio Code',
colors: {
background: '#1E1E1E',
foreground: '#D4D4D4',
cursor: '#AEAFAD',
cursorAccent: '#1E1E1E',
black: '#000000',
red: '#CD3131',
green: '#0DBC79',
yellow: '#E5E510',
blue: '#2472C8',
magenta: '#BC3FBC',
cyan: '#11A8CD',
white: '#E5E5E5',
brightBlack: '#666666',
brightRed: '#F14C4C',
brightGreen: '#23D18B',
brightYellow: '#F5F543',
brightBlue: '#3B8EEA',
brightMagenta: '#D670D6',
brightCyan: '#29B8DB',
brightWhite: '#FFFFFF',
},
},
{
id: 'dracula',
name: 'Dracula',
description: 'Classic dark theme',
colors: {
background: '#282A36',
foreground: '#F8F8F2',
cursor: '#F8F8F2',
cursorAccent: '#282A36',
black: '#21222C',
red: '#FF5555',
green: '#50FA7B',
yellow: '#F1FA8C',
blue: '#BD93F9',
magenta: '#FF79C6',
cyan: '#8BE9FD',
white: '#F8F8F2',
brightBlack: '#6272A4',
brightRed: '#FF6E6E',
brightGreen: '#69FF94',
brightYellow: '#FFFFA5',
brightBlue: '#D6ACFF',
brightMagenta: '#FF92DF',
brightCyan: '#A4FFFF',
brightWhite: '#FFFFFF',
},
},
{
id: 'nord',
name: 'Nord',
description: 'Arctic north-bluish palette',
colors: {
background: '#2E3440',
foreground: '#D8DEE9',
cursor: '#D8DEE9',
cursorAccent: '#2E3440',
black: '#3B4252',
red: '#BF616A',
green: '#A3BE8C',
yellow: '#EBCB8B',
blue: '#81A1C1',
magenta: '#B48EAD',
cyan: '#88C0D0',
white: '#E5E9F0',
brightBlack: '#4C566A',
brightRed: '#BF616A',
brightGreen: '#A3BE8C',
brightYellow: '#EBCB8B',
brightBlue: '#81A1C1',
brightMagenta: '#B48EAD',
brightCyan: '#8FBCBB',
brightWhite: '#ECEFF4',
},
},
];