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
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-29 12:05:46 +08:00
1 джерело e537dd43a0
коміт aea0b33585
21 files changed
+647 -124

No files matched your search

+11 -3
Переглянути файл
@@ -28,6 +28,8 @@ export interface SelectProps<V extends string = string> {
renderValue?: (option: SelectOption<V> | undefined) => ReactNode;
}
const TYPE_AHEAD_MS = 800;
/** Dropdown select with a listbox popup (↑↓ Enter Esc, type-ahead). */
export function Select<V extends string = string>({
value,
@@ -45,6 +47,7 @@ export function Select<V extends string = string>({
const triggerRef = useRef<HTMLButtonElement>(null);
const current = options.find((o) => o.value === value);
const [active, setActive] = useState(0);
const typeAhead = useRef({ text: "", at: -Infinity });
const isSection = (o: SelectOption<V>) => !!o.disabled;
const openPopup = () => {
@@ -76,11 +79,16 @@ export function Select<V extends string = string>({
return;
}
if (e.key.length === 1 && !e.metaKey && !e.ctrlKey) {
const ch = e.key.toLowerCase();
// Keys typed in quick succession extend the prefix ("me" → Menlo); a lone key cycles.
const ahead = typeAhead.current;
ahead.text = e.timeStamp - ahead.at < TYPE_AHEAD_MS ? ahead.text + e.key.toLowerCase() : e.key.toLowerCase();
ahead.at = e.timeStamp;
const repeat = ahead.text.length > 1 && [...ahead.text].every((c) => c === ahead.text[0]);
const prefix = repeat ? ahead.text[0]! : ahead.text;
const n = options.length;
for (let s = 1; s <= n; s++) {
for (let s = prefix.length > 1 ? 0 : 1; s <= n; s++) {
const i = (active + s) % n;
if (!options[i]!.disabled && tx(options[i]!.label).toLowerCase().startsWith(ch)) {
if (!options[i]!.disabled && tx(options[i]!.label).toLowerCase().startsWith(prefix)) {
setActive(i);
break;
}