Files
nexusterm/src/main/desktop.ts
T
ylhan e1193ed032 Use the desktop's own monospace font in the terminal
Read org.gnome.desktop.interface monospace-font-name at startup rather than
hardcoding a family, so the terminal matches GNOME Terminal exactly and keeps
matching after the system font changes.

Pango reports points and CSS wants pixels, so 13 becomes 17.33px — rendering
it as 13px made the terminal a third too small, which reads as the wrong font
rather than the wrong size. The size is the trailing number, since families
may contain digits. Families are always quoted, because an unquoted
"Noto Sans Mono" is a three-family stack to CSS.

Also fixes tests/dial.test.ts, which hardcoded the test server's fingerprint
and so went red after every /tmp cleanup. It now asks ssh-keygen, which is a
stronger assertion anyway: the expected value comes from the reference
implementation rather than from this project's own fingerprint code.
2026-09-21 23:02:20 +08:00

98 lines
3.3 KiB
TypeScript

import { execFileSync } from 'node:child_process'
/**
* The font the desktop's own terminal would use.
*
* GNOME Terminal is configured with `use-system-font` by default, so what it
* renders is whatever `org.gnome.desktop.interface monospace-font-name` says.
* Reading that — rather than hardcoding a family — is what makes "the same font
* as GNOME Terminal" keep being true after someone changes their system font.
*/
export interface TerminalFont {
/** A CSS font-family value. */
family: string
/** In CSS pixels, converted from the point size the desktop reports. */
sizePx: number
}
/**
* What to use when the desktop will not say, or is not GNOME.
*
* `ui-monospace` is deliberately first: it asks the engine for the system
* monospace face, which is the same intent as the GSettings lookup, just
* resolved one layer down.
*/
export const FALLBACK_TERMINAL_FONT: TerminalFont = {
family:
'ui-monospace, "JetBrains Mono", "Cascadia Code", Menlo, Consolas, "Noto Sans Mono CJK SC", monospace',
sizePx: 13,
}
const DEFAULT_SIZE_PX = 13
/** Pango reports points; CSS wants pixels. 1pt = 1/72in, 1px = 1/96in. */
const PT_TO_PX = 96 / 72
/**
* Parse a Pango font description such as `'Ubuntu Mono 13'`.
*
* The size is the *trailing* number, not the first: families legitimately
* contain digits (`Noto Sans Mono 12` is a family called "Noto Sans Mono",
* while `Fira Code 12` vs `Fira Code 2 12` are different things). Splitting on
* the last whitespace-delimited number is the only rule that survives both.
*/
export function parseMonospaceFont(raw: string): TerminalFont | null {
// Either quote style, since the description's quoting is not part of what it
// means and the caller should not have to care which one it was handed.
const unquoted = raw.trim().replace(/^['"](.*)['"]$/, '$1').trim()
if (!unquoted) return null
const match = /^(.*\S)\s+(\d+(?:\.\d+)?)$/.exec(unquoted)
if (!match) {
// A bare family name with no size. Usable, sized by our default.
return { family: quoteFamily(unquoted), sizePx: DEFAULT_SIZE_PX }
}
const family = match[1].trim()
const points = Number(match[2])
if (!family || !Number.isFinite(points) || points <= 0) return null
return {
family: quoteFamily(family),
sizePx: Math.round(points * PT_TO_PX * 100) / 100,
}
}
/**
* Always quote, on both paths.
*
* A CSS `font-family` value is a comma-separated stack, so an unquoted
* `Noto Sans Mono` is read as three families — "Noto", then "Sans", then
* "Mono" — and the terminal silently falls back to something else entirely.
* Quoting a single-word family costs nothing.
*/
function quoteFamily(family: string): string {
return `"${family.replace(/"/g, '')}"`
}
/**
* Ask the desktop for its monospace font.
*
* Runs `gsettings` once at startup and falls back silently. A desktop that is
* not GNOME-based simply will not answer, and that is not an error worth
* surfacing — the fallback stack is the same request made a different way.
*/
export function readTerminalFont(): TerminalFont {
try {
const raw = execFileSync(
'gsettings',
['get', 'org.gnome.desktop.interface', 'monospace-font-name'],
{ encoding: 'utf8', timeout: 3_000, stdio: ['ignore', 'pipe', 'ignore'] },
)
return parseMonospaceFont(raw) ?? FALLBACK_TERMINAL_FONT
} catch {
return FALLBACK_TERMINAL_FONT
}
}