nixos/apps/zen.nix
rope d9604900e3 zen: copy the real binary into the wrapper so autoconfig is read
wrapFirefox only copies the exe matching binaryName ("zen-beta"), but Gecko
execs lib/zen-bin-*/zen, a symlink into the unwrapped package. The app dir
resolved there, so mozilla.cfg and defaults/pref were never read.

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

128 lines
5.8 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 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 <mtime>", "missing <path>" or "error <exception>" 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
'';
}))
];
};
}