[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
189
application/state/uiFontStore.ts
Normal file
189
application/state/uiFontStore.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import { UI_FONTS, withUiCjkFallback, type UIFont } from '../../infrastructure/config/uiFonts';
|
||||
|
||||
/**
|
||||
* UI Font Store - singleton pattern using useSyncExternalStore
|
||||
* Fetches system fonts and combines with bundled fonts
|
||||
*/
|
||||
type Listener = () => void;
|
||||
|
||||
interface UIFontStoreState {
|
||||
availableFonts: UIFont[];
|
||||
isLoading: boolean;
|
||||
isLoaded: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type definition for Local Font Access API
|
||||
*/
|
||||
interface LocalFontData {
|
||||
family: string;
|
||||
}
|
||||
|
||||
class UIFontStore {
|
||||
private state: UIFontStoreState = {
|
||||
availableFonts: UI_FONTS,
|
||||
isLoading: false,
|
||||
isLoaded: false,
|
||||
error: null,
|
||||
};
|
||||
private listeners = new Set<Listener>();
|
||||
|
||||
getAvailableFonts = (): UIFont[] => this.state.availableFonts;
|
||||
getIsLoading = (): boolean => this.state.isLoading;
|
||||
getIsLoaded = (): boolean => this.state.isLoaded;
|
||||
|
||||
private notify = () => {
|
||||
Promise.resolve().then(() => {
|
||||
this.listeners.forEach(listener => listener());
|
||||
});
|
||||
};
|
||||
|
||||
private setState = (partial: Partial<UIFontStoreState>) => {
|
||||
this.state = { ...this.state, ...partial };
|
||||
this.notify();
|
||||
};
|
||||
|
||||
subscribe = (listener: Listener): (() => void) => {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
};
|
||||
|
||||
initialize = async (): Promise<void> => {
|
||||
if (this.state.isLoaded || this.state.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const localFonts = await this.getLocalFonts();
|
||||
|
||||
// Use a Map to deduplicate by normalized font name
|
||||
const fontMap = new Map<string, UIFont>();
|
||||
|
||||
// Add bundled fonts first (they have priority)
|
||||
UI_FONTS.forEach(font => fontMap.set(font.id, font));
|
||||
|
||||
// Add local fonts with a distinct ID namespace
|
||||
localFonts.forEach(font => {
|
||||
const localId = `local-${font.id}`;
|
||||
// Skip if a bundled font with similar name exists
|
||||
if (!fontMap.has(font.id)) {
|
||||
fontMap.set(localId, { ...font, id: localId });
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
availableFonts: Array.from(fontMap.values()),
|
||||
isLoading: false,
|
||||
isLoaded: true,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to load local fonts';
|
||||
console.warn('Failed to fetch local UI fonts, using defaults:', error);
|
||||
this.setState({
|
||||
availableFonts: UI_FONTS,
|
||||
isLoading: false,
|
||||
isLoaded: true,
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private async getLocalFonts(): Promise<UIFont[]> {
|
||||
if (typeof window === 'undefined' || !('queryLocalFonts' in window)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const queryLocalFonts = (window as unknown as { queryLocalFonts: () => Promise<LocalFontData[]> }).queryLocalFonts;
|
||||
const fonts = await queryLocalFonts();
|
||||
|
||||
// Deduplicate by family name
|
||||
const uniqueFamilies = new Set<string>();
|
||||
const dedupedFonts = fonts.filter(f => {
|
||||
if (uniqueFamilies.has(f.family)) return false;
|
||||
uniqueFamilies.add(f.family);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Map to UIFont structure
|
||||
return dedupedFonts.map(f => ({
|
||||
id: f.family.toLowerCase().replace(/\s+/g, '-'),
|
||||
name: f.family,
|
||||
family: withUiCjkFallback(`"${f.family}", system-ui`),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.warn('Failed to query local fonts:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getFontById = (fontId: string): UIFont => {
|
||||
const fonts = this.state.availableFonts;
|
||||
const found = fonts.find(f => f.id === fontId);
|
||||
if (found) return found;
|
||||
|
||||
// For local fonts that haven't been loaded yet, construct a fallback
|
||||
// This handles the case when main window receives a local font ID before fonts are loaded
|
||||
if (fontId.startsWith('local-')) {
|
||||
const fontName = fontId
|
||||
.replace(/^local-/, '')
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
return {
|
||||
id: fontId,
|
||||
name: fontName,
|
||||
family: withUiCjkFallback(`"${fontName}", system-ui`),
|
||||
};
|
||||
}
|
||||
|
||||
return fonts[0] || UI_FONTS[0];
|
||||
};
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const uiFontStore = new UIFontStore();
|
||||
|
||||
/**
|
||||
* Get available UI fonts - triggers initialization on first use
|
||||
*/
|
||||
export const useAvailableUIFonts = (): UIFont[] => {
|
||||
if (!uiFontStore.getIsLoaded() && !uiFontStore.getIsLoading()) {
|
||||
uiFontStore.initialize();
|
||||
}
|
||||
|
||||
return useSyncExternalStore(
|
||||
uiFontStore.subscribe,
|
||||
uiFontStore.getAvailableFonts
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get UI font loaded state
|
||||
*/
|
||||
export const useUIFontsLoaded = (): boolean => {
|
||||
return useSyncExternalStore(
|
||||
uiFontStore.subscribe,
|
||||
uiFontStore.getIsLoaded
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a font ID is valid
|
||||
*/
|
||||
export const isValidUiFontId = (fontId: string): boolean => {
|
||||
// Local fonts are always considered valid (they start with 'local-')
|
||||
if (fontId.startsWith('local-')) return true;
|
||||
return uiFontStore.getAvailableFonts().some(f => f.id === fontId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize UI fonts eagerly
|
||||
*/
|
||||
export const initializeUIFonts = (): void => {
|
||||
uiFontStore.initialize();
|
||||
};
|
||||
Reference in New Issue
Block a user