nixos/apps/zen.nix
rope 5ad1ac06ca zen: hot-reload the theme via autoconfig sheet watcher
Gecko has no userChrome.css reload, so wrap zen with a mozilla.cfg script
that registers chrome/zen-theme.css through nsIStyleSheetService and
re-registers it under a cache-busting URI when its mtime moves.

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

65 lines
2.7 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). A separate file,
# not userChrome.css: the legacy loader would hold its own copy of the same
# sheet and the two would race on cascade order.
themeHotReload = ''
try {
const { classes: kCc, interfaces: kCi } = Components;
const sss = kCc["@mozilla.org/content/style-sheet-service;1"]
.getService(kCi.nsIStyleSheetService);
const file = Services.dirsvc.get("ProfD", kCi.nsIFile);
file.append("chrome");
file.append("zen-theme.css");
const base = Services.io.newFileURI(file).spec;
let live = null;
const apply = () => {
if (!file.exists()) 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.
sss.loadAndRegisterSheet(Services.io.newURI(base + "?" + stamp),
sss.USER_SHEET);
if (live !== null) {
const old = Services.io.newURI(base + "?" + live);
if (sss.sheetRegistered(old, sss.USER_SHEET))
sss.unregisterSheet(old, sss.USER_SHEET);
}
live = stamp;
};
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.
Services.obs.addObserver({ timer, observe() {} }, "quit-application");
timer.initWithCallback({ notify: apply }, 2000,
kCi.nsITimer.TYPE_REPEATING_SLACK);
apply();
} catch (e) {
Components.utils.reportError(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;
})
];
};
}