Skip to content

Instantly share code, notes, and snippets.

@micschk
Last active September 10, 2026 08:59
Show Gist options
  • Select an option

  • Save micschk/8c3c3b38a91283f674429073e342233a to your computer and use it in GitHub Desktop.

Select an option

Save micschk/8c3c3b38a91283f674429073e342233a to your computer and use it in GitHub Desktop.
Inkscape Font Sync (macOS) — make Inkscape's font dropdown honour Font Book's active state via Fontconfig + a launchd-triggered symlink farm

Inkscape Font Sync (macOS)

Make Inkscape's font dropdown show only fonts you have active in Font Book — instead of every .otf/.ttf on your Mac including Apple's 300+ language and decorative fonts (Devanagari MT, Kohinoor Devanagari, Kannada MN, PingFang, Osaka, Chalkboard, etc.) and its ~80 dot-prefixed hidden system fonts.

The problem

Inkscape on macOS uses Fontconfig, not the native CoreText / Font Book stack. Fontconfig scans font directories directly and doesn't know about Font Book's activation state, so every font on disk shows up in Inkscape's dropdown regardless of whether Font Book has it enabled. On a stock macOS install that means hundreds of fonts, most of which you don't use.

Compounding the problem: Inkscape.app's bundled Fontconfig has been hand-edited by its maintainer to strip standard user-include paths. So even if you write a ~/.config/fontconfig/fonts.conf, Inkscape.app never reads it. This is why "just add a fontconfig" doesn't work.

There's no way to make Inkscape honour Font Book directly — the two systems don't talk.

The solution

Three decoupled pieces working together:

  1. A Fontconfig config at ~/.config/fontconfig/fonts.conf that hides fonts from /System/Library/Fonts/Supplemental/, ~/Library/Fonts/, /Library/Fonts/, and /Library/Application Support/Apple/Fonts/, AND rejects the ~80 dot-prefixed "hidden" macOS families (.CJK Symbols Fallback *, .SF Arabic, .LastResort, .Aqua Kana, .New York, etc. — Font Book hides these by the . prefix convention; Fontconfig has no such convention, hence the explicit list). Keeps /System/Library/Fonts/ (base UI fonts minus the dot-hidden ones) and a curated dir ~/InkscapeFonts/ visible.
  2. A sync script + launchd agent that populates ~/InkscapeFonts/ with symlinks to every font currently active in Font Book. Fires at load and whenever Font Book's state or ~/Library/Fonts/ changes (throttled to once every 30 s).
  3. A patch to Inkscape.app's bundled fonts.conf that adds an <include> for ~/.config/fontconfig/fonts.conf. Without this patch, the whole setup is a no-op from Inkscape.app's perspective (see problem section). The patch is lost on every Inkscape.app update — re-run install.sh to re-apply.

Net result: Inkscape sees only the base OS UI fonts (~30, minus the dot-hidden ones) plus whatever Font Book has active. Toggle a font in Font Book → Inkscape's list updates within 30 s of the next launch. Expected fc-list totals drop from ~4700 to ~900; unique families from ~390 to ~310.

Optional: CSS-generic family aliases

fonts.conf.template also contains a commented-out block of CSS-generic family aliases (sans-serif, serif, monospace, cursive, fantasy → real installed families, with multi-platform preference lineups). By default Fontconfig maps these generics to fonts in the noisy dirs we reject (Verdana, Times New Roman, etc.) — visible on-canvas but flagged with a ⚠️ in Inkscape's font picker as "not a real installed family." The optional aliases pin the generics to fonts that ARE visible in the picker.

To enable: after install, edit ~/.config/fontconfig/fonts.conf and remove the <!-- / --> wrappers around the alias block. See the block's header comment for the caveat (Inkscape still shows "sans-serif" verbatim in the font field for objects that use the CSS generic — that's a picker UI thing).

Requirements

  • macOS 13+ (tested on Tahoe 26.6.1; Font Book state must live at ~/Library/Containers/com.apple.FontBook/Data/Library/Preferences/com.apple.FontBook.plist — sandboxed Font Book)
  • jq (Apple ships one at /usr/bin/jq since macOS 15; earlier: brew install jq)
  • fontconfig (Homebrew: brew install fontconfig)
  • python3 (Apple ships one at /usr/bin/python3) — used by install.sh for the Inkscape.app fonts.conf patch
  • Inkscape.app (official DMG) or Homebrew Inkscape — both use Fontconfig

Install

One-shot

git clone https://gist.github.com/micschk/8c3c3b38a91283f674429073e342233a.git inkscape-font-sync
cd inkscape-font-sync
./install.sh

Or download the files individually from the gist page and run ./install.sh.

