feat: align terminal font settings with appearance fonts

- Terminal font dropdown now uses mono fonts from UI font options
- Unified font list between appearance section and terminal settings
- Terminal font persisted to GlobalSettings for import/export support
- Aligned global terminal settings popover with per-terminal popover:
  - Same settings in same order (Font Size, Run on New Terminal, Font Family, Scrollback, Line Height, Screen Reader)
  - Consistent styling (Radix Select instead of native select)
- Added terminal padding (12px vertical, 16px horizontal) for readability
This commit is contained in:
Stefan de Vogelaere
2026-01-17 10:18:11 +01:00
parent b771b51842
commit 3320b40d15
8 changed files with 227 additions and 80 deletions

View File

@@ -2,11 +2,19 @@ import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Slider } from '@/components/ui/slider';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { SquareTerminal } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import { toast } from 'sonner';
import { TERMINAL_FONT_OPTIONS } from '@/config/terminal-themes';
import { DEFAULT_FONT_VALUE } from '@/config/ui-font-options';
export function TerminalSection() {
const {
@@ -53,27 +61,32 @@ export function TerminalSection() {
{/* Font Family */}
<div className="space-y-3">
<Label className="text-foreground font-medium">Font Family</Label>
<select
value={fontFamily}
onChange={(e) => {
setTerminalFontFamily(e.target.value);
<Select
value={fontFamily || DEFAULT_FONT_VALUE}
onValueChange={(value) => {
setTerminalFontFamily(value);
toast.info('Font family changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className={cn(
'w-full px-3 py-2 rounded-lg',
'bg-accent/30 border border-border/50',
'text-foreground text-sm',
'focus:outline-none focus:ring-2 focus:ring-green-500/30'
)}
>
{TERMINAL_FONT_OPTIONS.map((font) => (
<option key={font.value} value={font.value}>
{font.label}
</option>
))}
</select>
<SelectTrigger className="w-full">
<SelectValue placeholder="Default (Menlo / Monaco)" />
</SelectTrigger>
<SelectContent>
{TERMINAL_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span
style={{
fontFamily: option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Default Font Size */}

View File

@@ -25,8 +25,17 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Slider } from '@/components/ui/slider';
import { Switch } from '@/components/ui/switch';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { TERMINAL_FONT_OPTIONS } from '@/config/terminal-themes';
import { DEFAULT_FONT_VALUE } from '@/config/ui-font-options';
import { toast } from 'sonner';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { TerminalPanel } from './terminal-view/terminal-panel';
@@ -232,6 +241,8 @@ export function TerminalView() {
setTerminalDefaultRunScript,
setTerminalFontFamily,
setTerminalLineHeight,
setTerminalScrollbackLines,
setTerminalScreenReaderMode,
updateTerminalPanelSizes,
} = useAppStore();
@@ -1457,9 +1468,9 @@ export function TerminalView() {
<Settings className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80" align="end">
<PopoverContent className="w-72" align="end">
<div className="space-y-4">
<div className="space-y-2">
<div className="space-y-1">
<h4 className="font-medium text-sm">Terminal Settings</h4>
<p className="text-xs text-muted-foreground">
Configure global terminal appearance
@@ -1469,15 +1480,15 @@ export function TerminalView() {
{/* Default Font Size */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm">Default Font Size</Label>
<span className="text-sm text-muted-foreground">
<Label className="text-xs font-medium">Default Font Size</Label>
<span className="text-xs text-muted-foreground">
{terminalState.defaultFontSize}px
</span>
</div>
<Slider
value={[terminalState.defaultFontSize]}
min={8}
max={24}
max={32}
step={1}
onValueChange={([value]) => setTerminalDefaultFontSize(value)}
onValueCommit={() => {
@@ -1488,37 +1499,79 @@ export function TerminalView() {
/>
</div>
{/* Default Run Script */}
<div className="space-y-2">
<Label className="text-xs font-medium">Run on New Terminal</Label>
<Input
value={terminalState.defaultRunScript}
onChange={(e) => setTerminalDefaultRunScript(e.target.value)}
placeholder="e.g., claude"
className="h-7 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
Command to run when creating a new terminal
</p>
</div>
{/* Font Family */}
<div className="space-y-2">
<Label className="text-sm">Font Family</Label>
<select
value={terminalState.fontFamily}
onChange={(e) => {
setTerminalFontFamily(e.target.value);
<Label className="text-xs font-medium">Font Family</Label>
<Select
value={terminalState.fontFamily || DEFAULT_FONT_VALUE}
onValueChange={(value) => {
setTerminalFontFamily(value);
toast.info('Font family changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className={cn(
'w-full px-2 py-1.5 rounded-md text-sm',
'bg-accent/50 border border-border',
'text-foreground',
'focus:outline-none focus:ring-2 focus:ring-ring'
)}
>
{TERMINAL_FONT_OPTIONS.map((font) => (
<option key={font.value} value={font.value}>
{font.label}
</option>
))}
</select>
<SelectTrigger className="w-full h-8 text-xs">
<SelectValue placeholder="Default (Menlo / Monaco)" />
</SelectTrigger>
<SelectContent>
{TERMINAL_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span
style={{
fontFamily:
option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Scrollback */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs font-medium">Scrollback</Label>
<span className="text-xs text-muted-foreground">
{(terminalState.scrollbackLines / 1000).toFixed(0)}k lines
</span>
</div>
<Slider
value={[terminalState.scrollbackLines]}
min={1000}
max={100000}
step={1000}
onValueChange={([value]) => setTerminalScrollbackLines(value)}
onValueCommit={() => {
toast.info('Scrollback changed', {
description: 'Restart terminal for changes to take effect',
});
}}
/>
</div>
{/* Line Height */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm">Line Height</Label>
<span className="text-sm text-muted-foreground">
<Label className="text-xs font-medium">Line Height</Label>
<span className="text-xs text-muted-foreground">
{terminalState.lineHeight.toFixed(1)}
</span>
</div>
@@ -1536,18 +1589,21 @@ export function TerminalView() {
/>
</div>
{/* Default Run Script */}
<div className="space-y-2">
<Label className="text-sm">Default Run Script</Label>
<Input
value={terminalState.defaultRunScript}
onChange={(e) => setTerminalDefaultRunScript(e.target.value)}
placeholder="e.g., claude, npm run dev"
className="h-8 text-sm"
{/* Screen Reader */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-xs font-medium">Screen Reader</Label>
<p className="text-[10px] text-muted-foreground">Enable accessibility mode</p>
</div>
<Switch
checked={terminalState.screenReaderMode}
onCheckedChange={(checked) => {
setTerminalScreenReaderMode(checked);
toast.info(checked ? 'Screen reader enabled' : 'Screen reader disabled', {
description: 'Restart terminal for changes to take effect',
});
}}
/>
<p className="text-xs text-muted-foreground">
Command to run when opening new terminals
</p>
</div>
</div>
</PopoverContent>

View File

@@ -30,6 +30,13 @@ import { Slider } from '@/components/ui/slider';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useDraggable, useDroppable } from '@dnd-kit/core';
import { useAppStore, DEFAULT_KEYBOARD_SHORTCUTS, type KeyboardShortcuts } from '@/store/app-store';
import { useShallow } from 'zustand/react/shallow';
@@ -38,7 +45,9 @@ import {
getTerminalTheme,
TERMINAL_FONT_OPTIONS,
DEFAULT_TERMINAL_FONT,
getTerminalFontFamily,
} from '@/config/terminal-themes';
import { DEFAULT_FONT_VALUE } from '@/config/ui-font-options';
import { toast } from 'sonner';
import { getElectronAPI } from '@/lib/electron';
import { getApiKey, getSessionToken, getServerUrlSync } from '@/lib/http-api-client';
@@ -567,7 +576,7 @@ export function TerminalPanel({
// Get settings from store (read at initialization time)
const terminalSettings = useAppStore.getState().terminalState;
const screenReaderEnabled = terminalSettings.screenReaderMode;
const terminalFontFamily = terminalSettings.fontFamily || DEFAULT_TERMINAL_FONT;
const terminalFontFamily = getTerminalFontFamily(terminalSettings.fontFamily);
const terminalScrollback = terminalSettings.scrollbackLines || 5000;
const terminalLineHeight = terminalSettings.lineHeight || 1.0;
@@ -1269,7 +1278,7 @@ export function TerminalPanel({
useEffect(() => {
if (xtermRef.current && isTerminalReady) {
xtermRef.current.options.fontFamily = fontFamily;
xtermRef.current.options.fontFamily = getTerminalFontFamily(fontFamily);
fitAddonRef.current?.fit();
}
}, [fontFamily, isTerminalReady]);
@@ -1902,22 +1911,33 @@ export function TerminalPanel({
<div className="space-y-2">
<Label className="text-xs font-medium">Font Family</Label>
<select
value={fontFamily}
onChange={(e) => {
setTerminalFontFamily(e.target.value);
<Select
value={fontFamily || DEFAULT_FONT_VALUE}
onValueChange={(value) => {
setTerminalFontFamily(value);
toast.info('Font family changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className="w-full h-7 text-xs bg-background border border-input rounded-md px-2"
>
{TERMINAL_FONT_OPTIONS.map((font) => (
<option key={font.value} value={font.value}>
{font.label}
</option>
))}
</select>
<SelectTrigger className="w-full h-8 text-xs">
<SelectValue placeholder="Default (Menlo / Monaco)" />
</SelectTrigger>
<SelectContent>
{TERMINAL_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span
style={{
fontFamily:
option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">

View File

@@ -4,6 +4,11 @@
*/
import type { ThemeMode } from '@/store/app-store';
import {
UI_MONO_FONT_OPTIONS,
DEFAULT_FONT_VALUE,
type UIFontOption,
} from '@/config/ui-font-options';
export interface TerminalTheme {
background: string;
@@ -37,27 +42,44 @@ export interface TerminalTheme {
/**
* Terminal font options for user selection
* These are monospace fonts commonly available on different platforms
*
* Uses the same fonts as UI_MONO_FONT_OPTIONS for consistency across the app.
* All fonts listed here are bundled with the app via @fontsource packages
* or are system fonts with appropriate fallbacks.
*/
export interface TerminalFontOption {
value: string;
label: string;
}
export const TERMINAL_FONT_OPTIONS: TerminalFontOption[] = [
{ value: "Menlo, Monaco, 'Courier New', monospace", label: 'Menlo / Monaco' },
{ value: "'SF Mono', Menlo, Monaco, monospace", label: 'SF Mono' },
{ value: "'JetBrains Mono', monospace", label: 'JetBrains Mono' },
{ value: "'Fira Code', monospace", label: 'Fira Code' },
{ value: "'Source Code Pro', monospace", label: 'Source Code Pro' },
{ value: "Consolas, 'Courier New', monospace", label: 'Consolas' },
{ value: "'Ubuntu Mono', monospace", label: 'Ubuntu Mono' },
];
// Re-export for backwards compatibility
export type TerminalFontOption = UIFontOption;
/**
* Default terminal font family (first option)
* Terminal font options - reuses UI_MONO_FONT_OPTIONS with terminal-specific default
*
* The 'default' value means "use the default terminal font" (Menlo/Monaco)
*/
export const DEFAULT_TERMINAL_FONT = TERMINAL_FONT_OPTIONS[0].value;
export const TERMINAL_FONT_OPTIONS: readonly UIFontOption[] = UI_MONO_FONT_OPTIONS.map((option) => {
// Replace the UI default label with terminal-specific default
if (option.value === DEFAULT_FONT_VALUE) {
return { value: option.value, label: 'Default (Menlo / Monaco)' };
}
return option;
});
/**
* Default terminal font family
* Uses the DEFAULT_FONT_VALUE sentinel which maps to Menlo/Monaco
*/
export const DEFAULT_TERMINAL_FONT = DEFAULT_FONT_VALUE;
/**
* Get the actual font family CSS value for terminal
* Converts DEFAULT_FONT_VALUE to the actual Menlo/Monaco font stack
*/
export function getTerminalFontFamily(fontValue: string | undefined): string {
if (!fontValue || fontValue === DEFAULT_FONT_VALUE) {
return "Menlo, Monaco, 'Courier New', monospace";
}
return fontValue;
}
// Dark theme (default)
const darkTheme: TerminalTheme = {

View File

@@ -600,6 +600,13 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
worktreePanelCollapsed: settings.worktreePanelCollapsed ?? false,
lastProjectDir: settings.lastProjectDir ?? '',
recentFolders: settings.recentFolders ?? [],
// Terminal font (nested in terminalState)
...(settings.terminalFontFamily && {
terminalState: {
...current.terminalState,
fontFamily: settings.terminalFontFamily,
},
}),
});
// Hydrate setup wizard state from global settings (API-backed)
@@ -653,6 +660,7 @@ function buildSettingsUpdateFromStore(): Record<string, unknown> {
worktreePanelCollapsed: state.worktreePanelCollapsed,
lastProjectDir: state.lastProjectDir,
recentFolders: state.recentFolders,
terminalFontFamily: state.terminalState.fontFamily,
};
}

View File

@@ -35,6 +35,7 @@ const SETTINGS_FIELDS_TO_SYNC = [
'theme',
'fontFamilySans',
'fontFamilyMono',
'terminalFontFamily', // Maps to terminalState.fontFamily
'sidebarOpen',
'chatHistoryOpen',
'maxConcurrency',
@@ -159,6 +160,9 @@ export function useSettingsSync(): SettingsSyncState {
if (field === 'currentProjectId') {
// Special handling: extract ID from currentProject object
updates[field] = appState.currentProject?.id ?? null;
} else if (field === 'terminalFontFamily') {
// Special handling: map terminalState.fontFamily to terminalFontFamily
updates[field] = appState.terminalState.fontFamily;
} else {
updates[field] = appState[field as keyof typeof appState];
}
@@ -260,6 +264,8 @@ export function useSettingsSync(): SettingsSyncState {
for (const field of SETTINGS_FIELDS_TO_SYNC) {
if (field === 'currentProjectId') {
updates[field] = appState.currentProject?.id ?? null;
} else if (field === 'terminalFontFamily') {
updates[field] = appState.terminalState.fontFamily;
} else {
updates[field] = appState[field as keyof typeof appState];
}
@@ -322,6 +328,12 @@ export function useSettingsSync(): SettingsSyncState {
changed = true;
break;
}
} else if (field === 'terminalFontFamily') {
// Special handling: compare terminalState.fontFamily
if (newState.terminalState.fontFamily !== prevState.terminalState.fontFamily) {
changed = true;
break;
}
} else {
const key = field as keyof typeof newState;
if (newState[key] !== prevState[key]) {
@@ -403,6 +415,8 @@ export async function forceSyncSettingsToServer(): Promise<boolean> {
for (const field of SETTINGS_FIELDS_TO_SYNC) {
if (field === 'currentProjectId') {
updates[field] = appState.currentProject?.id ?? null;
} else if (field === 'terminalFontFamily') {
updates[field] = appState.terminalState.fontFamily;
} else {
updates[field] = appState[field as keyof typeof appState];
}
@@ -505,6 +519,13 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
worktreePanelCollapsed: serverSettings.worktreePanelCollapsed ?? false,
lastProjectDir: serverSettings.lastProjectDir ?? '',
recentFolders: serverSettings.recentFolders ?? [],
// Terminal font (nested in terminalState)
...(serverSettings.terminalFontFamily && {
terminalState: {
...currentAppState.terminalState,
fontFamily: serverSettings.terminalFontFamily,
},
}),
});
// Also refresh setup wizard state

View File

@@ -880,6 +880,11 @@
background: var(--muted-foreground);
}
/* Terminal padding for better readability */
.xterm {
padding: 12px 16px;
}
/* ========================================
DEPENDENCY GRAPH STYLES
Theme-aware styling for React Flow graph

View File

@@ -472,6 +472,8 @@ export interface GlobalSettings {
fontFamilySans?: string;
/** Global Code/Mono font family (undefined = use default Geist Mono) */
fontFamilyMono?: string;
/** Terminal font family (undefined = use default Menlo/Monaco) */
terminalFontFamily?: string;
// UI State Preferences
/** Whether sidebar is currently open */