diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 6d205af..32fd6cf 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -9,6 +9,7 @@ import { UpdateBanner } from './components/UpdateBanner'; import { FirstRunWizard } from './components/FirstRunWizard'; import { useWelcomeTrigger } from './hooks/use-welcome-trigger'; import { useAutoUpdateCheck } from './hooks/useAutoUpdateCheck'; +import { useMonospaceClasses } from './hooks/use-monospace-classes'; import { useSettingsStore } from './stores/settings-store'; import { ipc } from './lib/ipc'; @@ -35,6 +36,7 @@ function scopeCSS(cssText: string, scopeSelector: string) { function App() { useWelcomeTrigger(); useAutoUpdateCheck(); + useMonospaceClasses(); const [printOpen, setPrintOpen] = useState(false); const customCssPath = useSettingsStore((s) => s.customCssPath); diff --git a/src/renderer/hooks/use-monospace-classes.ts b/src/renderer/hooks/use-monospace-classes.ts new file mode 100644 index 0000000..3e1bf6f --- /dev/null +++ b/src/renderer/hooks/use-monospace-classes.ts @@ -0,0 +1,41 @@ +export interface MonospaceSettings { + monospaceFont: 'jetbrains-mono' | 'fira-code'; + monospaceLigatures: boolean; +} + +const DEFAULTS: MonospaceSettings = { + monospaceFont: 'jetbrains-mono', + monospaceLigatures: false, +}; + +const FAMILY_CLASSES = ['mono-jetbrains-mono', 'mono-fira-code']; +const LIG_CLASSES = ['mono-ligatures-on', 'mono-ligatures-off']; + +function stripMonospaceClasses(): void { + for (const c of [...FAMILY_CLASSES, ...LIG_CLASSES]) { + document.body.classList.remove(c); + } +} + +export function applyMonospaceClasses(input: Partial | null | undefined): MonospaceSettings { + const safe: MonospaceSettings = { + monospaceFont: + input && input.monospaceFont === 'fira-code' ? 'fira-code' : DEFAULTS.monospaceFont, + monospaceLigatures: !!(input && input.monospaceLigatures === true), + }; + stripMonospaceClasses(); + document.body.classList.add(`mono-${safe.monospaceFont}`); + document.body.classList.add(`mono-ligatures-${safe.monospaceLigatures ? 'on' : 'off'}`); + return safe; +} + +export function useMonospaceClasses(): void { + if (typeof window === 'undefined') return; + const api = (window as unknown as { electronAPI?: { monospace?: { getSettings?: () => Promise } } }) + .electronAPI; + if (!api || !api.monospace || typeof api.monospace.getSettings !== 'function') return; + api.monospace + .getSettings() + .then(applyMonospaceClasses) + .catch(() => applyMonospaceClasses(null)); +} diff --git a/src/renderer/styles/globals.css b/src/renderer/styles/globals.css index 9c11d3b..d06cdfe 100644 --- a/src/renderer/styles/globals.css +++ b/src/renderer/styles/globals.css @@ -96,3 +96,22 @@ .cm-editor .cm-scroller { @apply font-mono text-sm; } + +/* Monospace font + ligature tokens — driven by body.mono-* classes */ +:root { + --font-mono-active: 'JetBrains Mono', 'SF Mono', Monaco, 'Courier New', monospace; + --font-mono-feature: 'liga' 0, 'calt' 0; +} +body.mono-fira-code { + --font-mono-active: 'Fira Code', 'JetBrains Mono', 'SF Mono', monospace; +} +body.mono-ligatures-on { + --font-mono-feature: 'liga' 1, 'calt' 1; +} +code, +pre, +kbd, +samp { + font-family: var(--font-mono-active); + font-feature-settings: var(--font-mono-feature); +} diff --git a/tests/unit/lib/use-monospace-classes.test.ts b/tests/unit/lib/use-monospace-classes.test.ts new file mode 100644 index 0000000..4baa44a --- /dev/null +++ b/tests/unit/lib/use-monospace-classes.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { applyMonospaceClasses } from "../../../src/renderer/hooks/use-monospace-classes"; + +describe('applyMonospaceClasses', () => { + beforeEach(() => { + document.body.className = ''; + }); + afterEach(() => { + document.body.className = ''; + }); + + it('applies jetbrains-mono + ligatures-off for default settings', () => { + applyMonospaceClasses({ monospaceFont: 'jetbrains-mono', monospaceLigatures: false }); + expect(document.body.classList.contains('mono-jetbrains-mono')).toBe(true); + expect(document.body.classList.contains('mono-ligatures-off')).toBe(true); + }); + + it('applies fira-code + ligatures-on when enabled', () => { + applyMonospaceClasses({ monospaceFont: 'fira-code', monospaceLigatures: true }); + expect(document.body.classList.contains('mono-fira-code')).toBe(true); + expect(document.body.classList.contains('mono-ligatures-on')).toBe(true); + }); + + it('strips prior mono-* classes before applying new ones', () => { + document.body.classList.add('mono-jetbrains-mono', 'mono-ligatures-off'); + applyMonospaceClasses({ monospaceFont: 'fira-code', monospaceLigatures: true }); + expect(document.body.classList.contains('mono-jetbrains-mono')).toBe(false); + expect(document.body.classList.contains('mono-ligatures-off')).toBe(false); + expect(document.body.classList.contains('mono-fira-code')).toBe(true); + }); + + it('falls back to defaults when given null', () => { + applyMonospaceClasses(null); + expect(document.body.classList.contains('mono-jetbrains-mono')).toBe(true); + expect(document.body.classList.contains('mono-ligatures-off')).toBe(true); + }); +});