#./apps/zen.nix { inputs, pkgs, lib, config, ... }: let # Zen keeps one live docshell for each logical tab and mirrors about:blank # placeholders into the other synced windows. Upstream 1.23b changes the # placeholder's ownership flags before confirming that the docshell swap # succeeded (zen-browser/desktop#15702), so one failed swap can promote the # blank placeholder and make later selections show "New Tab". Patch the # shipped browser module until Zen includes an upstream fix. windowSyncPatch = ../patches/zen-browser/window-sync-commit-ownership-after-swap.patch; # 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 register /chrome/zen-theme.css as a style 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). # # AGENT_SHEET, not USER_SHEET. theming.nix still writes userChrome.css as the # startup floor, and nsIStyleSheetService orders its user sheets BEFORE the # profile's userChrome.css — so as a user sheet ours registered fine and then # lost every `!important` to the copy parsed at startup, which is precisely a # theme frozen at launch. Agent sheets sort above ua.css, so agent-origin # !important outranks it. theming.nix wraps this file in @-moz-document to # keep that agent origin off web pages. # # 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.AGENT_SHEET); if (live !== null) { const old = io.newURI(base + "?" + live); if (sss.sheetRegistered(old, sss.AGENT_SHEET)) sss.unregisterSheet(old, sss.AGENT_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 # Zen ships its privileged browser modules inside browser/omni.ja. # Repack that archive with the local Window Sync fix; keeping this as # a normal patch makes an upstream update fail loudly when the source # changes instead of silently dropping the workaround. for omni in "$out"/lib/zen*/browser/omni.ja; do [ -f "$omni" ] || continue cp "$(readlink -f "$omni")" "$omni.local" mv -f "$omni.local" "$omni" omniWork="$(mktemp -d)" ${pkgs.unzip}/bin/unzip -q "$omni" -d "$omniWork" || { unzipStatus=$? # Mozilla's startup-optimized omnijar places its central # directory first. Info-ZIP extracts it correctly but exits 2 # after warning about the nonstandard layout. [ "$unzipStatus" -eq 2 ] || exit "$unzipStatus" } ${pkgs.patch}/bin/patch --directory="$omniWork" -p1 \ < ${windowSyncPatch} rm "$omni" ( cd "$omniWork" ${pkgs.zip}/bin/zip -0 -X -q -r "$omni" . ) rm -rf "$omniWork" done ''; })) ]; }; }