#./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). # # 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 rather than `Services`, # which is not a global in this scope. # # None of it runs without general.config.sandbox_enabled=false below: since # Firefox 62 autoconfig is sandboxed down to pref()/defaultPref(), so # `Components` is undefined and the script throws on its first line — as does # the catch that would have reported it. # # Diagnosing a silent failure: about:config -> fred.zenTheme.status holds # "ok ", "missing " or "error " from the last pass. themeHotReload = '' // Stage markers, in about:config. pref() is the one API autoconfig always // has, sandboxed or not, so this line cannot be what fails: // fred.zenTheme.autoconfig defaults/pref/autoconfig.js was read // fred.zenTheme.cfg mozilla.cfg executed // fred.zenTheme.status the watcher ran, and what it did pref("fred.zenTheme.cfg", true); 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 { # Lands in defaults/pref/autoconfig.js, the only place this pref is # read — setting it from mozilla.cfg is too late to unsandbox itself. extraAutoConfig = '' pref("general.config.sandbox_enabled", false); pref("fred.zenTheme.autoconfig", true); ''; extraPrefs = themeHotReload; }).overrideAttrs (old: { # wrapFirefox copies the executable it knows by name so that Gecko, # which derives its application directory from the binary's *resolved* # path, lands in the wrapper's tree. It knows this one as "zen-beta" # (binaryName), but the binary actually exec'd is lib/zen-bin-*/zen — # left behind as a symlink into the unwrapped package. So the app dir # resolved to the unwrapped tree, which has no defaults/pref and no # mozilla.cfg, and every pref and script above was silently skipped. buildCommand = old.buildCommand + '' for exe in "$out"/lib/zen*/zen "$out"/lib/zen*/zen-bin; do cp --remove-destination "$(readlink -f "$exe")" "$exe" chmod u+w "$exe" done ''; })) ]; }; }