The installer:

  1. Verifies prerequisites (jq, fc-cache, fc-list)
  2. Creates ~/.config/fontconfig/, ~/bin/, ~/InkscapeFonts/, ~/Library/LaunchAgents/
  3. Enumerates the dot-hidden macOS family list from fc-list (per your macOS version)
  4. Renders fonts.conf.template → ~/.config/fontconfig/fonts.conf with the hidden-family reject block embedded
  5. Installs sync-inkscape-fonts.sh and local.inkscape-font-sync.plist (renders __HOME__ to your actual home path)
  6. Patches /Applications/Inkscape.app/Contents/Resources/etc/fonts/fonts.conf to include your user config (backup saved as .orig-before-inkscape-font-sync)
  7. Clears Inkscape's font cache so it re-reads on next launch
  8. Runs the first sync manually (~15 s)
  9. Bootstraps the launchd agent

Fully quit Inkscape (⌘Q) and relaunch — Inkscape caches its font list on launch and won't pick up the change otherwise.

Manual

# 1. Fontconfig config — splice the dot-hidden family list from your local macOS
mkdir -p ~/.config/fontconfig
hidden=$(fc-list : family | tr ',' '\n' | sed 's/^[[:space:]]*//' | sort -u | grep '^\.' \
  | awk '{printf "      <pattern><patelt name=\"family\"><string>%s</string></patelt></pattern>\n", $0}')
awk -v repl="$hidden" '{gsub(/__HIDDEN_FAMILIES__/, repl); print}' fonts.conf.template \
  > ~/.config/fontconfig/fonts.conf

# 2. Sync script
mkdir -p ~/bin ~/InkscapeFonts
cp sync-inkscape-fonts.sh ~/bin/
chmod +x ~/bin/sync-inkscape-fonts.sh

# 3. Launchd agent — substitute your home path
sed "s|__HOME__|$HOME|g" local.inkscape-font-sync.plist \
  > ~/Library/LaunchAgents/local.inkscape-font-sync.plist

# 4. Patch Inkscape.app's bundled fontconfig to include our user config
INKCONF=/Applications/Inkscape.app/Contents/Resources/etc/fonts/fonts.conf
cp -n "$INKCONF" "$INKCONF.orig-before-inkscape-font-sync"
python3 -c "
import re
src = open('$INKCONF').read()
patch = '\n\t<include ignore_missing=\"yes\">~/.config/fontconfig/fonts.conf</include>\n'
open('$INKCONF','w').write(re.sub(r'(<include ignore_missing=\"yes\">conf\.d</include>\n)', r'\1'+patch, src, count=1))
"

# 5. Nuke Inkscape's font cache
rm -rf ~/Library/Caches/org.inkscape.Inkscape/fontconfig/*

# 6. First sync + bootstrap the agent
~/bin/sync-inkscape-fonts.sh
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/local.inkscape-font-sync.plist

macOS will show a background-activity notification for sync-inkscape-fonts.sh; grant it "Allow in the Background" under System Settings → General → Login Items & Extensions.

Verify

# What's Fontconfig seeing?
fc-list | wc -l

# Any dot-hidden families still visible? Should be 0.
fc-list : family | tr ',' '\n' | sed 's/^[[:space:]]*//' | sort -u | grep -c '^\.'

# What's in the curated dir?
ls ~/InkscapeFonts | wc -l

# What do the CSS generics resolve to now?
for g in sans-serif serif monospace cursive fantasy; do
  printf "%-11s → " "$g"; fc-match "$g"
done

# Agent state
launchctl print "gui/$(id -u)/local.inkscape-font-sync"

# Log tail
tail -f ~/Library/Logs/sync-inkscape-fonts.log

How to use it day-to-day

  • Enable/disable fonts in Font Book normally. The launchd agent detects the Font Book plist change (within 30 s) and resyncs ~/InkscapeFonts/. Restart Inkscape to pick up the new list.
  • Force a font into Inkscape independent of Font Book state: drop the real .otf file (or a manual symlink) into ~/InkscapeFonts/. The sync script only manages symlinks — any real file there survives every resync.
  • Manual resync: ~/bin/sync-inkscape-fonts.sh (takes ~15 s — the sync uses system_profiler which walks every font on the system).
  • Uninstall: ./install.sh --uninstall (reverts the Inkscape.app patch from backup, unloads the agent, removes the files).

Re-run after updates

Re-run ./install.sh after:

  • An Inkscape.app update — Inkscape overwrites its bundled fonts.conf, wiping our <include> patch. The installer is idempotent and will re-patch.
  • A macOS major update — Apple may add or remove dot-hidden font families. The installer regenerates the reject list from fc-list, so the updated set is picked up.

Configuration knobs

