nixos/apps/zen.nix
rope 316601db6c zen: write userChrome.css again alongside zen-theme.css
The watcher not landing left Zen with no theme at all. Keep the startup copy
so the floor is the old behaviour, and let the watcher fight for the live
update on top.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 19:35:08 +01:00

97 lines
4.2 KiB
Nix

#./apps/zen.nix
{ inputs, pkgs, lib, config, ... }:
let
# Autoconfig (mozilla.cfg) runs privileged JS in the parent process at
# startup — the only scriptable way into chrome, since Gecko has no reload
# for userChrome.css and Zen exposes no command for one.
#
# So don't use userChrome.css at all for the wallpaper theme. Register
# <profile>/chrome/zen-theme.css as a style-sheet-service USER_SHEET here
# and re-register it under a cache-busting URI whenever its mtime moves —
# that is what makes a `theme-apply` switch land in a running Zen
# (settings/theming.nix writes that file on every switch).
#
# theming.nix still writes userChrome.css too, so a broken watcher costs the
# live update and nothing else. Whether the watcher can win at all is the
# open question: nsIStyleSheetService documents its user sheets as ordered
# BEFORE the profile's userChrome.css, which would let the startup copy
# outrank every sheet we register.
#
# Everything here goes through Components.classes. The autoconfig sandbox is
# not a normal system-principal scope: `Services` is not a global in it, and
# reaching for it throws before a single sheet is registered.
#
# Diagnosing a silent failure: about:config -> fred.zenTheme.status holds
# "ok <mtime>", "missing <path>" or "error <exception>" from the last pass.
themeHotReload = ''
try {
const kCc = Components.classes;
const kCi = Components.interfaces;
const sss = kCc["@mozilla.org/content/style-sheet-service;1"]
.getService(kCi.nsIStyleSheetService);
const io = kCc["@mozilla.org/network/io-service;1"]
.getService(kCi.nsIIOService);
const dirs = kCc["@mozilla.org/file/directory_service;1"]
.getService(kCi.nsIProperties);
const prefs = kCc["@mozilla.org/preferences-service;1"]
.getService(kCi.nsIPrefBranch);
const note = (msg) => {
try { prefs.setStringPref("fred.zenTheme.status", msg); } catch (e) {}
};
let live = null;
const apply = () => {
try {
// Resolved per pass, not once: autoconfig runs early enough that
// ProfD is not guaranteed yet, and a throw here would kill the timer
// for the whole session.
const file = dirs.get("ProfD", kCi.nsIFile);
file.append("chrome");
file.append("zen-theme.css");
if (!file.exists()) { note("missing " + file.path); return; }
// install(1) stamps the copy with the current time, so mtime alone
// distinguishes one wallpaper's sheet from the next.
const stamp = String(file.lastModifiedTime);
if (stamp === live) return;
// A fresh query string dodges the CSS loader's per-URI cache; without
// it a re-register would hand back the sheet parsed at startup.
const base = io.newFileURI(file).spec;
sss.loadAndRegisterSheet(io.newURI(base + "?" + stamp), sss.USER_SHEET);
if (live !== null) {
const old = io.newURI(base + "?" + live);
if (sss.sheetRegistered(old, sss.USER_SHEET))
sss.unregisterSheet(old, sss.USER_SHEET);
}
live = stamp;
note("ok " + stamp);
} catch (e) {
note("error " + e);
}
};
const timer = kCc["@mozilla.org/timer;1"].createInstance(kCi.nsITimer);
// The observer service holds the only strong reference that keeps the
// timer (and this closure) off the GC's list for the session.
kCc["@mozilla.org/observer-service;1"].getService(kCi.nsIObserverService)
.addObserver({ timer, observe() {} }, "quit-application");
timer.initWithCallback({ notify: apply }, 2000,
kCi.nsITimer.TYPE_REPEATING_SLACK);
apply();
} catch (e) {
Components.utils.reportError("zen-theme: " + e);
}
'';
in
{
config = lib.mkIf (lib.elem config.networking.hostName [ "FredOS-Gaming" "FredOS-Macbook" ]) {
environment.systemPackages = [
(inputs.zen-browser.packages.${pkgs.stdenv.hostPlatform.system}.default.override {
extraPrefs = themeHotReload;
})
];
};
}