diff --git a/README.md b/README.md index 2057e59..7eacb1c 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,8 @@ without it. | `steamFrame.clipboardSync.enable` | bool | `true` | Clipboard bridge between the Steam session and the nested desktop. | | `steamFrame.clipboardSync.package` | package | built from `dnut/clipboard-sync` | The clipboard-sync package. | | `steamFrame.firefox.enable` | bool | `false` | Launcher for the Flathub Firefox Flatpak with the fixes below. | -| `steamFrame.firefox.vrFullscreenFix` | bool | `true` | Link a `user.js` with `full-screen-api.ignore-widgets` into profiles. | +| `steamFrame.firefox.vrFullscreenFix` | bool | `true` | Default `full-screen-api.ignore-widgets` to `true` (not in the desktop profile). | +| `steamFrame.firefox.prefs` | attrs of bool, int or str | `{ }` | `about:config` default values for every profile, e.g. `{ "media.av1.enabled" = false; }`. | | `steamFrame.firefox.desktopProfile` | null or str | `"desktop"` | Separate profile for the nested desktop; `null`: none. | | `steamFrame.jellyfin.hardwareDecoding.enable` | bool | `false` | Hardware video decoding in the Jellyfin Desktop Flatpak, see [Jellyfin](#jellyfin-hardware-decoding-jellyfinhardwaredecoding). | | `steamFrame.jellyfin.hardwareDecoding.hwdec` | str | `"v4l2m2m-copy,auto-copy"` | mpv `hwdec` used instead of Jellyfin's automatic one. | @@ -807,14 +808,35 @@ launcher shadows the Flatpak's own entry (same ID), so default-browser associations keep working. - **`vrFullscreenFix`:** gamescope never shows fullscreen windows, so - Firefox looks frozen. A `user.js` with `full-screen-api.ignore-widgets` - makes fullscreen fill just the window; it is linked into every profile - (existing non-symlink `user.js` files are left alone). **Remove when** - gamescope shows fullscreen X11 windows in VR. + Firefox looks frozen. `full-screen-api.ignore-widgets` makes fullscreen + fill just the window. **Remove when** gamescope shows fullscreen X11 + windows in VR. +- **`prefs`:** `about:config` values for every profile. E.g. + `"media.av1.enabled" = false`: the Frame's decoder has no AV1, so YouTube + and co. fall back to VP9/H.264, which it decodes in hardware. - **`desktopProfile`:** the sessions can't see each other's Firefox, so a second instance stops at the locked profile; in the nested desktop the launcher uses this separate profile. +`prefs` and the fullscreen fix are *default* values (`pref()`), not user +values: `about:config` can still change them per profile, and nothing is +written to `prefs.js`, so removing one leaves nothing behind. Firefox reads +default prefs from `defaults/pref/*.js` in its system config dir, which in +the Flatpak is `/app/etc/firefox`, the mount point of the +`org.mozilla.firefox.systemconfig` extension. home-manager provides that +extension as a link from +`~/.local/share/flatpak/extension/org.mozilla.firefox.systemconfig/aarch64/stable` +to a store directory; Flatpak mounts it itself, so the sandbox doesn't get +`/nix`. (A systemconfig extension of your own would conflict with it.) The +desktop profile undoes the fullscreen fix with a `user.js` linked to +`/app/etc/firefox/steam-frame-nix-desktop-user.js` (a sandbox path). +Changes take effect at the next start of Firefox. + +Older versions copied a `user.js` into every profile; the copies and the +values they left in `prefs.js` are removed on switch and before each launch, +for each profile not in use at that moment. `user.js` files of your own are +left alone. + ### Jellyfin hardware decoding (`jellyfin.hardwareDecoding.*`) For the Flathub [Jellyfin Desktop](https://github.com/jellyfin/jellyfin-desktop) diff --git a/modules/firefox.nix b/modules/firefox.nix index 9ae7249..ee1f9a8 100644 --- a/modules/firefox.nix +++ b/modules/firefox.nix @@ -1,30 +1,106 @@ # Firefox Flatpak (org.mozilla.firefox) launcher. +# - prefs and vrFullscreenFix are default prefs (pref(), the default branch: +# never written to prefs.js, so removing one leaves nothing behind). Firefox +# reads defaults/pref/*.js from its system config dir, /app/etc/firefox in +# the Flatpak, the mount point of the org.mozilla.firefox.systemconfig +# extension. The user installation's "unmaintained extension" dir +# ($XDG_DATA_HOME/flatpak/extension///) provides it: a +# home-manager link to a store dir, which Flatpak mounts itself (the +# sandbox needn't see /nix). # - vrFullscreenFix: in the Steam session gamescope focuses a fullscreen X11 # window but never shows it (Firefox looks frozen). ignore-widgets keeps -# fullscreen inside the window. Profile names are random and $HOME isn't -# readable at eval time, so user.js is written into each profile on switch -# (a copy: /nix isn't visible in the Flatpak sandbox, a store link dangles). +# fullscreen inside the window. # - desktopProfile: the sessions have separate buses/displays, so a second # Firefox can't reach the running one and hits the profile lock; the nested -# desktop gets its own profile (no fullscreen fix: fullscreen works there). -# - prefs: about:config values for every profile, desktop one included. +# desktop gets its own profile. Fullscreen works there, so the fix is undone +# by a user.js in that profile: a link to the extension's +# steam-frame-nix-desktop-user.js (a sandbox path; dangling on the host). +# - profileSync (on switch and before each launch) keeps those links and +# removes what older versions wrote: user.js copies starting with `marker` +# and links to *-firefox-*user.js store files, whose user_pref values +# Firefox had stored in prefs.js. They are taken out of prefs.js too, which +# needs the profile closed; profiles in use are left for the next run. # The entry shadows the Flatpak's (same ID), keeping MIME associations, and is # seen by the "+" menu (which reads only ~/.local/share/applications). { config, pkgs, lib, ... }: let cfg = config.steamFrame.firefox; - marker = "// Managed by steam-frame-nix (steamFrame.firefox); rewritten on switch."; - toUserJs = name: prefs: pkgs.writeText name (marker + "\n" + lib.concatStrings (lib.mapAttrsToList - (k: v: "user_pref(${builtins.toJSON k}, ${builtins.toJSON v});\n") prefs)); - vrPrefs = cfg.prefs // lib.optionalAttrs cfg.vrFullscreenFix { "full-screen-api.ignore-widgets" = true; }; - userJs = toUserJs "firefox-user.js" vrPrefs; - desktopUserJs = toUserJs "firefox-desktop-user.js" cfg.prefs; ffDir = "$HOME/.var/app/org.mozilla.firefox/config/mozilla/firefox"; profileDir = "${ffDir}/${cfg.desktopProfile}"; - # user.js for a profile dir ($prof): desktop profile -> prefs only. - pickUserJs = if cfg.desktopProfile == null then "js=${userJs}" else '' - js=${userJs} - [ "$(basename "$prof")" = ${lib.escapeShellArg cfg.desktopProfile} ] && js=${desktopUserJs} + + defaultPrefs = cfg.prefs // lib.optionalAttrs cfg.vrFullscreenFix { "full-screen-api.ignore-widgets" = true; }; + desktopFix = cfg.enable && cfg.vrFullscreenFix && cfg.desktopProfile != null; + # Only ever this one pref (profileSync relies on it). + desktopKeys = [ "full-screen-api.ignore-widgets" ]; + desktopUserJsName = "steam-frame-nix-desktop-user.js"; + + sysconfig = pkgs.runCommand "steam-frame-nix-firefox-systemconfig" { } ('' + mkdir -p $out/defaults/pref + cp ${pkgs.writeText "steam-frame-nix.js" (lib.concatStrings (lib.mapAttrsToList + (k: v: "pref(${builtins.toJSON k}, ${builtins.toJSON v});\n") defaultPrefs))} \ + $out/defaults/pref/steam-frame-nix.js + '' + lib.optionalString desktopFix '' + cp ${pkgs.writeText desktopUserJsName (lib.concatMapStrings + (k: "user_pref(${builtins.toJSON k}, false);\n") desktopKeys)} $out/${desktopUserJsName} + ''); + + # Written by versions that copied user.js into every profile. + marker = "// Managed by steam-frame-nix (steamFrame.firefox); rewritten on switch."; + + profileSync = pkgs.writeShellScript "firefox-profile-sync" '' + PATH=${lib.makeBinPath (with pkgs; [ coreutils diffutils findutils gnugrep gnused ])} + ffDir="${ffDir}" + desktopJs=/app/etc/firefox/${desktopUserJsName} + [ -d "$ffDir" ] || exit 0 + + # Firefox holds .parentlock open while it uses a profile (the sandbox + # sees the profile at the same path). + inUse() { find /proc/[0-9]*/fd -lname "$1/.parentlock" -print -quit 2>/dev/null | grep -q .; } + keysOf() { sed -n 's/^[[:space:]]*user_pref(\("[^"]*"\),.*/\1/p' "$1" 2>/dev/null; } + + for prof in "$ffDir"/*/; do + prof=''${prof%/}; name=''${prof##*/} + [ -f "$prof/prefs.js" ] || [ "$name" = ${lib.escapeShellArg (toString cfg.desktopProfile)} ] || continue + want= wantKeys= + ${lib.optionalString desktopFix '' + [ "$name" = ${lib.escapeShellArg cfg.desktopProfile} ] && + want=$desktopJs wantKeys=${lib.escapeShellArg (lib.concatMapStrings (k: builtins.toJSON k + "\n") desktopKeys)} + ''} + userJs=$prof/user.js + if [ -L "$userJs" ]; then + case $(readlink "$userJs") in + "$want") continue ;; + "$desktopJs") oldKeys=${lib.escapeShellArg (lib.concatMapStrings (k: builtins.toJSON k + "\n") desktopKeys)} ;; + /nix/store/*-firefox-user.js|/nix/store/*-firefox-desktop-user.js) oldKeys=$(keysOf "$userJs") ;; + *) oldKeys=foreign ;; + esac + elif [ -f "$userJs" ]; then + if [ "$(head -n1 "$userJs")" = ${lib.escapeShellArg marker} ]; then oldKeys=$(keysOf "$userJs"); else oldKeys=foreign; fi + elif [ -e "$userJs" ]; then oldKeys=foreign + else oldKeys= + fi + if [ "$oldKeys" = foreign ]; then + [ -z "$want" ] || echo "firefox: skipping $userJs (not managed by steam-frame-nix, move it away to adopt)" + continue + fi + + if [ -e "$userJs" ] || [ -L "$userJs" ]; then + # Values our user.js set, no longer set by one: out of prefs.js. + keys=$(printf '%s\n' "$oldKeys" | grep -vxF -f <(printf '%s\n' "$wantKeys") | grep .) + if [ -n "$keys" ]; then + if inUse "$prof"; then + echo "firefox: $name is in use; its old user.js stays until the next switch or launch with Firefox closed" + continue + fi + grep -vF "$(sed 's/.*/user_pref(&,/' <<< "$keys")" "$prof/prefs.js" > "$prof/prefs.js.sfn" || true + if cmp -s "$prof/prefs.js" "$prof/prefs.js.sfn"; then rm "$prof/prefs.js.sfn" + else cat "$prof/prefs.js.sfn" > "$prof/prefs.js"; rm "$prof/prefs.js.sfn"; echo "firefox: $name: removed from prefs.js:" $keys + fi + fi + rm -f "$userJs" + fi + if [ -n "$want" ]; then ln -s "$want" "$userJs"; fi + done ''; firefox = pkgs.writeShellScript "firefox-launcher" ('' @@ -36,6 +112,7 @@ let profile=(--profile "${profileDir}") fi '' + '' + ${profileSync} >&2 || true exec /usr/bin/flatpak run --branch=stable --arch=aarch64 --command=firefox \ --file-forwarding org.mozilla.firefox "''${profile[@]}" "$@" ''); @@ -48,8 +125,8 @@ in { type = lib.types.bool; default = true; description = '' - Set full-screen-api.ignore-widgets in every profile except the - desktop one, so fullscreen fills the window instead of freezing in the + Default full-screen-api.ignore-widgets to true (except in the desktop + profile), so fullscreen fills the window instead of freezing in the Steam session. ''; }; @@ -59,9 +136,11 @@ in { example = { "media.av1.enabled" = false; }; description = '' about:config preferences for every profile (desktop one included), - set through a linked user.js on each switch. E.g. media.av1.enabled = + as default values: about:config can still change them for a profile, + and removing one here leaves nothing behind. E.g. media.av1.enabled = false: the Frame's decoder has no AV1, so sites like YouTube fall back - to VP9/H.264, which it decodes in hardware. + to VP9/H.264, which it decodes in hardware. Take effect at the next + start of Firefox. ''; }; desktopProfile = lib.mkOption { @@ -74,47 +153,41 @@ in { }; }; - config = lib.mkIf cfg.enable { - # Written into every existing profile (profile names are random); a - # user.js with no prefs is harmless and keeps turning prefs off revertible. - # Ours: a symlink (older versions) or a file starting with the marker. - home.activation.firefoxUserJs = - (lib.hm.dag.entryAfter [ "writeBoundary" ] '' - for prof in "${ffDir}"/*/; do - [ -f "$prof/prefs.js" ] || continue # only real profiles - ${pickUserJs} - target="$prof/user.js" - if [ -e "$target" ] && [ ! -L "$target" ] && [ "$(head -n1 "$target")" != ${lib.escapeShellArg marker} ]; then - echo "firefox: skipping $target (not managed by us, move it away to adopt)" - continue - fi - cmp -s "$js" "$target" && [ ! -L "$target" ] && continue - run rm -f "$target" - run install -m644 "$js" "$target" - done - ''); + config = lib.mkMerge [ + { + # Always, so disabling removes our user.js links (and old copies). + home.activation.firefoxProfiles = + lib.hm.dag.entryAfter [ "writeBoundary" ] "run ${profileSync}\n"; + } + (lib.mkIf cfg.enable { + # stable: the branch the launcher runs (the extension point has no + # version, so it takes the app's branch). + xdg.dataFile = lib.optionalAttrs (defaultPrefs != { } || desktopFix) { + "flatpak/extension/org.mozilla.firefox.systemconfig/aarch64/stable".source = sysconfig; + } // { + "applications/org.mozilla.firefox.desktop".text = '' + [Desktop Entry] + Type=Application + Name=Firefox + GenericName=Web Browser + Icon=org.mozilla.firefox + Exec=${firefox} @@u %u @@ + StartupWMClass=firefox + StartupNotify=true + Terminal=false + Categories=Network;WebBrowser; + MimeType=application/json;application/pdf;application/rdf+xml;application/rss+xml;application/x-xpinstall;application/xhtml+xml;application/xml;audio/flac;audio/ogg;audio/webm;image/avif;image/gif;image/jpeg;image/png;image/svg+xml;image/webp;text/html;text/xml;video/ogg;video/webm;x-scheme-handler/chrome;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/mailto; + Actions=new-window;new-private-window; - xdg.dataFile."applications/org.mozilla.firefox.desktop".text = '' - [Desktop Entry] - Type=Application - Name=Firefox - GenericName=Web Browser - Icon=org.mozilla.firefox - Exec=${firefox} @@u %u @@ - StartupWMClass=firefox - StartupNotify=true - Terminal=false - Categories=Network;WebBrowser; - MimeType=application/json;application/pdf;application/rdf+xml;application/rss+xml;application/x-xpinstall;application/xhtml+xml;application/xml;audio/flac;audio/ogg;audio/webm;image/avif;image/gif;image/jpeg;image/png;image/svg+xml;image/webp;text/html;text/xml;video/ogg;video/webm;x-scheme-handler/chrome;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/mailto; - Actions=new-window;new-private-window; + [Desktop Action new-window] + Name=New Window + Exec=${firefox} --new-window @@u %u @@ - [Desktop Action new-window] - Name=New Window - Exec=${firefox} --new-window @@u %u @@ - - [Desktop Action new-private-window] - Name=New Private Window - Exec=${firefox} --private-window @@u %u @@ - ''; - }; + [Desktop Action new-private-window] + Name=New Private Window + Exec=${firefox} --private-window @@u %u @@ + ''; + }; + }) + ]; } diff --git a/template/home.nix b/template/home.nix index 76c8719..ec9ae8e 100644 --- a/template/home.nix +++ b/template/home.nix @@ -54,6 +54,8 @@ # frameControls.enable = true; # }; # firefox.enable = true; # launcher for the Firefox Flatpak + # # about:config defaults; the Frame decodes VP9/H.264 but not AV1: + # firefox.prefs."media.av1.enabled" = false; # # Hardware video decoding in the Jellyfin Desktop Flatpak (install # # org.jellyfin.JellyfinDesktop yourself); gives it devices=all: # jellyfin.hardwareDecoding.enable = true;