Edit ~/.config/fontconfig/fonts.conf if:

  • You want to enable the CSS-generic family aliases — remove the <!-- / --> wrappers around the alias block near the end of the file. Fontconfig will then map sans-serif / serif / monospace / cursive / fantasy to specific fonts (Helvetica Neue, Georgia, Menlo, Apple Chancery, Impact by default). Trim/reorder the preference lineups to taste.
  • You want /System/Library/Fonts/ (base) hidden too — add <glob>/System/Library/Fonts/*</glob> to the reject list. Warning: this may break Inkscape's UI text rendering if it loses its fallback fonts.
  • You want a different curated dir — change ~/InkscapeFonts in both the <dir> element AND the <acceptfont> <glob>, and update sync-inkscape-fonts.sh (DEST) + the plist (WatchPaths and hardcoded paths).

Edit ~/Library/LaunchAgents/local.inkscape-font-sync.plist if:

  • You want a different throttle window (ThrottleInterval)
  • You want to watch different paths (WatchPaths)

After any plist change: launchctl bootout gui/$(id -u)/local.inkscape-font-sync && launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.inkscape-font-sync.plist.

Caveats

  • Inkscape has its own font-list cache and only refreshes on launch. A full quit (⌘Q) + relaunch is required after the sync updates the set (or after enabling the CSS aliases).
  • The bundled Fontconfig inside Inkscape.app strips user-include paths — patched by install.sh, but the patch is lost on every Inkscape update. Re-run install.sh after each update. Homebrew Inkscape's config is normal Fontconfig and reads ~/.config/fontconfig/fonts.conf automatically.
  • system_profiler SPFontsDataType is slow (~10-30 s). The launchd ThrottleInterval: 30 prevents burst-firing.
  • The system_profiler JSON schema could drift on future macOS updates. If the sync suddenly reports 0 fonts, inspect one entry with system_profiler -json SPFontsDataType | jq '.SPFontsDataType[0]' and adjust the jq filter in the script.
  • Inkscape's font picker still shows sans-serif / serif / etc. verbatim for text objects that use those CSS generics (with a strikethrough + ⚠️ icon). That's Inkscape flagging the generic as "not a specific installed family." To make new text default to a real family: Edit → Preferences → Tools → Text and set the default font family. The optional CSS-generic aliases in fonts.conf control what those generics render as, but don't change what the picker displays.

Files in this gist

File Installs to
fonts.conf.template ~/.config/fontconfig/fonts.conf (with __HIDDEN_FAMILIES__ rendered from local fc-list at install time; CSS-generic aliases included commented-out for opt-in)
sync-inkscape-fonts.sh ~/bin/sync-inkscape-fonts.sh (executable)
local.inkscape-font-sync.plist ~/Library/LaunchAgents/local.inkscape-font-sync.plist (__HOME__ substituted at install time)
— Also patches /Applications/Inkscape.app/Contents/Resources/etc/fonts/fonts.conf (backup saved as .orig-before-inkscape-font-sync)
install.sh (run once, not installed)
README.md (this)

License

Public domain / do what you like. No warranty.

<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
<!--
Inkscape Font Sync — Fontconfig snippet TEMPLATE.
Installer renders __HIDDEN_FAMILIES__ from the local fc-list output (macOS-version-
dependent — macOS 26 Tahoe has ~82; earlier macOS versions have different sets).
Installs to: ~/.config/fontconfig/fonts.conf
Purpose: keep Inkscape's font dropdown short by only surfacing:
(a) fonts under ~/InkscapeFonts/ (managed by sync-inkscape-fonts.sh: symlinks
to fonts currently "active" in Font Book, or manually dropped by hand)
(b) the base /System/Library/Fonts/ dir (needed as fallback for UI text rendering) —
MINUS the dot-prefixed "hidden" macOS families (`.ADT Slab Numeric`, `.CJK
Symbols Fallback *`, `.SF Arabic`, `.LastResort`, etc.). Font Book hides these
by the `.` prefix convention; Fontconfig has no such convention, hence the
explicit list generated by install.sh.
Fonts under /System/Library/Fonts/Supplemental/, ~/Library/Fonts/, /Library/Fonts/,
and /Library/Application Support/Apple/Fonts/ are hidden — they're the noisy set
(Devanagari MT, Kohinoor Devanagari, ITF Devanagari, Kannada MN, PingFang, Osaka,
Chalkboard, etc.). Any font from those paths that Font Book has activated will still
show up in Inkscape via its symlink under ~/InkscapeFonts/.
Interaction: <acceptfont> overrides <rejectfont>, so anything under ~/InkscapeFonts/
survives even though its symlink target may live in a rejected path.
Also patched into Inkscape.app's bundled fonts.conf via install.sh — because that
bundled config was hand-edited by the Inkscape maintainer to strip standard user-
config includes and only scan the base macOS font dirs. Without that patch,
Inkscape.app never reads this file (it happily reads its own bundled conf.d and
nothing else).
See sync-inkscape-fonts.sh and local.inkscape-font-sync.plist.
-->
<fontconfig>
<!-- Curated Inkscape font dir (symlinked farm from Font Book active state). -->
<dir>~/InkscapeFonts</dir>
<selectfont>
<rejectfont>
<!-- Apple's supplementary language/decorative fonts (Devanagari, PingFang, Osaka, etc.).
Also includes some common ones (Georgia, Verdana, Trebuchet MS, Comic Sans) — those
will re-appear in Inkscape only if Font Book has them active AND the sync script
symlinks them into ~/InkscapeFonts/. -->
<glob>/System/Library/Fonts/Supplemental/*</glob>
<!-- Apple's application-support fonts (Kannada MN, Diwan Kufi, InaiMathi, etc.). -->
<glob>/Library/Application Support/Apple/Fonts/*</glob>
<!-- User-installed and machine-wide user font dirs. Fonts in here that Font Book has
active will re-appear via the sync script. -->
<glob>~/Library/Fonts/*</glob>
<glob>/Library/Fonts/*</glob>
</rejectfont>
<!-- Any font whose file path lives under our curated dir survives even if its
symlink target sits under a rejected glob above. -->
<acceptfont>
<glob>~/InkscapeFonts/*</glob>
</acceptfont>
</selectfont>
<!-- Dot-prefixed macOS "hidden" families that live in /System/Library/Fonts/ base
(kept alive above for UI fallbacks). Font Book hides these by the `.` prefix
convention; Fontconfig has no such convention, hence the explicit list.
Generated at install time from `fc-list : family | grep '^\.'` — re-run
install.sh to regenerate after a macOS update. -->
<selectfont>
<rejectfont>
__HIDDEN_FAMILIES__
</rejectfont>
</selectfont>
<!-- ============================================================
CSS generic family aliases (OPTIONAL — uncomment to enable)
============================================================
Map the CSS generic family names (sans-serif, serif, monospace, cursive, fantasy)
to specific installed families. Fontconfig picks the first available from the
preference list.
WHY: Fontconfig's default 45-latin.conf resolves sans-serif → Verdana (which
lives in /System/Library/Fonts/Supplemental/ — hidden by our reject rules from
the font picker). Inkscape then flags text with font-family="sans-serif" as
⚠️ "unavailable" even though rendering works. These aliases pin the CSS generics
to fonts that ARE visible in the picker, so rendering matches the picker's
preview and the resolved family is one the user can find in the list.
CAVEAT: Inkscape's font-picker still displays the literal string "sans-serif"
in the font field for objects whose font-family is set to a CSS generic —
that's an Inkscape UI thing, not a Fontconfig thing. To make new text default
to a real family: Edit → Preferences → Tools → Text.
LINEUPS: multi-platform CSS stacks (macOS priority first, then Windows, then
Linux fallbacks). Feel free to trim/reorder for your taste. Comment out any
block you don't want an alias for.
TO ENABLE: remove the "<!==" opening and "==>" closing lines below.
-->
<!--
<alias binding="same">
<family>sans-serif</family>
<prefer>
<family>Helvetica Neue</family>
<family>Helvetica</family>
<family>Arial</family>
<family>Segoe UI</family>
<family>Roboto</family>
<family>Noto Sans</family>
<family>Liberation Sans</family>
<family>DejaVu Sans</family>
</prefer>
</alias>
<alias binding="same">
<family>serif</family>
<prefer>
<family>Georgia</family>
<family>Times New Roman</family>
<family>Times</family>
<family>Iowan Old Style</family>
<family>Charter</family>
<family>Noto Serif</family>
<family>Liberation Serif</family>
<family>DejaVu Serif</family>
</prefer>
</alias>
<alias binding="same">
<family>monospace</family>
<prefer>
<family>Menlo</family>
<family>Monaco</family>
<family>SF Mono</family>
<family>Consolas</family>
<family>Courier New</family>
<family>Courier</family>
<family>Liberation Mono</family>
<family>DejaVu Sans Mono</family>
</prefer>
</alias>
<alias binding="same">
<family>cursive</family>
<prefer>
<family>Apple Chancery</family>
<family>Snell Roundhand</family>
<family>Brush Script MT</family>
<family>Comic Sans MS</family>
<family>Zapfino</family>
<family>URW Chancery L</family>
</prefer>
</alias>
<alias binding="same">
<family>fantasy</family>
<prefer>
<family>Impact</family>
<family>Papyrus</family>
<family>Chalkduster</family>
<family>Herculanum</family>
<family>Copperplate</family>
<family>Arial Black</family>
</prefer>
</alias>
-->
</fontconfig>
#!/usr/bin/env bash
# Inkscape Font Sync — installer.
# Idempotent: safe to re-run. Supports --uninstall.
#
# What it does:
# 1. Verifies jq and fc-cache are available
# 2. Creates ~/.config/fontconfig/, ~/bin/, ~/InkscapeFonts/, ~/Library/LaunchAgents/
# 3. Renders fonts.conf.template → ~/.config/fontconfig/fonts.conf
# (embeds the current macOS's dot-hidden family list, enumerated from fc-list)
# 4. Installs sync-inkscape-fonts.sh, local.inkscape-font-sync.plist
# (renders __HOME__ → $HOME in the plist)
# 5. PATCHES /Applications/Inkscape.app/Contents/Resources/etc/fonts/fonts.conf
# to <include> our user config — Inkscape.app's bundled config is hand-edited by
# its maintainer to strip user-include paths, so without this patch Inkscape.app
# never reads ~/.config/fontconfig/fonts.conf. Backup saved next to the original.
# 6. Nukes Inkscape's own font cache so it re-reads on next launch
# 7. Runs the first sync (~15 s)
# 8. Bootstraps the launchd agent
#
# Uninstall:
# ./install.sh --uninstall
# removes the four files + the launchd agent + reverts the Inkscape.app patch from
# the .orig-before-inkscape-font-sync backup. Leaves ~/InkscapeFonts contents (may
# contain user-added real fonts) — delete manually if you want a clean sweep.
#
# After a macOS update that adds new hidden fonts, OR after an Inkscape.app update
# that overwrites our patch: re-run this script (idempotent).
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
LABEL="local.inkscape-font-sync"
TEMPLATE_SRC="$HERE/fonts.conf.template"
CFG_DEST="$HOME/.config/fontconfig/fonts.conf"
SCRIPT_SRC="$HERE/sync-inkscape-fonts.sh"
SCRIPT_DEST="$HOME/bin/sync-inkscape-fonts.sh"
PLIST_SRC="$HERE/$LABEL.plist"
PLIST_DEST="$HOME/Library/LaunchAgents/$LABEL.plist"
FONTS_DIR="$HOME/InkscapeFonts"
INKSCAPE_APP="/Applications/Inkscape.app"
INKSCAPE_CONF="$INKSCAPE_APP/Contents/Resources/etc/fonts/fonts.conf"
INKSCAPE_CONF_BAK="${INKSCAPE_CONF}.orig-before-inkscape-font-sync"
INKSCAPE_CACHE="$HOME/Library/Caches/org.inkscape.Inkscape/fontconfig"
info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m ✓\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m ! \033[0m %s\n' "$*" >&2; }
fail() { printf '\033[1;31m ✗ %s\033[0m\n' "$*" >&2; exit 1; }
uninstall() {
info "Uninstalling Inkscape Font Sync"
if launchctl print "gui/$(id -u)/$LABEL" >/dev/null 2>&1; then
launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true
ok "launchd agent unloaded"
else
ok "launchd agent was not loaded"
fi
rm -f "$CFG_DEST" && ok "removed $CFG_DEST"
rm -f "$SCRIPT_DEST" && ok "removed $SCRIPT_DEST"
rm -f "$PLIST_DEST" && ok "removed $PLIST_DEST"
if [ -f "$INKSCAPE_CONF_BAK" ]; then
mv -f "$INKSCAPE_CONF_BAK" "$INKSCAPE_CONF"
ok "reverted $INKSCAPE_CONF from backup"
else
warn "no Inkscape.app backup found — $INKSCAPE_CONF left as-is"
fi
if [ -d "$INKSCAPE_CACHE" ]; then
rm -rf "$INKSCAPE_CACHE"/* 2>/dev/null || true
ok "cleared Inkscape font cache"
fi
warn "$FONTS_DIR left in place (may contain user-added real fonts)"
warn " → delete manually with: rm -rf $FONTS_DIR"
info "Done. Restart Inkscape to pick up the change."
exit 0
}
# --------------------------------------------------------------------------
if [ "${1:-}" = "--uninstall" ] || [ "${1:-}" = "-u" ]; then
uninstall
fi
info "Inkscape Font Sync — installer"
# ---- 1. Prereqs ----------------------------------------------------------
info "Checking prerequisites"
if ! command -v jq >/dev/null 2>&1; then
fail "jq not found. Install with: brew install jq (or upgrade to macOS 15+ for Apple-shipped jq)"
fi
ok "jq: $(jq --version)"
if ! command -v fc-cache >/dev/null 2>&1; then
fail "fc-cache not found. Install with: brew install fontconfig"
fi
ok "fontconfig: $(fc-cache -V 2>&1 | head -1)"
if ! command -v fc-list >/dev/null 2>&1; then
fail "fc-list not found. Install with: brew install fontconfig"
fi
# Sanity: macOS 13+ (Font Book sandboxed path)
macos_major=$(sw_vers -productVersion | cut -d. -f1)
if [ "$macos_major" -lt 13 ] 2>/dev/null; then
warn "macOS $macos_major detected; this setup assumes 13+ (sandboxed Font Book)."
warn " The Font Book plist WatchPath may not exist on your version."
fi
# ---- 2. Directories -------------------------------------------------------
info "Creating dirs"
mkdir -p "$(dirname "$CFG_DEST")" "$(dirname "$SCRIPT_DEST")" "$(dirname "$PLIST_DEST")" "$FONTS_DIR"
ok "dirs ready"
# ---- 3. Enumerate hidden families for the reject block --------------------
info "Enumerating dot-prefixed 'hidden' macOS families (via fc-list, from the CURRENT stock state)"
tmp_hidden=$(mktemp)
tmp_reject=$(mktemp)
trap 'rm -f "$tmp_hidden" "$tmp_reject"' EXIT
# Enumerate BEFORE installing our config so we get the stock set.
fc-list : family 2>/dev/null | tr ',' '\n' | sed 's/^[[:space:]]*//' | sort -u | grep "^\." > "$tmp_hidden" || true
hidden_count=$(wc -l < "$tmp_hidden" | tr -d ' ')
ok "found $hidden_count dot-hidden families to reject"
# XML-escape + wrap each as a <pattern> element (2-space indent to match template).
awk '{
gsub(/&/, "\\&amp;")
gsub(/</, "\\&lt;")
gsub(/>/, "\\&gt;")
printf " <pattern><patelt name=\"family\"><string>%s</string></patelt></pattern>\n", $0
}' "$tmp_hidden" > "$tmp_reject"
# ---- 4. Install user config -----------------------------------------------
info "Installing user Fontconfig config"
if [ -e "$CFG_DEST" ] && [ ! -e "${CFG_DEST}.bak" ]; then
cp "$CFG_DEST" "${CFG_DEST}.bak.$(date +%s)"
warn " backed up existing $CFG_DEST"
fi
# Splice the reject block into the template.
awk -v repl_file="$tmp_reject" '
/__HIDDEN_FAMILIES__/ {
while ((getline line < repl_file) > 0) print line
close(repl_file)
next
}
{ print }
' "$TEMPLATE_SRC" > "$CFG_DEST"
ok "installed $CFG_DEST (with $hidden_count hidden-family rejects embedded)"
# ---- 5. Install sync script + plist --------------------------------------
info "Installing sync script + launchd agent"
cp "$SCRIPT_SRC" "$SCRIPT_DEST"
chmod +x "$SCRIPT_DEST"
ok "installed $SCRIPT_DEST (chmod +x)"
sed "s|__HOME__|$HOME|g" "$PLIST_SRC" > "$PLIST_DEST"
if ! plutil -lint "$PLIST_DEST" >/dev/null; then
fail "plist failed lint after rendering — inspect $PLIST_DEST"
fi
ok "installed $PLIST_DEST (rendered __HOME__ → $HOME, lint OK)"
# ---- 6. Patch Inkscape.app's bundled fonts.conf --------------------------
if [ -f "$INKSCAPE_CONF" ]; then
info "Patching Inkscape.app's bundled fontconfig to include user config"
if grep -qF "PATCHED (inkscape-font-sync)" "$INKSCAPE_CONF"; then
ok "Inkscape.app already patched"
else
# Verify it's user-writable (Inkscape.app installed by drag-to-Applications is typically user-owned)
if [ ! -w "$INKSCAPE_CONF" ]; then
warn "Inkscape.app fonts.conf is not user-writable — skipping patch."
warn " Run: sudo chown \$USER $INKSCAPE_CONF"
warn " Then re-run install.sh"
else
cp -n "$INKSCAPE_CONF" "$INKSCAPE_CONF_BAK"
ok "backed up original to $(basename $INKSCAPE_CONF_BAK)"
# Splice in an <include> for our user config after the existing conf.d include.
python3 - "$INKSCAPE_CONF" <<'PY'
import sys, re, io
path = sys.argv[1]
with open(path, "r", encoding="utf-8") as f:
src = f.read()
patch = """
<!-- PATCHED (inkscape-font-sync): read the user's Fontconfig config so our reject
rules for the noisy dirs + dot-prefixed hidden families actually apply. The
original bundled config was hand-edited to strip standard user-include paths;
see fonts.conf.orig-before-inkscape-font-sync backup. This patch will be lost
on the next Inkscape update — re-run install.sh to re-apply. -->
<include ignore_missing="yes">~/.config/fontconfig/fonts.conf</include>
"""
# Insert right after the existing `<include ignore_missing="yes">conf.d</include>` line.
new = re.sub(
r'(<include ignore_missing="yes">conf\.d</include>\n)',
r'\1' + patch,
src, count=1
)
if new == src:
# No conf.d include found — insert just before </fontconfig>
new = src.replace("</fontconfig>", patch + "\n</fontconfig>")
with open(path, "w", encoding="utf-8") as f:
f.write(new)
PY
ok "patched $INKSCAPE_CONF (include added)"
fi
fi
else
warn "Inkscape.app not found at $INKSCAPE_APP — skipping app patch"
warn " (Homebrew Inkscape / other installs may still work if they respect ~/.config/fontconfig/fonts.conf)"
fi
# ---- 7. Nuke Inkscape's cached font list ---------------------------------
if [ -d "$INKSCAPE_CACHE" ]; then
info "Clearing Inkscape's font cache so it re-reads on next launch"
rm -rf "$INKSCAPE_CACHE"/* 2>/dev/null || true
ok "cache cleared"
fi
# Also refresh the system fontconfig cache
fc-cache -f >/dev/null 2>&1 || true
# ---- 8. Reload launchd agent ---------------------------------------------
info "Reloading launchd agent"
if launchctl print "gui/$(id -u)/$LABEL" >/dev/null 2>&1; then
launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true
ok "unloaded existing agent"
fi
launchctl bootstrap "gui/$(id -u)" "$PLIST_DEST"
ok "bootstrapped $LABEL"
# ---- 9. First sync -------------------------------------------------------
info "Running first sync (this may take 10-30 s — system_profiler enumerating fonts)"
"$SCRIPT_DEST" || warn "sync exited non-zero — check ~/Library/Logs/sync-inkscape-fonts.log"
# ---- 10. Verify + report -------------------------------------------------
count=$(find "$FONTS_DIR" -maxdepth 1 \( -type l -o -type f \) 2>/dev/null | wc -l | tr -d ' ')
info "Post-install state"
printf " ~/InkscapeFonts entries: %s\n" "$count"
printf " Fontconfig visible fonts (fc-list): %s\n" "$(fc-list 2>/dev/null | wc -l | tr -d ' ')"
printf " Unique families: %s\n" "$(fc-list : family 2>/dev/null | sort -u | wc -l | tr -d ' ')"
printf " Dot-hidden families still visible: %s (should be 0)\n" \
"$(fc-list : family 2>/dev/null | tr ',' '\n' | sed 's/^[[:space:]]*//' | sort -u | grep -c '^\.')"
printf " Agent state: %s\n" \
"$(launchctl print "gui/$(id -u)/$LABEL" 2>/dev/null | awk '/^\tstate =/ {print $NF; exit}')"
cat <<'EOM'
Done.
Next steps:
1. Fully quit Inkscape (⌘Q) and relaunch. Inkscape caches its font list on launch.
2. macOS may show a background-activity notification for "sync-inkscape-fonts.sh".
Grant it under: System Settings → General → Login Items & Extensions.
3. Toggle a font in Font Book to test — the sync fires within 30 s.
Manual resync anytime: ~/bin/sync-inkscape-fonts.sh
Log tail: tail -f ~/Library/Logs/sync-inkscape-fonts.log
Uninstall: ./install.sh --uninstall
After Inkscape.app updates OR macOS updates: re-run install.sh.
EOM
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
Inkscape Font Sync — launchd agent.
Installs to: ~/Library/LaunchAgents/local.inkscape-font-sync.plist
__HOME__ is a placeholder — the installer substitutes it with your $HOME.
If installing by hand, sed-replace __HOME__ with /Users/<yourname>.
After changes: `launchctl bootout gui/$(id -u)/local.inkscape-font-sync` then
`launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.inkscape-font-sync.plist`.
-->
<plist version="1.0">
<dict>
<key>Label</key>
<string>local.inkscape-font-sync</string>
<!-- Invoke the script directly (not via /bin/bash) so macOS's background-activity
notification shows "sync-inkscape-fonts.sh" instead of "bash". The script's
own `#!/usr/bin/env bash` shebang still picks up bash for interpretation. -->
<key>ProgramArguments</key>
<array>
<string>__HOME__/bin/sync-inkscape-fonts.sh</string>
</array>
<!-- Ensure Homebrew's fc-cache is on PATH when launchd invokes the script. -->
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
<!-- Fire on load (first sync), and whenever any of these paths changes. -->
<key>RunAtLoad</key>
<true/>
<key>WatchPaths</key>
<array>
<!-- Font Book's sandboxed prefs plist (macOS 13+). Font Book rewrites this atomically
when the user toggles a font on/off. -->
<string>__HOME__/Library/Containers/com.apple.FontBook/Data/Library/Preferences/com.apple.FontBook.plist</string>
<!-- User-installed font dir: fires on install/uninstall of any user font. -->
<string>__HOME__/Library/Fonts</string>
<!-- Font Book collections: fires when collections change or when Font Book rewrites them. -->
<string>__HOME__/Library/FontCollections</string>
</array>
<!-- Coalesce bursts: Font Book often rewrites the plist twice in a row when toggling.
30 s throttle means we won't fire more than once every 30 s regardless of how
many FS events land. -->
<key>ThrottleInterval</key>
<integer>30</integer>
<!-- Log stderr/stdout somewhere findable. -->
<key>StandardErrorPath</key>
<string>__HOME__/Library/Logs/sync-inkscape-fonts.stderr.log</string>
<key>StandardOutPath</key>
<string>__HOME__/Library/Logs/sync-inkscape-fonts.stdout.log</string>
</dict>
</plist>
#!/usr/bin/env bash
# sync-inkscape-fonts.sh
#
# Populate ~/InkscapeFonts/ with symlinks to every font currently ACTIVE in
# Font Book. Any symlink whose target is no longer active gets removed.
# Manually-placed real files (non-symlinks) are left alone — that's the escape
# hatch for forcing a font into Inkscape regardless of Font Book state.
#
# Companion Fontconfig config: ~/.config/fontconfig/fonts.conf
# Companion launchd agent: ~/Library/LaunchAgents/local.inkscape-font-sync.plist
#
# Triggered on FS change to Font Book's plist and ~/Library/Fonts/, plus at load.
# Also runnable manually.
#
# macOS 13+ (uses `system_profiler -json`, Font Book sandboxed prefs).
# bash 3.2 compatible (no associative arrays) — macOS default /bin/bash.
# Requires: jq, fc-cache (Homebrew fontconfig).
set -euo pipefail
DEST="${HOME}/InkscapeFonts"
LOG="${HOME}/Library/Logs/sync-inkscape-fonts.log"
log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG"; }
mkdir -p "$DEST" "$(dirname "$LOG")"
log "=== run start ==="
tmp=$(mktemp -t inkfonts)
active_paths="${tmp}.active_paths" # full paths of active fonts on disk
active_names="${tmp}.active_names" # basenames of the above
existing_names="${tmp}.existing_names" # basenames of current symlinks in DEST
to_create="${tmp}.to_create" # basenames only in active (need new symlink)
to_remove="${tmp}.to_remove" # basenames only in existing (stale symlink)
to_verify="${tmp}.to_verify" # basenames in both (verify target correctness)
trap 'rm -f "$tmp" "$tmp".*' EXIT
# 1. Enumerate active fonts via system_profiler. Slow (~10-30s) but authoritative.
if ! system_profiler -json SPFontsDataType 2>/dev/null > "$tmp"; then
log "! system_profiler failed"
exit 1
fi
# 2. Extract paths of enabled + valid fonts.
# System fonts often lack an "enabled" field; only skip when it's explicitly "no".
jq -r '.SPFontsDataType[]
| select((.enabled // "yes") != "no" and (.valid // "yes") == "yes")
| .path' "$tmp" \
| awk 'NF' | sort -u > "$active_paths"
active_count=$(wc -l < "$active_paths" | tr -d ' ')
log "active fonts reported by system_profiler: $active_count"
# 3. Basename lists (sorted for comm).
awk -F/ '{print $NF}' "$active_paths" | sort -u > "$active_names"
find "$DEST" -maxdepth 1 -type l -exec basename {} \; 2>/dev/null | sort -u > "$existing_names"
# 4. Set-diff via comm to figure out what to create / remove / verify.
comm -23 "$active_names" "$existing_names" > "$to_create"
comm -13 "$active_names" "$existing_names" > "$to_remove"
comm -12 "$active_names" "$existing_names" > "$to_verify"
# 5. Create new symlinks. Need to map basename -> source path (use grep against active_paths).
created=0
skipped_missing=0
while IFS= read -r name; do
[ -n "$name" ] || continue
src=$(grep -F "/$name" "$active_paths" | head -1 || true)
[ -n "$src" ] || continue
if [ ! -e "$src" ]; then
log " ? source missing on disk: $src"
skipped_missing=$((skipped_missing + 1))
continue
fi
link="$DEST/$name"
if [ -e "$link" ] && [ ! -L "$link" ]; then
# Real file already there (manually placed) — leave alone
log " = leaving real file untouched: $name"
continue
fi
ln -sf "$src" "$link"
created=$((created + 1))
done < "$to_create"
# 6. Verify existing symlinks point at the correct source; retarget if not.
retargeted=0
while IFS= read -r name; do
[ -n "$name" ] || continue
src=$(grep -F "/$name" "$active_paths" | head -1 || true)
[ -n "$src" ] || continue
link="$DEST/$name"
cur_target=$(readlink "$link" 2>/dev/null || echo "")
if [ "$cur_target" != "$src" ]; then
ln -sf "$src" "$link"
retargeted=$((retargeted + 1))
fi
done < "$to_verify"
# 7. Remove stale symlinks (were in DEST, no longer active).
removed=0
while IFS= read -r name; do
[ -n "$name" ] || continue
link="$DEST/$name"
# Only remove if it's still a symlink (defensive — could have been replaced by a file).
if [ -L "$link" ]; then
rm -f "$link"
removed=$((removed + 1))
fi
done < "$to_remove"
log "created=$created retargeted=$retargeted removed=$removed skipped_missing=$skipped_missing"
# 8. Refresh Fontconfig cache for the curated dir so Inkscape sees the new set.
if command -v fc-cache >/dev/null 2>&1; then
fc-cache -f "$DEST" >/dev/null 2>&1 || log "! fc-cache returned non-zero"
else
log "! fc-cache not found — Inkscape may need a manual restart"
fi
# 9. Small "did it work?" sanity report to stdout (useful for manual runs).
final_count=$(find "$DEST" -maxdepth 1 \( -type l -o -type f \) 2>/dev/null | wc -l | tr -d ' ')
echo "Synced. ~/InkscapeFonts now holds $final_count entries (created=$created retargeted=$retargeted removed=$removed skipped_missing=$skipped_missing)."
log "=== run end: dest_count=$final_count ==="
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment