From 81a5fef0b35a1517d1853de36cbe152b500d34e4 Mon Sep 17 00:00:00 2001 From: rope Date: Mon, 5 Oct 2026 11:58:58 +0100 Subject: [PATCH] Patch Zen window sync blank tabs --- apps/zen.nix | 96 ++++++--- flake.lock | 12 +- ...dow-sync-commit-ownership-after-swap.patch | 203 ++++++++++++++++++ 3 files changed, 278 insertions(+), 33 deletions(-) create mode 100644 patches/zen-browser/window-sync-commit-ownership-after-swap.patch diff --git a/apps/zen.nix b/apps/zen.nix index 402dcb3..d593da8 100644 --- a/apps/zen.nix +++ b/apps/zen.nix @@ -1,6 +1,19 @@ #./apps/zen.nix -{ inputs, pkgs, lib, config, ... }: -let +{ + 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. @@ -97,33 +110,62 @@ let Components.utils.reportError("zen-theme: " + e); } ''; -in -{ - config = lib.mkIf (lib.elem config.networking.hostName [ "FredOS-Gaming" "FredOS-Macbook" ]) { +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 - ''; - })) + # 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 + ''; + })) ]; }; } diff --git a/flake.lock b/flake.lock index d4fe97d..41a7c0c 100644 --- a/flake.lock +++ b/flake.lock @@ -299,11 +299,11 @@ }, "nixpkgs_4": { "locked": { - "lastModified": 1789286504, - "narHash": "sha256-eiEK7cKZORNEvX0GeF3RtNEF/JXhgf2RqSp3230q13E=", + "lastModified": 1790689126, + "narHash": "sha256-ilerN1WLSvF+HMjziC/Wv99J5y02maDH+6hZPwsORKg=", "owner": "nixos", "repo": "nixpkgs", - "rev": "ef34387ddd751e1ab8857adf4676492d32eb24ec", + "rev": "b4fd65b198c599cbe814fcb9f42d25d021595ec9", "type": "github" }, "original": { @@ -526,11 +526,11 @@ "nixpkgs": "nixpkgs_4" }, "locked": { - "lastModified": 1789702344, - "narHash": "sha256-DfevaOxGu6NKUF+q5hUAg7kFscWQvuC9Yd/CgR4qpgc=", + "lastModified": 1791022445, + "narHash": "sha256-jr7YVkvOYS+bJjzBwoqtN8b8inJN8UDWziWufAWH1SY=", "owner": "0xc000022070", "repo": "zen-browser-flake", - "rev": "ef5fe624be04120db77c08c5df6f75a6a4201517", + "rev": "e56900732d1fa6f360db502451a457ffe3ca1e6e", "type": "github" }, "original": { diff --git a/patches/zen-browser/window-sync-commit-ownership-after-swap.patch b/patches/zen-browser/window-sync-commit-ownership-after-swap.patch new file mode 100644 index 0000000..69b282a --- /dev/null +++ b/patches/zen-browser/window-sync-commit-ownership-after-swap.patch @@ -0,0 +1,203 @@ +diff --git a/modules/zen/ZenWindowSync.sys.mjs b/modules/zen/ZenWindowSync.sys.mjs +index 8daa5b3..33474a5 100644 +--- a/modules/zen/ZenWindowSync.sys.mjs ++++ b/modules/zen/ZenWindowSync.sys.mjs +@@ -781 +781 @@ class nsZenWindowSync { +- return; ++ return false; +@@ -785 +785 @@ class nsZenWindowSync { +- return; ++ return false; +@@ -787 +787 @@ class nsZenWindowSync { +- await this.#styleSwapedBrowsers(aOurTab, aOtherTab, () => { ++ return this.#styleSwapedBrowsers(aOurTab, aOtherTab, () => { +@@ -789 +789 @@ class nsZenWindowSync { +- this.#swapBrowserDocShellsInner(aOurTab, aOtherTab); ++ return this.#swapBrowserDocShellsInner(aOurTab, aOtherTab); +@@ -794,0 +795 @@ class nsZenWindowSync { ++ return false; +@@ -829,7 +830,8 @@ class nsZenWindowSync { +- tabListener = new otherTabBrowser.zenTabProgressListener( +- aTab, +- otherBrowser, +- true, +- false +- ); +- otherTabBrowser.zenSetTabProgressListener(aTab, tabListener); ++ try { ++ tabListener = new otherTabBrowser.zenTabProgressListener( ++ aTab, ++ otherBrowser, ++ true, ++ false ++ ); ++ otherTabBrowser.zenSetTabProgressListener(aTab, tabListener); +@@ -837,3 +839,6 @@ class nsZenWindowSync { +- const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL; +- filter.addProgressListener(tabListener, notifyAll); +- otherBrowser.webProgress.addProgressListener(filter, notifyAll); ++ const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL; ++ filter.addProgressListener(tabListener, notifyAll); ++ otherBrowser.webProgress.addProgressListener(filter, notifyAll); ++ } catch (e) { ++ console.error(`Error restoring tab progress listener:`, e); ++ } +@@ -899 +904 @@ class nsZenWindowSync { +- return; ++ return false; +@@ -903,0 +909 @@ class nsZenWindowSync { ++ let didSwap = false; +@@ -913,0 +920 @@ class nsZenWindowSync { ++ didSwap = true; +@@ -918,0 +926 @@ class nsZenWindowSync { ++ return; +@@ -962,0 +971,3 @@ class nsZenWindowSync { ++ if (!didSwap) { ++ return false; ++ } +@@ -972,9 +983,13 @@ class nsZenWindowSync { +- for (let attr of kAttributesToRemove) { +- aOtherTab.removeAttribute(attr); +- } +- if (focus) { +- // Recalculate the focus in order to allow the user to continue typing +- // inside the web content area without having to click outside and back in. +- aOurTab.linkedBrowser.blur(); +- aOurTab.documentGlobal.gBrowser._adjustFocusAfterTabSwitch(aOurTab); +- aOurTab.linkedBrowser.docShellIsActive = true; ++ try { ++ for (let attr of kAttributesToRemove) { ++ aOtherTab.removeAttribute(attr); ++ } ++ if (focus) { ++ // Recalculate the focus in order to allow the user to continue typing ++ // inside the web content area without having to click outside and back in. ++ aOurTab.linkedBrowser.blur(); ++ aOurTab.documentGlobal.gBrowser._adjustFocusAfterTabSwitch(aOurTab); ++ aOurTab.linkedBrowser.docShellIsActive = true; ++ } ++ } catch (e) { ++ console.error(`Error cleaning up browser after docshell swap:`, e); +@@ -981,0 +997 @@ class nsZenWindowSync { ++ return true; +@@ -997,7 +1013,17 @@ class nsZenWindowSync { +- const browserBlob = +- await aOtherTab.documentGlobal.PageThumbs.captureToBlob( +- aOtherTab.linkedBrowser, +- { +- fullScale: true, +- fullViewport: true, +- backgroundColor: "transparent", ++ let mySrc = ""; ++ try { ++ const browserBlob = ++ await aOtherTab.documentGlobal.PageThumbs.captureToBlob( ++ aOtherTab.linkedBrowser, ++ { ++ fullScale: true, ++ fullViewport: true, ++ backgroundColor: "transparent", ++ } ++ ); ++ ++ mySrc = await new Promise(r => { ++ const reader = new FileReader(); ++ if (!browserBlob) { ++ r(""); ++ return; +@@ -1005,17 +1031,9 @@ class nsZenWindowSync { +- ); +- +- let mySrc = await new Promise(r => { +- const reader = new FileReader(); +- if (!browserBlob) { +- r(""); +- return; +- } +- reader.readAsDataURL(browserBlob); +- reader.onloadend = function () { +- // result includes identifier 'data:image/png;base64,' plus the base64 data +- r(reader.result); +- }; +- reader.onerror = function () { +- r(""); +- }; +- }); ++ reader.readAsDataURL(browserBlob); ++ reader.onloadend = function () { ++ // result includes identifier 'data:image/png;base64,' plus the base64 data ++ r(reader.result); ++ }; ++ reader.onerror = function () { ++ r(""); ++ }; ++ }); +@@ -1023,2 +1041,7 @@ class nsZenWindowSync { +- await this.#createPseudoImageForBrowser(otherBrowser, mySrc); +- callback(); ++ await this.#createPseudoImageForBrowser(otherBrowser, mySrc); ++ } catch (e) { ++ // The thumbnail only hides the visual transition. It must not block ++ // the docshell transfer or leave the global swap queue pending. ++ console.error(`Error preparing docshell swap styling:`, e); ++ } ++ const didSwap = callback(); +@@ -1026,5 +1049,10 @@ class nsZenWindowSync { +- otherBrowser.setAttribute("zen-pseudo-hidden", "true"); +- ourBrowser.removeAttribute("zen-pseudo-hidden"); +- this.#maybeRemovePseudoImageForBrowser(ourBrowser); +- ourBrowser.focus(); +- resolve(); ++ try { ++ otherBrowser.setAttribute("zen-pseudo-hidden", "true"); ++ ourBrowser.removeAttribute("zen-pseudo-hidden"); ++ this.#maybeRemovePseudoImageForBrowser(ourBrowser); ++ ourBrowser.focus(); ++ } catch (e) { ++ console.error(`Error cleaning up docshell swap styling:`, e); ++ } finally { ++ resolve(didSwap); ++ } +@@ -1126 +1153,0 @@ class nsZenWindowSync { +- targetTab._zenContentsVisible = true; +@@ -1130 +1157 @@ class nsZenWindowSync { +- delete tab._zenContentsVisible; ++ let didSwap = false; +@@ -1132 +1159 @@ class nsZenWindowSync { +- this.#swapBrowserDocShellsInner(targetTab, tab, { ++ didSwap = this.#swapBrowserDocShellsInner(targetTab, tab, { +@@ -1141,0 +1169,5 @@ class nsZenWindowSync { ++ if (!didSwap) { ++ continue; ++ } ++ targetTab._zenContentsVisible = true; ++ delete tab._zenContentsVisible; +@@ -1186,3 +1218,8 @@ class nsZenWindowSync { +- otherTabToShow._zenContentsVisible = true; +- delete tab._zenContentsVisible; +- await this.#swapBrowserDocShellsAsync(otherTabToShow, tab); ++ const didSwap = await this.#swapBrowserDocShellsAsync( ++ otherTabToShow, ++ tab ++ ); ++ if (didSwap) { ++ otherTabToShow._zenContentsVisible = true; ++ delete tab._zenContentsVisible; ++ } +@@ -1204 +1240,0 @@ class nsZenWindowSync { +- selectedTab._zenContentsVisible = true; +@@ -1206 +1241,0 @@ class nsZenWindowSync { +- delete otherSelectedTab._zenContentsVisible; +@@ -1208 +1243,8 @@ class nsZenWindowSync { +- this.#swapBrowserDocShellsAsync(selectedTab, otherSelectedTab) ++ this.#swapBrowserDocShellsAsync(selectedTab, otherSelectedTab).then( ++ didSwap => { ++ if (didSwap) { ++ selectedTab._zenContentsVisible = true; ++ delete otherSelectedTab._zenContentsVisible; ++ } ++ } ++ ) +@@ -1209,0 +1252,2 @@ class nsZenWindowSync { ++ } else { ++ selectedTab._zenContentsVisible = true;