Builds a small, standalone Nerd Font fallback font from custom SVG icons, for use with Ghostty + Nerd-Font-aware tools like eza and mini.icons in Neovim.
It does not patch a whole font. It builds a slim font containing only the glyphs
icon-mapping.json overrides or adds, meant to sit in your terminal's
font-fallback chain after your primary font - everything you don't override
keeps coming from Ghostty's own bundled Nerd Font Symbols, as normal.
The original approach here was to patch icons directly into a copy of the primary text font (0xProto). That turned out to be the wrong shape for Ghostty specifically:
- Ghostty only applies its own dynamic Nerd Font icon scaling
(
adjust-icon-height, and the sizing logic it added in 1.2 that mirrors the official Nerd Fonts patcher's constraints) to glyphs it resolves through its own implicit fallback - never to a font you name explicitly infont-family, whether as the primary font or as an explicit fallback entry. So a fully patched primary font gets no help from Ghostty at all; icons end up exactly whatever size you baked into the font file, with no config-side lever to fix it. - Different Nerd-Font-aware tools render the same icon from different
codepoints. For example
ezadraws.cssfiles fromdev-css3(U+E749), while Neovim's mini.icons draws them frommd-language_css3(U+F031C). A classic one-codepoint-per-icon patch can only ever satisfy one of them.
So instead: icon-mapping.json can list multiple Nerd Font codepoints per
icon (one per consuming app, when they differ - see its _comment), this repo
builds a tiny font with just those codepoints filled in, and you add that font
as a fallback. Everything else - every codepoint you didn't touch - still comes
from Ghostty's built-in Nerd Font Symbols, auto-scaled exactly as if you'd never
patched anything. Since Ghostty won't auto-scale our glyphs either way (see
above), patch_fonts.py overscans them past the nominal cell box by a tunable
factor (--shrink, default 1.35) to compensate by hand.
fontforge(with its Python scripting module),python3with thefonttoolsandbrotlipackages, andcurl. Nix is the easiest way to get all four without touching your system - see below - but it's not a hard requirement if you'd rather install them another way.- A licensed copy of Monokai Pro, for either a JetBrains IDE or VS Code (see below) - or your own SVG icon set, substituting for it.
- Ghostty (or another terminal that supports font-fallback chains and Nerd Font codepoints) and, optionally, Neovim with mini.icons.
Nix with flakes enabled, and optionally direnv (with
nix-direnv) - run direnv allow once in this directory and
fontforge, python3 (with fonttools/brotli), and curl are on PATH
automatically from then on. Without direnv, run nix develop yourself.
patch-fonts.sh also works standalone, outside the dev shell: if it doesn't
find fontforge/python3 on PATH already, it re-execs itself inside a
nix-shell that provides them, so plain ./patch-fonts.sh works even if you
never ran nix develop - as long as Nix itself is installed.
Install the four tools above through your usual package manager, then just run
./patch-fonts.sh / ./extract-monokai-icons.py / ./patch_fonts.py
directly - they detect that fontforge and python3 are already on PATH and
skip the nix-shell re-exec entirely, no flags needed.
- Debian/Ubuntu:
sudo apt install fontforge python3-fontforge python3-fonttools python3-brotli curl - Fedora:
sudo dnf install fontforge python3-fonttools python3-brotli curl - Arch:
sudo pacman -S fontforge python-fonttools python-brotli curl - macOS (Homebrew):
brew install fontforge curl, thenpip install fonttools brotli(orpipx install fonttools/ a venv, per your usual Python setup) - Homebrew'sfontforgebundles its own Python interpreter, so thefontforge -scriptinvocations work regardless of your system Python; only the separatepython3 -c ...steppatch-fonts.shuses to produce the.woff2needsfonttools/brotlion apython3that's actually onPATH.
Either way, verify it worked with fontforge --version and
python3 -c "import fontTools, brotli" (no output = success) before running the
build.
direnv allow # or: nix develop - skip both if the tools are on PATH already
cp /path/to/your/monokai-pro/icons/*.svg customicons/ # see "Supplying your own icons" below
./patch-fonts.shThis writes fonts-patched/MonokaIconic-Fallback.ttf (+ .woff2). Install it as a
font and add it to Ghostty's fallback chain (also below).
customicons/*.svg is not included in this repository and never will be.
The icon artwork this project was built around comes from the
Monokai Pro VS Code theme, which is a paid product - its icons
aren't freely redistributable, so committing them here isn't an option.
.gitignore excludes them explicitly; icon-mapping.json (which only encodes
names and Nerd Font codepoints, not artwork) is committed as normal.
To use this repo you need to supply customicons/*.svg yourself, named to match
the keys in icon-mapping.json's overrides and new_icons (e.g.
customicons/python.svg). Three ways to do that:
Whichever way you get them, note that
multi-tone source art gets flattened either way: patch_fonts.py compiles
everything into a plain glyf-table TTF with no COLR/CPAL color-glyph
tables, and a glyph outline in that kind of font is just a shape - it carries no
color of its own. Any per-path fill colors in the source SVG (like archive's
two-tone zipper) are discarded at build time; the terminal renders the glyph in
a single color, same as it does for every other Nerd Font icon (whatever
foreground color the app - eza, mini.icons, etc. - applies around it). So it's
not worth seeking out multi-tone source icons specifically for this project.
If you own the Monokai Pro plugin for a JetBrains IDE (PhpStorm, IntelliJ IDEA, WebStorm, ...), it ships the icons as plain individual SVG files already - no extraction needed, just copy them in. The plugin installs unpacked onto disk as a regular directory, under your IDE's plugin data directory:
- Linux:
~/.local/share/JetBrains/<Product><Version>/<plugin-dir>/lib/icons/<palette>/ - macOS:
~/Library/Application Support/JetBrains/<Product><Version>/<plugin-dir>/lib/icons/<palette>/ - Windows:
%APPDATA%\JetBrains\<Product><Version>\<plugin-dir>\lib\icons\<palette>\
<Product><Version> is e.g. PhpStorm2026.2; <plugin-dir> is whatever the
plugin unpacked itself into (look for a directory whose name contains
monokai-pro, e.g. monokai-pro-lifetime - the exact name has varied by
license tier). <palette> picks the color scheme - there's a subdirectory per
variant (monokai-pro, monokai-pro-filter-octagon, monokai-pro-monochrome,
monokai-pro-light, ...); each is a complete, self-contained set, already
colored.
Once you've found it:
cp "<plugin-dir>/lib/icons/monokai-pro/"*.svg customicons/That's the whole process - no script, no build step. The filenames already match
icon-mapping.json's icon names 1:1. If a plugin update ever reshuffles that
directory layout,
find ~/.local/share/JetBrains -ipath '*monokai-pro*/lib/icons*' -name '*.svg'
(adjust the base path per OS above) will usually relocate it.
If you only have the VS Code extension, its icons ship differently - as a single colored webfont plus a JSON file mapping each icon name to a glyph codepoint and a hex color, not as individual SVGs. VS Code extensions are plain installed directories too, so you can read these two files straight out of your local install without needing the theme active or VS Code running:
<extension dir>/icon-themes/monokai-pro-icons.woff<extension dir>/icon-themes/Monokai Pro icon-theme.json(or any of the other*icon-theme.jsonpalette variants in that same directory, e.g. a "Filter" or "monochrome" variant, if you'd rather use a different color scheme)
<extension dir> is normally:
- Linux/macOS:
~/.vscode/extensions/monokai.theme-monokai-pro-vscode-<version>/ - Windows:
%USERPROFILE%\.vscode\extensions\monokai.theme-monokai-pro-vscode-<version>\
Then run the extraction script from this repo (inside the dev shell):
./extract-monokai-icons.py \
--woff "<extension dir>/icon-themes/monokai-pro-icons.woff" \
--theme-json "<extension dir>/icon-themes/Monokai Pro icon-theme.json"usage: extract-monokai-icons.py [-h] --woff WOFF --theme-json THEME_JSON
[--out-dir OUT_DIR] [--mapping MAPPING]
[--all] [--force]
--woff WOFF path to the extension's monokai-pro-icons.woff
--theme-json THEME_JSON path to one of the extension's '*icon-theme.json'
variants (picks the color palette)
--out-dir OUT_DIR where to write <icon-name>.svg files
(default: ./customicons)
--mapping MAPPING icon-mapping.json to read the needed icon names
from (default: ./icon-mapping.json)
--all extract every icon the theme json defines, instead
of only the ones icon-mapping.json references
--force overwrite SVGs that already exist in --out-dir
By default it only extracts the icons icon-mapping.json actually references
(skipping ones that already exist, unless --force), and warns about any it
couldn't find in the theme JSON.
This gets you a single-color SVG per icon, since the VS Code webfont format only carries one color per glyph to begin with. As covered above, the font build flattens multi-tone source art to one color regardless of where the SVG came from, so this is really only a fallback for when all you have is the VS Code extension, not a lesser version of Option A for the purposes of this project.
Any SVG works as long as it's a single
<svg viewBox="..."><path d="..." fill="..."/></svg>-shaped file (multiple
<path> elements are fine too, e.g. if that's just how your source draws the
shape - but see above, their individual fill colors won't survive into the
font) named <icon-name>.svg matching an icon-mapping.json key, placed in
customicons/. patch_fonts.py scales and centers whatever outline it finds,
so exact source dimensions don't matter.
overrides:{ "<icon-name>": "<nf-glyph-name>" }, or{ "<icon-name>": ["<nf-glyph-name>", ...] }when different apps render that file type from different Nerd Font codepoints (css, markdown, archive, image, and most language logos among them - see the file's own_comment).<icon-name>must have a matchingcustomicons/<icon-name>.svg. Glyph names come from Nerd Fonts'glyphnames.json(cached locally under.nf-cache/on first build).new_icons: a plain list of icon names with no existing Nerd Font equivalent. Each gets a fresh codepoint allocated in the Supplementary Private Use Area-A (starting atU+F0001, outside every range Nerd Fonts itself uses), in list order.
Edit freely and re-run ./patch-fonts.sh.
Usage: patch-fonts.sh [options]
-o, --output-dir DIR write the built font here (default: ./fonts-patched)
-n, --name NAME output font basename, without extension (default: MonokaIconic-Fallback)
-m, --mapping PATH path to icon-mapping.json (default: ./icon-mapping.json)
-i, --icons-dir DIR directory of source SVGs (default: ./customicons)
-c, --cache-dir DIR directory to cache glyphnames.json in (default: ./.nf-cache)
--shrink FACTOR glyph overscan factor, see patch_fonts.py --help (default: 1.35)
--no-woff2 skip building the .woff2 variant
-h, --help show this help
patch-fonts.sh is a thin wrapper that fetches glyphnames.json if needed,
calls patch_fonts.py to build the .ttf, then converts it to .woff2. It
re-execs itself inside nix-shell automatically if fontforge/python3 aren't
already on PATH - handy for running it standalone outside the dev shell.
patch_fonts.py (invoke directly for finer control, e.g. to retune sizing
without touching the shell wrapper):
usage: patch_fonts.py -o OUTPUT [--mapping MAPPING] [--icons-dir ICONS_DIR]
[--cache-dir CACHE_DIR] [--family-name FAMILY_NAME]
[--shrink SHRINK] [--em EM] [--ascent ASCENT]
[--descent DESCENT] [--cell-width CELL_WIDTH]
-o, --output OUTPUT output font path
--mapping MAPPING path to icon-mapping.json
--icons-dir ICONS_DIR directory of source SVGs
--cache-dir CACHE_DIR directory holding glyphnames.json
--family-name NAME font family name to embed (default: 'MonokaIconic Fallback')
--shrink SHRINK glyph overscan factor (default: 1.35)
--em / --ascent / --descent / --cell-width
override the font's cell metrics directly
(defaults: 1000 / 800 / 200 / em*0.62)
The cell metrics are fixed, reasonable monospace proportions, not read from any
real font file - terminals size their grid from the primary font and only use
a fallback glyph's outline, not its declared advance width, so there's nothing
to gain by tying these to a specific font. --shrink is the one you'll actually
want to tune: raise it if icons still look small in your terminal, lower it if
they start clipping into neighboring cells.
mkdir -p ~/.local/share/fonts/MonokaIconic-Fallback
cp fonts-patched/MonokaIconic-Fallback.ttf ~/.local/share/fonts/MonokaIconic-Fallback/
fc-cache -f ~/.local/share/fonts/MonokaIconic-FallbackThen in your Ghostty config, list it as a fallback after your primary font (order matters - this is what makes it a fallback rather than a primary):
font-family = "<your primary font>"
font-family = "MonokaIconic Fallback"
Restart Ghostty (not just ghostty +reload-config - the font-fallback chain is
resolved at startup). Anything you overrode in icon-mapping.json now renders
from customicons/; everything else keeps coming from Ghostty's bundled Nerd
Font Symbols, unaffected.
Neovim's mini.icons needs no special configuration for this - it just writes the
codepoints icon-mapping.json targets into the buffer, and however the terminal
resolves and sizes those codepoints is entirely Ghostty's call, the same as for
eza.
A folder of empty files, one representative name/extension per icon in
icon-mapping.json's overrides (file.css, Dockerfile, package.json,
...) - every overridden icon has exactly one file here that should trigger
it. Handy for eyeballing icon coverage with eza --icons or opening the
folder in Neovim once a build is installed. There's no script for this -
if you add or rename an override in icon-mapping.json, just add or rename
the matching file here by hand.
The code and data in this repository (patch_fonts.py, patch-fonts.sh,
extract-monokai-icons.py, icon-mapping.json) are MIT-licensed - see
LICENSE. That does not cover whatever you put in customicons/: if you
extracted those from Monokai Pro, they're subject to that product's own license,
and are yours to use locally, not to redistribute.