mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
b1ab4d7105
* Enable Nerd Font glyphs in terminal font picker and rendering
- Grant local-fonts permission on the default session so queryLocalFonts()
can enumerate user-installed fonts; without it the picker only showed
the 20 hard-coded built-ins, hiding Nerd Font sub-families like
"JetBrainsMono Nerd Font Mono".
- Append a Symbols Nerd Font fallback to the terminal fontFamily chain so
PUA icons (powerline / devicons / etc.) resolve even when the primary
font lacks them, matching the cross-font fallback behavior CoreText-based
terminals like Ghostty already provide.
- Whitelist "Symbols Nerd Font" / "Symbols Nerd Font Mono" in the local
monospace allow-list so the symbol-only icon font is not filtered out.
Refs #843
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Restrict permission handler to app origin
Address review feedback on PR #845: the previous permissive fallthrough
granted every permission request/check that hit the default session,
which the in-app OAuth flow uses too. That meant remote OAuth pages
(accounts.google.com, login.microsoftonline.com, ...) could be auto-
approved for camera, microphone, geolocation, notifications, etc.
Gate the handler on the requesting origin: only the app's own renderer
(app://netcatty plus the dev server in dev) gets the local-fonts grant
and the prior approve-by-default behavior. Anything loaded from a
third-party origin is denied outright.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Use explicit permission allow-list for app origin
Address Codex P1 on PR #845 commit 975ca7e8: even after gating on the
app origin, the previous fallthrough still called callback(true) for
every non-local-fonts permission, so the main/settings renderers were
silently auto-granted notifications, geolocation, pointer lock, media,
etc. — none of which the app uses.
Replace the fallthrough with an explicit allow-list of the permissions
the renderer actually exercises (local-fonts plus clipboard read/write
for terminal + SFTP copy-paste). Anything outside that set is now
denied for the app origin too, matching the deny-by-default posture
Codex flagged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Match app:// origin by protocol+host, not URL.origin
Address Codex P1 on PR #845: in the packaged build the renderer loads
app://netcatty/index.html, but Node's WHATWG URL parser does not treat
app: as a standard scheme, so `new URL('app://netcatty/...').origin`
evaluates to the string "null". The previous Set-based origin check
therefore never matched the production renderer, causing the new
permission handlers to deny local-fonts as well as the existing
clipboard-read / clipboard-sanitized-write — breaking the font picker
and clipboard flows in release builds.
Compare protocol + host directly for app://, and keep the .origin
lookup for the dev server (which is HTTP-family and parses normally).
Verified against the relevant URL shapes (packaged main + settings,
dev server, third-party OAuth, file://).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
132 lines
3.8 KiB
TypeScript
132 lines
3.8 KiB
TypeScript
import { TerminalFont, withCjkFallback } from "../infrastructure/config/fonts"
|
|
|
|
/**
|
|
* Type definition for Local Font Access API
|
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/Local_Font_Access_API
|
|
*/
|
|
interface LocalFontData {
|
|
family: string;
|
|
}
|
|
|
|
/**
|
|
* Known monospace font families that don't follow naming conventions.
|
|
* These are popular programming/terminal fonts that should be included.
|
|
*/
|
|
const KNOWN_MONOSPACE_FONTS = new Set([
|
|
// Popular programming fonts
|
|
'iosevka',
|
|
'hack',
|
|
'consolas',
|
|
'menlo',
|
|
'monaco',
|
|
'inconsolata',
|
|
'mononoki',
|
|
'fantasque sans mono',
|
|
'anonymous pro',
|
|
'liberation mono',
|
|
'dejavu sans mono',
|
|
'droid sans mono',
|
|
'ubuntu mono',
|
|
'roboto mono',
|
|
'source code pro',
|
|
'fira code',
|
|
'fira mono',
|
|
'jetbrains mono',
|
|
'cascadia code',
|
|
'cascadia mono',
|
|
'victor mono',
|
|
'ibm plex mono',
|
|
'sf mono',
|
|
'operator mono',
|
|
'input mono',
|
|
'pragmata pro',
|
|
'berkeley mono',
|
|
'monaspace',
|
|
'geist mono',
|
|
'comic mono',
|
|
'courier',
|
|
'courier new',
|
|
'lucida console',
|
|
'pt mono',
|
|
'overpass mono',
|
|
'space mono',
|
|
'go mono',
|
|
'noto sans mono',
|
|
'sarasa mono',
|
|
'maple mono',
|
|
'meslolgs nf',
|
|
'symbols nerd font mono',
|
|
'symbols nerd font',
|
|
]);
|
|
|
|
/**
|
|
* Suffix indicators that suggest a font is monospace
|
|
*/
|
|
const MONO_SUFFIX_INDICATORS = ['mono', 'monospace', 'code', 'terminal', 'console'];
|
|
|
|
/**
|
|
* Checks if a font family name indicates a monospace font.
|
|
* Uses both known font list and suffix matching for comprehensive detection.
|
|
*/
|
|
function isMonospaceFont(familyName: string): boolean {
|
|
const familyLower = familyName.toLowerCase().trim();
|
|
|
|
// Check against known monospace fonts (exact or partial match)
|
|
for (const knownFont of KNOWN_MONOSPACE_FONTS) {
|
|
if (familyLower === knownFont || familyLower.startsWith(knownFont + ' ')) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Check suffix indicators with word boundary
|
|
return MONO_SUFFIX_INDICATORS.some(indicator => {
|
|
return (
|
|
familyLower === indicator ||
|
|
familyLower.endsWith(' ' + indicator) ||
|
|
familyLower.endsWith('-' + indicator) ||
|
|
familyLower.includes(' ' + indicator + ' ')
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Queries local monospace fonts from the system using the Font Access API.
|
|
* Returns an empty array if the API is not available or permission is denied.
|
|
*/
|
|
export async function getMonospaceFonts(): Promise<TerminalFont[]> {
|
|
// Check if the Font Access API is available
|
|
if (typeof window === "undefined" || !("queryLocalFonts" in window)) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const queryLocalFonts = (window as unknown as { queryLocalFonts: () => Promise<LocalFontData[]> }).queryLocalFonts;
|
|
const fonts = await queryLocalFonts();
|
|
|
|
// Filter monospace fonts using robust word boundary matching
|
|
const monoFonts = fonts.filter(f => isMonospaceFont(f.family));
|
|
|
|
// Deduplicate by family name, case-insensitive (API may return multiple entries per family)
|
|
const uniqueFamilies = new Set<string>();
|
|
const dedupedFonts = monoFonts.filter(f => {
|
|
const key = f.family.toLowerCase();
|
|
if (uniqueFamilies.has(key)) return false;
|
|
uniqueFamilies.add(key);
|
|
return true;
|
|
});
|
|
|
|
// Map to TerminalFont structure with CJK fallback applied
|
|
return dedupedFonts.map(f => ({
|
|
id: f.family,
|
|
name: f.family,
|
|
family: withCjkFallback(f.family + ', monospace'),
|
|
description: `Local font: ${f.family}`,
|
|
category: 'monospace' as const,
|
|
}));
|
|
} catch (error) {
|
|
// Handle permission denied or other errors gracefully
|
|
console.warn('Failed to query local fonts:', error);
|
|
return [];
|
|
}
|
|
}
|