"use client"; import { useEffect, useState } from "react"; import { IconTerminal2 } from "@tabler/icons-react"; import { Badge } from "@kandev/ui/badge"; import { CardContent, CardHeader, CardTitle } from "@kandev/ui/card"; import { Input } from "@kandev/ui/input"; import { Label } from "@kandev/ui/label"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, } from "@kandev/ui/select"; import { Separator } from "@kandev/ui/separator"; import { SettingsSection } from "@/components/settings/settings-section"; import { SettingsCard } from "@/components/settings/settings-card"; import { ShellSettingsCard } from "@/components/settings/shell-settings-card"; import { useAppStore, useAppStoreApi } from "@/components/state-provider"; import { updateUserSettings } from "@/lib/api"; import { useShellSettings } from "@/hooks/domains/settings/use-shell-settings"; import { useSettingsSaveContributor } from "./settings-save-provider"; import { TERMINAL_FONT_PRESETS } from "@/lib/terminal/terminal-font"; import type { FontCategory } from "@/lib/terminal/terminal-font"; import { useTranslation } from "react-i18next"; import { TERMINAL_FONT_FAMILY_TARGET, TERMINAL_FONT_SIZE_TARGET, TERMINAL_LINKS_TARGET, } from "@/lib/settings-discovery/catalog/preferences"; const CUSTOM_VALUE = "__custom__"; /** * Catalog KEYS, not copy: a `t()` in a module-scope table freezes at the boot * locale, and neither the lint guard nor the pseudo-locale can see that. * Resolved at render in `FontGroupOptions`; the record keys are data. */ export const CATEGORY_LABEL_KEYS: Record = { icons: "settings:nerdFonts", ligatures: "settings:programming", system: "common:system", }; export const CATEGORY_BADGE_KEYS: Partial> = { icons: "settings:icons", ligatures: "settings:ligatures", }; const FONT_GROUPS: Record = TERMINAL_FONT_PRESETS.reduce( (acc, p) => { (acc[p.category] ??= []).push(p); return acc; }, {} as Record, ); const FONT_CATEGORIES: FontCategory[] = ["icons", "ligatures", "system"]; export function normalizeTerminalFontSize(value: number, fallback: number): number { const base = Number.isFinite(value) ? value : fallback; return Math.min(24, Math.max(8, base)); } function FontGroupOptions() { const { t } = useTranslation(); return FONT_CATEGORIES.map((category) => { const badgeKey = CATEGORY_BADGE_KEYS[category]; return ( {t(CATEGORY_LABEL_KEYS[category])} {badgeKey && ( {t(badgeKey)} )} {/* Preset labels are font family names - data, never translated. The one entry naming no typeface carries `labelKey` instead. */} {(FONT_GROUPS[category] ?? []).map((preset) => ( {preset.labelKey ? t(preset.labelKey) : preset.label} ))} ); }); } function TerminalFontSizeCard({ fontSize, isDirty, onChange, }: { fontSize: number; isDirty: boolean; onChange: (value: number) => void; }) { const { t } = useTranslation(); const handleFontSizeBlur = () => { onChange(normalizeTerminalFontSize(fontSize, 13)); }; return ( {t("settings:terminalFontSize")}
onChange(Number(e.target.value))} onBlur={handleFontSizeBlur} onKeyDown={(e) => { if (e.key === "Enter") handleFontSizeBlur(); }} className="w-20" data-testid="terminal-font-size-input" /> {t("settings:pxRange")}

{t("settings:setTheFontSizeForThe")}

); } function TerminalFontCard({ fontFamily, isDirty, onChange, }: { fontFamily: string | null; isDirty: boolean; onChange: (value: string | null) => void; }) { const { t } = useTranslation(); const [isCustom, setIsCustom] = useState(() => { const current = fontFamily; if (!current) return false; return !TERMINAL_FONT_PRESETS.some((p) => p.value === current); }); const [customValue, setCustomValue] = useState(() => (isCustom ? fontFamily : "") ?? ""); useEffect(() => { const nextIsCustom = Boolean( fontFamily && !TERMINAL_FONT_PRESETS.some((preset) => preset.value === fontFamily), ); setIsCustom(nextIsCustom); setCustomValue(nextIsCustom ? (fontFamily ?? "") : ""); }, [fontFamily]); const handleSelectChange = (value: string) => { if (value === CUSTOM_VALUE) { setIsCustom(true); return; } setIsCustom(false); setCustomValue(""); onChange(value === "default" ? null : value); }; const handleCustomBlur = () => { const trimmed = customValue.trim(); if (trimmed) onChange(trimmed); }; const selectValue = isCustom ? CUSTOM_VALUE : fontFamily || "default"; return ( {t("settings:terminalFont")}
{isCustom && ( setCustomValue(e.target.value)} onBlur={handleCustomBlur} onKeyDown={(e) => { if (e.key === "Enter") handleCustomBlur(); }} data-testid="terminal-font-custom-input" /> )}

{t("settings:chooseAMonospaceFontForThe")}

); } function TerminalLinksCard({ value, isDirty, onChange, }: { value: "new_tab" | "browser_panel"; isDirty: boolean; onChange: (value: "new_tab" | "browser_panel") => void; }) { const { t } = useTranslation(); return ( {t("settings:terminalLinks")}

{t("settings:clickAUrlInTheTerminal")}

); } export function TerminalSettings() { const { t } = useTranslation(); const userSettings = useAppStore((state) => state.userSettings); const setUserSettings = useAppStore((state) => state.setUserSettings); const storeApi = useAppStoreApi(); const shellSettings = useShellSettings(); const [saved, setSaved] = useState(() => ({ preferredShell: shellSettings.preferredShell ?? "", terminalFontFamily: userSettings.terminalFontFamily, terminalFontSize: userSettings.terminalFontSize ?? 13, terminalLinkBehavior: userSettings.terminalLinkBehavior, })); const [draft, setDraft] = useState(saved); const revision = JSON.stringify(draft); const validFontSize = normalizeTerminalFontSize(draft.terminalFontSize, 13); useSettingsSaveContributor({ id: "general-terminal", revision, isDirty: revision !== JSON.stringify(saved), canSave: Number.isFinite(draft.terminalFontSize), invalidReason: t("settings:terminalFontSizeMustBeA"), save: async () => { const submitted = { ...draft, terminalFontSize: validFontSize }; const current = storeApi.getState().userSettings; await updateUserSettings({ workspace_id: current.workspaceId || "", repository_ids: current.repositoryIds || [], preferred_shell: submitted.preferredShell.trim(), terminal_font_family: submitted.terminalFontFamily ?? "", terminal_font_size: submitted.terminalFontSize, terminal_link_behavior: submitted.terminalLinkBehavior, }); setSaved(submitted); setDraft((latest) => (latest === draft ? submitted : latest)); setUserSettings({ ...storeApi.getState().userSettings, preferredShell: submitted.preferredShell.trim() || null, terminalFontFamily: submitted.terminalFontFamily, terminalFontSize: submitted.terminalFontSize, terminalLinkBehavior: submitted.terminalLinkBehavior, }); }, discard: () => setDraft(saved), }); return (
setDraft((current) => ({ ...current, preferredShell })) } shellLoaded={shellSettings.loaded} shellOptions={shellSettings.shellOptions ?? []} /> } title={t("settings:terminal")} description={t("settings:configureTerminalAppearanceAndBehavior")} > setDraft((current) => ({ ...current, terminalFontFamily })) } /> setDraft((current) => ({ ...current, terminalFontSize }))} /> setDraft((current) => ({ ...current, terminalLinkBehavior })) } />
); }