fxcss 0.7.0__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {fxcss-0.7.0 → fxcss-0.9.0}/PKG-INFO +71 -9
  2. {fxcss-0.7.0 → fxcss-0.9.0}/README.md +70 -8
  3. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss/__init__.py +1 -1
  4. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss/cli.py +108 -16
  5. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss/compare.py +13 -3
  6. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss/core.py +46 -12
  7. fxcss-0.9.0/fxcss/scaffold.py +152 -0
  8. fxcss-0.9.0/fxcss/templates/firefox-watch.yml +180 -0
  9. fxcss-0.9.0/fxcss/templates/pr-preview-cleanup.yml +46 -0
  10. fxcss-0.9.0/fxcss/templates/pr-preview-publish.yml +305 -0
  11. fxcss-0.9.0/fxcss/templates/pr-preview.yml +148 -0
  12. fxcss-0.9.0/fxcss/templates/showcase.yml +106 -0
  13. fxcss-0.9.0/fxcss/tweaks.py +191 -0
  14. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss.egg-info/PKG-INFO +71 -9
  15. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss.egg-info/SOURCES.txt +7 -0
  16. {fxcss-0.7.0 → fxcss-0.9.0}/pyproject.toml +3 -0
  17. {fxcss-0.7.0 → fxcss-0.9.0}/tests/test_units.py +110 -0
  18. {fxcss-0.7.0 → fxcss-0.9.0}/LICENSE +0 -0
  19. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss/__main__.py +0 -0
  20. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss/audit.py +0 -0
  21. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss/catalogue.py +0 -0
  22. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss/fetch.py +0 -0
  23. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss/probe.py +0 -0
  24. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss.egg-info/dependency_links.txt +0 -0
  25. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss.egg-info/entry_points.txt +0 -0
  26. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss.egg-info/requires.txt +0 -0
  27. {fxcss-0.7.0 → fxcss-0.9.0}/fxcss.egg-info/top_level.txt +0 -0
  28. {fxcss-0.7.0 → fxcss-0.9.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fxcss
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: Live-reload, inspect and screenshot-test Firefox userChrome.css themes
5
5
  Author: AdamXweb
6
6
  License: MIT
@@ -29,6 +29,14 @@ Edit your CSS and see it live, click any part of the UI to get its selector,
29
29
  and screenshot-test changes in CI.
30
30
  </p>
31
31
 
32
+ **Three ways in:**
33
+
34
+ | You are… | Start with |
35
+ | --- | --- |
36
+ | **Building a theme** | `fxcss watch` — edit CSS, see it live in ~50ms. `fxcss pick` names any element you click. |
37
+ | **Trying someone's theme** | `fxcss try owner/repo` — test-drive it in a throwaway profile; close the window and nothing remains. |
38
+ | **Maintaining a theme repo** | `fxcss init` — before/after screenshots on every pull request, on macOS, Windows and Linux. |
39
+
32
40
  ## Description
33
41
 
34
42
  Working on a Firefox theme normally means: edit CSS, restart Firefox, squint,
@@ -41,10 +49,10 @@ gives you a live-reload loop, an element picker, and a screenshot differ.
41
49
 
42
50
  Your real Firefox profile is never touched.
43
51
 
44
- ![The example theme rendered in light and dark](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch.png)
52
+ ![Three saved edits in fxcss watch, each recolouring the chrome](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch-loop.gif)
45
53
 
46
- <p align="center"><sub><code>examples/minimal-theme</code>, light and dark — every screenshot in this
47
- README was generated by fxcss itself.</sub></p>
54
+ <p align="center"><sub>Three saves in <code>fxcss watch</code> — the window updates in ~50ms.
55
+ Every image in this README was generated by fxcss itself.</sub></p>
48
56
 
49
57
  ## Requirements
50
58
 
@@ -93,6 +101,8 @@ at it with `--theme /path/to/theme`.
93
101
  | [`watch`](#fxcss-watch) | Edit CSS and see it live, no restart |
94
102
  | [`pick`](#fxcss-pick) | Click any part of the UI to get its CSS selector |
95
103
  | [`inspect`](#fxcss-inspect) | Look up a selector you already have |
104
+ | [`init`](#fxcss-init) | Add PR previews and CI checks to your theme repo |
105
+ | [`tweaks`](#fxcss-tweaks) | Screenshot every install option into a committable doc |
96
106
  | [`audit`](#fxcss-audit) | Find every selector that no longer matches, and suggest fixes |
97
107
  | [`changelog`](#fxcss-changelog) | Diff two Firefox builds to see what chrome changed |
98
108
  | [`snapshot`](#fxcss-changelog) | Record a Firefox's chrome names, to diff against later |
@@ -171,6 +181,8 @@ a file in your editor and the running window updates in about 50ms.
171
181
  The window is yours to drive — open menus, resize it, type in the address bar,
172
182
  right-click things. Nothing is scripted.
173
183
 
184
+ ![The example theme rendered in light and dark](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch.png)
185
+
174
186
  | flag | effect |
175
187
  | --- | --- |
176
188
  | `--dark` | start in dark mode, for testing `prefers-color-scheme` rules |
@@ -233,6 +245,55 @@ styled `#urlbar-background` by id, which many older themes still do. The id was
233
245
  replaced by a class, so the rule silently did nothing and the address bar
234
246
  rendered unstyled. One command found it; the fix was `.urlbar-background`.
235
247
 
248
+ ### fxcss init
249
+
250
+ ```bash
251
+ fxcss init # before/after previews on every PR
252
+ fxcss init --watch --showcase # plus the weekly Firefox audit and
253
+ # release screenshots
254
+ ```
255
+
256
+ **Turn any theme repository into one with CI.** Run it from your theme's root
257
+ and it writes the preview workflows into `.github/workflows/`, ready to commit:
258
+ every pull request then gets a comment showing the browser chrome before and
259
+ after the change, with changed pixels highlighted — rendered on macOS, Windows
260
+ and Linux, across sixteen views and every variant stylesheet you ship.
261
+
262
+ The generation is the point, not a convenience: the fxcss version is pinned to
263
+ the one doing the generating, and the publish allowlist is enumerated from
264
+ *your* theme's variant folder — the two things that had to be hand-edited, and
265
+ the second one silently drops views when forgotten. Existing files are never
266
+ overwritten without `--force`, and the output tells you the things that
267
+ otherwise surprise people (the comment starts after the workflows reach your
268
+ default branch; first-time contributors need one approval click).
269
+
270
+ If you'd like people to know:
271
+
272
+ [![theme previews by fxcss](https://img.shields.io/badge/theme%20previews-fxcss-ff7139)](https://github.com/AdamXweb/fxcss)
273
+
274
+ ### fxcss tweaks
275
+
276
+ ```bash
277
+ fxcss tweaks
278
+ fxcss tweaks --combo compact-tabs+tabs-swapclose
279
+ ```
280
+
281
+ **Document your install options with screenshots.** Themes describe their
282
+ optional stylesheets in prose — accordions of flags, `install.sh -c -n -s`
283
+ incantations — and a user assembles their preferred setup in their head. This
284
+ renders the answer instead: the base theme, every optional stylesheet, and any
285
+ combination you bless with `--combo`, each with a labelled **before/after crop
286
+ of the region it actually changes** and how much of the chrome it touches.
287
+
288
+ The output is a folder of PNGs plus `TWEAKS.md`, written to be committed:
289
+ relative links, and a `<details>` accordion per option so a long list stays
290
+ scannable on GitHub. If your README documents installer flags, they are parsed
291
+ and included as a table.
292
+
293
+ A tweak that changes nothing is reported as exactly that — *"changes nothing on
294
+ current Firefox, possibly stale"*. Optional sheets rot at least as fast as
295
+ selectors do, and nobody notices because nobody has them enabled.
296
+
236
297
  ### fxcss audit
237
298
 
238
299
  ```bash
@@ -480,11 +541,12 @@ Two things to know before wiring this up:
480
541
  posted as a comment, build the images in the `pull_request` job (no write
481
542
  permissions, no secrets) and publish from a separate `workflow_run` job.
482
543
 
483
- `examples/` has working workflows to copy — the pull request preview pair, the
484
- scheduled Firefox-channel watcher, and the showcase capture. See
485
- [`examples/README.md`](examples/README.md) for which to start with and why the
486
- preview is split into two files. This repo's own `.github/workflows/ci.yml`
487
- runs the whole thing against `examples/minimal-theme` on macOS and Windows.
544
+ Don't copy workflow files by hand — `fxcss init` generates them for your theme,
545
+ allowlist and version pin included. [`examples/README.md`](examples/README.md)
546
+ explains the shape of what it writes, most importantly why the preview is two
547
+ workflows (fork PRs get a read-only token, so the half that runs their code
548
+ cannot be the half that posts the comment). This repo's own CI runs the full
549
+ pipeline against `examples/minimal-theme` on macOS, Windows and Linux.
488
550
 
489
551
  ## Things worth knowing
490
552
 
@@ -8,6 +8,14 @@ Edit your CSS and see it live, click any part of the UI to get its selector,
8
8
  and screenshot-test changes in CI.
9
9
  </p>
10
10
 
11
+ **Three ways in:**
12
+
13
+ | You are… | Start with |
14
+ | --- | --- |
15
+ | **Building a theme** | `fxcss watch` — edit CSS, see it live in ~50ms. `fxcss pick` names any element you click. |
16
+ | **Trying someone's theme** | `fxcss try owner/repo` — test-drive it in a throwaway profile; close the window and nothing remains. |
17
+ | **Maintaining a theme repo** | `fxcss init` — before/after screenshots on every pull request, on macOS, Windows and Linux. |
18
+
11
19
  ## Description
12
20
 
13
21
  Working on a Firefox theme normally means: edit CSS, restart Firefox, squint,
@@ -20,10 +28,10 @@ gives you a live-reload loop, an element picker, and a screenshot differ.
20
28
 
21
29
  Your real Firefox profile is never touched.
22
30
 
23
- ![The example theme rendered in light and dark](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch.png)
31
+ ![Three saved edits in fxcss watch, each recolouring the chrome](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch-loop.gif)
24
32
 
25
- <p align="center"><sub><code>examples/minimal-theme</code>, light and dark — every screenshot in this
26
- README was generated by fxcss itself.</sub></p>
33
+ <p align="center"><sub>Three saves in <code>fxcss watch</code> — the window updates in ~50ms.
34
+ Every image in this README was generated by fxcss itself.</sub></p>
27
35
 
28
36
  ## Requirements
29
37
 
@@ -72,6 +80,8 @@ at it with `--theme /path/to/theme`.
72
80
  | [`watch`](#fxcss-watch) | Edit CSS and see it live, no restart |
73
81
  | [`pick`](#fxcss-pick) | Click any part of the UI to get its CSS selector |
74
82
  | [`inspect`](#fxcss-inspect) | Look up a selector you already have |
83
+ | [`init`](#fxcss-init) | Add PR previews and CI checks to your theme repo |
84
+ | [`tweaks`](#fxcss-tweaks) | Screenshot every install option into a committable doc |
75
85
  | [`audit`](#fxcss-audit) | Find every selector that no longer matches, and suggest fixes |
76
86
  | [`changelog`](#fxcss-changelog) | Diff two Firefox builds to see what chrome changed |
77
87
  | [`snapshot`](#fxcss-changelog) | Record a Firefox's chrome names, to diff against later |
@@ -150,6 +160,8 @@ a file in your editor and the running window updates in about 50ms.
150
160
  The window is yours to drive — open menus, resize it, type in the address bar,
151
161
  right-click things. Nothing is scripted.
152
162
 
163
+ ![The example theme rendered in light and dark](https://raw.githubusercontent.com/AdamXweb/fxcss/main/docs/watch.png)
164
+
153
165
  | flag | effect |
154
166
  | --- | --- |
155
167
  | `--dark` | start in dark mode, for testing `prefers-color-scheme` rules |
@@ -212,6 +224,55 @@ styled `#urlbar-background` by id, which many older themes still do. The id was
212
224
  replaced by a class, so the rule silently did nothing and the address bar
213
225
  rendered unstyled. One command found it; the fix was `.urlbar-background`.
214
226
 
227
+ ### fxcss init
228
+
229
+ ```bash
230
+ fxcss init # before/after previews on every PR
231
+ fxcss init --watch --showcase # plus the weekly Firefox audit and
232
+ # release screenshots
233
+ ```
234
+
235
+ **Turn any theme repository into one with CI.** Run it from your theme's root
236
+ and it writes the preview workflows into `.github/workflows/`, ready to commit:
237
+ every pull request then gets a comment showing the browser chrome before and
238
+ after the change, with changed pixels highlighted — rendered on macOS, Windows
239
+ and Linux, across sixteen views and every variant stylesheet you ship.
240
+
241
+ The generation is the point, not a convenience: the fxcss version is pinned to
242
+ the one doing the generating, and the publish allowlist is enumerated from
243
+ *your* theme's variant folder — the two things that had to be hand-edited, and
244
+ the second one silently drops views when forgotten. Existing files are never
245
+ overwritten without `--force`, and the output tells you the things that
246
+ otherwise surprise people (the comment starts after the workflows reach your
247
+ default branch; first-time contributors need one approval click).
248
+
249
+ If you'd like people to know:
250
+
251
+ [![theme previews by fxcss](https://img.shields.io/badge/theme%20previews-fxcss-ff7139)](https://github.com/AdamXweb/fxcss)
252
+
253
+ ### fxcss tweaks
254
+
255
+ ```bash
256
+ fxcss tweaks
257
+ fxcss tweaks --combo compact-tabs+tabs-swapclose
258
+ ```
259
+
260
+ **Document your install options with screenshots.** Themes describe their
261
+ optional stylesheets in prose — accordions of flags, `install.sh -c -n -s`
262
+ incantations — and a user assembles their preferred setup in their head. This
263
+ renders the answer instead: the base theme, every optional stylesheet, and any
264
+ combination you bless with `--combo`, each with a labelled **before/after crop
265
+ of the region it actually changes** and how much of the chrome it touches.
266
+
267
+ The output is a folder of PNGs plus `TWEAKS.md`, written to be committed:
268
+ relative links, and a `<details>` accordion per option so a long list stays
269
+ scannable on GitHub. If your README documents installer flags, they are parsed
270
+ and included as a table.
271
+
272
+ A tweak that changes nothing is reported as exactly that — *"changes nothing on
273
+ current Firefox, possibly stale"*. Optional sheets rot at least as fast as
274
+ selectors do, and nobody notices because nobody has them enabled.
275
+
215
276
  ### fxcss audit
216
277
 
217
278
  ```bash
@@ -459,11 +520,12 @@ Two things to know before wiring this up:
459
520
  posted as a comment, build the images in the `pull_request` job (no write
460
521
  permissions, no secrets) and publish from a separate `workflow_run` job.
461
522
 
462
- `examples/` has working workflows to copy — the pull request preview pair, the
463
- scheduled Firefox-channel watcher, and the showcase capture. See
464
- [`examples/README.md`](examples/README.md) for which to start with and why the
465
- preview is split into two files. This repo's own `.github/workflows/ci.yml`
466
- runs the whole thing against `examples/minimal-theme` on macOS and Windows.
523
+ Don't copy workflow files by hand — `fxcss init` generates them for your theme,
524
+ allowlist and version pin included. [`examples/README.md`](examples/README.md)
525
+ explains the shape of what it writes, most importantly why the preview is two
526
+ workflows (fork PRs get a read-only token, so the half that runs their code
527
+ cannot be the half that posts the comment). This repo's own CI runs the full
528
+ pipeline against `examples/minimal-theme` on macOS, Windows and Linux.
467
529
 
468
530
  ## Things worth knowing
469
531
 
@@ -1,3 +1,3 @@
1
1
  """fxcss — a testing toolkit for Firefox userChrome.css themes."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.9.0"
@@ -2,6 +2,8 @@
2
2
  """fxcss - a testing toolkit for Firefox userChrome.css themes.
3
3
 
4
4
  fxcss try download a theme from GitHub and test-drive it
5
+ fxcss init add PR previews and CI checks to your theme repo
6
+ fxcss tweaks screenshot every install option, into a committable doc
5
7
  fxcss watch edit CSS and see it live, no restart
6
8
  fxcss pick click any part of the UI to get its CSS selector
7
9
  fxcss inspect look up a selector you already have
@@ -26,6 +28,23 @@ from pathlib import Path
26
28
  from . import core
27
29
  from . import __version__
28
30
 
31
+ LANDING = """fxcss - a testing toolkit for Firefox userChrome.css themes
32
+
33
+ Building a theme?
34
+ fxcss watch edit CSS, see it live, no restart
35
+ fxcss pick click any part of the UI to get its selector
36
+
37
+ Trying someone else's theme?
38
+ fxcss try owner/repo test-drive it in a throwaway profile
39
+
40
+ Maintaining a theme repository?
41
+ fxcss init add before/after PR previews and CI checks
42
+ fxcss tweaks screenshot every install option for your README
43
+ fxcss audit find selectors Firefox has renamed
44
+
45
+ Run `fxcss --help` for the full command list, or `fxcss <command> --help`
46
+ for one command. Nothing here ever touches your real Firefox profile."""
47
+
29
48
  TOOLBOX_KEY = {
30
49
  "darwin": "Cmd+Opt+Shift+I",
31
50
  }.get(sys.platform, "Ctrl+Alt+Shift+I")
@@ -159,6 +178,54 @@ def cmd_try(args):
159
178
  shutil.rmtree(workdir, ignore_errors=True)
160
179
 
161
180
 
181
+ def cmd_tweaks(args):
182
+ from . import tweaks
183
+ theme = args.theme.resolve()
184
+ if not (theme / "chrome" / "userChrome.css").exists():
185
+ print(f"error: no chrome/userChrome.css under {theme}", file=sys.stderr)
186
+ return 2
187
+ available = core.find_variant_sheets(theme)
188
+ if not available:
189
+ print("This theme ships no optional stylesheets (looked in custom/, "
190
+ "optional/, options/, extras/, variants/), so there is nothing "
191
+ "to document.", file=sys.stderr)
192
+ return 2
193
+ variants = {slug: [path] for slug, path in available.items()}
194
+ for combo in args.combo or []:
195
+ try:
196
+ variants.update(core.parse_variant_spec(combo, available))
197
+ except ValueError as exc:
198
+ print(f"error: {exc}", file=sys.stderr)
199
+ return 2
200
+
201
+ firefox = core.find_firefox(args.firefox)
202
+ out = args.out.resolve()
203
+ print(f"fxcss tweaks\n theme: {theme}\n out: {out}\n")
204
+ with core.Session(theme, firefox, dark=args.dark) as session:
205
+ entries = tweaks.build(session, theme, out, variants, tweaks.readme_flags(theme))
206
+ stale = [e["slug"] for e in entries if not e["image"]]
207
+ print(f"\n wrote {out / 'TWEAKS.md'} with {len(entries)} option(s)")
208
+ if stale:
209
+ print(f" note: {', '.join(stale)} changed nothing — possibly stale, "
210
+ f"worth a look")
211
+ print(" Commit the folder and link TWEAKS.md from your README.")
212
+ return 0
213
+
214
+
215
+ def cmd_init(args):
216
+ from . import scaffold
217
+ theme = args.theme.resolve()
218
+ if not (theme / "chrome" / "userChrome.css").exists():
219
+ print(f"error: no chrome/userChrome.css under {theme} - run this from "
220
+ f"your theme's root, or pass --theme", file=sys.stderr)
221
+ return 2
222
+ slugs = sorted(core.find_variant_sheets(theme))
223
+ written, skipped = scaffold.write_workflows(
224
+ theme, slugs, watch=args.watch, showcase=args.showcase, force=args.force)
225
+ print(scaffold.next_steps(written, skipped, slugs, args.watch, args.showcase))
226
+ return 0
227
+
228
+
162
229
  def cmd_watch(args):
163
230
  theme = args.theme.resolve()
164
231
  firefox = core.find_firefox(args.firefox)
@@ -361,21 +428,13 @@ def cmd_shot(args):
361
428
  variants = {}
362
429
  spec = getattr(args, "variants", None)
363
430
  if spec:
364
- available = core.find_variant_sheets(theme)
365
- if spec.strip().lower() == "all":
366
- variants = available
367
- if not variants:
368
- print(" note: --variants all found no optional stylesheets",
369
- flush=True)
370
- else:
371
- wanted = [w.strip().lower() for w in spec.split(",") if w.strip()]
372
- missing = [w for w in wanted if w not in available]
373
- if missing:
374
- print("error: no optional stylesheet named "
375
- + ", ".join(missing) + "; available: "
376
- + (", ".join(sorted(available)) or "none"), file=sys.stderr)
377
- return 2
378
- variants = {w: available[w] for w in wanted}
431
+ try:
432
+ variants = core.parse_variant_spec(spec, core.find_variant_sheets(theme))
433
+ except ValueError as exc:
434
+ print(f"error: {exc}", file=sys.stderr)
435
+ return 2
436
+ if not variants:
437
+ print(" note: --variants all found no optional stylesheets", flush=True)
379
438
 
380
439
  firefox = core.find_firefox(args.firefox)
381
440
  with core.Session(theme, firefox) as session:
@@ -442,6 +501,8 @@ def cmd_doctor(args):
442
501
  print(f"\ntheme: {len(sheets)} stylesheets, {total:,} bytes")
443
502
  missing = [p for p in ("chrome/userChrome.css",) if not (theme / p).exists()]
444
503
  print("missing expected files: " + (", ".join(missing) if missing else "none"))
504
+ print("\nnew here? `fxcss try owner/repo` test-drives a theme; `fxcss init`")
505
+ print("adds PR previews to yours.")
445
506
  return 0
446
507
 
447
508
 
@@ -468,7 +529,7 @@ def main(argv=None):
468
529
  prog="fxcss", description=__doc__,
469
530
  formatter_class=argparse.RawDescriptionHelpFormatter)
470
531
  ap.add_argument("--version", action="version", version=f"fxcss {__version__}")
471
- sub = ap.add_subparsers(dest="cmd", required=True)
532
+ sub = ap.add_subparsers(dest="cmd", required=False)
472
533
 
473
534
  for verb, helptext in (("try", "download a theme from GitHub and test-drive it"),
474
535
  ("install", "alias for try")):
@@ -492,6 +553,31 @@ def main(argv=None):
492
553
  _menus(tr)
493
554
  tr.set_defaults(func=cmd_try)
494
555
 
556
+ tw = sub.add_parser("tweaks",
557
+ help="screenshot every install option into a committable doc")
558
+ tw.add_argument("--theme", type=Path, default=Path.cwd(),
559
+ help="theme root, the folder containing chrome/ (default: cwd)")
560
+ tw.add_argument("--firefox", default=None,
561
+ help="path to the firefox binary (default: autodetect)")
562
+ tw.add_argument("--out", type=Path, default=Path("docs/tweaks"),
563
+ help="output folder (default: docs/tweaks)")
564
+ tw.add_argument("--combo", action="append", metavar="NAME+NAME",
565
+ help="also render options together, e.g. --combo "
566
+ "compact-tabs+tabs-swapclose; repeatable")
567
+ tw.add_argument("--dark", action="store_true", help="render in dark mode")
568
+ tw.set_defaults(func=cmd_tweaks)
569
+
570
+ ini = sub.add_parser("init", help="add PR previews and CI checks to your theme repo")
571
+ ini.add_argument("--theme", type=Path, default=Path.cwd(),
572
+ help="theme root, the folder containing chrome/ (default: cwd)")
573
+ ini.add_argument("--watch", action="store_true",
574
+ help="also add the weekly Firefox release/beta/nightly audit")
575
+ ini.add_argument("--showcase", action="store_true",
576
+ help="also add the on-release showcase screenshot workflow")
577
+ ini.add_argument("--force", action="store_true",
578
+ help="replace workflow files that already exist")
579
+ ini.set_defaults(func=cmd_init)
580
+
495
581
  w = sub.add_parser("watch", help="live-reload the theme as you edit")
496
582
  _common(w); _menus(w)
497
583
  w.add_argument("--dark", action="store_true", help="start in dark mode")
@@ -577,6 +663,12 @@ def main(argv=None):
577
663
  d.set_defaults(func=cmd_doctor)
578
664
 
579
665
  args = ap.parse_args(argv)
666
+ if args.cmd is None:
667
+ # Bare `fxcss` used to be an argparse error. Greet by task instead:
668
+ # the three reasons someone installs this are not obvious from a
669
+ # subcommand list.
670
+ print(LANDING)
671
+ return 0
580
672
  # Long-running commands are a live log; keep them readable when piped.
581
673
  try:
582
674
  sys.stdout.reconfigure(line_buffering=True)
@@ -73,9 +73,19 @@ def normalise(im, width=PANEL_WIDTH):
73
73
 
74
74
 
75
75
  def diff_stats(base, head):
76
- """Return (changed_pixel_count, total, mask) ignoring sub-threshold noise."""
77
- delta = ImageChops.difference(base, head).convert("L")
78
- mask = delta.point(lambda v: 255 if v >= NOISE_THRESHOLD else 0)
76
+ """Return (changed_pixel_count, total, mask) ignoring sub-threshold noise.
77
+
78
+ Thresholds the per-channel *maximum* difference, not the luminance of the
79
+ difference. Luminance averages the channels, so a chroma-only shift -- a
80
+ toolbar tinted from blue-grey to peach, say -- can slide under the
81
+ threshold while being plainly visible. Recolouring is exactly the kind of
82
+ change a theme differ exists to catch.
83
+ """
84
+ delta = ImageChops.difference(base, head)
85
+ channel_max = delta.split()[0]
86
+ for channel in delta.split()[1:]:
87
+ channel_max = ImageChops.lighter(channel_max, channel)
88
+ mask = channel_max.point(lambda v: 255 if v >= NOISE_THRESHOLD else 0)
79
89
  changed = mask.histogram()[255]
80
90
  return changed, base.width * base.height, mask
81
91
 
@@ -634,21 +634,19 @@ return true;
634
634
  LOAD_VARIANT_SHEET = """
635
635
  const win = Services.wm.getMostRecentWindow("navigator:browser");
636
636
  const u = win.windowUtils;
637
- if (win._fxcssVariantSheet) {
638
- try { u.removeSheetUsingURIString(win._fxcssVariantSheet, u.USER_SHEET); } catch (e) {}
639
- }
637
+ if (!win._fxcssVariantSheets) { win._fxcssVariantSheets = []; }
640
638
  u.loadSheetUsingURIString(arguments[0], u.USER_SHEET);
641
- win._fxcssVariantSheet = arguments[0];
642
- return true;
639
+ win._fxcssVariantSheets.push(arguments[0]);
640
+ return win._fxcssVariantSheets.length;
643
641
  """
644
642
 
645
- UNLOAD_VARIANT_SHEET = """
643
+ UNLOAD_VARIANT_SHEETS = """
646
644
  const win = Services.wm.getMostRecentWindow("navigator:browser");
647
645
  const u = win.windowUtils;
648
- if (win._fxcssVariantSheet) {
649
- try { u.removeSheetUsingURIString(win._fxcssVariantSheet, u.USER_SHEET); } catch (e) {}
650
- win._fxcssVariantSheet = null;
646
+ for (const uri of (win._fxcssVariantSheets || [])) {
647
+ try { u.removeSheetUsingURIString(uri, u.USER_SHEET); } catch (e) {}
651
648
  }
649
+ win._fxcssVariantSheets = [];
652
650
  return true;
653
651
  """
654
652
 
@@ -981,11 +979,14 @@ def capture_views(session: Session, outdir: Path, modes=("light", "dark"),
981
979
  # Optional stylesheets, one capture per variant. Loaded live as a user
982
980
  # sheet by file URI -- relative url()s inside the sheet keep resolving --
983
981
  # then removed, so variants never contaminate each other.
984
- for slug, sheet in sorted((variants or {}).items()):
985
- m.script(LOAD_VARIANT_SHEET, [sheet.resolve().as_uri()])
982
+ for slug, sheets in sorted((variants or {}).items()):
983
+ # A value may be one sheet or several: a combo loads them together, the
984
+ # way a user stacking install options would run them.
985
+ for sheet in ([sheets] if isinstance(sheets, Path) else sheets):
986
+ m.script(LOAD_VARIANT_SHEET, [sheet.resolve().as_uri()])
986
987
  time.sleep(1.5)
987
988
  _shot(m, outdir, "variant-" + slug)
988
- m.script(UNLOAD_VARIANT_SHEET)
989
+ m.script(UNLOAD_VARIANT_SHEETS)
989
990
  time.sleep(0.8)
990
991
 
991
992
  (outdir / "render-info.json").write_text(json.dumps(info, indent=2), encoding="utf-8")
@@ -1069,6 +1070,39 @@ def find_variant_sheets(theme: Path):
1069
1070
  return sheets
1070
1071
 
1071
1072
 
1073
+ def parse_variant_spec(spec, available):
1074
+ """Turn a --variants value into {slug: [sheet, ...]}.
1075
+
1076
+ Commas separate captures; a plus combines sheets within one capture, the
1077
+ way a user stacking install options would run them:
1078
+
1079
+ all every optional sheet, one capture each
1080
+ a,b two captures
1081
+ a+b,c a and b together, then c alone
1082
+
1083
+ Raises ValueError naming anything unknown, listing what exists.
1084
+ """
1085
+ if not spec:
1086
+ return {}
1087
+ spec = spec.strip()
1088
+ if spec.lower() == "all":
1089
+ return {slug: [path] for slug, path in available.items()}
1090
+ chosen = {}
1091
+ unknown = []
1092
+ for part in (p.strip().lower() for p in spec.split(",") if p.strip()):
1093
+ names = [n.strip() for n in part.split("+") if n.strip()]
1094
+ missing = [n for n in names if n not in available]
1095
+ if missing:
1096
+ unknown += missing
1097
+ continue
1098
+ chosen["+".join(names)] = [available[n] for n in names]
1099
+ if unknown:
1100
+ raise ValueError(
1101
+ "no optional stylesheet named " + ", ".join(sorted(set(unknown)))
1102
+ + "; available: " + (", ".join(sorted(available)) or "none"))
1103
+ return chosen
1104
+
1105
+
1072
1106
  def find_firefox(explicit=None):
1073
1107
  """Locate a Firefox binary, preferring an explicit path.
1074
1108