mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-14 12:46:05 +00:00
feat(web): add terminal theme preference
This commit is contained in:
parent
93ba0064bd
commit
d019559f2b
6 changed files with 222 additions and 58 deletions
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
149
web/src/client/utils/terminal-themes.ts
Normal file
149
web/src/client/utils/terminal-themes.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
Loading…
Reference in a new issue