дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(settings): List every installed font family in the font pickers (#225)
The font pickers never read the installed fonts: they probed a hard-coded list of ~30 names with canvas width checks, so anything outside the list never appeared and the monospaced filter changed nothing. A new ide-fonts crate asks the platform font system for families and their fixed-pitch flag (CoreText on macOS, DirectWrite on Windows, fc-list on Linux/BSD), exposed as system_font_families (spawn_blocking, cached). Every font picker — editor font and fallback, terminal font and fallback, UI custom font — uses one FontSelect with prefix type-ahead, a real monospaced filter, and keeps an uninstalled selection visible. Not verified on Windows or Linux machines. Fixes #199 Claude-Session: https://claude.ai/code/session_01Dq6jQQn8ZhRfkkyj3PRutA
Цей коміт міститься в:
1 джерело
e537dd43a0
коміт
aea0b33585
21 files changed
+647
-124
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { monoFontFamily } from "./fonts";
|
||||
import { monoFontFamily, uiFontFamily } from "./fonts";
|
||||
|
||||
const BASE = "var(--font-mono)";
|
||||
|
||||
@@ -22,3 +22,13 @@ describe("monoFontFamily", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("uiFontFamily", () => {
|
||||
it.each([
|
||||
["Inter", '"Inter Variable", "Inter", system-ui, sans-serif'],
|
||||
["JetBrains Mono", '"JetBrains Mono Variable", "JetBrains Mono", system-ui, sans-serif'],
|
||||
[' "PingFang SC" ', '"PingFang SC", system-ui, sans-serif'],
|
||||
])("%j", (family, expected) => {
|
||||
expect(uiFontFamily(family)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -30,6 +30,20 @@ export function monoFontFamily(family: string | undefined, fallback = "", base =
|
||||
return [...head, ...(fb ? [quote(fb)] : []), base].join(", ");
|
||||
}
|
||||
|
||||
export const BUNDLED_UI_FAMILY = "Inter";
|
||||
const BUNDLED_UI_FACES = ['"Inter Variable"', '"Inter"'];
|
||||
|
||||
/**
|
||||
* The `--font-ui` stack for a custom UI font. The bundled families map to their
|
||||
* bundled faces, like `monoFontFamily`, so they render without a system install.
|
||||
*/
|
||||
export function uiFontFamily(family: string): string {
|
||||
const name = family.replace(/"/g, "").trim();
|
||||
const head =
|
||||
name === BUNDLED_UI_FAMILY ? BUNDLED_UI_FACES : name === BUNDLED_MONO_FAMILY ? BUNDLED_MONO_FACES : [quote(name)];
|
||||
return [...head, "system-ui", "sans-serif"].join(", ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves once the faces `family` uses at `size` are loaded, so code that measures
|
||||
* glyphs (xterm's cell size) measures the real font rather than the swap fallback.
|
||||
|
||||
@@ -2038,6 +2038,8 @@
|
||||
"Template name": "模板名称",
|
||||
"Template text": "模板文本",
|
||||
"Terminal": "终端",
|
||||
"Terminal fallback font": "终端备用字体",
|
||||
"Terminal font": "终端字体",
|
||||
"Terminal font size": "终端字体大小",
|
||||
"Terminal line height": "终端行高",
|
||||
"Terminal sessions": "终端会话",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fontNames } from "./systemFonts";
|
||||
|
||||
const fonts = [
|
||||
{ name: "menlo", monospaced: true },
|
||||
{ name: "Arial", monospaced: false },
|
||||
{ name: "Courier New", monospaced: true },
|
||||
{ name: "Ångström Sans", monospaced: false },
|
||||
];
|
||||
|
||||
describe("fontNames", () => {
|
||||
it("sorts installed families case- and accent-insensitively", () => {
|
||||
expect(fontNames(fonts)).toEqual(["Ångström Sans", "Arial", "Courier New", "menlo"]);
|
||||
});
|
||||
|
||||
it("filters to monospaced families", () => {
|
||||
expect(fontNames(fonts, { monospacedOnly: true })).toEqual(["Courier New", "menlo"]);
|
||||
});
|
||||
|
||||
it("always includes the extra names, spelled as given, without duplicates", () => {
|
||||
expect(fontNames(fonts, { monospacedOnly: true, include: ["JetBrains Mono", "Menlo", "Arial", " "] })).toEqual([
|
||||
"Arial",
|
||||
"Courier New",
|
||||
"JetBrains Mono",
|
||||
"Menlo",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the current family when nothing is installed", () => {
|
||||
expect(fontNames([], { include: ["Fira Code"] })).toEqual(["Fira Code"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* The font families installed on this machine (`system_font_families`: CoreText,
|
||||
* DirectWrite or fontconfig), for every font picker. Read once per window; the
|
||||
* backend caches the list for the run.
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import { call, commands, type FontFamily } from "./ipc";
|
||||
|
||||
let cached: FontFamily[] | null = null;
|
||||
let pending: Promise<FontFamily[]> | null = null;
|
||||
|
||||
/** The installed families; empty when the font system can't be read. */
|
||||
export function loadSystemFonts(): Promise<FontFamily[]> {
|
||||
pending ??= call(commands.systemFontFamilies())
|
||||
.catch(() => [] as FontFamily[])
|
||||
.then((list) => (cached = list));
|
||||
return pending;
|
||||
}
|
||||
|
||||
/** The installed families, empty until the first read resolves. */
|
||||
export function useSystemFonts(): FontFamily[] {
|
||||
const [fonts, setFonts] = useState<FontFamily[]>(() => cached ?? []);
|
||||
useEffect(() => {
|
||||
if (cached) return;
|
||||
let live = true;
|
||||
void loadSystemFonts().then((list) => live && setFonts(list));
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
}, []);
|
||||
return fonts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Picker entries: the installed families (only monospaced ones when asked),
|
||||
* plus `include` (the bundled faces, and the current value so a family that
|
||||
* isn't installed here still shows as selected), deduplicated and sorted.
|
||||
*/
|
||||
export function fontNames(
|
||||
fonts: readonly FontFamily[],
|
||||
{ monospacedOnly = false, include = [] }: { monospacedOnly?: boolean; include?: readonly string[] } = {},
|
||||
): string[] {
|
||||
const seen = new Map<string, string>();
|
||||
const add = (name: string) => {
|
||||
const n = name.trim();
|
||||
if (n && !seen.has(n.toLowerCase())) seen.set(n.toLowerCase(), n);
|
||||
};
|
||||
include.forEach(add);
|
||||
for (const f of fonts) if (!monospacedOnly || f.monospaced) add(f.name);
|
||||
return [...seen.values()].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
|
||||
}
|
||||
|
||||
/** Test hook: forget the cached list. */
|
||||
export function resetSystemFontsForTests(): void {
|
||||
cached = null;
|
||||
pending = null;
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача