|
Some checks failed
ci / test (push) Has been cancelled
ci / nix (push) Has been cancelled
ci / binaries (push) Has been cancelled
ci / packages (amd64) (push) Has been cancelled
ci / packages (arm64) (push) Has been cancelled
ci / freebsd-packages (push) Has been cancelled
ci / images (girlc) (push) Has been cancelled
ci / images (girlpages) (push) Has been cancelled
ci / images (girlserve) (push) Has been cancelled
ci / release (push) Has been cancelled
They closed on a click, but a finger that moves a little, or lands while the page is still moving, is a scroll to the browser, with no click, so a phone's tap off a menu could leave it open. |
||
|---|---|---|
| .github/workflows | ||
| cmd | ||
| dist | ||
| doc | ||
| internal | ||
| man | ||
| test/e2e | ||
| .dockerignore | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
girlpages 💅
man, but she's ✨ that girl ✨.
girl is a drop-in for man(1) that shows every manual page rewritten in
the voice of a Gen Z teenage girl, with colour and emoji, in your terminal.
The vibe changes; the facts don't. Every option, argument, default, file,
example and caveat from the original is kept, and code and flags are copied
exactly.
$ girl ls
ls(1) ✨ girl manual ✨ ls(1)
💖✨ ls(1) ✨💖
bestie this one lives in the General Commands Manual 🎀 aka the
everyday-icons section
🌸 NAME 🌸
ls - List directory contents. Ignore files and directories starting
with a '.' by default 🙈
okay so ls is literally the first command you ever learn and she is
still the main character, no cap 👑 …
(Picture that in pink, purple and mint.)
girlpages.dev
Every page, girlified, on the web: girlpages.dev. Browse by section, read any version from Debian, Ubuntu, Fedora, Arch or upstream (and RFCs, too), and search across all of them, typos and all. It installs as an app and works offline.
It's also the official girlpages repository, at
girlpages.dev/repo: girl looks there for a
page before asking a model, girlc reads from it, and girlrepo import
can mirror it, in a few big packs. See Repositories.
How it works
girlfinds the page withman -w, so names, sections and search order are exactly man's.- If it's cached, it's shown right away.
- Otherwise the page is rendered to plain text, sent to an LLM (long pages go in parts, split at sections), and the reply is cached. If the model drops any command-line options, that part is retried once.
- The result is laid out like a man page and shown in your pager.
The LLM can be the Claude Code CLI
(claude -p) or anything that speaks the OpenAI chat completions API:
OpenAI, OpenRouter, Ollama, llama.cpp, vLLM, LM Studio and friends.
Install
You need Go 1.25+, man-db, and either the claude CLI or an
OpenAI-compatible endpoint.
Just for you
make install-user
This puts girl, girlrepo and girlc in ~/.local/bin, their man pages
in ~/.local/share/man, a config file in ~/.config/girlpages/config.toml, and (with systemd) a daily
user timer that refreshes pages you've converted. Pages are cached in
~/.cache/girlpages.
Or just go install github.com/NaomiAmethyst/girlpages/cmd/girl@latest.
System-wide
make
sudo make install
This installs girl, girlrepo and girlc to /usr/local (change with
PREFIX=), with the config in
/etc/girlpages/config.toml and a shared cache in /var/cache/girlpages
that every user reads from. It also installs:
girlpages-update.service, which runsgirl --bulk --update --prune;- a hook for each package manager it finds that starts that service when man
pages change: an apt
DPkg::Post-Invokehook, a pacman hook, a dnf post-transaction action, and an apk commit hook, with a daily run from crond in/etc/periodic/dailyas Alpine has no systemd (HOOKS="apt pacman dnf apk"orHOOKS=noneto choose); girlpages-update.timer, a daily run for pages that arrive any other way.
The hooks don't convert anything new until you opt in by girlifying the whole system once:
sudo girl --bulk --dry-run # how many pages is that, exactly
sudo girl --bulk -j 8 # go off, queen
After that, new and updated pages are converted automatically. Heads up:
a typical system has thousands of man pages, so a full run is a lot of LLM
calls. You can do it a section at a time (sudo girl --bulk 1 8), and since
it's incremental you can interrupt it and pick up where you left off.
make uninstall / make uninstall-user undo it all (config and caches are
left alone). Packagers: see the top of the Makefile for DESTDIR and
friends.
Packages and images
Each release has deb, rpm, apk and Arch packages for amd64 and arm64: of
girlpages, which is everything above with each package manager's hook and
the daily timer, and of girlc alone, one static binary with no
dependencies. There are images too, ghcr.io/naomiamethyst/girlpages
(everything, on Debian) and ghcr.io/naomiamethyst/girlc (girlc on its
own, from scratch):
docker run --rm ghcr.io/naomiamethyst/girlc ls
Nix
The repository is a flake, with girlpages (everything) and girlc, for
Linux and macOS:
nix run github:NaomiAmethyst/girlpages#girlc -- ls # read a page
nix profile install github:NaomiAmethyst/girlpages # girl, girlrepo, girlc, girlserve
On NixOS, its module installs girlpages with its config and girlifies what's new daily (Nix has no hooks for when packages change):
{
inputs.girlpages.url = "github:NaomiAmethyst/girlpages";
# … then, in a configuration's modules: girlpages.nixosModules.default, and
services.girlpages = {
enable = true;
settings.openai = { base_url = "https://api.example.org/v1"; model = "some-model"; api_key_file = "/run/secrets/girlpages"; };
};
}
make packages builds the packages here (it needs
nfpm; ARCH=arm64 for the other one).
Each release has binaries for using without a package manager, too:
girlc on its own for Linux (amd64, arm64, ARMv6, 386, riscv64), macOS,
Windows and FreeBSD (girlc-VERSION-OS-ARCH, one static binary to copy
anywhere), and everything, girl, girlrepo, girlc and girlserve with
their man pages and an example config, in a tarball per Linux architecture
(girlpages-VERSION-linux-ARCH.tar.gz), and the same for FreeBSD amd64 and
arm64. make binaries builds them here.
FreeBSD packages are in each release too, of girlpages (with a pkg trigger
and a daily periodic script to keep it fresh) and girlc alone, for
FreeBSD 14 and 15 on amd64 and aarch64: pkg add girlpages-VERSION-freebsd15-amd64.pkg. Without man-db, as on FreeBSD,
girl renders pages with mandoc (or groff).
In your browser
girl --serve # http://localhost:1989/
girl --html ls > ls.html # one page, as a self-contained web page
--serve lets you browse pages by section, search names and descriptions,
and read them with a table of contents, clickable references to other pages,
and a link to the boring original. It's pink and white in light mode and
purple and black in dark mode, following your browser (or the 🌗 button).
Open a page that isn't girlified yet and it's converted on the spot, with a
waiting page that refreshes itself until she's ready. Everyone asking for the
same page shares one conversion, and no more than jobs requests hit the
model at once.
There's no login, so anyone who can reach it can spend your LLM budget: keep
it on localhost (the default), or use --cached-only to only serve what's
already converted (sudo girl --bulk first, say).
Repositories
Someone already girlified the page you want? A girlpages repository is a pile
of ready-made pages on a static web host, and girl reads from one before
bothering a model: girlpages.dev's, unless you say otherwise.
[repo]
url = "https://girlpages.dev/repo" # the default; another, or "none" for none
Only the hash of a page's source is sent (to fetch its conversion), so the
repo learns which pages you read, and nothing else; url = "none" (or
GIRLPAGES_REPO=none) asks nothing of anyone but your model.
Pages are matched by the hash of their roff source, so you only ever get a
conversion of exactly the version you have installed; anything else is
converted as usual. With a repo set, girl --bulk works even with no model
at all, caching everything the repo has. girl --import URL-or-dir does the
same for a repo you name, and girl --export DIR writes your own cache out as
a repo, with the package each page came from, for other machines to import
or for girlrepo to publish.
A repo has more than what's installed here, too: girl -r reads any page in
it, in any version and variant, and searches it.
girl -r open # open(1), installed or not
girl -r --versions ls # the versions it has, and how to pick them
girl -r --page-version debian/bookworm ls # one of them (or 9.4, or a hash)
girl -r --variant man ls # the original
girl -r -k "list directory" # search it (--ngrams for words inside words)
girlc
Just want to read? girlc is the same thing on its own: a little man that
only reads from a repo. It's one static binary (about 7 MB) with no config
file and no libraries, and brings its own root certificates for systems that
have none. So it's a light way to keep the manual in containers that leave
man pages out for their size: copy it into any image, even scratch, and
it reads from girlpages.dev (or another repo, with GIRLC_REPO or --repo;
packagers can build in another with make girlc GIRLC_REPO=https://…/repo). It takes the same
options as girl -r (make install and make install-user put it in too,
or make girlc && sudo make install-girlc installs just it; documented in
girlc(1)).
COPY girlc /usr/local/bin/girlc
girlc ls # ls(1), girlified
girlc -a printf # printf(1), (3) and (3p)
girlc -k compress # search
girlc rfc 9110 # an RFC
girlc --markdown https://girlpages.dev/girl/1/ls/ # a page by its address, as Markdown
Running one
girlrepo (installed with girl, documented in girlrepo(1)) builds them:
girlrepo ingest --distro debian --release trixie pool/*.deb # or .pkg.tar.zst, .rpm, .apk, dirs
girlrepo ingest --system # or this machine, with its packages
girlrepo ingest git+https://github.com/tmux/tmux#3.5a # or a git repository
girlrepo crawl apt https://deb.debian.org/debian trixie # or a whole package repository
girlrepo crawl rpm https://dl.rockylinux.org/pub/rocky/9/BaseOS/x86_64/os
girlrepo crawl pacman https://geo.mirror.pkgbuild.com/core/os/x86_64
girlrepo crawl apk https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64
girlrepo crawl freebsd https://download.freebsd.org/releases/amd64/15.1-RELEASE
girlrepo crawl nix https://channels.nixos.org/nixos-25.05
girlrepo girlify -n --price 0.10/0.50 # what would it cost?
girlrepo import https://girlpages.dev # merge in another repo (by its packs)
girlrepo export /srv/packs # or write yours as just packs, for mirroring
girlrepo girlify -j 8 # convert what isn't yet
girlrepo girlify --variant pirate fork # or in another voice (--list-variants)
girlrepo girlify --variant pirate --names-from common.txt # or for a list of pages
girlrepo publish --url https://example.org --gc # index it and build the site
girlrepo serve # preview at localhost:1990
girlrepo inspect describe 'ls(1)' # and look around one (below)
publish also writes .zst, .br and .gz copies of everything for the
web server to send as they are (dist/hosting/ has configurations for
static-web-server and nginx, with the right cache lifetimes), and girl
asks for them. That leaves a directory to upload anywhere static: the repo in repo/, and
a website around it. The static part is what search engines see: a page for
the latest version (by version number) of every man page, girlified, with
OpenGraph, JSON-LD and a sitemap, plus section listings (in pages of 200,
which scroll on into each other with JavaScript). Everything else runs
in the browser straight from the repo: originals, older versions, browsing
just Debian, just Debian trixie, just Arch or upstream git, and search over
every page and version from every source (typos and all: grpe finds
grep). Pages made in other voices too (girlrepo girlify --variant pirate)
offer each, and any of them can be made your default, kept in your browser. The site installs as an app (a PWA) and works offline: every repo file
fetched is kept (in the browser's Cache API), so pages you've read and
searches you've done work without a network, and offline the app renders
any page from them. Each section listing has a "save for offline" button
that fetches all its pages, and the search page has ones that fetch the
search index (and, separately, its typo matching), and a switch to show only
pages that are saved. When it notices a newly published repository it clears out the
search index files that aren't used any more, in the background. Each page version is stored once however
many packages, distros or aliases share it, and everything but a few pointer
files is content-addressed, so it caches forever and syncs cheaply. The format is specified in doc/repo-format.md, for anyone building tools against it.
A repo is a couple of million small files, though, which is a lot of
requests to mirror. So publish also writes them as packs: a log of
zstd-compressed tar files of about 256 MiB (the whole girlpages repo's
8 GiB is 670 MiB of them), with each publish's changes in a small tail
pack. girlrepo import syncs a repo by its packs when it has them,
fetching just the new ones next time; girlrepo export DIR writes a
pack-only copy to host anywhere, adding to it each time; and girlrepo unpack merges packs you've copied over by hand.
Serving millions of small files off a network file system is slow too, so
a site can also live in an archive: a few big files (segments of every
file's bytes, stored in each form it's sent in, and an index of where they
are) that girlrepo publish --archive DIR adds each new version to, writing
only what changed; publish --from-packs PACKS --archive DIR goes straight
from packs to an archive, with no repository (or millions of files) between. girlserve, a small static server (and image) of its own,
serves an archive, sending each file in the smallest form the client accepts,
and switches to new versions as they're written, with health checks,
Prometheus metrics and access logs for running it in production.
dist/hosting/kubernetes.yaml has a CronJob
that publishes packs dropped into a directory into an archive and a Deployment
serving it; doc/archive-format.md has the format.
Looking around a repository
girlrepo inspect shows a repository on its own terms, without parsing its
JSON yourself, whether it's yours or someone else's: a remote repository has no
directory listings, so everything's reached by following what refers to it,
from manifest.json down. Use --repo for anywhere but -d's.
girlrepo inspect ls # chapters, sections, pages, sources, packs, index
girlrepo inspect ls chapters/3 # its pointer, sections and shards
girlrepo inspect ls chapters/3 --follow # its pages
girlrepo inspect describe 'ls(1)' # its versions, and why the current one's current
girlrepo inspect ls 'ls(1)/girl' # its headings and older generations
girlrepo inspect cat 'ls(1)@9.7/man' # a variant of a version
girlrepo inspect cat index/words/grep # the pages a word's in, by name
girlrepo inspect get index/packages/coreutils # the JSON
girlrepo inspect get page/page-53/53a4….json --raw # or a file by its path, as it's stored
girlrepo inspect describe packs/2 --repo https://girlpages.dev # a pack's header
girlrepo inspect check 'ls(1)' -r # is it right, and all that's under it?
girlrepo inspect check / -r --sample 20 --repo https://girlpages.dev
girlrepo inspect check stray.json --as page # a file of yours, as a listing
ls shows the addresses it can take. check looks at each thing's hash, shape
and what follows from it (is current what its versions make it?), and exits
non-zero if anything's wrong.
Configure
# ~/.config/girlpages/config.toml (or /etc/girlpages/config.toml)
backend = "auto" # auto, claude or openai
[claude]
model = "haiku" # the default
[openai]
base_url = "http://localhost:11434/v1" # Ollama, for example
model = "qwen3:32b"
[repo]
url = "" # a girlpages repository to try first
Converting thousands of pages? Batch APIs are about half the price, and
girl --bulk (and girlrepo girlify) can use OpenRouter's or Anthropic's:
batch = "anthropic" # or "openrouter", which uses [openai]
[anthropic]
api_key_file = "/etc/girlpages/anthropic-key" # or ANTHROPIC_API_KEY
model = "claude-haiku-5-5"
Results come back within a day. Runs save what they sent, so Ctrl-C is fine
and --no-wait just sends them off; the next run collects them.
Use more than one service? Name each in a [providers.NAME] section (any
of [openai]'s settings, or [claude]'s with backend = "claude", and
its own batch) and switch with --provider instead of editing the file:
provider = "sail" # the default; must come before any [section]
[providers.sail]
base_url = "https://api.sailresearch.com/v1"
model = "deepseek-ai/DeepSeek-V4.1-Flash"
api_key_file = "/home/me/.config/girlpages/sail-key"
api = "responses"
completion_window = "flex"
batch = "sail"
[providers.haiku]
backend = "claude"
model = "claude-haiku-5-5"
girlrepo girlify --provider haiku -j 8
girl --doctor shows what it's actually using. Everything is documented in
dist/config.toml and in girl girl. Want a different vibe? Set
extra_instructions.
Usage
All the man you know:
girl ls # a page
girl 3 printf # from a section
girl -a printf # every match
girl -k compress # apropos (straight to man -k)
girl -l ./thing.1 # a local file
Plus:
girl --refresh tar # convert again
girl --model opus tar # with a different model
girl --no-cache tar # ask the LLM fresh, without saving it
girl --cached-only ls # never call the LLM
girl --repo URL ls # try a girlpages repository first
girl -r ls # read it from the repository instead
girl --raw ls # the stored markup
girl --doctor # config, backend and cache status
girl --html ls # a standalone HTML page
girl --serve # the web app
girl --bulk [sections] # convert everything
Hacking
make check # tests, vet, gofmt
make e2e # the website's navigation in a real browser (needs Python playwright)
make lint # type-check the website's JavaScript (needs npx)
Pages are stored as a small Markdown dialect with colour tags (<pink>…</pink>
and friends, plus <rainbow>), and <bold>, <italic> and <kbd>, which
models write anyway; internal/render turns that into ANSI or
HTML, and internal/web is the server, templates and stylesheet. The prompt
lives in internal/llm/prompt.go. Repositories are internal/repo (format,
store, client, search index), internal/ingest and internal/site; the
browser search in internal/site/static/search.js must match
internal/repo/search.go, which a test checks with node.
License
Copyright © 2026 Naomi Persephone Amethyst naomi@amethyst.name
girlpages is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See LICENSE.