Files
opencode/services/site/script/local-mono-fonts.ts

82 lines
3.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bun
// Lists the monospaced font families installed on this Mac (the post table's isFixedPitch, read with
// fontkit) and writes their names to src/lib/local-mono-fonts.json, for Settings › Mono font's
// "Installed on this Mac" group. Only the names are kept; the fonts are never copied. fontkit
// installs into a temporary folder, so the app takes no dependency on it.
// Run from packages/console/app: bun script/local-mono-fonts.ts
import { mkdtemp, readdir, rm } from "fs/promises"
import { homedir, tmpdir } from "os"
import { dirname, join } from "path"
import { fileURLToPath } from "url"
const root = join(dirname(fileURLToPath(import.meta.url)), "..")
const tmp = await mkdtemp(join(tmpdir(), "fontkit-"))
await Bun.$`bun add fontkit@2.0.4`.cwd(tmp).quiet()
type Face = {
familyName?: string
name?: { records?: { preferredFamily?: Record<string, string> } }
post?: { isFixedPitch?: number }
glyphsForString?: (text: string) => ({ id: number; advanceWidth: number } | null)[]
}
const fontkit: { openSync: (path: string) => Face & { fonts?: Face[] } } = await import(
join(tmp, "node_modules/fontkit/dist/main.cjs")
)
// The families the site offers on their own, and Apple's hidden UI faces.
const skipped = ["IBM Plex Mono", "OpenTUI Mono", "Apple Color Emoji", "LastResort", "GB18030 Bitmap"]
const ascii = Array.from({ length: 95 }, (_, index) => String.fromCharCode(32 + index)).join("")
const folders = [join(homedir(), "Library/Fonts"), "/Library/Fonts", "/System/Library/Fonts"]
const files = (
await Promise.all(
folders.map((folder) =>
readdir(folder, { recursive: true })
.then((names) => names.map((name) => join(folder, name)))
.catch(() => []),
),
)
)
.flat()
.filter((file) => /\.(otf|ttf|ttc|otc|dfont)$/i.test(file))
const faces = files.flatMap((file) => {
const font = (() => {
try {
return fontkit.openSync(file)
} catch {
return undefined
}
})()
if (!font) return []
return (font.fonts ?? [font]).flatMap((face) => {
const legacy = face.familyName?.trim()
if (!legacy) return []
// The typographic family ("Geist Mono"), not the legacy one per weight ("Geist Mono Light").
const family = face.name?.records?.preferredFamily?.en?.trim() ?? legacy
return [{ family, legacy, mono: monospaced(face) }]
})
})
await rm(tmp, { recursive: true, force: true })
// A mono face in a family with proportional faces (Apercu Pro's Mono) goes by its legacy name, which
// CSS also matches, so the name never picks the proportional faces.
const mixed = new Set(faces.filter((face) => !face.mono).map((face) => face.family))
const list = [
...new Set(faces.filter((face) => face.mono).map((face) => (mixed.has(face.family) ? face.legacy : face.family))),
]
.filter((name) => !name.startsWith(".") && !skipped.includes(name))
.toSorted((a, b) => a.localeCompare(b))
await Bun.write(join(root, "src/lib/local-mono-fonts.json"), JSON.stringify(list, null, 2) + "\n")
console.log(`${list.length} monospaced families\n${list.join("\n")}`)
// Some monospaced fonts leave isFixedPitch unset (Monaco, Monaspace): every printable ASCII
// character then has the same advance. Fonts without Latin (Braille, STIX, Kailasa) map those
// characters to the missing glyph, so each needs a glyph of its own either way.
function monospaced(face: Face) {
const glyphs = (() => {
try {
return face.glyphsForString?.(ascii) ?? []
} catch {
return []
}
})()
if (glyphs.length !== ascii.length || glyphs.some((glyph) => !glyph || glyph.id === 0)) return false
return !!face.post?.isFixedPitch || glyphs.every((glyph) => glyph?.advanceWidth === glyphs[0]?.advanceWidth)
}