#./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 # /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). # # userChrome.css can't stay as a safety net: nsIStyleSheetService user # sheets are ordered BEFORE the profile's userChrome.css, so a leftover copy # would outrank every sheet we register and pin Zen to whichever wallpaper # was live at startup — the exact bug this fixes. # # 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 ", "missing " or "error " 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; }) ]; }; }