import { useEffect, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { X, Minus, Plus, TerminalSquare, Ghost, type LucideIcon } from "lucide-react"; import { useStore } from "../stores/useStore"; import { LINE_SPACING_STEP, MIN_LINE_SPACING, MAX_LINE_SPACING, FONT_SIZE_STEP, MIN_FONT_SIZE, MAX_FONT_SIZE, clampFontSize, fontSizeFor, TERMINAL_BACKENDS, TERMINAL_FONTS, type BackendId, type FontId, } from "../settings"; const BACKEND_ICON: Record = { xterm: TerminalSquare, ghostty: Ghost, }; // A vertical list of the selectable terminal fonts; each row previews itself in its own font. // Shared by both emulator sections (bound to that emulator's font state). function FontPicker({ value, onChange }: { value: FontId; onChange: (id: FontId) => void }) { return (
{TERMINAL_FONTS.map(({ id, label, stack }) => { const active = value === id; return ( ); })}
); } // A −/+ stepper with an editable number in the middle. `value` is the committed size; `onCommit` // clamps and persists. The text buffer lets the field go empty/partial mid-edit; it re-syncs to // `value` whenever that changes externally (font/emulator switch, or a clamp on commit), and // commits on blur/Enter — reverting to `value` on empty or non-numeric input. function FontSizeStepper({ value, onCommit }: { value: number; onCommit: (n: number) => void }) { const [text, setText] = useState(String(value)); useEffect(() => { setText(String(value)); }, [value]); const atMin = value <= MIN_FONT_SIZE; const atMax = value >= MAX_FONT_SIZE; // Commit the field's live DOM value (not the `text` closure, which can lag a blur that lands in // the same tick as the edit). Always re-sync the buffer to the committed value: clamping here // (not just in onCommit) keeps the field honest even when the clamped result equals the current // `value` (so the re-sync effect below wouldn't fire). Empty/NaN reverts to the current value. const commit = (raw: string) => { const n = Number(raw); if (raw.trim() !== "" && Number.isFinite(n)) { const clamped = clampFontSize(n); onCommit(clamped); setText(String(clamped)); } else { setText(String(value)); } }; return (
setText(e.target.value)} onBlur={(e) => commit(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === "Enter") { commit(e.currentTarget.value); e.currentTarget.blur(); } }} className="w-10 text-center font-mono text-sm text-content tabular-nums bg-transparent outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" />
); } // Global appearance settings, docked to the right edge and toggled by the header gear. // Mirrors Sidebar's slide-in; sits above it (z-[60]) so the two never collide. Holds the // emulator backend selector, then a section of options for the selected emulator only — a // per-emulator font picker (both), plus line spacing (stepper) under xterm. The store persists // each field. export function SettingsPanel() { const settingsOpen = useStore((s) => s.settingsOpen); const setSettingsOpen = useStore((s) => s.setSettingsOpen); const terminalBackend = useStore((s) => s.terminalBackend); const setTerminalBackend = useStore((s) => s.setTerminalBackend); const lineSpacing = useStore((s) => s.lineSpacing); const setLineSpacing = useStore((s) => s.setLineSpacing); const xtermFont = useStore((s) => s.xtermFont); const setXtermFont = useStore((s) => s.setXtermFont); const ghosttyFont = useStore((s) => s.ghosttyFont); const setGhosttyFont = useStore((s) => s.setGhosttyFont); const fontSizes = useStore((s) => s.fontSizes); const setFontSize = useStore((s) => s.setFontSize); const atMin = lineSpacing <= MIN_LINE_SPACING; const atMax = lineSpacing >= MAX_LINE_SPACING; const activeBackendLabel = TERMINAL_BACKENDS.find((b) => b.id === terminalBackend)?.label ?? terminalBackend; return ( {settingsOpen && ( {/* Header */}

Settings

{/* Body */}
Emulator
{TERMINAL_BACKENDS.map(({ id, label }) => { const Icon = BACKEND_ICON[id]; const active = terminalBackend === id; return ( ); })}
{/* Options specific to the selected emulator */}
{terminalBackend === "xterm" && ( <>
Font
Font size for selected font setFontSize("xterm", xtermFont, n)} />
Line spacing
{lineSpacing.toFixed(2)}
)} {terminalBackend === "ghostty" && ( <>
Font
Font size for selected font setFontSize("ghostty", ghosttyFont, n)} />
)}
)}
); }