Metadata-Version: 2.4
Name: fxcss
Version: 0.16.0
Summary: Live-reload, inspect and screenshot-test Firefox userChrome.css themes
Author: AdamXweb
License: MIT
Project-URL: Homepage, https://github.com/AdamXweb/fxcss
Project-URL: Documentation, https://github.com/AdamXweb/fxcss#readme
Project-URL: Changelog, https://github.com/AdamXweb/fxcss/releases
Project-URL: Issues, https://github.com/AdamXweb/fxcss/issues
Keywords: firefox,userchrome,css,theme,firefoxcss,testing
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: images
Requires-Dist: pillow>=10.1; extra == "images"
Dynamic: license-file

## fxcss

<p align="center">
<img width="120" src="https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/icon.png" alt="fxcss">
<br>
<a href="https://pypi.org/project/fxcss/"><img src="https://img.shields.io/pypi/v/fxcss" alt="PyPI"></a>
<img src="https://github.com/AdamXweb/fxcss/actions/workflows/ci.yml/badge.svg" alt="CI">
<br>
A testing toolkit for <code>userChrome.css</code> Firefox themes.<br>
Edit your CSS and see it live, click any part of the UI to get its selector,
and screenshot-test changes in CI.
</p>

**Three ways in:**

| You are… | Start with |
| --- | --- |
| **Building a theme** | `fxcss new my-theme` scaffolds one; `fxcss watch` shows edits live in ~50ms; `fxcss pick` names any element you click. |
| **Trying someone's theme** | `fxcss try owner/repo` — test-drive it in a throwaway profile; close the window and nothing remains. Sure about it? `fxcss install owner/repo` puts it in your real profile, with a backup. |
| **Maintaining a theme repo** | `fxcss init` — before/after screenshots on every pull request, on macOS, Windows and Linux. |

## Your first ten minutes

```bash
pipx install "fxcss[images]"    # no pipx? brew install pipx / sudo apt install pipx

# See it work on a real theme before touching your own:
fxcss try AdamXweb/WhiteSurFirefoxThemeMacOS

# No theme yet? Start from a small working one:
fxcss new my-theme

# Point it at your theme (the folder containing chrome/) and edit live:
cd my-theme && fxcss watch

# Can't name the element you want to style? Click it:
fxcss pick

# Happy? Give the repo CI previews:
fxcss init && git add .github && git commit -m "ci: theme previews"
```

