fxcss 0.6.1__py3-none-any.whl
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.
- fxcss/__init__.py +3 -0
- fxcss/__main__.py +6 -0
- fxcss/audit.py +595 -0
- fxcss/catalogue.py +385 -0
- fxcss/cli.py +558 -0
- fxcss/compare.py +189 -0
- fxcss/core.py +999 -0
- fxcss/fetch.py +255 -0
- fxcss/probe.py +220 -0
- fxcss-0.6.1.dist-info/METADATA +572 -0
- fxcss-0.6.1.dist-info/RECORD +15 -0
- fxcss-0.6.1.dist-info/WHEEL +5 -0
- fxcss-0.6.1.dist-info/entry_points.txt +2 -0
- fxcss-0.6.1.dist-info/licenses/LICENSE +21 -0
- fxcss-0.6.1.dist-info/top_level.txt +1 -0
fxcss/cli.py
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""fxcss - a testing toolkit for Firefox userChrome.css themes.
|
|
3
|
+
|
|
4
|
+
fxcss try download a theme from GitHub and test-drive it
|
|
5
|
+
fxcss watch edit CSS and see it live, no restart
|
|
6
|
+
fxcss pick click any part of the UI to get its CSS selector
|
|
7
|
+
fxcss inspect look up a selector you already have
|
|
8
|
+
fxcss audit find selectors that no longer match, and suggest fixes
|
|
9
|
+
fxcss changelog diff two Firefox builds to see what chrome changed
|
|
10
|
+
fxcss snapshot record this Firefox's chrome names, to diff against later
|
|
11
|
+
fxcss catalogue build a directory of themeable UI parts
|
|
12
|
+
fxcss shot capture a set of screenshots
|
|
13
|
+
fxcss compare diff two sets into before/after/diff images
|
|
14
|
+
fxcss doctor report what this Firefox supports
|
|
15
|
+
|
|
16
|
+
Run `fxcss <command> --help` for the options of each.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import signal
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from . import core
|
|
27
|
+
|
|
28
|
+
TOOLBOX_KEY = {
|
|
29
|
+
"darwin": "Cmd+Opt+Shift+I",
|
|
30
|
+
}.get(sys.platform, "Ctrl+Alt+Shift+I")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _watched_files(theme: Path):
|
|
34
|
+
for root in (theme / "chrome", theme / "custom"):
|
|
35
|
+
if root.exists():
|
|
36
|
+
for p in root.rglob("*"):
|
|
37
|
+
if p.is_file() and p.suffix in (".css", ".svg"):
|
|
38
|
+
yield p
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _fingerprint(theme: Path):
|
|
42
|
+
return {p: p.stat().st_mtime_ns for p in _watched_files(theme)}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _install_signal_handlers():
|
|
46
|
+
"""Make a terminate signal exit like Ctrl-C, so the browser is shut down."""
|
|
47
|
+
def _bail(signum, frame):
|
|
48
|
+
raise KeyboardInterrupt
|
|
49
|
+
for sig in (getattr(signal, "SIGTERM", None), getattr(signal, "SIGHUP", None)):
|
|
50
|
+
if sig is None:
|
|
51
|
+
continue
|
|
52
|
+
try:
|
|
53
|
+
signal.signal(sig, _bail)
|
|
54
|
+
except (ValueError, OSError):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _references(theme, selector):
|
|
59
|
+
from .catalogue import css_references
|
|
60
|
+
return css_references(theme, selector)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def cmd_try(args):
|
|
64
|
+
import shutil
|
|
65
|
+
import tempfile
|
|
66
|
+
from . import fetch
|
|
67
|
+
|
|
68
|
+
_install_signal_handlers()
|
|
69
|
+
owner, name = fetch.parse_repo(args.repo)
|
|
70
|
+
|
|
71
|
+
if args.ref:
|
|
72
|
+
ref, why = args.ref, f"ref {args.ref}"
|
|
73
|
+
info = None
|
|
74
|
+
else:
|
|
75
|
+
info = fetch.resolve(owner, name)
|
|
76
|
+
print()
|
|
77
|
+
for line in fetch.humanise(info):
|
|
78
|
+
print(" " + line)
|
|
79
|
+
prefer = "commit" if args.commit else "release"
|
|
80
|
+
ref, why = fetch.choose_ref(info, prefer)
|
|
81
|
+
|
|
82
|
+
workdir = Path(tempfile.mkdtemp(prefix="fxcss-try-"))
|
|
83
|
+
keep = args.keep.resolve() if args.keep else None
|
|
84
|
+
try:
|
|
85
|
+
print(f"\n fetching {why} …")
|
|
86
|
+
repo_root = fetch.download(owner, name, ref, workdir / "src")
|
|
87
|
+
theme_root = fetch.find_theme_root(repo_root)
|
|
88
|
+
if theme_root is None:
|
|
89
|
+
print("\n No chrome/userChrome.css anywhere in this repository, so "
|
|
90
|
+
"there is\n nothing for Firefox to load. Is it a userChrome theme?")
|
|
91
|
+
return 2
|
|
92
|
+
|
|
93
|
+
facts = fetch.describe(repo_root, theme_root)
|
|
94
|
+
where = theme_root.relative_to(repo_root)
|
|
95
|
+
print(f" theme found at {where if str(where) != '.' else 'the repository root'}"
|
|
96
|
+
f" ({facts['stylesheets']} stylesheets, {facts['bytes'] // 1024} KB)")
|
|
97
|
+
|
|
98
|
+
if facts["scripts"]:
|
|
99
|
+
print(f"\n This theme ships {', '.join(facts['scripts'])}. fxcss does not run "
|
|
100
|
+
f"it —\n it installs the files itself, which is all those scripts do.")
|
|
101
|
+
if facts["flags"]:
|
|
102
|
+
print("\n Options its README documents:")
|
|
103
|
+
for entry in facts["flags"]:
|
|
104
|
+
print(f" {entry['flag']:<6} {entry['text']}")
|
|
105
|
+
if facts["variants"]:
|
|
106
|
+
names = [v.stem for v in facts["variants"]]
|
|
107
|
+
print(f"\n Optional stylesheets you can layer on with --with:")
|
|
108
|
+
print(" " + ", ".join(names))
|
|
109
|
+
|
|
110
|
+
chosen = []
|
|
111
|
+
if args.with_sheets:
|
|
112
|
+
wanted = {w.strip().lower() for w in args.with_sheets.split(",") if w.strip()}
|
|
113
|
+
by_name = {v.stem.lower(): v for v in facts["variants"]}
|
|
114
|
+
missing = wanted - set(by_name)
|
|
115
|
+
if missing:
|
|
116
|
+
print(f"\n error: no optional sheet named {', '.join(sorted(missing))}",
|
|
117
|
+
file=sys.stderr)
|
|
118
|
+
return 2
|
|
119
|
+
chosen = [by_name[w] for w in sorted(wanted)]
|
|
120
|
+
|
|
121
|
+
if args.info:
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
firefox = core.find_firefox(args.firefox)
|
|
125
|
+
session = core.Session(theme_root, firefox, dark=args.dark,
|
|
126
|
+
native_menus=args.native_menus,
|
|
127
|
+
devtools=not args.no_devtools)
|
|
128
|
+
if chosen:
|
|
129
|
+
fetch.apply_variants(session.profile, chosen)
|
|
130
|
+
print(f" layering on: {', '.join(c.stem for c in chosen)}")
|
|
131
|
+
|
|
132
|
+
with session:
|
|
133
|
+
session.setup_window()
|
|
134
|
+
version = session.info()["version"]
|
|
135
|
+
print(f"\n Firefox {version} — this is a throwaway profile; your own "
|
|
136
|
+
f"Firefox\n profile has not been touched.")
|
|
137
|
+
if args.shot:
|
|
138
|
+
core.capture_views(session, args.shot.resolve())
|
|
139
|
+
print(f"\n wrote screenshots to {args.shot}")
|
|
140
|
+
return 0
|
|
141
|
+
print(" Ctrl-C here when you are done.\n")
|
|
142
|
+
try:
|
|
143
|
+
while True:
|
|
144
|
+
time.sleep(0.5)
|
|
145
|
+
except KeyboardInterrupt:
|
|
146
|
+
print("\n closing.")
|
|
147
|
+
return 0
|
|
148
|
+
finally:
|
|
149
|
+
if keep:
|
|
150
|
+
keep.parent.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
if keep.exists():
|
|
152
|
+
shutil.rmtree(keep, ignore_errors=True)
|
|
153
|
+
shutil.copytree(workdir / "src", keep)
|
|
154
|
+
print(f" kept the download at {keep}")
|
|
155
|
+
shutil.rmtree(workdir, ignore_errors=True)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def cmd_watch(args):
|
|
159
|
+
theme = args.theme.resolve()
|
|
160
|
+
firefox = core.find_firefox(args.firefox)
|
|
161
|
+
_install_signal_handlers()
|
|
162
|
+
|
|
163
|
+
print(f"fxcss watch\n theme: {theme}\n firefox: {firefox}")
|
|
164
|
+
if args.native_menus is False:
|
|
165
|
+
print(" menus: XUL (themeable) — right-click to inspect them live")
|
|
166
|
+
|
|
167
|
+
# An empty userChrome.css hands the stylesheet entirely to us, so replacing
|
|
168
|
+
# it reflects deletions as well as additions: a rule you remove really
|
|
169
|
+
# disappears instead of lingering from the copy loaded at startup.
|
|
170
|
+
session = core.Session(theme, firefox, dark=args.dark,
|
|
171
|
+
native_menus=args.native_menus,
|
|
172
|
+
empty_user_chrome=True, devtools=not args.no_devtools)
|
|
173
|
+
with session:
|
|
174
|
+
session.setup_window()
|
|
175
|
+
session.reload_theme()
|
|
176
|
+
print("\n theme loaded. Saving any file under chrome/ reloads it.")
|
|
177
|
+
if not args.no_devtools:
|
|
178
|
+
print(f" Browser Toolbox: press {TOOLBOX_KEY} in the browser window")
|
|
179
|
+
print(" to inspect the UI itself (not page content).")
|
|
180
|
+
print(" Ctrl-C to stop.\n")
|
|
181
|
+
|
|
182
|
+
state = _fingerprint(theme)
|
|
183
|
+
reloads = 0
|
|
184
|
+
try:
|
|
185
|
+
while True:
|
|
186
|
+
time.sleep(args.interval)
|
|
187
|
+
current = _fingerprint(theme)
|
|
188
|
+
if current == state:
|
|
189
|
+
continue
|
|
190
|
+
changed = sorted({p for p in set(current) | set(state)
|
|
191
|
+
if current.get(p) != state.get(p)})
|
|
192
|
+
state = current
|
|
193
|
+
t0 = time.perf_counter()
|
|
194
|
+
try:
|
|
195
|
+
session.reload_theme()
|
|
196
|
+
except Exception as exc: # a half-saved file must not kill the loop
|
|
197
|
+
print(f" ! reload failed: {exc}")
|
|
198
|
+
continue
|
|
199
|
+
reloads += 1
|
|
200
|
+
names = ", ".join(p.name for p in changed[:3])
|
|
201
|
+
if len(changed) > 3:
|
|
202
|
+
names += f" +{len(changed) - 3} more"
|
|
203
|
+
print(f" [{reloads:>3}] {names} -> reloaded in "
|
|
204
|
+
f"{(time.perf_counter() - t0) * 1000:.0f} ms")
|
|
205
|
+
if args.shot:
|
|
206
|
+
args.shot.parent.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
args.shot.write_bytes(session.m.screenshot())
|
|
208
|
+
print(f" wrote {args.shot}")
|
|
209
|
+
except KeyboardInterrupt:
|
|
210
|
+
print("\n stopping.")
|
|
211
|
+
return 0
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def cmd_pick(args):
|
|
215
|
+
from . import probe
|
|
216
|
+
theme = args.theme.resolve()
|
|
217
|
+
firefox = core.find_firefox(args.firefox)
|
|
218
|
+
_install_signal_handlers()
|
|
219
|
+
print(f"fxcss pick\n theme: {theme}\n firefox: {firefox}")
|
|
220
|
+
with core.Session(theme, firefox, dark=args.dark,
|
|
221
|
+
native_menus=args.native_menus, devtools=True) as session:
|
|
222
|
+
session.setup_window()
|
|
223
|
+
return probe.pick(session, theme, _references)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def cmd_inspect(args):
|
|
227
|
+
from . import probe
|
|
228
|
+
theme = args.theme.resolve()
|
|
229
|
+
firefox = core.find_firefox(args.firefox)
|
|
230
|
+
with core.Session(theme, firefox, dark=args.dark, devtools=True) as session:
|
|
231
|
+
session.setup_window()
|
|
232
|
+
return probe.inspect_selector(session, args.selector, theme, _references)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def cmd_audit(args):
|
|
236
|
+
from . import audit
|
|
237
|
+
theme = args.theme.resolve()
|
|
238
|
+
firefox = core.find_firefox(args.firefox)
|
|
239
|
+
print(f"fxcss audit\n theme: {theme}\n firefox: {firefox}\n")
|
|
240
|
+
static = None if args.no_unused else audit.collect_unused(theme)
|
|
241
|
+
with core.Session(theme, firefox) as session:
|
|
242
|
+
result = audit.audit(session, theme)
|
|
243
|
+
|
|
244
|
+
unused = None
|
|
245
|
+
if static:
|
|
246
|
+
# Ask an *unthemed* Firefox which custom properties it knows about.
|
|
247
|
+
# Against the themed browser every name resolves, because the theme
|
|
248
|
+
# itself set them, and nothing could be told apart.
|
|
249
|
+
candidates = (set(static["used"]) - set(static["defined"])) | (
|
|
250
|
+
set(static["defined"]) - set(static["used"]))
|
|
251
|
+
known = set()
|
|
252
|
+
if candidates:
|
|
253
|
+
with core.Session(theme, firefox, empty_user_chrome=True) as vanilla:
|
|
254
|
+
# Sweep the same states first: panel-scoped properties like
|
|
255
|
+
# --arrowpanel-background do not exist until the panel has been
|
|
256
|
+
# built, and would otherwise look like names Firefox never had.
|
|
257
|
+
audit.collect_dom(vanilla, verbose=False)
|
|
258
|
+
known = audit.probe_properties(vanilla, candidates)
|
|
259
|
+
unused = audit.classify_unused(static, known)
|
|
260
|
+
audit.report(result, show_all=args.all, colour=not args.no_colour)
|
|
261
|
+
if unused:
|
|
262
|
+
audit.report_unused(unused, colour=not args.no_colour, show_all=args.all)
|
|
263
|
+
if args.patch:
|
|
264
|
+
files = audit.write_patch(result, theme, args.patch.resolve())
|
|
265
|
+
if files:
|
|
266
|
+
print(f" wrote {args.patch} ({files} file(s)); review it, then:")
|
|
267
|
+
print(f" git apply {args.patch}\n")
|
|
268
|
+
else:
|
|
269
|
+
print(" nothing confident enough to patch\n")
|
|
270
|
+
actionable = [f for f in result["findings"] if f["confidence"] != "unresolved"]
|
|
271
|
+
if args.strict and actionable:
|
|
272
|
+
print(f" --strict: failing because {len(actionable)} selector(s) need attention\n")
|
|
273
|
+
return 1
|
|
274
|
+
return 0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def cmd_snapshot(args):
|
|
278
|
+
import json
|
|
279
|
+
from . import audit
|
|
280
|
+
theme = args.theme.resolve()
|
|
281
|
+
firefox = core.find_firefox(args.firefox)
|
|
282
|
+
with core.Session(theme, firefox) as session:
|
|
283
|
+
snap = audit.make_snapshot(session, verbose=True)
|
|
284
|
+
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
285
|
+
args.out.write_text(json.dumps(snap, indent=1) + "\n", encoding="utf-8")
|
|
286
|
+
print(f"\n Firefox {snap['version']}: {len(snap['ids'])} ids, "
|
|
287
|
+
f"{len(snap['classes'])} classes")
|
|
288
|
+
print(f" wrote {args.out}")
|
|
289
|
+
return 0
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def cmd_changelog(args):
|
|
293
|
+
from . import audit
|
|
294
|
+
theme = args.theme.resolve()
|
|
295
|
+
if not args.baseline and not args.against:
|
|
296
|
+
print("error: pass --against <firefox> or --baseline <snapshot.json>",
|
|
297
|
+
file=sys.stderr)
|
|
298
|
+
return 2
|
|
299
|
+
after_bin = core.find_firefox(args.firefox)
|
|
300
|
+
|
|
301
|
+
snapshots = {}
|
|
302
|
+
if args.baseline:
|
|
303
|
+
data, dom = audit.load_snapshot(args.baseline)
|
|
304
|
+
snapshots["before"], snapshots["before_version"] = dom, data["version"]
|
|
305
|
+
print(f"fxcss changelog\n before: {args.baseline} (Firefox {data['version']})")
|
|
306
|
+
print(f" after: {after_bin}\n")
|
|
307
|
+
with core.Session(theme, after_bin) as session:
|
|
308
|
+
snapshots["after_version"] = session.info()["version"]
|
|
309
|
+
print(f" after — Firefox {snapshots['after_version']}")
|
|
310
|
+
snapshots["after"] = audit.collect_dom(session)
|
|
311
|
+
else:
|
|
312
|
+
before_bin = core.find_firefox(args.against)
|
|
313
|
+
print(f"fxcss changelog\n before: {before_bin}\n after: {after_bin}\n")
|
|
314
|
+
for label, binary in (("before", before_bin), ("after", after_bin)):
|
|
315
|
+
with core.Session(theme, binary) as session:
|
|
316
|
+
version = session.info()["version"]
|
|
317
|
+
print(f" {label} — Firefox {version}")
|
|
318
|
+
snapshots[label] = audit.collect_dom(session)
|
|
319
|
+
snapshots[label + "_version"] = version
|
|
320
|
+
|
|
321
|
+
tokens = audit.extract_tokens(theme)
|
|
322
|
+
delta = audit.changelog(snapshots["before"], snapshots["after"], tokens)
|
|
323
|
+
|
|
324
|
+
print(f"\n Firefox {snapshots['before_version']} → {snapshots['after_version']}")
|
|
325
|
+
print(f" {len(delta['removed'])} chrome names gone, {len(delta['added'])} new")
|
|
326
|
+
|
|
327
|
+
affected = delta.get("affects_theme") or []
|
|
328
|
+
if affected:
|
|
329
|
+
print(f"\n {len(affected)} of them are used by this theme:")
|
|
330
|
+
for token in affected:
|
|
331
|
+
first = tokens[token][0]
|
|
332
|
+
print(f" {token:<44} {first['file']}:{first['line']}")
|
|
333
|
+
print("\n Run `fxcss audit` for suggested replacements.")
|
|
334
|
+
else:
|
|
335
|
+
print("\n None of them are used by this theme.")
|
|
336
|
+
|
|
337
|
+
if args.show_all:
|
|
338
|
+
print("\n gone:")
|
|
339
|
+
for token in delta["removed"]:
|
|
340
|
+
print(f" - {token}")
|
|
341
|
+
print("\n new:")
|
|
342
|
+
for token in delta["added"]:
|
|
343
|
+
print(f" + {token}")
|
|
344
|
+
print()
|
|
345
|
+
return 0
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def cmd_shot(args):
|
|
349
|
+
theme = args.theme.resolve()
|
|
350
|
+
if not (theme / "chrome" / "userChrome.css").exists():
|
|
351
|
+
print(f"error: no chrome/userChrome.css under {theme}", file=sys.stderr)
|
|
352
|
+
return 2
|
|
353
|
+
firefox = core.find_firefox(args.firefox)
|
|
354
|
+
urls = getattr(args, "url", [])
|
|
355
|
+
with core.Session(theme, firefox) as session:
|
|
356
|
+
if not getattr(args, "only_live", False):
|
|
357
|
+
core.capture_views(session, args.out.resolve())
|
|
358
|
+
if urls:
|
|
359
|
+
print("\n live sites (captured, never compared):", flush=True)
|
|
360
|
+
core.capture_live(session, args.out.resolve(), urls)
|
|
361
|
+
print(f"\nwrote screenshots to {args.out}")
|
|
362
|
+
return 0
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def cmd_compare(args):
|
|
366
|
+
from . import compare
|
|
367
|
+
return compare.run(args.base.resolve(), args.head.resolve(),
|
|
368
|
+
args.out.resolve(), args.platform)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def cmd_catalogue(args):
|
|
372
|
+
from . import catalogue
|
|
373
|
+
theme = args.theme.resolve()
|
|
374
|
+
firefox = core.find_firefox(args.firefox)
|
|
375
|
+
out = args.out.resolve()
|
|
376
|
+
with core.Session(theme, firefox, native_menus=args.native_menus) as session:
|
|
377
|
+
result = catalogue.build(session, theme, out,
|
|
378
|
+
self_contained=args.self_contained)
|
|
379
|
+
found = sum(1 for e in result["entries"]
|
|
380
|
+
if any(m["found"] for m in e["modes"].values()))
|
|
381
|
+
print(f"\n{found}/{len(result['entries'])} landmarks resolved")
|
|
382
|
+
print(f"open {out / 'index.html'}")
|
|
383
|
+
if args.open:
|
|
384
|
+
opener = {"darwin": "open", "win32": "start"}.get(sys.platform, "xdg-open")
|
|
385
|
+
subprocess.run([opener, str(out / "index.html")], check=False)
|
|
386
|
+
return 0
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def cmd_doctor(args):
|
|
390
|
+
theme = args.theme.resolve()
|
|
391
|
+
firefox = core.find_firefox(args.firefox)
|
|
392
|
+
print(f"theme: {theme}")
|
|
393
|
+
print(f"firefox: {firefox}")
|
|
394
|
+
with core.Session(theme, firefox, devtools=True) as session:
|
|
395
|
+
info = session.info()
|
|
396
|
+
print(f"\nversion: {info['version']} (build {info['buildID']}) on {info['os']}")
|
|
397
|
+
print(f"window: {info['outer']} at dpr {info['dpr']}")
|
|
398
|
+
print(f"userChrome.css enabled: {info['legacyStylesheets']}")
|
|
399
|
+
print(f"Browser Toolbox: enabled in the temp profile ({TOOLBOX_KEY})")
|
|
400
|
+
|
|
401
|
+
native = info["nativeContextMenus"]
|
|
402
|
+
print("\ncontext menus:")
|
|
403
|
+
for platform, value in native.items():
|
|
404
|
+
if value is None:
|
|
405
|
+
continue
|
|
406
|
+
state = "native (CSS cannot style them)" if value else "XUL (themeable)"
|
|
407
|
+
print(f" {platform:<8} native-context-menus={value} -> {state}")
|
|
408
|
+
if native.get("macos"):
|
|
409
|
+
print(" note: on macOS this defaults to true, so menupopup rules have no\n"
|
|
410
|
+
" effect on right-click menus there. Use --native-menus=false\n"
|
|
411
|
+
" to work on them.")
|
|
412
|
+
|
|
413
|
+
sheets = sorted((theme / "chrome").rglob("*.css")) if (theme / "chrome").exists() else []
|
|
414
|
+
total = sum(p.stat().st_size for p in sheets)
|
|
415
|
+
print(f"\ntheme: {len(sheets)} stylesheets, {total:,} bytes")
|
|
416
|
+
missing = [p for p in ("chrome/userChrome.css",) if not (theme / p).exists()]
|
|
417
|
+
print("missing expected files: " + (", ".join(missing) if missing else "none"))
|
|
418
|
+
return 0
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _bool(v):
|
|
422
|
+
return v.lower() not in ("false", "0", "no")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _common(p, theme=True):
|
|
426
|
+
if theme:
|
|
427
|
+
p.add_argument("--theme", type=Path, default=Path.cwd(),
|
|
428
|
+
help="theme root, the folder containing chrome/ (default: cwd)")
|
|
429
|
+
p.add_argument("--firefox", default=None,
|
|
430
|
+
help="path to the firefox binary (default: autodetect, or $FIREFOX_BIN)")
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _menus(p):
|
|
434
|
+
p.add_argument("--native-menus", dest="native_menus", default=None, type=_bool,
|
|
435
|
+
metavar="BOOL",
|
|
436
|
+
help="false makes right-click menus XUL, so a theme can style them")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def main(argv=None):
|
|
440
|
+
ap = argparse.ArgumentParser(
|
|
441
|
+
prog="fxcss", description=__doc__,
|
|
442
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
443
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
444
|
+
|
|
445
|
+
for verb, helptext in (("try", "download a theme from GitHub and test-drive it"),
|
|
446
|
+
("install", "alias for try")):
|
|
447
|
+
tr = sub.add_parser(verb, help=helptext)
|
|
448
|
+
tr.add_argument("repo", help="owner/name, or a github.com URL")
|
|
449
|
+
tr.add_argument("--firefox", default=None,
|
|
450
|
+
help="path to the firefox binary (default: autodetect)")
|
|
451
|
+
tr.add_argument("--ref", default=None, help="tag, branch or commit to fetch")
|
|
452
|
+
tr.add_argument("--commit", action="store_true",
|
|
453
|
+
help="use the latest commit rather than the latest release")
|
|
454
|
+
tr.add_argument("--with", dest="with_sheets", default=None, metavar="NAME[,NAME]",
|
|
455
|
+
help="also load named optional stylesheets")
|
|
456
|
+
tr.add_argument("--dark", action="store_true", help="start in dark mode")
|
|
457
|
+
tr.add_argument("--shot", type=Path, default=None,
|
|
458
|
+
help="capture screenshots instead of opening interactively")
|
|
459
|
+
tr.add_argument("--keep", type=Path, default=None,
|
|
460
|
+
help="keep the downloaded theme at this path")
|
|
461
|
+
tr.add_argument("--info", action="store_true",
|
|
462
|
+
help="report what was found and stop, without launching")
|
|
463
|
+
tr.add_argument("--no-devtools", action="store_true")
|
|
464
|
+
_menus(tr)
|
|
465
|
+
tr.set_defaults(func=cmd_try)
|
|
466
|
+
|
|
467
|
+
w = sub.add_parser("watch", help="live-reload the theme as you edit")
|
|
468
|
+
_common(w); _menus(w)
|
|
469
|
+
w.add_argument("--dark", action="store_true", help="start in dark mode")
|
|
470
|
+
w.add_argument("--interval", type=float, default=0.4, help="poll seconds")
|
|
471
|
+
w.add_argument("--shot", type=Path, default=None,
|
|
472
|
+
help="also write a screenshot here after every reload")
|
|
473
|
+
w.add_argument("--no-devtools", action="store_true",
|
|
474
|
+
help="do not enable the Browser Toolbox in the temp profile")
|
|
475
|
+
w.set_defaults(func=cmd_watch)
|
|
476
|
+
|
|
477
|
+
k = sub.add_parser("pick", help="click any part of the UI to get its selector")
|
|
478
|
+
_common(k); _menus(k)
|
|
479
|
+
k.add_argument("--dark", action="store_true", help="start in dark mode")
|
|
480
|
+
k.set_defaults(func=cmd_pick)
|
|
481
|
+
|
|
482
|
+
i = sub.add_parser("inspect", help="look up a selector you already have")
|
|
483
|
+
_common(i)
|
|
484
|
+
i.add_argument("selector", help="a CSS selector, e.g. '#urlbar' or '.tab-close-button'")
|
|
485
|
+
i.add_argument("--dark", action="store_true", help="report dark-mode styles")
|
|
486
|
+
i.set_defaults(func=cmd_inspect)
|
|
487
|
+
|
|
488
|
+
a = sub.add_parser("audit", help="find selectors that no longer match anything")
|
|
489
|
+
_common(a)
|
|
490
|
+
a.add_argument("--patch", type=Path, default=None,
|
|
491
|
+
help="write the confident replacements as a unified diff")
|
|
492
|
+
a.add_argument("--all", action="store_true",
|
|
493
|
+
help="also list tokens with no suggestion")
|
|
494
|
+
a.add_argument("--no-colour", action="store_true", help="plain output")
|
|
495
|
+
a.add_argument("--strict", action="store_true",
|
|
496
|
+
help="exit non-zero if any selector needs attention (for CI)")
|
|
497
|
+
a.add_argument("--no-unused", action="store_true",
|
|
498
|
+
help="skip the unused/unreachable section")
|
|
499
|
+
a.set_defaults(func=cmd_audit)
|
|
500
|
+
|
|
501
|
+
cl = sub.add_parser("changelog",
|
|
502
|
+
help="diff two Firefox builds to see what chrome changed")
|
|
503
|
+
_common(cl)
|
|
504
|
+
cl.add_argument("--against", default=None,
|
|
505
|
+
help="path to the other firefox binary to compare with")
|
|
506
|
+
cl.add_argument("--baseline", type=Path, default=None,
|
|
507
|
+
help="compare against a saved snapshot instead of a second browser")
|
|
508
|
+
cl.add_argument("--show-all", action="store_true",
|
|
509
|
+
help="list every name that changed, not just ones the theme uses")
|
|
510
|
+
cl.set_defaults(func=cmd_changelog)
|
|
511
|
+
|
|
512
|
+
s = sub.add_parser("shot", help="capture the standard screenshot set")
|
|
513
|
+
_common(s)
|
|
514
|
+
s.add_argument("--out", type=Path, required=True)
|
|
515
|
+
s.add_argument("--url", action="append", default=[], metavar="URL",
|
|
516
|
+
help="also capture the theme against a live site, light and "
|
|
517
|
+
"dark; repeatable. Written to <out>/live/ and never "
|
|
518
|
+
"included in a comparison")
|
|
519
|
+
s.add_argument("--only-live", action="store_true",
|
|
520
|
+
help="capture just the --url views, skipping the standard set")
|
|
521
|
+
s.set_defaults(func=cmd_shot)
|
|
522
|
+
|
|
523
|
+
c = sub.add_parser("compare", help="diff two screenshot sets")
|
|
524
|
+
_common(c, theme=False)
|
|
525
|
+
c.add_argument("--base", type=Path, required=True)
|
|
526
|
+
c.add_argument("--head", type=Path, required=True)
|
|
527
|
+
c.add_argument("--out", type=Path, required=True)
|
|
528
|
+
c.add_argument("--platform", default="local")
|
|
529
|
+
c.set_defaults(func=cmd_compare)
|
|
530
|
+
|
|
531
|
+
g = sub.add_parser("catalogue", help="build the themeable UI directory")
|
|
532
|
+
_common(g); _menus(g)
|
|
533
|
+
g.add_argument("--out", type=Path, default=Path("fxcss-catalogue"))
|
|
534
|
+
g.add_argument("--open", action="store_true", help="open the result when done")
|
|
535
|
+
g.add_argument("--self-contained", dest="self_contained", action="store_true",
|
|
536
|
+
help="also write catalogue.html with images inlined, as one file")
|
|
537
|
+
g.set_defaults(func=cmd_catalogue)
|
|
538
|
+
|
|
539
|
+
sn = sub.add_parser("snapshot", help="record this Firefox's chrome names")
|
|
540
|
+
_common(sn)
|
|
541
|
+
sn.add_argument("--out", type=Path, required=True)
|
|
542
|
+
sn.set_defaults(func=cmd_snapshot)
|
|
543
|
+
|
|
544
|
+
d = sub.add_parser("doctor", help="report what this Firefox supports")
|
|
545
|
+
_common(d)
|
|
546
|
+
d.set_defaults(func=cmd_doctor)
|
|
547
|
+
|
|
548
|
+
args = ap.parse_args(argv)
|
|
549
|
+
# Long-running commands are a live log; keep them readable when piped.
|
|
550
|
+
try:
|
|
551
|
+
sys.stdout.reconfigure(line_buffering=True)
|
|
552
|
+
except AttributeError:
|
|
553
|
+
pass
|
|
554
|
+
return args.func(args)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
if __name__ == "__main__":
|
|
558
|
+
sys.exit(main())
|