Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 81 additions & 35 deletions config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ import { buildHeadlessAutomationWorktreeCreateArgs } from './automations/headles
import { AgentAwakeService } from './agent-awake-service'
import { normalizeComputerAwakeMode } from '../shared/computer-awake-mode'
import { registerSystemResumeBroadcast } from './system-resume-broadcast'
import { listSystemFontFamilies } from './system-fonts'
import { settleTeardownWithinDeadline } from './quit-teardown-deadline'
import { quitTeardownStartGate } from './quit-teardown-start-gate'
import { beginSshShutdown } from './ipc/ssh'
Expand Down Expand Up @@ -3087,6 +3088,15 @@ void app.whenReady().then(async () => {
}
}

// Why: enumerating fonts takes ~20s on a macOS box with a large catalog, and the
// settings font pickers show a 5-entry placeholder until it lands — long enough
// that the real list reads as broken. Warm the cache once the window is up so it
// is ready well before settings can be opened; failures fall back as before.
win.once('show', () => {
const warmUp = setTimeout(() => void listSystemFontFamilies().catch(() => undefined), 2_000)
warmUp.unref()
})

// Why: macOS notification permission dialog must fire after the window is shown, else it's hidden behind the maximized window.
win.once('show', () => {
// Why: store can be null if init failed earlier; bail rather than throw inside an Electron event listener.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ export function buildPreviewAppearanceOptions(
const fontWeights = resolveTerminalFontWeights(settings?.terminalFontWeight)
return {
fontSize: settings?.terminalFontSize ?? 14,
fontFamily: buildFontFamily(settings?.terminalFontFamily ?? ''),
fontFamily: buildFontFamily(
settings?.terminalFontFamily ?? '',
settings?.terminalCjkFontFamily ?? ''
),
fontWeight: fontWeights.fontWeight,
fontWeightBold: fontWeights.fontWeightBold,
cursorStyle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,34 @@ import {
fontFamilyHasKnownLigatures,
resolveTerminalLigaturesEnabled
} from '../../../../shared/terminal-ligatures'
import { NumberField, SettingsRow, SettingsSegmentedControl } from './SettingsFormControls'
import {
FontAutocomplete,
NumberField,
SettingsRow,
SettingsSegmentedControl
} from './SettingsFormControls'
import { SearchableSetting } from './SearchableSetting'
import { clampNumber } from '@/lib/terminal-theme'
import { translate } from '@/i18n/i18n'
import { getTerminalAdvancedTypographySearchEntries } from './terminal-typography-search'

const NO_FONT_SUGGESTIONS: string[] = []

type TerminalAdvancedTypographyControlsProps = {
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void
fontSuggestions?: string[]
onRequestFontSuggestions?: () => void
}

/** Low-frequency terminal typography knobs (weight, line height, ligatures).
* Split out of the primary font controls so the default Terminal scan stays
* compact while these stay searchable inside the Advanced disclosure. */
export function TerminalAdvancedTypographyControls({
settings,
updateSettings
updateSettings,
fontSuggestions = NO_FONT_SUGGESTIONS,
onRequestFontSuggestions
}: TerminalAdvancedTypographyControlsProps): React.JSX.Element {
const searchEntries = getTerminalAdvancedTypographySearchEntries()

Expand Down Expand Up @@ -183,6 +194,31 @@ export function TerminalAdvancedTypographyControls({
.
</p>
</SearchableSetting>

<SearchableSetting
title={translate(
'auto.components.settings.TerminalAppearanceSection.9d8a37e2ae',
'CJK Font Family'
)}
description={searchEntries[3]?.description}
keywords={searchEntries[3]?.keywords ?? ['terminal', 'typography', 'cjk', 'korean']}
>
<SettingsRow
label={translate(
'auto.components.settings.TerminalAppearanceSection.9d8a37e2ae',
'CJK Font Family'
)}
description={searchEntries[3]?.description}
control={
<FontAutocomplete
value={settings.terminalCjkFontFamily ?? ''}
suggestions={fontSuggestions}
onRequestSuggestions={onRequestFontSuggestions}
onChange={(value) => updateSettings({ terminalCjkFontFamily: value })}
/>
}
/>
</SearchableSetting>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ export function TerminalAppearanceSection({
<TerminalAdvancedTypographyControls
settings={settings}
updateSettings={updateSettings}
fontSuggestions={terminalFontSuggestions}
onRequestFontSuggestions={onRequestFontSuggestions}
/>
</AppearanceAdvancedDisclosure>
</div>
Expand Down
20 changes: 20 additions & 0 deletions src/renderer/src/components/settings/terminal-typography-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,26 @@ const getTerminalTypographySearchEntryCatalog = createLocalizedCatalog(() => [
'font features'
)
]
},
{
title: translate('auto.components.settings.terminal.search.90f1cfbd2f', 'CJK Font Family'),
description: translate(
'auto.components.settings.terminal.search.aa68dda11b',
'Font for Chinese, Japanese and Korean text, which most coding fonts lack. Empty uses the built-in fallback chain.'
),
keywords: [
...translateSearchKeyword('auto.components.settings.terminal.search.f66a7cf715', 'terminal'),
...translateSearchKeyword(
'auto.components.settings.terminal.search.103cdb862f',
'typography'
),
...translateSearchKeyword('auto.components.settings.terminal.search.b0bb76ae6b', 'font'),
...translateSearchKeyword('auto.components.settings.terminal.search.d48d686fe5', 'cjk'),
...translateSearchKeyword('auto.components.settings.terminal.search.3e652f1627', 'korean'),
...translateSearchKeyword('auto.components.settings.terminal.search.84ae68de79', 'hangul'),
...translateSearchKeyword('auto.components.settings.terminal.search.24f7977756', 'japanese'),
...translateSearchKeyword('auto.components.settings.terminal.search.8511986b75', 'chinese')
]
}
])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import {
collectLeafIdsInOrder,
collectLeafIdsInReplayCreationOrder
} from './layout-serialization'
import { DEFAULT_TERMINAL_FONT_FAMILY } from '@/lib/terminal-font-family'
import { buildDefaultTerminalOptions } from '@/lib/pane-manager/pane-terminal-options'

// ---------------------------------------------------------------------------
// Helper to create mock elements
Expand All @@ -73,7 +75,7 @@ const LEAF_4 = '44444444-4444-4444-8444-444444444444'
// buildFontFamily
// ---------------------------------------------------------------------------
const FULL_FALLBACK =
'"SF Mono", "Menlo", "Monaco", "Cascadia Mono", "Consolas", "DejaVu Sans Mono", "Liberation Mono", "Orca Nerd Font Symbols", "Symbols Nerd Font Mono", "MesloLGS Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", monospace'
'"SF Mono", "Menlo", "Monaco", "Cascadia Mono", "Consolas", "DejaVu Sans Mono", "Liberation Mono", "Orca Nerd Font Symbols", "Symbols Nerd Font Mono", "MesloLGS Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", "D2Coding", "NanumGothicCoding", "나눔고딕코딩", "Sarasa Mono K", "Noto Sans Mono CJK KR", "Apple SD Gothic Neo", "Apple SD 산돌고딕 Neo", "Malgun Gothic", "맑은 고딕", "MS Gothic", "MS ゴシック", "Hiragino Sans", "ヒラギノ角ゴシック", monospace'

describe('buildFontFamily', () => {
it('puts custom font first with full cross-platform fallback chain', () => {
Expand All @@ -84,7 +86,7 @@ describe('buildFontFamily', () => {
it('does not duplicate SF Mono when it is the input', () => {
const result = buildFontFamily('SF Mono')
expect(result).toBe(
'"SF Mono", "Menlo", "Monaco", "Cascadia Mono", "Consolas", "DejaVu Sans Mono", "Liberation Mono", "Orca Nerd Font Symbols", "Symbols Nerd Font Mono", "MesloLGS Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", monospace'
'"SF Mono", "Menlo", "Monaco", "Cascadia Mono", "Consolas", "DejaVu Sans Mono", "Liberation Mono", "Orca Nerd Font Symbols", "Symbols Nerd Font Mono", "MesloLGS Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", "D2Coding", "NanumGothicCoding", "나눔고딕코딩", "Sarasa Mono K", "Noto Sans Mono CJK KR", "Apple SD Gothic Neo", "Apple SD 산돌고딕 Neo", "Malgun Gothic", "맑은 고딕", "MS Gothic", "MS ゴシック", "Hiragino Sans", "ヒラギノ角ゴシック", monospace'
)
})

Expand All @@ -101,30 +103,90 @@ describe('buildFontFamily', () => {
it('does not duplicate when font name contains "sf mono" (case-insensitive)', () => {
const result = buildFontFamily('My SF Mono Custom')
expect(result).toBe(
'"My SF Mono Custom", "Menlo", "Monaco", "Cascadia Mono", "Consolas", "DejaVu Sans Mono", "Liberation Mono", "Orca Nerd Font Symbols", "Symbols Nerd Font Mono", "MesloLGS Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", monospace'
'"My SF Mono Custom", "Menlo", "Monaco", "Cascadia Mono", "Consolas", "DejaVu Sans Mono", "Liberation Mono", "Orca Nerd Font Symbols", "Symbols Nerd Font Mono", "MesloLGS Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", "D2Coding", "NanumGothicCoding", "나눔고딕코딩", "Sarasa Mono K", "Noto Sans Mono CJK KR", "Apple SD Gothic Neo", "Apple SD 산돌고딕 Neo", "Malgun Gothic", "맑은 고딕", "MS Gothic", "MS ゴシック", "Hiragino Sans", "ヒラギノ角ゴシック", monospace'
)
})

it('does not duplicate Consolas when it is the input', () => {
const result = buildFontFamily('Consolas')
expect(result).toBe(
'"Consolas", "SF Mono", "Menlo", "Monaco", "Cascadia Mono", "DejaVu Sans Mono", "Liberation Mono", "Orca Nerd Font Symbols", "Symbols Nerd Font Mono", "MesloLGS Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", monospace'
'"Consolas", "SF Mono", "Menlo", "Monaco", "Cascadia Mono", "DejaVu Sans Mono", "Liberation Mono", "Orca Nerd Font Symbols", "Symbols Nerd Font Mono", "MesloLGS Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", "D2Coding", "NanumGothicCoding", "나눔고딕코딩", "Sarasa Mono K", "Noto Sans Mono CJK KR", "Apple SD Gothic Neo", "Apple SD 산돌고딕 Neo", "Malgun Gothic", "맑은 고딕", "MS Gothic", "MS ゴシック", "Hiragino Sans", "ヒラギノ角ゴシック", monospace'
)
})

it('does not duplicate MesloLGS Nerd Font when it is the input', () => {
const result = buildFontFamily('MesloLGS Nerd Font')
expect(result).toBe(
'"MesloLGS Nerd Font", "SF Mono", "Menlo", "Monaco", "Cascadia Mono", "Consolas", "DejaVu Sans Mono", "Liberation Mono", "Orca Nerd Font Symbols", "Symbols Nerd Font Mono", "JetBrainsMono Nerd Font", "Hack Nerd Font", monospace'
'"MesloLGS Nerd Font", "SF Mono", "Menlo", "Monaco", "Cascadia Mono", "Consolas", "DejaVu Sans Mono", "Liberation Mono", "Orca Nerd Font Symbols", "Symbols Nerd Font Mono", "JetBrainsMono Nerd Font", "Hack Nerd Font", "D2Coding", "NanumGothicCoding", "나눔고딕코딩", "Sarasa Mono K", "Noto Sans Mono CJK KR", "Apple SD Gothic Neo", "Apple SD 산돌고딕 Neo", "Malgun Gothic", "맑은 고딕", "MS Gothic", "MS ゴシック", "Hiragino Sans", "ヒラギノ角ゴシック", monospace'
)
})

it('does not duplicate the bundled Nerd Font symbol fallback', () => {
const result = buildFontFamily('Orca Nerd Font Symbols')
expect(result).toBe(
'"Orca Nerd Font Symbols", "SF Mono", "Menlo", "Monaco", "Cascadia Mono", "Consolas", "DejaVu Sans Mono", "Liberation Mono", "Symbols Nerd Font Mono", "MesloLGS Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", monospace'
'"Orca Nerd Font Symbols", "SF Mono", "Menlo", "Monaco", "Cascadia Mono", "Consolas", "DejaVu Sans Mono", "Liberation Mono", "Symbols Nerd Font Mono", "MesloLGS Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", "D2Coding", "NanumGothicCoding", "나눔고딕코딩", "Sarasa Mono K", "Noto Sans Mono CJK KR", "Apple SD Gothic Neo", "Apple SD 산돌고딕 Neo", "Malgun Gothic", "맑은 고딕", "MS Gothic", "MS ゴシック", "Hiragino Sans", "ヒラギノ角ゴシック", monospace'
)
})

// Without one of these the browser substitutes a proportional face for Hangul,
// whose advance is not two cells wide, and CJK output drifts out of the grid.
it.each(['D2Coding', 'Noto Sans Mono CJK KR', 'Malgun Gothic', 'Apple SD Gothic Neo'])(
'carries the CJK-capable fallback %s',
(font) => {
expect(buildFontFamily('')).toContain(`"${font}"`)
}
)

it('keeps every CJK fallback behind the Latin monospace fonts', () => {
const chain = buildFontFamily('')
expect(chain.indexOf('"Hack Nerd Font"')).toBeLessThan(chain.indexOf('"D2Coding"'))
})

it('puts a chosen CJK font ahead of the built-in CJK fallbacks', () => {
const chain = buildFontFamily('JetBrains Mono', 'D2Coding Nerd Font')
expect(chain.indexOf('"D2Coding Nerd Font"')).toBeLessThan(
chain.indexOf('"Apple SD Gothic Neo"')
)
})

it('keeps a chosen CJK font behind the Nerd Fonts so it cannot claim PUA glyphs', () => {
const chain = buildFontFamily('JetBrains Mono', 'Apple SD Gothic Neo')
expect(chain.indexOf('"Symbols Nerd Font Mono"')).toBeLessThan(
chain.indexOf('"Apple SD Gothic Neo"')
)
})

it('does not list a chosen CJK font twice when it is already a fallback', () => {
const chain = buildFontFamily('', 'Apple SD Gothic Neo')
expect(chain.split('"Apple SD Gothic Neo"').length - 1).toBe(1)
})

it('ignores a whitespace-only CJK font', () => {
expect(buildFontFamily('SF Mono', ' ')).toBe(buildFontFamily('SF Mono'))
})

it('prefers the macOS Korean default over a Windows font that happens to be installed', () => {
const chain = buildFontFamily('')
expect(chain.indexOf('"Apple SD Gothic Neo"')).toBeLessThan(chain.indexOf('"Malgun Gothic"'))
})

// A CJK-locale OS registers these under the localized family name only, so the
// English name alone can silently match nothing.
it.each([
['Apple SD Gothic Neo', 'Apple SD 산돌고딕 Neo'],
['Malgun Gothic', '맑은 고딕'],
['Hiragino Sans', 'ヒラギノ角ゴシック']
])('lists %s under its localized name too', (english, localized) => {
const chain = buildFontFamily('')
expect(chain).toContain(`"${english}"`)
expect(chain).toContain(`"${localized}"`)
expect(chain.indexOf(`"${english}"`)).toBeLessThan(chain.indexOf(`"${localized}"`))
})

it('is the same chain the default pane options use', () => {
expect(DEFAULT_TERMINAL_FONT_FAMILY).toBe(buildFontFamily(''))
expect(buildDefaultTerminalOptions().fontFamily).toBe(buildFontFamily(''))
})
})

// ---------------------------------------------------------------------------
Expand Down
37 changes: 4 additions & 33 deletions src/renderer/src/components/terminal-pane/layout-serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,45 +22,16 @@ export {
normalizeTerminalLayoutSnapshot
} from './terminal-layout-leaf-ids'

// Re-exported from lib so lib-layer pane defaults can share the one chain without
// importing back into components.
export { buildFontFamily } from '@/lib/terminal-font-family'

export const EMPTY_LAYOUT: TerminalLayoutSnapshot = {
root: null,
activeLeafId: null,
expandedLeafId: null
}

// Cross-platform monospace chain: browsers skip fonts absent on the current OS, so listing all is safe.
// Nerd Fonts come last to cover PUA glyphs (U+E000–U+F8FF) from OMP/Powerline that standard monospace fonts lack.
const FALLBACK_FONTS = [
'SF Mono', // macOS 10.12+
'Menlo', // macOS (older)
'Monaco', // macOS (legacy)
'Cascadia Mono', // Windows 11+
'Consolas', // Windows Vista+
'DejaVu Sans Mono', // Linux (common)
'Liberation Mono', // Linux (common)
'Orca Nerd Font Symbols', // bundled PUA fallback for OMP/Powerline glyphs
'Symbols Nerd Font Mono', // purpose-built Nerd Fonts symbols-only fallback
'MesloLGS Nerd Font', // p10k's recommended font; very common on zsh setups
'JetBrainsMono Nerd Font', // widely installed; Ghostty ships a JBM-derived font
'Hack Nerd Font', // common Nerd Font among Linux developers
'monospace' // ultimate generic fallback
] as const

export function buildFontFamily(fontFamily: string): string {
const trimmed = fontFamily.trim()
const parts = trimmed ? [`"${trimmed}"`] : []
const lowerParts = parts.map((p) => p.toLowerCase())
// Append each fallback unless already present (case-insensitive) to avoid duplicates.
for (const fallback of FALLBACK_FONTS) {
const lower = fallback.toLowerCase()
if (!lowerParts.some((p) => p.includes(lower))) {
// Generic keywords like "monospace" are unquoted; named fonts are quoted.
parts.push(fallback === 'monospace' ? fallback : `"${fallback}"`)
}
}
return parts.join(', ')
}

export function getLayoutChildNodes(split: HTMLElement): HTMLElement[] {
return Array.from(split.children).filter(
(child): child is HTMLElement =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,10 @@ export function applyTerminalAppearance(
const paneSize = paneFontSizes.get(pane.id)
const metricOptions = {
fontSize: paneSize ?? settings.terminalFontSize,
fontFamily: buildFontFamily(settings.terminalFontFamily),
fontFamily: buildFontFamily(
settings.terminalFontFamily,
settings.terminalCjkFontFamily ?? ''
),
fontWeight: terminalFontWeights.fontWeight,
fontWeightBold: terminalFontWeights.fontWeightBold,
lineHeight: normalizeTerminalLineHeight(settings.terminalLineHeight)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1492,7 +1492,10 @@ export function useTerminalPaneLifecycle({
...windowsPtyCompatibilityOptions,
...keyboardProtocolOptions,
fontSize: currentSettings?.terminalFontSize ?? 14,
fontFamily: buildFontFamily(currentSettings?.terminalFontFamily ?? ''),
fontFamily: buildFontFamily(
currentSettings?.terminalFontFamily ?? '',
currentSettings?.terminalCjkFontFamily ?? ''
),
fontWeight: terminalFontWeights.fontWeight,
fontWeightBold: terminalFontWeights.fontWeightBold,
scrollback: normalizeDesktopTerminalScrollbackRows(
Expand Down
Loading
Loading