-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
A deliberately small Go program. Roughly 2,000 lines of implementation across five internal packages, each with one job.
| Package | Owns |
|---|---|
cmd/nerd-fonts-installer |
Flags, command flow, exit codes. |
internal/config |
Loading, defaults, normalization, validation, discovery. |
internal/nerdfonts |
GitHub releases API client. |
internal/fonts |
Download, verify, extract, atomically replace, refresh cache. |
internal/fontname |
The single shared family-name validator. |
internal/tui |
The Bubble Tea picker. |
main() does almost nothing: it wires os values into a testable
run(ctx, args, stdin, stdout, stderr, deps) int. Dependencies are injected as a
struct of function-typed fields β loadConfig, discoverConfig,
listReleases, runTUI, installFonts, isTerminal β rather than interfaces.
For a CLI this size, function fields give the same test seams without the
ceremony.
For each family, concurrently, up to four at a time:
resolve release
β
fetch SHA-256.txt manifest (best effort β a warning if absent)
β
download <family>.zip (size-capped)
β
verify sha256 against manifest (mismatch β abort)
β
extract .ttf/.otf/.ttc only (size-capped, into a temp directory)
β
move current family dir β .old
move temp dir β destination
remove .old (best effort)
β
fc-cache -f <destination> (once, at the end, if enabled)
Concurrency is safe because each family writes to a disjoint path. That is the property to preserve if you ever touch the install loop. A synchronized writer keeps progress lines from interleaving mid-line.
The tool downloads archives from the internet and writes files into your home directory, so the boundaries are explicit.
Family names are the trust boundary. Every name passes
internal/fontname.Validate before it is joined onto a filesystem path or a URL:
no separators, no .., no leading -, no empty names. There is exactly one
implementation, used by both config and fonts β duplicating it is how these
guards rot.
Downloads are verified. Each archive is checked against the release's
SHA-256.txt. A mismatch aborts before anything is written. A missing manifest
warns and proceeds.
Zip bombs are bounded.
| Limit | Value |
|---|---|
| One downloaded zip | 768 MiB |
| One extracted font file | 128 MiB |
| Total uncompressed per archive | 2 GiB |
Config decoding is strict. Unknown keys are rejected in both YAML and JSON, so a typo fails loudly instead of silently doing nothing.
No shell. fc-cache runs through exec.CommandContext with an argument
list β there is no string interpolated into a shell.
URL segments are escaped. Release tags go through url.PathEscape before
they are used in a download URL.
Writes are flushed and closed explicitly. A swallowed close error can promote
a truncated font file, so both Sync and Close results are surfaced.
| Code | Meaning |
|---|---|
0 |
Success, or the user cancelled |
1 |
Runtime failure β network, filesystem, extraction, checksum |
2 |
Correctable input β missing config, unknown release, bad flag value |
The mapping lives in one function (exitCodeFor) so it cannot drift between
call sites.
internal/nerdfonts.Client is a zero-value-with-defaults struct: leave it empty
and it uses a 30-second HTTP client against the public API; set HTTPClient,
BaseURL, or MaxPages in tests. Pagination stops on an empty raw API page
rather than a filtered-empty one, so a page of releases without font assets does
not truncate the list.
Two steps β release, then families β sharing one Bubble Tea model, with a
stepDone state that renders the confirmation before handing control back.
The layout is budgeted: the banner, separators, panel chrome, and help line have a known row cost, and the list gets whatever is left. That budget matters β a frame even one row taller than the terminal makes Bubble Tea truncate the top of the view, which silently eats the banner's top border. A test asserts the rendered height never exceeds the terminal height across a range of sizes.
The layout is responsive: the side panel is dropped when it will not fit beside the list, and the banner drops its subtitle and badges on short terminals.
Icon sets are data tables, not switch statements β adding a family glyph means editing a map.
-
runtakes explicit I/O, so the whole command is testable without touchingos. - HTTP goes through an injectable
*http.Client; tests use aroundTripFuncwith in-memory zip archives, orhttptest. - Table-driven tests with
t.TempDir()for anything filesystem-shaped. -
Everything must pass under
-raceβ the install path is concurrent.
The terminal screenshots in the README and on the website are generated from
real runs, not mocked up. A harness runs the tool in a pty, captures the
final screen with a terminal emulator, and renders it to SVG with box-drawing
characters drawn as vectors. bwrap shadows $HOME so a genuine install lands
in a throwaway directory while the paths on screen still look ordinary. See
scripts/screenshots/.
Next: π€ Contributing
MIT licensed Β· Fonts by ryanoasis/nerd-fonts Β· TUI by Charm
Getting started
Reference
Help
Internals