Skip to content

Releases: AdamXweb/fxcss

v0.17.0 — cache the base render between pushes

Choose a tag to compare

@adamXbot adamXbot released this 23 Aug 13:41
c661dd8

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

Choose a tag to compare

@adamXbot adamXbot released this 23 Aug 07:24
c392cf8

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

Choose a tag to compare

@adamXbot adamXbot released this 21 Aug 09:41
976e307

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

Choose a tag to compare

@adamXbot adamXbot released this 18 Aug 02:56

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-16

Three 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 fxcss

That 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 added

Nothing 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.
  • 429 joins 403 in the rate-limit message pointing at GITHUB_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

Choose a tag to compare

@adamXbot adamXbot released this 16 Aug 13:12

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 fxcss

v0.14.0 — tab completion, and an install that explains itself

Choose a tag to compare

@adamXbot adamXbot released this 16 Aug 12:29

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-palenight

Subcommands, 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 --previews adds a workflow that renders every view and variant on each default-branch change and publishes them to a previews branch, for READMEs whose screenshots keep themselves current.
  • Both --out help strings and the README now say where each command writes: shot flat into --out, compare comparisons plus a normalised full/. 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 fxcss

v0.13.0 — a real install, and previews that notice new views

Choose a tag to compare

@adamXbot adamXbot released this 14 Aug 07:35

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 uninstall

Your 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 fxcss

v0.12.0 — sidebar, vertical tabs, custom toolbar

Choose a tag to compare

@adamXbot adamXbot released this 11 Aug 05:45

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 fxcss

v0.11.2

Choose a tag to compare

@adamXbot adamXbot released this 11 Aug 04:28

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

Choose a tag to compare

@adamXbot adamXbot released this 11 Aug 04:16

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.