From 4205c1fc748e0003d5091323e28ebca30a4de9ce Mon Sep 17 00:00:00 2001 From: MrQubo Date: Wed, 31 Jul 2024 00:28:12 +0200 Subject: [PATCH] firefox: Enable userChrome in about:config Enable "toolkit.legacyUserProfileCustomizations.stylesheets" in about:config if userChrome or userContent is not empty. --- modules/programs/firefox/mkFirefoxModule.nix | 780 +++++++++--------- .../profile-settings-expected-containers.json | 2 +- .../programs/firefox/profile-settings.nix | 31 +- 3 files changed, 433 insertions(+), 380 deletions(-) diff --git a/modules/programs/firefox/mkFirefoxModule.nix b/modules/programs/firefox/mkFirefoxModule.nix index f22af019..5b78f496 100644 --- a/modules/programs/firefox/mkFirefoxModule.nix +++ b/modules/programs/firefox/mkFirefoxModule.nix @@ -343,410 +343,421 @@ in { profiles = mkOption { inherit visible; - type = types.attrsOf (types.submodule ({ config, name, ... }: { - options = { - name = mkOption { - type = types.str; - default = name; - description = "Profile name."; - }; + type = types.attrsOf (types.submodule ({ config, name, ... }: + let - id = mkOption { - type = types.ints.unsigned; - default = 0; - description = '' - Profile ID. This should be set to a unique number per profile. - ''; - }; + profile = config; - settings = mkOption { - type = types.attrsOf (jsonFormat.type // { - description = - "${name} preference (int, bool, string, and also attrs, list, float as a JSON string)"; - }); - default = { }; - example = literalExpression '' - { - "browser.startup.homepage" = "https://nixos.org"; - "browser.search.region" = "GB"; - "browser.search.isUS" = false; - "distribution.searchplugins.defaultLocale" = "en-GB"; - "general.useragent.locale" = "en-GB"; - "browser.bookmarks.showMobileBookmarks" = true; - "browser.newtabpage.pinned" = [{ - title = "NixOS"; - url = "https://nixos.org"; - }]; - } - ''; - description = '' - Attribute set of ${name} preferences. + in { + options = { + name = mkOption { + type = types.str; + default = name; + description = "Profile name."; + }; - ${name} only supports int, bool, and string types for - preferences, but home-manager will automatically - convert all other JSON-compatible values into strings. - ''; - }; - - extraConfig = mkOption { - type = types.lines; - default = ""; - description = '' - Extra preferences to add to {file}`user.js`. - ''; - }; - - userChrome = mkOption { - type = types.lines; - default = ""; - description = "Custom ${name} user chrome CSS."; - example = '' - /* Hide tab bar in FF Quantum */ - @-moz-document url(chrome://browser/content/browser.xul), url(chrome://browser/content/browser.xhtml) { - #TabsToolbar { - visibility: collapse !important; - margin-bottom: 21px !important; - } - - #sidebar-box[sidebarcommand="treestyletab_piro_sakura_ne_jp-sidebar-action"] #sidebar-header { - visibility: collapse !important; - } - } - ''; - }; - - userContent = mkOption { - type = types.lines; - default = ""; - description = "Custom ${name} user content CSS."; - example = '' - /* Hide scrollbar in FF Quantum */ - *{scrollbar-width:none !important} - ''; - }; - - bookmarks = mkOption { - type = let - bookmarkSubmodule = types.submodule ({ config, name, ... }: { - options = { - name = mkOption { - type = types.str; - default = name; - description = "Bookmark name."; - }; - - tags = mkOption { - type = types.listOf types.str; - default = [ ]; - description = "Bookmark tags."; - }; - - keyword = mkOption { - type = types.nullOr types.str; - default = null; - description = "Bookmark search keyword."; - }; - - url = mkOption { - type = types.str; - description = "Bookmark url, use %s for search terms."; - }; - }; - }) // { - description = "bookmark submodule"; - }; - - bookmarkType = types.addCheck bookmarkSubmodule (x: x ? "url"); - - directoryType = types.submodule ({ config, name, ... }: { - options = { - name = mkOption { - type = types.str; - default = name; - description = "Directory name."; - }; - - bookmarks = mkOption { - type = types.listOf nodeType; - default = [ ]; - description = "Bookmarks within directory."; - }; - - toolbar = mkOption { - type = types.bool; - default = false; - description = '' - Make this the toolbar directory. Note, this does _not_ - mean that this directory will be added to the toolbar, - this directory _is_ the toolbar. - ''; - }; - }; - }) // { - description = "directory submodule"; - }; - - nodeType = types.either bookmarkType directoryType; - in with types; - coercedTo (attrsOf nodeType) attrValues (listOf nodeType); - default = [ ]; - example = literalExpression '' - [ - { - name = "wikipedia"; - tags = [ "wiki" ]; - keyword = "wiki"; - url = "https://en.wikipedia.org/wiki/Special:Search?search=%s&go=Go"; - } - { - name = "kernel.org"; - url = "https://www.kernel.org"; - } - { - name = "Nix sites"; - toolbar = true; - bookmarks = [ - { - name = "homepage"; - url = "https://nixos.org/"; - } - { - name = "wiki"; - tags = [ "wiki" "nix" ]; - url = "https://wiki.nixos.org/"; - } - ]; - } - ] - ''; - description = '' - Preloaded bookmarks. Note, this may silently overwrite any - previously existing bookmarks! - ''; - }; - - path = mkOption { - type = types.str; - default = name; - description = "Profile path."; - }; - - isDefault = mkOption { - type = types.bool; - default = config.id == 0; - defaultText = "true if profile ID is 0"; - description = "Whether this is a default profile."; - }; - - search = { - force = mkOption { - type = with types; bool; - default = false; + id = mkOption { + type = types.ints.unsigned; + default = 0; description = '' - Whether to force replace the existing search - configuration. This is recommended since ${name} will - replace the symlink for the search configuration on every - launch, but note that you'll lose any existing - configuration by enabling this. + Profile ID. This should be set to a unique number per profile. ''; }; - default = mkOption { - type = with types; nullOr str; - default = null; - example = "DuckDuckGo"; - description = '' - The default search engine used in the address bar and search bar. - ''; - }; - - privateDefault = mkOption { - type = with types; nullOr str; - default = null; - example = "DuckDuckGo"; - description = '' - The default search engine used in the Private Browsing. - ''; - }; - - order = mkOption { - type = with types; uniq (listOf str); - default = [ ]; - example = [ "DuckDuckGo" "Google" ]; - description = '' - The order the search engines are listed in. Any engines - that aren't included in this list will be listed after - these in an unspecified order. - ''; - }; - - engines = mkOption { - type = with types; attrsOf (attrsOf jsonFormat.type); + settings = mkOption { + type = types.attrsOf (jsonFormat.type // { + description = + "${name} preference (int, bool, string, and also attrs, list, float as a JSON string)"; + }); default = { }; example = literalExpression '' { - "Nix Packages" = { - urls = [{ - template = "https://search.nixos.org/packages"; - params = [ - { name = "type"; value = "packages"; } - { name = "query"; value = "{searchTerms}"; } - ]; - }]; - - icon = "''${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg"; - definedAliases = [ "@np" ]; - }; - - "NixOS Wiki" = { - urls = [{ template = "https://wiki.nixos.org/index.php?search={searchTerms}"; }]; - iconUpdateURL = "https://wiki.nixos.org/favicon.png"; - updateInterval = 24 * 60 * 60 * 1000; # every day - definedAliases = [ "@nw" ]; - }; - - "Bing".metaData.hidden = true; - "Google".metaData.alias = "@g"; # builtin engines only support specifying one additional alias + "browser.startup.homepage" = "https://nixos.org"; + "browser.search.region" = "GB"; + "browser.search.isUS" = false; + "distribution.searchplugins.defaultLocale" = "en-GB"; + "general.useragent.locale" = "en-GB"; + "browser.bookmarks.showMobileBookmarks" = true; + "browser.newtabpage.pinned" = [{ + title = "NixOS"; + url = "https://nixos.org"; + }]; } ''; description = '' - Attribute set of search engine configurations. Engines - that only have {var}`metaData` specified will - be treated as builtin to ${name}. + Attribute set of ${name} preferences. - See [SearchEngine.jsm](https://searchfox.org/mozilla-central/rev/669329e284f8e8e2bb28090617192ca9b4ef3380/toolkit/components/search/SearchEngine.jsm#1138-1177) - in Firefox's source for available options. We maintain a - mapping to let you specify all options in the referenced - link without underscores, but it may fall out of date with - future options. - - Note, {var}`icon` is also a special option - added by Home Manager to make it convenient to specify - absolute icon paths. + ${name} only supports int, bool, and string types for + preferences, but home-manager will automatically + convert all other JSON-compatible values into strings. ''; }; - }; - containersForce = mkOption { - type = types.bool; - default = false; - description = '' - Whether to force replace the existing containers configuration. - This is recommended since Firefox will replace the symlink on - every launch, but note that you'll lose any existing configuration - by enabling this. - ''; - }; + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra preferences to add to {file}`user.js`. + ''; + }; - containers = mkOption { - type = types.attrsOf (types.submodule ({ name, ... }: { - options = { - name = mkOption { - type = types.str; - default = name; - description = "Container name, e.g., shopping."; + userChrome = mkOption { + type = types.lines; + default = ""; + description = "Custom ${name} user chrome CSS."; + example = '' + /* Hide tab bar in FF Quantum */ + @-moz-document url(chrome://browser/content/browser.xul), url(chrome://browser/content/browser.xhtml) { + #TabsToolbar { + visibility: collapse !important; + margin-bottom: 21px !important; + } + + #sidebar-box[sidebarcommand="treestyletab_piro_sakura_ne_jp-sidebar-action"] #sidebar-header { + visibility: collapse !important; + } + } + ''; + }; + + userContent = mkOption { + type = types.lines; + default = ""; + description = "Custom ${name} user content CSS."; + example = '' + /* Hide scrollbar in FF Quantum */ + *{scrollbar-width:none !important} + ''; + }; + + bookmarks = mkOption { + type = let + bookmarkSubmodule = types.submodule ({ config, name, ... }: { + options = { + name = mkOption { + type = types.str; + default = name; + description = "Bookmark name."; + }; + + tags = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Bookmark tags."; + }; + + keyword = mkOption { + type = types.nullOr types.str; + default = null; + description = "Bookmark search keyword."; + }; + + url = mkOption { + type = types.str; + description = "Bookmark url, use %s for search terms."; + }; + }; + }) // { + description = "bookmark submodule"; }; - id = mkOption { - type = types.ints.unsigned; - default = 0; - description = '' - Container ID. This should be set to a unique number per container in this profile. - ''; + bookmarkType = types.addCheck bookmarkSubmodule (x: x ? "url"); + + directoryType = types.submodule ({ config, name, ... }: { + options = { + name = mkOption { + type = types.str; + default = name; + description = "Directory name."; + }; + + bookmarks = mkOption { + type = types.listOf nodeType; + default = [ ]; + description = "Bookmarks within directory."; + }; + + toolbar = mkOption { + type = types.bool; + default = false; + description = '' + Make this the toolbar directory. Note, this does _not_ + mean that this directory will be added to the toolbar, + this directory _is_ the toolbar. + ''; + }; + }; + }) // { + description = "directory submodule"; }; - # List of colors at - # https://searchfox.org/mozilla-central/rev/5ad226c7379b0564c76dc3b54b44985356f94c5a/toolkit/components/extensions/parent/ext-contextualIdentities.js#32 - color = mkOption { - type = types.enum [ - "blue" - "turquoise" - "green" - "yellow" - "orange" - "red" - "pink" - "purple" - "toolbar" - ]; - default = "pink"; - description = "Container color."; - }; + nodeType = types.either bookmarkType directoryType; + in with types; + coercedTo (attrsOf nodeType) attrValues (listOf nodeType); + default = [ ]; + example = literalExpression '' + [ + { + name = "wikipedia"; + tags = [ "wiki" ]; + keyword = "wiki"; + url = "https://en.wikipedia.org/wiki/Special:Search?search=%s&go=Go"; + } + { + name = "kernel.org"; + url = "https://www.kernel.org"; + } + { + name = "Nix sites"; + toolbar = true; + bookmarks = [ + { + name = "homepage"; + url = "https://nixos.org/"; + } + { + name = "wiki"; + tags = [ "wiki" "nix" ]; + url = "https://wiki.nixos.org/"; + } + ]; + } + ] + ''; + description = '' + Preloaded bookmarks. Note, this may silently overwrite any + previously existing bookmarks! + ''; + }; - icon = mkOption { - type = types.enum [ - "briefcase" - "cart" - "circle" - "dollar" - "fence" - "fingerprint" - "gift" - "vacation" - "food" - "fruit" - "pet" - "tree" - "chill" - ]; - default = "fruit"; - description = "Container icon."; - }; + path = mkOption { + type = types.str; + default = name; + description = "Profile path."; + }; + + isDefault = mkOption { + type = types.bool; + default = config.id == 0; + defaultText = "true if profile ID is 0"; + description = "Whether this is a default profile."; + }; + + search = { + force = mkOption { + type = with types; bool; + default = false; + description = '' + Whether to force replace the existing search + configuration. This is recommended since ${name} will + replace the symlink for the search configuration on every + launch, but note that you'll lose any existing + configuration by enabling this. + ''; }; - })); - default = { }; - example = { - "shopping" = { - id = 1; - color = "blue"; - icon = "cart"; + + default = mkOption { + type = with types; nullOr str; + default = null; + example = "DuckDuckGo"; + description = '' + The default search engine used in the address bar and search bar. + ''; }; - "dangerous" = { - id = 2; - color = "red"; - icon = "fruit"; + + privateDefault = mkOption { + type = with types; nullOr str; + default = null; + example = "DuckDuckGo"; + description = '' + The default search engine used in the Private Browsing. + ''; + }; + + order = mkOption { + type = with types; uniq (listOf str); + default = [ ]; + example = [ "DuckDuckGo" "Google" ]; + description = '' + The order the search engines are listed in. Any engines + that aren't included in this list will be listed after + these in an unspecified order. + ''; + }; + + engines = mkOption { + type = with types; attrsOf (attrsOf jsonFormat.type); + default = { }; + example = literalExpression '' + { + "Nix Packages" = { + urls = [{ + template = "https://search.nixos.org/packages"; + params = [ + { name = "type"; value = "packages"; } + { name = "query"; value = "{searchTerms}"; } + ]; + }]; + + icon = "''${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg"; + definedAliases = [ "@np" ]; + }; + + "NixOS Wiki" = { + urls = [{ template = "https://wiki.nixos.org/index.php?search={searchTerms}"; }]; + iconUpdateURL = "https://wiki.nixos.org/favicon.png"; + updateInterval = 24 * 60 * 60 * 1000; # every day + definedAliases = [ "@nw" ]; + }; + + "Bing".metaData.hidden = true; + "Google".metaData.alias = "@g"; # builtin engines only support specifying one additional alias + } + ''; + description = '' + Attribute set of search engine configurations. Engines + that only have {var}`metaData` specified will + be treated as builtin to ${name}. + + See [SearchEngine.jsm](https://searchfox.org/mozilla-central/rev/669329e284f8e8e2bb28090617192ca9b4ef3380/toolkit/components/search/SearchEngine.jsm#1138-1177) + in Firefox's source for available options. We maintain a + mapping to let you specify all options in the referenced + link without underscores, but it may fall out of date with + future options. + + Note, {var}`icon` is also a special option + added by Home Manager to make it convenient to specify + absolute icon paths. + ''; }; }; - description = '' - Attribute set of container configurations. See - [Multi-Account - Containers](https://support.mozilla.org/en-US/kb/containers) - for more information. - ''; + + containersForce = mkOption { + type = types.bool; + default = false; + description = '' + Whether to force replace the existing containers configuration. + This is recommended since Firefox will replace the symlink on + every launch, but note that you'll lose any existing configuration + by enabling this. + ''; + }; + + containers = mkOption { + type = types.attrsOf (types.submodule ({ name, ... }: { + options = { + name = mkOption { + type = types.str; + default = name; + description = "Container name, e.g., shopping."; + }; + + id = mkOption { + type = types.ints.unsigned; + default = 0; + description = '' + Container ID. This should be set to a unique number per container in this profile. + ''; + }; + + # List of colors at + # https://searchfox.org/mozilla-central/rev/5ad226c7379b0564c76dc3b54b44985356f94c5a/toolkit/components/extensions/parent/ext-contextualIdentities.js#32 + color = mkOption { + type = types.enum [ + "blue" + "turquoise" + "green" + "yellow" + "orange" + "red" + "pink" + "purple" + "toolbar" + ]; + default = "pink"; + description = "Container color."; + }; + + icon = mkOption { + type = types.enum [ + "briefcase" + "cart" + "circle" + "dollar" + "fence" + "fingerprint" + "gift" + "vacation" + "food" + "fruit" + "pet" + "tree" + "chill" + ]; + default = "fruit"; + description = "Container icon."; + }; + }; + })); + default = { }; + example = { + "shopping" = { + id = 1; + color = "blue"; + icon = "cart"; + }; + "dangerous" = { + id = 2; + color = "red"; + icon = "fruit"; + }; + }; + description = '' + Attribute set of container configurations. See + [Multi-Account + Containers](https://support.mozilla.org/en-US/kb/containers) + for more information. + ''; + }; + + extensions = mkOption { + type = types.listOf types.package; + default = [ ]; + example = literalExpression '' + with pkgs.nur.repos.rycee.firefox-addons; [ + privacy-badger + ] + ''; + description = '' + List of ${name} add-on packages to install for this profile. + Some pre-packaged add-ons are accessible from the + [Nix User Repository](https://github.com/nix-community/NUR). + Once you have NUR installed run + + ```console + $ nix-env -f '' -qaP -A nur.repos.rycee.firefox-addons + ``` + + to list the available ${name} add-ons. + + Note that it is necessary to manually enable these extensions + inside ${name} after the first installation. + + To automatically enable extensions add + `"extensions.autoDisableScopes" = 0;` + to + [{option}`${moduleName}.profiles..settings`](#opt-${moduleName}.profiles._name_.settings) + ''; + }; + }; - extensions = mkOption { - type = types.listOf types.package; - default = [ ]; - example = literalExpression '' - with pkgs.nur.repos.rycee.firefox-addons; [ - privacy-badger - ] - ''; - description = '' - List of ${name} add-on packages to install for this profile. - Some pre-packaged add-ons are accessible from the - [Nix User Repository](https://github.com/nix-community/NUR). - Once you have NUR installed run - - ```console - $ nix-env -f '' -qaP -A nur.repos.rycee.firefox-addons - ``` - - to list the available ${name} add-ons. - - Note that it is necessary to manually enable these extensions - inside ${name} after the first installation. - - To automatically enable extensions add - `"extensions.autoDisableScopes" = 0;` - to - [{option}`${moduleName}.profiles..settings`](#opt-${moduleName}.profiles._name_.settings) - ''; + config = { + settings."toolkit.legacyUserProfileCustomizations.stylesheets" = + mkIf (profile.userChrome != "" || profile.userContent != "") true; }; - }; - })); + })); default = { }; description = "Attribute set of ${name} profiles."; }; @@ -801,7 +812,28 @@ in { } (mkNoDuplicateAssertion cfg.profiles "profile") - ] ++ (mapAttrsToList + ] ++ ( + + # Assert "toolkit.legacyUserProfileCustomizations.stylesheets" is enabled if userChrome/userContent is used. + let + + assertProfile = userChromeAttr: profile: + profile.${userChromeAttr} != "" + -> (profile.settings."toolkit.legacyUserProfileCustomizations.stylesheets" + != false); + + mkAssertion = userChromeAttr: { + assertion = + all (assertProfile userChromeAttr) (attrValues cfg.profiles); + message = '' + {option}`${userChromeAttr}` won't work with + {option}`settings."toolkit.legacyUserProfileCustomizations.stylesheets"` set to false. + ''; + }; + + in [ (mkAssertion "userChrome") (mkAssertion "userContent") ] + + ) ++ (mapAttrsToList (_: profile: mkNoDuplicateAssertion profile.containers "container") cfg.profiles); diff --git a/tests/modules/programs/firefox/profile-settings-expected-containers.json b/tests/modules/programs/firefox/profile-settings-expected-containers.json index 8eea5555..d957b0c5 100644 --- a/tests/modules/programs/firefox/profile-settings-expected-containers.json +++ b/tests/modules/programs/firefox/profile-settings-expected-containers.json @@ -1 +1 @@ -{"identities":[{"color":"yellow","icon":"circle","name":"shopping","public":true,"userContextId":6},{"accessKey":"","color":"","icon":"","name":"userContextIdInternal.thumbnail","public":false,"userContextId":4294967294},{"accessKey":"","color":"","icon":"","name":"userContextIdInternal.webextStorageLocal","public":false,"userContextId":4294967295}],"lastUserContextId":6,"version":4} +{"identities":[{"color":"yellow","icon":"circle","name":"shopping","public":true,"userContextId":0},{"accessKey":"","color":"","icon":"","name":"userContextIdInternal.thumbnail","public":false,"userContextId":4294967294},{"accessKey":"","color":"","icon":"","name":"userContextIdInternal.webextStorageLocal","public":false,"userContextId":4294967295}],"lastUserContextId":0,"version":4} diff --git a/tests/modules/programs/firefox/profile-settings.nix b/tests/modules/programs/firefox/profile-settings.nix index d7776eb4..65149709 100644 --- a/tests/modules/programs/firefox/profile-settings.nix +++ b/tests/modules/programs/firefox/profile-settings.nix @@ -9,6 +9,9 @@ let firefoxMockOverlay = import ./setup-firefox-mock-overlay.nix modulePath; + userChromeExample = "#example-user-chrome { display: none; }"; + userContentExample = "#example-user-content { display: none; }"; + in { imports = [ firefoxMockOverlay ]; @@ -154,12 +157,18 @@ in { id = 5; containers = { "shopping" = { - id = 6; icon = "circle"; color = "yellow"; }; }; }; + + profiles.userChrome = { + id = 6; + userChrome = userChromeExample; + userContent = userContentExample; + }; + } // { nmt.script = '' @@ -173,10 +182,6 @@ in { home-files/${cfg.configPath}/test/user.js \ ${./profile-settings-expected-user.js} - assertFileContent \ - home-files/${cfg.configPath}/containers/containers.json \ - ${./profile-settings-expected-containers.json} - bookmarksUserJs=$(normalizeStorePaths \ home-files/${cfg.configPath}/bookmarks/user.js) @@ -210,6 +215,22 @@ in { assertFirefoxSearchContent \ home-files/${cfg.configPath}/searchWithoutDefault/search.json.mozlz4 \ ${./profile-settings-expected-search-without-default.json} + + assertFileContent \ + home-files/${cfg.configPath}/containers/containers.json \ + ${./profile-settings-expected-containers.json} + + assertFileContains \ + home-files/.mozilla/firefox/userChrome/user.js \ + 'user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true)' + + assertFileContains \ + home-files/.mozilla/firefox/userChrome/chrome/userChrome.css \ + '${userChromeExample}' + + assertFileContains \ + home-files/.mozilla/firefox/userChrome/chrome/userContent.css \ + '${userContentExample}' ''; }); }