Every one of these runs in a throwaway profile. Looking for themes to try?
Browse [firefoxcss-store.github.io](https://firefoxcss-store.github.io/) or
[r/FirefoxCSS](https://www.reddit.com/r/FirefoxCSS/) — anything with a
`userChrome.css` on GitHub works with `fxcss try owner/repo`.

## Description

Working on a Firefox theme normally means: edit CSS, restart Firefox, squint,
repeat — and guessing at element names, because the browser's own UI isn't in
any page inspector you're used to.

fxcss removes both problems. It installs your theme into a throwaway profile,
drives Firefox over **Marionette** (Firefox's built-in automation protocol), and
gives you a live-reload loop, an element picker, and a screenshot differ.

Your real Firefox profile is never touched — except by the one command whose
job that is: `fxcss install`, which backs up what it replaces and keeps a
manifest so `fxcss uninstall` can put everything back.

![Three saved edits in fxcss watch, each recolouring the chrome](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch-loop.gif)

<p align="center"><sub>Three saves in <code>fxcss watch</code> — the window updates in ~50ms.
Every image in this README was generated by fxcss itself.</sub></p>

## Requirements

- Python 3.9+
- Firefox (any recent release; the toolkit finds it automatically on macOS,
  Windows and Linux, or set `FIREFOX_BIN`)
- `pillow`, only for `catalogue`, `compare` and `tweaks` — every other
  command is standard library. Added later with `pipx inject fxcss pillow`.

## Installation

fxcss is [on PyPI](https://pypi.org/project/fxcss/). Install it with **pipx**,
which gives it its own environment and puts `fxcss` on your PATH:

```bash
pipx install "fxcss[images]"
```

No pipx yet? `brew install pipx` (macOS), `sudo apt install pipx` (Debian and
Ubuntu), or `python3 -m pip install --user pipx` elsewhere.

> **Why not plain pip?** On current Homebrew, Debian and Ubuntu Pythons,
> `python3 -m pip install` refuses with `error: externally-managed-environment`
> — that's [PEP 668](https://peps.python.org/pep-0668/) protecting your system
> Python, not fxcss being broken. pipx is the intended answer for installing an
> application. pip still works fine *inside a virtual environment*:
>
> ```bash
> python3 -m venv ~/.venvs/fxcss && ~/.venvs/fxcss/bin/pip install "fxcss[images]"
> ```

For CI, or anywhere a surprise upgrade would be unwelcome, pin the release —
the [releases page](https://github.com/AdamXweb/fxcss/releases) has the latest.
CI runners' Pythons are not externally managed, so plain pip is fine there:

```bash
pip install "fxcss[images]==0.16.0"
```

Either gives you an `fxcss` command. To hack on it, clone and install editable:

```bash
git clone https://github.com/AdamXweb/fxcss.git
cd fxcss && python3 -m pip install -e ".[images]"
```

And if you would rather install nothing at all, the repo runs as-is:

```bash
python3 -m fxcss <command>
```

Run commands from your theme's root (the folder containing `chrome/`), or point
at it with `--theme /path/to/theme`.

## Commands

| Command | What it's for |
| --- | --- |
| `new` | Start a theme from a small, working scaffold |
| [`try`](#fxcss-try) | Download a theme from GitHub and test-drive it |
| [`install`](#fxcss-install) | Install a theme into your real Firefox profile |
| [`uninstall`](#fxcss-install) | Remove it again, restoring what was there |
| [`upgrade`](#fxcss-upgrade) | Fetch a newer version of the theme you installed |
| [`rollback`](#fxcss-upgrade) | Put the previous version back |
| [`adopt`](#fxcss-adopt) | Take over a theme installed some other way |
| [`profiles`](#fxcss-profiles) | List every Firefox profile and what is themed in it |
| [`watch`](#fxcss-watch) | Edit CSS and see it live, no restart |
| [`pick`](#fxcss-pick) | Click any part of the UI to get its CSS selector |
| [`inspect`](#fxcss-inspect) | Look up a selector you already have |
| [`init`](#fxcss-init) | Add PR previews and CI checks to your theme repo |
| [`tweaks`](#fxcss-tweaks) | Screenshot every install option into a committable doc |
| [`audit`](#fxcss-audit) | Find every selector that no longer matches, and suggest fixes |
| [`changelog`](#fxcss-changelog) | Diff two Firefox builds to see what chrome changed |
| [`snapshot`](#fxcss-changelog) | Record a Firefox's chrome names, to diff against later |
| [`catalogue`](#fxcss-catalogue) | Build a directory of themeable UI parts |
| [`shot`](#fxcss-shot) / [`compare`](#fxcss-compare) | Screenshot and diff two versions |
| [`doctor`](#fxcss-doctor) | Report what your Firefox supports |

### fxcss try

```bash
fxcss try adamXweb/WhiteSurFirefoxThemeMacOS
fxcss try github.com/owner/theme --with compact-tabs
fxcss try owner/theme --info            # report what's there, launch nothing
```

**Test-drive a theme before committing to it.** Downloads it, installs it into a
throwaway profile, and opens Firefox so you can actually use it. Your own profile
is never touched — close the window and nothing remains.

It reports what it found before doing anything:

```
  adamxweb/whitesurfirefoxthememacos  ★614  MIT
    MacOS Big Sur like theme for Firefox on MacOS & Windows.
    latest release   v1.6.3  (2025-07-26)
    latest commit    b10c574  (2025-07-26)  Merge pull request #167 …

  fetching release v1.6.3 …
  theme found at the repository root  (39 stylesheets, 134 KB)

  This theme ships install.sh. fxcss does not run it —
  it installs the files itself, which is all those scripts do.

  Options its README documents:
    -c     Left hand side tab close button
    -p     Makes tabs height compact like current Safari
    …

  Optional stylesheets you can layer on with --with:
    compact-tabs, hideextension, noidentity, tabs-swapclose, …
```

Releases are preferred over branch tips, since that is what the author blessed;
`--commit` takes the latest commit instead, and `--ref` takes any tag, branch or
SHA. `--with name,name` layers on the theme's optional stylesheets so you can see
a variant without hunting through install flags. `--shot dir` captures the
standard screenshots instead of opening a window, and `--keep dir` leaves the
download behind so you can start editing it with `watch`.

#### It does not run the theme's install script

That is deliberate, and worth being plain about: fetching a shell script from a
URL and executing it to preview a stylesheet is a bad trade. Those scripts are,
in substance, `cp -r chrome/ <profile>/` plus flipping a pref — which fxcss
already does. So it finds the script, tells you it exists, parses the options its
README documents, and then installs the files itself.

What is left is the theme's own content: CSS, SVG, and occasionally a `.js` file.
Firefox does not execute a `.js` file sitting in a profile's chrome folder; that
requires an autoconfig hook in the *application* directory, which fxcss does not
create. Archives are size-capped and path-checked on extraction, and symlinks in
them are skipped.

If you decide you want the theme permanently, `fxcss install` is the same
resolution and the same file copying — pointed, deliberately, at your real
profile.

### fxcss install

```bash
fxcss install owner/theme                      # into your default profile
fxcss install owner/theme --with compact-tabs  # optional sheets, permanently
fxcss install ~/src/my-theme                   # a local checkout works too
fxcss install --list-profiles                  # see what it found first
fxcss uninstall                                # put everything back
```

**Put a theme into the Firefox profile you actually use** — the cross-platform
replacement for each theme's own `install.sh` (and the answer for themes whose
install script never covered Windows). Resolution is the same as `try`:
a GitHub `owner/name` or URL with `--ref`/`--commit`, or a local directory.
As with `try`, the theme's own install script is never executed.

It finds your real profiles by parsing `profiles.ini` in the platform's
Firefox directory (macOS `~/Library/Application Support/Firefox`, Windows
`%APPDATA%\Mozilla\Firefox`, Linux `~/.mozilla/firefox` plus the snap and
flatpak locations) and installs into the profile Firefox itself would open.
`--profile <name-or-path>` overrides; with several profiles and no clear
default, interactive runs get a picker and scripts get an error — CI is never
prompted. Profiles kept somewhere unusual can be added to the search with
`FXCSS_PROFILE_ROOTS=/path/to/dir`, mirroring `FXCSS_FIREFOX_ROOTS`.

Firefox's default profile decides only for `install`, which is choosing where
to *put* a theme. Every command that acts on one already installed —
`uninstall`, `upgrade`, `rollback` — looks for the profile that **has** it,
so a theme installed deliberately into your Developer Edition profile is
still found by a bare `fxcss uninstall`. `adopt` looks for the opposite: a
`chrome/` folder fxcss did not install. Firefox's default breaks the tie only
when more than one profile qualifies, and `--profile` always wins.

The picker and `--list-profiles` say which Firefox each profile belongs to,
because `default-release` and `dev-edition-default` are one word apart in a
list of hashed directory names — and installing into the wrong one looks
exactly like the theme not working:

```
Several Firefox profiles exist:
  1. default-release        [Release]              …/Profiles/8f2b1a.default-release  (Enter)
  2. dev-edition-default    [Developer Edition]    …/Profiles/c41d9e.dev-edition-default
  3. work                   [unrecognised]         …/Profiles/7ab3.work
```

The label comes from the directory suffix Firefox itself assigns, so a profile
you named yourself reads `[unrecognised]` rather than being guessed at.

Run it without `--with` and it offers the theme's optional stylesheets rather
than leaving you to find them in the repository:

```
  This theme ships optional stylesheets:
    1. compact-tabs
    2. theme-dracula
    …
    Numbers separated by commas, `all`, or Enter for none.
  Include:
```

#### Options that cancel each other out

Some of a theme's optional sheets are alternatives rather than additions.
Installing two colour themes is two `@import`s, and nothing about that warns
you — the later one silently wins outright, leaving a browser that looks like
neither the one you picked nor the one before it. `install` measures for this
and stops:

```console
$ fxcss install AdamXweb/WhiteSurFirefoxThemeMacOS --with theme-nord,theme-dracula

  theme-dracula and theme-nord are alternatives, not additions: both set the
  same 122 declaration(s), so whichever loads last replaces the other entirely
    :root { --gnome-browser-before-load-background: #282a36 }  vs  { …: #2e3440 }

  Refusing to install stylesheets that cancel each other out — only one
  of them would have any effect, and which one is decided by import
  order rather than by you. Pick one, or pass --force.
```

It is a measurement, not a guess about names. Two sheets are alternatives when
they set **the same properties on the same selectors** — provable from their
text, and true whatever they are called. Matching on a `theme-` prefix would
be a convention rather than a fact: it would tell a theme shipping `theme-blue`
and `theme-compact` that those clash when they compose perfectly well, and
miss a pair named `dark.css` and `nord.css`. Sheets that agree exactly are not
in conflict, because two options setting the same border radius the same way
compete for nothing.

`try` reports the same thing and continues — a throwaway profile is a fine
place to watch two colour themes cancel out — and `tweaks` says so before
screenshotting a `--combo` that cannot take effect.

> **What this does not see.** It compares declarations, so it catches sheets
> fighting over the same property. Two sheets that rearrange the same area
> through *different* selectors — WhiteSur's `tabs-swapclose` and
> `windows-swapclose` both move a close button and share no declarations at
> all — are invisible to it. `fxcss tweaks --combo a+b` is what proves those:
> if `a+b` renders identically to `b` alone, `a` was overridden. Silence here
> is "nothing measurable", never "verified compatible".

When the theme's default branch has moved on since its newest release, that
choice is put to you as well — a tag can be a year behind a fix you are
looking for, and equally the branch can be mid-rewrite, so neither is right to
assume:

```
  The default branch has moved on since the latest release:
    1. release v2.0                 2025-01-03  what the author last published  (Enter)
    2. latest commit on master      2026-08-14  newer than the release — fix tab colours
  Install [1-2]:
```

Scripts and CI never see any of these prompts: without a terminal the release
wins, as before, with a one-line note that `--commit` exists.

The install is what a theme's install script does, done carefully:

- your existing `chrome/` is moved to a timestamped `chrome.backup-*` sibling
  first — nothing is overwritten in place;
- the theme's `chrome/` is copied in, along with any `--with` optional sheets
  (placed where the theme's own `@import`s expect them);
- `toolkit.legacyUserProfileCustomizations.stylesheets` is enabled in
  `user.js` — inside a clearly marked block, so it can be removed cleanly —
  together with any `configuration/user.js` the theme ships;
- a manifest (`chrome/fxcss-install.json`) records every file written, its
  sha256, and where the theme came from — which repo, which ref, and whether
  that ref was a release or a branch.

`fxcss uninstall` reads that manifest, removes exactly the files it lists,
restores the backup, and strips the `user.js` block. Files it cannot prove
fxcss wrote are never deleted — they are kept, or moved aside, never removed.
Restart Firefox after either command; it reads `userChrome.css` at startup.

> **Changed in 0.13:** before 0.13, `fxcss install` was an alias for `try`
> and touched nothing real. The throwaway test-drive lives on, unchanged, as
> `fxcss try`.

### fxcss upgrade

```bash
fxcss upgrade                        # take the newest version of what you have
fxcss upgrade --check                # report only; exit code says what it found
fxcss upgrade --audit                # check the new version against your Firefox first
fxcss upgrade --ref v2.1.0           # somewhere specific
fxcss rollback                       # …and back again
```

`upgrade` re-installs the theme the profile already has, at whatever is newest
*of the kind it tracks*: an install that took a release moves to the newest
tag, one that followed a branch moves to that branch's current commit, and one
pinned with `--ref` does not move at all unless you say so.

It stops rather than surprise you, in three places:

- **Files you edited yourself.** Every install records a sha256 per file, so
  an upgrade knows which ones you have since changed and refuses to write over
  them until `--force`. Files you *added* are never touched either way.
- **Options that vanished.** If you installed `--with theme-nord` and the new
  version renamed or dropped that sheet, the `@import` would simply stop
  resolving and the option would turn itself off. `upgrade` names the loss and
  makes you choose instead.
- **Selectors the new version needs and your Firefox lacks** — with `--audit`,
  which runs the same check as [`fxcss audit`](#fxcss-audit) against the
  fetched copy *before* anything is installed.

`--check` changes nothing and answers with its exit code, for cron, launchd or
CI: **0** up to date, **1** an upgrade is available, **2** it cannot be told
(no install here, an unreachable repo, or a manifest too old to say what it
tracked). fxcss deliberately ships no scheduler of its own — this is the piece
you point yours at.

```console
$ fxcss upgrade

  profile: default-release  (~/Library/…/8f2h1kqp.default-release)
  installed: AdamXweb/WhiteSurFirefoxThemeMacOS @ v1.6.3
  upstream:  v2.0.0  — 2026-08-16

  fetching v2.0.0 …
  keeping optional sheets: theme-nord

  Upgrade to v2.0.0? [Y/n]

  upgraded to v2.0.0
  the previous version is kept as chrome.backup-20260817014202

  Restart Firefox to see it. `fxcss rollback` puts the previous version back.
```

#### Going back

Every install and every upgrade leaves a `chrome.backup-*` behind, and the
manifest travels inside `chrome/` — so each backup can say what it holds:

```console
$ fxcss rollback --list

  Backups, newest first:

    chrome.backup-20260817014202
      AdamXweb/WhiteSurFirefoxThemeMacOS@v1.6.3
    chrome.backup-20260817014143  (the original)
      your own chrome/, from before fxcss
```

`fxcss rollback` restores the most recent, or `--to <name>` any of them.
What was installed becomes a backup in its turn, so a rollback can itself be
rolled back, and `user.js` follows: each version records the prefs it asked
for, and rolling back to the original — the one backup with no manifest in it,
because it is *your* chrome folder from before any of this — takes the fxcss
pref block out with it.

That original is the reason upgrades chain rather than stack blindly. After
five upgrades the newest backup holds *the theme*, not your files, so the
manifest carries the original's name forward and `fxcss uninstall` still
restores what you had before you ever ran fxcss. `--keep N` (default 3) prunes
older backups; the original is never one of them.

### fxcss adopt

```bash
fxcss adopt owner/theme              # identify what is already installed
fxcss adopt                          # …if chrome/ says where it came from
fxcss adopt owner/theme --ref v2.0.0 # check against one version only
```

Most themed profiles were not themed by fxcss — someone ran the theme's
`install.sh`, or copied a `chrome/` folder in by hand, long before any of this.
`fxcss profiles` can *describe* those, but nothing can act on them: there is no
record of what the theme is. `adopt` writes that record, and then `upgrade`,
`rollback` and `uninstall` all work.

**It identifies the theme by its contents.** Every file under `chrome/` is
hashed exactly the way git hashes a blob and compared against the repository's
own tree at each recent version. A version where every file matches is not a
guess — it is the same bytes:

```console
$ fxcss adopt AdamXweb/WhiteSurFirefoxThemeMacOS

  profile: default-release  (~/Library/…/8f2h1kqp.default-release)

  comparing 148 file(s) against AdamXweb/WhiteSurFirefoxThemeMacOS …
    v2.0.0: 129/135 files match, 6 edited, 7 added
    v1.6.3: 114/132 files match, 18 edited, 10 added

  best match: v2.0.0: 129/135 files match, 6 edited, 7 added
  Recorded as that version plus local differences, so an upgrade knows
  not to overwrite them without being told.
```

Comparison uses GitHub's git-tree API rather than downloading anything, so
checking ten versions costs ten small requests instead of ten archives — and
archives are the first thing GitHub rate-limits.

Naming the repository is usually necessary. `adopt` checks `chrome/` for a git
remote (definitive — someone cloned it there) and for GitHub URLs in the
theme's own files (a hint worth confirming), but plenty of themes leave no
trace at all once installed: WhiteSur's `chrome/` contains no URL anywhere.
That is normal, not a failure, and the message says so.

**Nothing is installed or replaced.** The `chrome/` already there is *copied*
to a `chrome.backup-*` and then described, so `uninstall` has somewhere to put
things back to — verified as a byte-identical round trip. `user.js` is left
exactly as it is: the pref that turns `userChrome.css` on is evidently already
set, since the theme is working, and writing an fxcss block to say so again
would edit a file for no gain. The next `upgrade` writes one properly, from the
theme it fetches.

Files that already differed from the release are recorded, and
[`upgrade`](#fxcss-upgrade) treats them exactly like edits made after an
install — it will not overwrite them without `--force`. That matters here more
than anywhere: a theme someone has been hand-editing for a year is the most
likely thing to be adopted.

> Two versions can also be identical in content while differing in line
> endings, which is what a Windows clone with `core.autocrlf` produces.
> That is reported as a match, noting the difference, rather than as
> "nothing matches".

### fxcss profiles

```bash
fxcss profiles                       # what is themed where
fxcss profiles --check               # …and whether anything newer exists
fxcss profiles --json                # machine-readable
```

Read-only. Firefox keeps its profiles in directories named after a hash, so
"which profile has the theme in it" is a genuinely hard question to answer by
looking:

```console
$ fxcss profiles --check

  Firefox profiles on this machine

  ● default-release          [Release]
    ~/Library/Application Support/Firefox/Profiles/8f2h1kqp.default-release
    theme    AdamXweb/WhiteSurFirefoxThemeMacOS @ v1.0.0
             installed 2026-08-14 09:12:44  (tracking the release)
    sheets   theme-nord
    files    137 file(s), 1 edited since install, 1 added by hand
    update   v2.0.0 available  — 2026-08-16

    dev-edition-default      [Developer Edition]
    ~/Library/Application Support/Firefox/Profiles/p93kd0zx.dev-edition-default
    chrome/  41 file(s), not installed by fxcss
             `fxcss install` here would back this up first

  ● the profile Firefox opens by default
```

Three states, kept distinct on purpose: a profile fxcss installed into and can
speak for, a profile with a `chrome/` folder someone put there by hand, and a
profile with no theme at all. Only the first can be described in detail; for
the second, all fxcss honestly knows is that files are there and that
installing would move them aside.

`--check` asks GitHub once per theme, not once per profile, and compares like
for like: an install tracking releases is measured against the newest tag, one
tracking a branch against the commit that branch points at now. An install
pinned with `--ref` reports as pinned rather than as behind. Where the
manifest predates fxcss recording which of those applied, it says so instead
of guessing — "up to date" is never printed unless it was actually checked.

The same reservation covers local edits: installs from before 0.16 recorded no
file hashes, so `fxcss profiles` reports them as *not checked for edits*
rather than as unmodified. Reinstalling records them.

### fxcss completions

```bash
eval "$(fxcss completions bash)"     # add to ~/.bashrc
eval "$(fxcss completions zsh)"      # add to ~/.zshrc
fxcss completions fish | source      # add to config.fish
```

Tab-completes subcommands, the flags each one takes, `--firefox` channel names
— and, reading the theme in front of it, the names of its optional stylesheets:

```console
$ fxcss install ~/src/whitesur --with theme-mat<TAB>
theme-material-ocean  theme-material-palenight
```

Comma-separated lists complete element by element, and values already chosen
are not offered twice. The candidates are read off the real argument parser, so
a command or flag becomes completable the moment it exists rather than when
someone remembers to update a shell script. Completion never touches the
network: sheet names for a remote `owner/repo` are not known locally, and a Tab
that pauses to talk to GitHub would be worse than no completion at all — the
picker during `install` covers that case instead.

### fxcss watch

```bash
fxcss watch
```

Opens Firefox with your theme applied and watches `chrome/` and `custom/`. Save
a file in your editor and the running window updates in about 50ms.

The window is yours to drive — open menus, resize it, type in the address bar,
right-click things. Nothing is scripted.

![The example theme rendered in light and dark](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch.png)

| flag | effect |
| --- | --- |
| `--dark` | start in dark mode, for testing `prefers-color-scheme` rules |
| `--native-menus=false` | make right-click menus themeable (see [Context menus](#context-menus-are-native-on-macos)) |
| `--shot out.png` | write a screenshot after every reload |
| `--no-devtools` | don't enable the Browser Toolbox |

### fxcss pick

```bash
fxcss pick
```

**The answer to "what is this thing called?"** Move the mouse over the browser
window and the element under the cursor is outlined, with its selector shown in
a label:

![The picker outlining the address bar, labelled #urlbar](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/pick.png)

Click it and your terminal prints everything you need:

```
  toolbarbutton  →  #back-button
  classes   toolbarbutton-1 chromeclass-toolbar-additional
  box       32×36 at (88, 8)
  styles
    color: rgba(46, 52, 54, 0.35)
    border-radius: 8px
    list-style-image: url("chrome://browser/skin/back.svg")
  styled by 11 rules in this theme
    chrome/parts/buttons-fixes.css:5    :root:not([uidensity=compact]) #back-button {
    chrome/parts/custom-icons.css:6     #nav-bar #back-button .toolbarbutton-icon {
    chrome/parts/headerbar.css:76       #nav-bar #back-button:not(#hack) {
```

That last section is the useful part: not just what the element is, but which of
your files already style it, with line numbers. Keep clicking to pick more; Esc
in the browser or Ctrl-C in the terminal stops.

### fxcss inspect

```bash
fxcss inspect '#urlbar'
fxcss inspect '.tab-close-button' --dark
```

The same report, for a selector you already have. Useful for checking whether a
selector still matches anything after a Firefox update — a common cause of
themes quietly breaking.

If it matches nothing, it says so:

```
$ fxcss inspect '#urlbar-background'
no elements match '#urlbar-background' in this Firefox
```

That is a real example, not a contrived one: this repo's own example theme
styled `#urlbar-background` by id, which many older themes still do. The id was
replaced by a class, so the rule silently did nothing and the address bar
rendered unstyled. One command found it; the fix was `.urlbar-background`.

### fxcss init

```bash
fxcss init                       # before/after previews on every PR
fxcss init --watch --showcase    # plus the weekly Firefox audit and
                                 # release screenshots
fxcss init --previews            # plus README screenshots that keep themselves
                                 # current: every view and variant, re-rendered
                                 # on each change and pushed to a previews branch
```

**Turn any theme repository into one with CI.** Run it from your theme's root
and it writes the preview workflows into `.github/workflows/`, ready to commit:
every pull request then gets a comment showing the browser chrome before and
after the change, with changed pixels highlighted — rendered on macOS, Windows
and Linux, across sixteen views and every variant stylesheet you ship.

The generation is the point, not a convenience: the fxcss version is pinned to
the one doing the generating, and the publish allowlist is enumerated from
*your* theme's variant folder — the two things that had to be hand-edited, and
the second one silently drops views when forgotten. Existing files are never
overwritten without `--force`, and the output tells you the things that
otherwise surprise people (the comment starts after the workflows reach your
default branch; first-time contributors need one approval click).

If you'd like people to know:

[![theme previews by fxcss](https://img.shields.io/badge/theme%20previews-fxcss-ff7139)](https://github.com/AdamXweb/fxcss)

### fxcss tweaks

```bash
fxcss tweaks
fxcss tweaks --combo compact-tabs+tabs-swapclose
```

**Document your install options with screenshots.** Themes describe their
optional stylesheets in prose — accordions of flags, `install.sh -c -n -s`
incantations — and a user assembles their preferred setup in their head. This
renders the answer instead: the base theme, every optional stylesheet, and any
combination you bless with `--combo`, each with a labelled **before/after crop
of the region it actually changes** and how much of the chrome it touches.

The crop is built from the changed pixels, so it needs no per-option
configuration and cannot drift when Firefox moves something. It centres on the
busiest *cluster* of changes rather than the bounding box of all of them,
which is what makes the common cases readable: swapping the tab close button
changes every tab, and a crop of all of them is the tab strip again, shrunk
until nothing is visible. One tab, magnified, shows the option. Panels scale up
as well as down for the same reason — a correctly cropped 16px button is still
a 16px button.

The output is a folder of PNGs plus `TWEAKS.md`, written to be committed:
relative links, and a `<details>` accordion per option so a long list stays
scannable on GitHub. If your README documents installer flags, they are parsed
and included as a table.

A tweak that changes nothing is reported as exactly that — *"changes nothing on
current Firefox, possibly stale"*. Optional sheets rot at least as fast as
selectors do, and nobody notices because nobody has them enabled.

### fxcss audit

```bash
fxcss audit
fxcss audit --patch fix.diff     # write the confident fixes as a patch
fxcss audit --strict             # exit non-zero if anything needs attention
```

**Upgrading a theme after Firefox moved on.** `inspect` answers the question one
selector at a time; `audit` does the whole theme at once. It walks every id and
class your CSS mentions, resolves each against a running Firefox, and shows what
to change — with the real line from your file and the replacement applied:

```
  14 selectors need attention

  RENAMED  #urlbar-background  →  .urlbar-background
           same name, now a class rather than an id

    chrome/parts/headerbar-urlbar.css:52
    - #urlbar-background {
    + .urlbar-background {

  SIMILAR  #appMenu-fullscreen-button  →  #appMenu-fullscreen-button2
           no exact match; closest live name is #appMenu-fullscreen-button2

    chrome/parts/icons.css:198
    - #appMenu-fullscreen-button {
    + #appMenu-fullscreen-button2 {
```

That output is real — it is what this finds in a long-running theme. The
`…-button2` pattern is how Firefox has been versioning app-menu controls, and it
breaks menu styling silently.

Findings come in three kinds:

| | meaning |
| --- | --- |
| **RENAMED** | The same name exists, but as a class instead of an id, or the reverse. The suggestion is exact. |
| **SIMILAR** | No exact counterpart, but a close name exists. Usually a Firefox suffix change, or a typo in your CSS. |
| *unresolved* | Nothing close. Listed separately with `--all` and **not** counted as a problem — normally an element that only appears in a state fxcss cannot reach, not one that was removed. |

That last distinction is the point. Reporting every unmatched selector as broken
would be noise; a theme legitimately styles things that only exist in private
windows, on other platforms, or inside popups.

Suggestions are inferred from the live browser, not from a hardcoded list of
Firefox versions, so they keep working for releases that came out after this
tool did.

`--patch` writes a unified diff of the **RENAMED** findings only — the ones where
the replacement is certain. Review it, then `git apply`. SIMILAR findings are
deliberately excluded: they are usually right, but "usually" is not good enough
to rewrite your CSS unattended.

### fxcss changelog

```bash
fxcss changelog --firefox /path/to/old/firefox --against /path/to/new/firefox
```

**What actually changed between two Firefox releases.** Collects every chrome id
and class from both builds, diffs them, and tells you which of the removals your
theme depends on:

```
  Firefox 140.13.0 → 153.0.3
    52 chrome names gone, 221 new

  2 of them are used by this theme:
    #urlbar-background          chrome/parts/headerbar-urlbar.css:52
    #urlbar-go-button           chrome/parts/buttons-fixes.css:202
```

Point it at an ESR build and current release to see what a year of Firefox did
to your theme, or at a Beta to find out what is about to break before your users
do. `--show-all` lists every name that changed, not just the ones you use.

You do not need to keep an old browser around. `fxcss snapshot --out
baseline.json` records what a Firefox has; commit that file and compare later
with `--baseline`:

```bash
fxcss snapshot --out .fxcss/firefox-140.json     # once
fxcss changelog --baseline .fxcss/firefox-140.json
```

#### Watching Firefox for breakage

Firefox ships every few weeks, and a theme does not break loudly when it
renames something. A scheduled job can audit each channel and tell you before
your users find out — Beta and Nightly give weeks of warning.

`examples/firefox-watch.yml` is a working workflow that does this: it downloads
release, beta and nightly, audits the theme against each, opens a **pull
request** when the fixes are ones `--patch` is certain about, opens an issue
when they are not, and closes the issue once the channel is clean again.

#### Unused and unreachable code

`audit` also reports housekeeping, in its own section, separate from breakage:

- **Stylesheets nothing imports.** Files under `chrome/` unreachable by
  following `@import` from `userChrome.css`. Sheets in a `custom/` or
  `optional/` folder are excluded — being opt-in is the point of those.
- **Custom properties used but never set**, where an unthemed Firefox does not
  provide them either. These are usually typos: the `var()` silently falls back.
- **Custom properties set but read nowhere.** Reported cautiously — setting
  `--arrowpanel-background` exists precisely so Firefox's own rules pick it up,
  so this section excludes every name an unthemed Firefox resolves.

That last check is why `audit` briefly starts a second, unthemed browser: asked
of the themed one, every name resolves, because the theme set it.

Pass `--no-unused` to skip the section.

**Should it gate CI?** Report it, don't fail on it. `--strict` covers selectors
that no longer match, which is real breakage. Unused code is tidiness, and a
tidiness check that blocks merges gets disabled. The example CI here runs
`audit --strict` and lets the unused section be advisory.

### fxcss catalogue

```bash
fxcss catalogue --open
```

Builds an HTML directory of the UI parts a theme can target. For each one: a
cropped screenshot of the real element in light and dark, its selector, the
styles in effect, and every rule in your theme that targets it. Plus an
annotated overview screenshot with each part numbered.

![The generated catalogue page, with a numbered overview and per-element cards](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/catalogue.png)

Everything is measured from a running browser rather than hardcoded, so it stays
honest as Firefox changes — an element that no longer exists is reported as
missing rather than quietly documented.

Add `--self-contained` to also get a single `catalogue.html` with the images
inlined, for attaching to an issue.

### fxcss shot

```bash
fxcss shot --out shots/before
```

Captures the standard set of views as PNGs: browser window, focused address bar
and find bar in light and dark, then a tab playing audio, the same tab muted,
container tabs, an overflowing tab strip, a private window, compact density, the
sidebar, right-to-left chrome, and customize mode.

The captures land **flat** in `--out`, one file per view (`shots/before/light-01-window.png`);
`--url` captures go to `<out>/live/`. This is the directory to publish from if
you want plain screenshots — `fxcss compare` writes a different shape, below.

```bash
fxcss shot --out shots --variants all
```

`--variants` additionally captures one view per optional stylesheet the theme
ships (`custom/`, `optional/`, `variants/`…), each loaded on its own and removed
again — so `tabs-swapclose` or `compact-tabs` are checked by CI without a
separate install. Name specific ones (`--variants a,b`) or take them all.

#### Against real websites

```bash
fxcss shot --out shots --url https://github.com/AdamXweb/WhiteSurFirefoxThemeMacOS
fxcss shot --out shots --only-live --url https://example.com --url https://news.ycombinator.com
```

Captures the theme against live sites, light and dark, for showing it off —
README screenshots, release notes, an issue thread.

These land in `<out>/live/` and are **never part of a comparison**. That is the
whole point of keeping them separate: someone else's page can change its
content, title or favicon between two runs, and a theme pull request should not
be blamed for it. `compare` only looks at PNGs at the top level, so they are
excluded by construction rather than by a rule someone has to remember.

`examples/showcase.yml` automates it — regenerate on every release, publish to a
`showcase` branch, and link stable raw URLs from your README.

### fxcss compare

```bash
fxcss compare --base shots/before --head shots/after --out diff/
```

Diffs two sets and writes one stacked **before / after / changed-pixels** image
per view that differs. Views that render identically are reported rather than
pictured, so you only look at what actually changed.

`--out` therefore holds comparison images for changed views only, plus a
`summary.json` and a `full/` directory carrying a normalised copy of *every*
head capture, changed or not. So `<out>/full/` is what a preview comment shows
when nothing differs — and `shot`'s own `--out` (flat, no `full/`) is what to
read when you just want the screenshots.

![Before, after and changed-pixels panels for a one-line accent colour change](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/compare.png)

<p align="center"><sub>One changed value — the accent colour behind the active tab. The bottom panel
highlights the 0.09% of pixels that moved.</sub></p>

This is what makes it useful in CI: render your theme at the base commit and at
a pull request, and the diff shows a reviewer exactly what the change does. See
[Using it in CI](#using-it-in-ci).

### fxcss doctor

```bash
fxcss doctor
```

Reports your Firefox version, whether `userChrome.css` is enabled, whether
context menus are themeable on your platform, how many stylesheets your theme
has — and **every Gecko build installed on the machine**, with versions. Start
here if something isn't behaving.

#### Browser states it captures

`fxcss shot` renders 18 views, so a change is judged against the states people
actually use rather than one idle window: light and dark, the focused address
bar, the find bar, audio and muted tabs, container tabs, an overflowing tab
strip, a private window, compact density, right-to-left chrome, Customize
mode — and three that a theme is most likely to have never been tested in:

- **Sidebar — bookmarks and history.** Both panels, with their trees expanded,
  because a fresh profile shows them collapsed and a collapsed panel has almost
  nothing in it to style.
- **Vertical tabs.** Firefox 133+ does not restyle the tab strip here, it
  *moves* it: `#tabbrowser-tabs` leaves `#TabsToolbar` for `#vertical-tabs`, so
  every `#TabsToolbar > …` rule a theme owns silently stops matching while its
  unscoped `.tabbrowser-tab` rules keep applying horizontal geometry to a
  vertical column. Older builds without vertical tabs skip the view.
- **Customised toolbar.** The nav bar with widgets moved into it — by default
  including the new tab button, which is the rearrangement plenty of theme
  READMEs ask users to make by hand and which nothing could test until now.

Set your own arrangement with `--toolbar`, on `shot`, `watch` or `try`:

```bash
fxcss watch --toolbar "new-tab-button>nav-bar, -downloads-button"
fxcss shot  --toolbar "home-button>nav-bar@0" --out shots/
```

`widget>area` moves a widget (optionally `@position`), `-widget` removes one.
Areas are `nav-bar`, `TabsToolbar`, `PersonalToolbar`, `vertical-tabs`,
`unified-extensions-area`. A widget id Firefox does not recognise is reported
rather than ignored — Firefox itself accepts any string and then quietly
renders nothing.

#### Testing against Nightly, Developer Edition, ESR — or a fork

Every command that opens a browser takes a channel name as well as a path:

```bash
fxcss watch --firefox nightly
fxcss audit --firefox dev          # what will break before it ships
fxcss shot  --firefox esr --out shots/esr
```

Recognised names: `stable`, `beta`, `dev`, `nightly`, `esr`, and the Gecko
forks theme users actually run — `librewolf`, `floorp`, `waterfox`, `zen`.
They resolve against what is installed in the usual places; a build kept
somewhere unusual can be added with `FXCSS_FIREFOX_ROOTS=/path/to/dir`.

With **several builds installed and no `--firefox` given**, interactive
commands show a picker — press Enter for stable, or a number for another
build. CI and scripts are never prompted: non-interactive runs keep the old
behaviour exactly.

## Inspecting the UI with devtools

Firefox's normal inspector only sees page content. The **Browser Toolbox** is
the version that can inspect the browser's own UI, and it's off by default
behind four prefs. fxcss turns them on in its throwaway profile, so in `watch`
and `pick` you can just press:

- **macOS** — `Cmd+Opt+Shift+I`
- **Windows / Linux** — `Ctrl+Alt+Shift+I`

You get a full inspector over the browser chrome: hover to highlight, read
computed styles, and live-edit rules to try things before committing them to
your CSS. `fxcss pick` is the fast path for "what is this called"; the Browser
Toolbox is the thorough one for "why is this rule not winning".

## Using it in CI

`shot` and `compare` are designed to run on a hosted runner. The shape is:
check out the base revision and the pull request revision, render both, compare,
and publish the result.

```yaml
- run: pip install "fxcss[images]==0.16.0"   # pin: your CI, your upgrades
- run: fxcss shot --theme base --out shots/base
- run: fxcss shot --theme head --out shots/head
- run: fxcss compare --base shots/base --head shots/head --out out/ --platform ${{ runner.os }}
```

Two things to know before wiring this up:

- **Don't use headless mode.** Firefox headless renders no browser chrome at
  all, so a headless screenshot is an empty window. Runners need a real display;
  macOS and Windows runners have one, Linux needs `xvfb-run`.
- **Pull requests from forks get a read-only token.** If you want the result
  posted as a comment, build the images in the `pull_request` job (no write
  permissions, no secrets) and publish from a separate `workflow_run` job.

Don't copy workflow files by hand — `fxcss init` generates them for your theme,
allowlist and version pin included. [`examples/README.md`](examples/README.md)
explains the shape of what it writes, most importantly why the preview is two
workflows (fork PRs get a read-only token, so the half that runs their code
cannot be the half that posts the comment). This repo's own CI runs the full
pipeline against the packaged starter theme on macOS, Windows and Linux.

## Things worth knowing

### Context menus are native on macOS

Firefox sets `widget.macos.native-context-menus` to `true` by default, which
means **macOS draws right-click menus itself and CSS cannot style them at all**.
`menupopup` and `menuitem` rules have no effect there. They do apply on Windows
and Linux.

`fxcss doctor` reports the setting for your platform, and
`fxcss watch --native-menus=false` switches Firefox to XUL menus so you can work
on that styling from a Mac.

### Popups can't be screenshotted

Menus and the app menu are separate OS-level windows, so they appear in neither
a Marionette chrome screenshot nor a `drawWindow` rasterisation of the browser
window. Capturing the whole screen instead is worse: it depends on window
stacking and picks up whatever else is on your desktop. Every view `shot`
captures is therefore an in-document surface.

You can still *look* at popups in `watch`, and inspect them with the Browser
Toolbox. They just can't be captured.

### Why not Selenium?

Marionette is plain TCP with length-prefixed JSON, so the client here is about a
hundred lines of standard library. No geckodriver to keep in step with your
Firefox version — a common source of CI breakage — and no dependency to install
for the core commands.

More importantly, screenshots are taken in Marionette's **chrome context**,
which captures the browser window's own document. An ordinary WebDriver
screenshot only captures page content, so toolbars and tabs would never appear
at all.

### Reproducibility

Screenshot comparison only works if an unchanged theme renders identically
twice. The throwaway profile pins what would otherwise drift: first-run tours,
telemetry prompts, update checks and animations are off; pages are local files
rather than live sites; and Nimbus/Normandy are disabled so Mozilla can't switch
a toolbar feature on remotely between two runs.

Two CSS rules hide artifacts of the harness itself — the robot icon Firefox
shows in automated sessions, and the rollout-gated IP Protection button. Neither
is part of your theme.

Each session also picks its own Marionette port. Firefox's fixed default of 2828
means a browser leaked by an earlier run would silently accept the next
session's connection, which shows up as your theme mysteriously not applying.

## Contributing

Issues and pull requests welcome — particularly landmark definitions for UI
parts the catalogue doesn't cover yet, and reports of selectors that changed in
a new Firefox release.

## How this was built

fxcss was written with the assistance of **Claude** (Anthropic's Claude Opus 5),
working alongside [@AdamXweb](https://github.com/AdamXweb). Every change was
reviewed by a human before it landed.

Which commits are which is recorded in the history rather than asserted here:

| Author | |
| --- | --- |
| **`adamXbot`** | AI-assisted. Every one carries a `Co-Authored-By: Claude` trailer. |
| **`AdamXweb`** | Adam. |

Both halves of that are checkable:

```bash
git log --format='%an'                        # who authored each commit
git log --format='%b' | grep Co-Authored-By   # which were AI-assisted
```

Behaviour is not taken on trust either. CI runs on macOS and Windows on every
push and asserts the comparison in **both** directions: an unchanged theme must
render identically across runs, and an obvious CSS change must be detected.
That check found most of the real bugs in this tool — a random temp path leaking
into the address bar, Firefox flashing the find bar yellow as it opens, a
scrollbar appearing in one private-window capture and not the next — none of
which review had caught.

## Credits

Built while adding visual PR previews to
[WhiteSurFirefoxThemeMacOS](https://github.com/AdamXweb/WhiteSurFirefoxThemeMacOS),
and generalised so it works for any userChrome theme.

## License

[MIT](LICENSE)
