175 lines
7.9 KiB
Nix
175 lines
7.9 KiB
Nix
#./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 <profile>/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 <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);
|
|
|
|
// Drop min/max/close: inert under Hyprland, and with the single toolbar
|
|
// they are all that keeps the empty strip above the page alive.
|
|
pref("zen.view.experimental-no-window-controls", 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
|
|
'';
|
|
}))
|
|
];
|
|
};
|
|
}
|