Releases: AdamXweb/fxcss
Release list
v0.17.0 — cache the base render between pushes
One change, to the PR preview template that fxcss init ships: the base-branch render is cached between pushes to a pull request.
Every preview run rendered both the base branch and the pull request. On a real theme repo that was 87% of all runner minutes, and a fifth of everything went to runs a newer push cancelled — usually after the base half had already finished. Base captures are deterministic (it is the contract fxcss's own CI asserts on every change), so re-rendering them on each push reproduced a result already proven identical.
The cache key names everything that can change a pixel — base commit, runner image, Firefox version, fxcss version, and the workflow file — with no near-match fallback: a base captured on one Firefox diffed against a head on another would blame the pull request for the upgrade. The base is saved the moment it renders, not at job end, so a run that a later push cancels still leaves its base behind for that push.
What it does and does not save. GitHub scopes caches to the branch that created them, so this helps a pull request's second push onward, not its first. A PR that takes six pushes to land renders the base once instead of six times.
Regenerate with fxcss init --force to pick it up. actions/cache@v5 joins the Node 24 floor test.
v0.16.2 — firefox-watch fix, templates off Node 20
Two fixes to the workflow templates that fxcss init ships. Both affect every theme that has run fxcss init — regenerate with fxcss init --force --watch --previews to pick them up.
firefox-watch has been silently failing
The weekly watch exists to catch a Firefox channel break before it reaches release. It was downloading firefox.dmg into the repo checkout and then staging its fix with git add -A, so the 150 MB dmg landed in the commit and GitHub's 100 MB limit rejected the push — on every channel, every week, with nothing to show for it but a red run.
WhiteSur's watch ran against Firefox 154 beta on Aug 17, four days before 154 broke captures. It found a real selector rename, wrote the patch, and died pushing it.
Now the dmg goes to $RUNNER_TEMP, and the fix is staged with git apply --index — exactly the files the patch touches, nothing else that a step might leave in the tree.
What the watch does and does not cover: it runs fxcss audit, so it catches selector renames. It does not exercise the capture pipeline, and would not have caught the 154 module move fixed in 0.16.1. The preview workflows cover that.
Templates off Node 20
The shipped templates were on checkout@v4 / setup-python@v5 / *-artifact@v4 / github-script@v7 — all Node 20, which GitHub's runners are retiring — while fxcss's own CI had moved on months ago. Dependabot bumps .github/workflows/ and does not know the templates hold the same actions.
All five now match what fxcss's CI runs (checkout@v7, setup-python@v7, upload-artifact@v7, download-artifact@v8, github-script@v9). A test pins the templates to the workflows so they cannot drift apart again, and another asserts no template action declares a Node 20 runtime.
v0.16.1 — Firefox 154 compatibility
A patch release for one thing: Firefox 154 broke every capture, and 0.16.0 or anything older will fail against it.
What happened
Firefox 154 moved ContextualIdentityService from resource://gre/modules/ContextualIdentityService.sys.mjs to moz-src:///toolkit/components/contextualidentity/. Importing either URL hard fails on the build that does not have it, so the container-tabs capture died the day 154 shipped:
MarionetteError: Failed to load resource://gre/modules/ContextualIdentityService.sys.mjs
Nothing in 0.16.0 caused it — core.py was byte-identical to 0.15.0. The same 0.16.0 passed on Firefox 153.0.4 three days before it failed on 154.0.
The fix
browser.js declares these modules with ChromeUtils.defineESModuleGetters(this, …) on both current Firefox and ESR 140, so the window property is the same object either side and names no URL to go stale. ContextualIdentityService now comes off the window, the way CustomizableUI already did for exactly this reason.
PlacesUtils moves to the same pattern. It has not moved and was not broken — it was the identical assumption waiting for the next uplift.
A test scans core.py for importESModule and fails on any that reappears.
Verified
All three CI platforms on Firefox 154.0, each capturing the container view that was failing, with no module-load errors.
If your CI pins fxcss, bump it to 0.16.1 — 154 is rolling out now.
v0.16.0 — theme management
Theme management: fxcss can now keep an installed theme up to date, put it back, say what is installed in every profile, and take over a theme it did not install.
fxcss profiles (#10)
Firefox names profile directories after a hash, so "which profile has the theme in it" is a hard question to answer by looking. This answers it, read-only:
$ fxcss profiles --check
● default-release [Release]
theme AdamXweb/WhiteSurFirefoxThemeMacOS @ v1.6.3
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-16Three states are kept apart: a profile fxcss installed into, one with a chrome/ someone put there by hand, and one with no theme at all. --check asks GitHub once per theme and reports five outcomes rather than a boolean — pinned and unknown are distinct from current, because "up to date" that was never actually checked is the one wrong answer someone would act on. --json for scripting.
fxcss upgrade and fxcss rollback (#11)
upgrade re-installs what a profile already has, at whatever is newest of the kind it tracks: the newest tag for a release install, the current commit for a branch, nothing at all for one pinned with --ref.
It stops rather than surprise you in three places — files you have edited since installing, an optional stylesheet the new version renamed or dropped (a missing @import fails silently, so that option would otherwise switch itself off with no output), and, with --audit, selectors the new version needs that your Firefox does not have.
--check changes nothing and answers through its exit code: 0 up to date, 1 an upgrade is available, 2 cannot be told. No scheduler ships with fxcss; this is the piece to point cron or launchd at.
rollback restores a backup, and what was installed becomes a backup in its turn, so it undoes itself. The backup stack describes itself, because the manifest travels inside chrome/:
$ fxcss rollback --list
chrome.backup-20260817014202
AdamXweb/WhiteSurFirefoxThemeMacOS@v1.6.3
chrome.backup-20260817014143 (the original)
your own chrome/, from before fxcssThat last one is what makes uninstall keep meaning what it says: after five upgrades the newest backup holds the theme, not your files, so the original is carried forward and is never pruned by --keep.
Optional sheets that cancel each other out (#12)
A theme's options are separate stylesheets, and installing two is two @imports. Some pairs are alternatives rather than additions: two colour themes means the later one silently wins outright, leaving a browser that looks like neither.
fxcss now measures this instead of guessing at names — two sheets are alternatives when they set the same properties on the same selectors, which is provable from their text and true whatever they are called. install refuses (--force overrides), try reports and continues, and tweaks says so before screenshotting a --combo that cannot take effect.
fxcss adopt (#13)
Most themed profiles were not themed by fxcss — someone ran the theme's install.sh, or copied a folder in by hand. adopt identifies what is there by hashing every file the way git hashes a blob and comparing against the repository's own tree at each recent version, so a match is the same bytes rather than a guess:
$ fxcss adopt AdamXweb/WhiteSurFirefoxThemeMacOS
comparing 148 file(s) against AdamXweb/WhiteSurFirefoxThemeMacOS …
v2.0.0: 129/135 files match, 6 edited, 7 addedNothing is installed or replaced: your chrome/ is copied to a backup and then described, so upgrade, rollback and uninstall all start working. Files that already differed from the release are recorded, and an upgrade will not overwrite them without being told.
Also
- The install manifest gained a schema version, structured source fields, a sha256 per file, and the prefs each version asked for. Manifests written by earlier releases are read and filled out in memory; nothing rewrites a file it did not author, and where an old manifest cannot answer, fxcss says so rather than assuming.
429joins403in the rate-limit message pointing atGITHUB_TOKEN.- A duplicated test class was removed (#9).
Upgrading from 0.15: nothing changes for existing installs. Profiles installed by an earlier fxcss keep working; they report as not checked for edits until reinstalled, because no file hashes were recorded at the time.
v0.15.0 — option previews that show the option
Option previews that show the option.
The problem
Reported from a theme using these previews: the screenshot for an install option didn't show the option. "Tab close button on the left" rendered a whole browser window in which a 16px button had moved. "Re-enable the tab view button" showed a window that happened to contain one more button.
fxcss tweaks already cropped each option to the region it changes, derived from a pixel diff so it needs no per-option configuration. Two things stopped that working.
The crop was the union of every change
Right for an option that moves one widget; wrong for the ones people actually ask about. Swapping the tab close button changes every tab, so the union spans the whole strip and the "crop" is the window again.
Measured on a real capture:
| case | crop before | crop after |
|---|---|---|
| one button appears | 227×200 | 340×132, magnified |
| close button moves on every tab | 881×255 — 92% of the window width | 340×132 — 35% |
The crop now centres on the busiest cluster of changes — one tab — grown for context, pulling in any other cluster that lands inside it, and capped so it can never become the whole window again.
Clustering is connected components on an 8px grid, in pure Python: the grid is a sixty-fourth of the work, and a clustering dependency for this would be absurd. ~0.03s on a real mask.
Panels only ever shrank
So a correctly cropped 16px button arrived in a README as a 16px button. They now scale up as well as down, capped at 3×, past which a chrome screenshot is only blur.
Also
fxcss init --previews now renders those crops and publishes them beside the full windows, so a README's option accordions can embed <slug>-diff.png instead of a full browser window.
pipx upgrade fxcssv0.14.0 — tab completion, and an install that explains itself
Completions, an install that explains itself, and cleaner variant captures.
Tab completion
eval "$(fxcss completions bash)" # or zsh, or: fxcss completions fish | source$ fxcss install ~/src/whitesur --with theme-mat<TAB>
theme-material-ocean theme-material-palenightSubcommands, each command's flags, --firefox channel names, and — read from the theme in front of it — its optional stylesheets, comma-separated lists included. Candidates come off the real argument parser, so new commands are completable the moment they exist. Never touches the network, never raises.
install says what it is about to do
Three questions it used to answer silently:
Which stylesheets? Without --with, it now offers what the theme ships instead of leaving you to find them in the repository.
Which revision? When the default branch has moved on since the newest release, that choice is put to you, with dates and the newest commit message. A tag can sit a year behind the fix you came for; a branch can equally be mid-rewrite.
Which profile? Profiles now say which Firefox they belong to — [Release], [Developer Edition], [ESR], plus snap/flatpak on Linux:
1. default-release [Release] …/8f2b1a.default-release (Enter)
2. dev-edition-default [Developer Edition] …/c41d9e.dev-edition-default
3. work [unrecognised] …/7ab3.work
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. A profile you named yourself reads [unrecognised] rather than being guessed at.
Scripts and CI see none of these prompts: without a terminal the previous behaviour is unchanged.
Variant captures now render a clean window
shot --variants reused the session that had already been through the container-tabs and many-tabs views, so every variant image was really 18 overflowed tabs with a container tab selected — measured at 0.63% from the many-tabs view but 3.69% from a clean window. The container stripe that put on the active tab was misread as a theme bug during a real review. Variants now capture in their own session, which matters most for themes publishing these images in a README.
Also
fxcss init --previewsadds a workflow that renders every view and variant on each default-branch change and publishes them to apreviewsbranch, for READMEs whose screenshots keep themselves current.- Both
--outhelp strings and the README now say where each command writes:shotflat into--out,comparecomparisons plus a normalisedfull/. Conflating them silently published nothing. - Windows startup-race retry budget widened to five attempts over ~30s with exponential backoff, after the previous budget ran dry on a real runner.
pipx upgrade fxcssv0.13.0 — a real install, and previews that notice new views
Three changes, one of them the feature install was always pretending to be.
fxcss install — a real install, with a way back
Until now install was an alias for try: a themed throwaway profile that vanished on exit. It now installs into your actual profile — the one Firefox itself opens, found via profiles.ini — and can undo itself:
fxcss install AdamXweb/WhiteSurFirefoxThemeMacOS
fxcss install owner/theme --with compact-tabs,noidentity # optional sheets too
fxcss uninstallYour existing chrome/ is backed up first and a manifest records every file written, so uninstall removes exactly what was installed and restores the backup. Files fxcss did not write are never deleted — the manifest is treated as untrusted on the way back out, so a doctored one cannot reach outside chrome/. --with lands optional sheets where the theme's own @imports expect them, --profile picks a non-default profile, --list-profiles shows what was found.
Preview comments notice brand-new views
A PR that adds a variant stylesheet used to read "no visual change" — its captures exist only on the head side, so there was nothing to diff. compare now lists them in summary.only_in_head, and the publish workflow renders each one as "new in this PR". The PR whose whole point is a new look finally looks like one.
Windows runners: startup race fixed
Intermittently on windows-latest, the first loadURI of a session died with TypeError: … this._browser.frameLoader.remoteTab is null — Marionette answers commands before the initial browser has attached its content-process handle. setup_window now polls that exact attachment point (best-effort, 10s) and retries the tab setup only for this specific race; every other error surfaces unchanged. Found twice in WhiteSur preview CI, at 0.9.0 and 0.12.0.
One subtlety the fix itself uncovered: the initial browser can be a lazy browser whose frameLoader does not exist until a load forces it into existence — so the poll treats "no frameLoader" as ready rather than deadlocking on it, which the first CI run of the fix demonstrated in practice.
Verified on 153.0.4: full smoke runs green on macOS, Windows and Linux, and the unit suite grows from 49 to 84 — the install/uninstall filesystem paths, the head-only summary handling, and the startup-race error classifier are all pinned without needing a Firefox.
pipx upgrade fxcssv0.12.0 — sidebar, vertical tabs, custom toolbar
Three browser states a theme has probably never been tested in, captured on every run.
Sidebar — bookmarks and history
Two views now, with their trees expanded — a fresh profile shows them collapsed, so the old single view was mostly empty panel.
Vertical tabs
Firefox 133+ doesn't restyle the tab strip, it moves it: #tabbrowser-tabs leaves #TabsToolbar for #vertical-tabs. Every #TabsToolbar > … rule a theme owns silently stops matching, while unscoped .tabbrowser-tab rules keep applying horizontal geometry to a vertical column. Skipped automatically on builds without it.
Customised toolbar
The nav bar with widgets moved into it — by default the new tab button, the rearrangement many theme READMEs ask users to make by hand.
fxcss watch --toolbar "new-tab-button>nav-bar, -downloads-button"
fxcss shot --toolbar "home-button>nav-bar@0" --out shots/widget>area@position moves, -widget removes. Unknown widget ids are reported rather than silently rendering nothing.
Verified on 153.0.3: two full runs identical across all 18 views, and rules only these views can render change exactly those views and no others — now asserted in CI.
pipx upgrade fxcssv0.11.2
Fixes three views (many-tabs, compact, sidebar) that could render at different tab-strip scroll positions between two runs of an unchanged theme — Firefox's scroll-selected-into-view lands at a bistable offset after the strip overflows. The strip is now pinned to its start before those captures, so both runs land in the same place by construction.
pipx upgrade fxcss
v0.11.1
Fixes fxcss pick crashing with ModuleNotFoundError: No module named 'PIL' on installs without the [images] extra — reported from the field on the first real pick session.
The cause: the pure function pick uses to report "styled by N rules" lived in catalogue.py, whose module header imports Pillow. It now lives in a PIL-free module, and a regression test imports every core module with PIL blocked, so the base install can never grow an accidental Pillow dependency again.
The three commands that genuinely need Pillow (compare, catalogue, tweaks) now explain instead of crashing:
error: `fxcss compare` needs Pillow, which the base install leaves out.
pipx: pipx inject fxcss pillow (or reinstall: pipx install "fxcss[images]")
Already installed? pipx upgrade fxcss — or pipx inject fxcss pillow if you also want the image commands.