diff --git a/modules/programs/firefox.nix b/modules/programs/firefox.nix index e1594a946..c43781a7a 100644 --- a/modules/programs/firefox.nix +++ b/modules/programs/firefox.nix @@ -8,6 +8,32 @@ let extensionPath = "extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"; + profiles = + flip mapAttrs' cfg.profiles (_: profile: + nameValuePair "Profile${toString profile.id}" { + Name = profile.name; + Path = profile.path; + IsRelative = 1; + Default = if profile.isDefault then 1 else 0; + } + ) // { + General = { + StartWithLastProfile = 1; + }; + }; + + profilesIni = generators.toINI {} profiles; + + mkUserJs = prefs: extraPrefs: '' + // Generated by Home Manager. + + ${concatStrings (mapAttrsToList (name: value: '' + user_pref("${name}", ${builtins.toJSON value}); + '') prefs)} + + ${extraPrefs} + ''; + in { @@ -40,6 +66,84 @@ in ''; }; + profiles = mkOption { + type = types.attrsOf (types.submodule ({config, name, ...}: { + options = { + name = mkOption { + type = types.str; + default = name; + description = "Profile name."; + }; + + id = mkOption { + type = types.ints.unsigned; + default = 0; + description = '' + Profile ID. This should be set to a unique number per profile. + ''; + }; + + settings = mkOption { + type = with types; attrsOf (either bool (either int str)); + default = {}; + example = literalExample '' + { + "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; + } + ''; + description = "Attribute set of Firefox preferences."; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra preferences to add to user.js. + ''; + }; + + userChrome = mkOption { + type = types.lines; + default = ""; + description = "Custom Firefox CSS."; + example = '' + /* Hide tab bar in FF Quantum */ + @-moz-document url("chrome://browser/content/browser.xul") { + #TabsToolbar { + visibility: collapse !important; + margin-bottom: 21px !important; + } + + #sidebar-box[sidebarcommand="treestyletab_piro_sakura_ne_jp-sidebar-action"] #sidebar-header { + visibility: collapse !important; + } + } + ''; + }; + + 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."; + }; + }; + })); + default = {}; + description = "Attribute set of Firefox profiles."; + }; + enableAdobeFlash = mkOption { type = types.bool; default = false; @@ -81,6 +185,39 @@ in }; config = mkIf cfg.enable { + assertions = [ + ( + let + defaults = + catAttrs "name" (filter (a: a.isDefault) (attrValues cfg.profiles)); + in { + assertion = cfg.profiles == {} || length defaults == 1; + message = + "Must have exactly one default Firefox profile but found " + + toString (length defaults) + + optionalString (length defaults > 1) + (", namely " + concatStringsSep ", " defaults); + } + ) + + ( + let + duplicates = + filterAttrs (_: v: length v != 1) + (zipAttrs + (mapAttrsToList (n: v: { "${toString v.id}" = n; }) + (cfg.profiles))); + + mkMsg = n: v: " - ID ${n} is used by ${concatStringsSep ", " v}"; + in { + assertion = duplicates == {}; + message = + "Must not have Firefox profiles with duplicate IDs but\n" + + concatStringsSep "\n" (mapAttrsToList mkMsg duplicates); + } + ) + ]; + home.packages = let # A bit of hackery to force a config into the wrapper. @@ -99,17 +236,35 @@ in in [ (wrapper cfg.package { }) ]; - home.file.".mozilla/${extensionPath}" = mkIf (cfg.extensions != []) ( - let - extensionsEnv = pkgs.buildEnv { - name = "hm-firefox-extensions"; - paths = cfg.extensions; + home.file = mkMerge ( + [{ + ".mozilla/${extensionPath}" = mkIf (cfg.extensions != []) ( + let + extensionsEnv = pkgs.buildEnv { + name = "hm-firefox-extensions"; + paths = cfg.extensions; + }; + in { + source = "${extensionsEnv}/share/mozilla/${extensionPath}"; + recursive = true; + } + ); + + ".mozilla/firefox/profiles.ini" = mkIf (cfg.profiles != {}) { + text = profilesIni; }; - in - { - source = "${extensionsEnv}/share/mozilla/${extensionPath}"; - recursive = true; - } + }] + ++ flip mapAttrsToList cfg.profiles (_: profile: { + ".mozilla/firefox/${profile.path}/chrome/userChrome.css" = + mkIf (profile.userChrome != "") { + text = profile.userChrome; + }; + + ".mozilla/firefox/${profile.path}/user.js" = + mkIf (profile.settings != {} || profile.extraConfig != "") { + text = mkUserJs profile.settings profile.extraConfig; + }; + }) ); }; }