hylonix/modules/home-manager/gecko-browser/gecko-lib/profiles.nix
hylodon 0f0c4c7727 Added a module to configure browser based on the gecko engine, such as Firefox.
This module should provide similar functionality to the firefox module in
home-manager. Some notable differences between the two include:

* home-manager configures a single browser. This means that any configuration
  that cannot be done on a per-profile basis is shared between all
  profiles. This module configures a new copy of the browser for every profile,
  ensuring that *all* configuration can be on a per-profile basis.

  This might be seen as insanity in a regular distro, but in NixOS this is
  trivial to do and requires no extra storage space.

* home-manager modifies files in the user's directory to configure things such
  as extensions and search engines. This module avoids that when possible by
  pushing configuration into policies and preferences at a browser level.
  This is much nicer for impermanence-based systems.
2026-02-22 19:57:42 +00:00

175 lines
4.7 KiB
Nix

{
lib,
pkgs,
sLib,
info,
geckoLib,
...
}@args:
let
# (a -> b -> c) -> [a] -> [b] -> [c]
mapProduct =
f: xs: ys:
builtins.concatMap (y: builtins.concatMap (x: f x y) xs) ys;
# (Lib -> Profile -> c) -> ProfileSettings -> [c]
mapLibAndProfile = f: x: mapProduct f allLibs <| builtins.attrValues <| x;
allLibs = builtins.attrValues <| lib.filterAttrs (k: _: k != "profiles") <| geckoLib;
allLibs' = allLibs ++ [ geckoLib.profiles ];
submodule =
{ config, name, ... }:
{
options = {
id = lib.mkOption {
type = lib.types.ints.unsigned;
default = 0;
description = "Profile ID";
};
name = lib.mkOption {
type = lib.types.str;
default = name;
description = "Profile name";
};
path = lib.mkOption {
type = lib.types.str;
default = name;
description = "Profile path";
};
isDefault = lib.mkOption {
type = lib.types.bool;
default = config.id == 0;
defaultText = "config.id == 0";
description = "Is profile the default?";
};
languagePacks = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"en-GB"
"de"
];
description = "The language packs to install.";
};
}
// builtins.foldl' (x: y: x // y.options) { } allLibs;
};
writeProfilesIni = configPath: profileSettings: {
name = "${configPath}/profiles.ini";
value.text =
lib.generators.toINI { }
<|
lib.mapAttrs' (
_: v:
lib.nameValuePair "Profile${builtins.toString v.id}" {
Name = v.name;
Path = v.path;
IsRelative = 1;
Default = if v.isDefault then 1 else 0;
}
) profileSettings
// {
General = {
StartWithLastProfile = 1;
Version = 2;
};
};
};
in
{
options = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule submodule);
default = { };
description = "Sets browser settings on a per-profile basis.";
};
getDefaultProfileName =
profileSettings:
builtins.head
<| builtins.filter (x: profileSettings.${x}.isDefault)
<| builtins.attrNames
<| profileSettings;
process =
{ languagePacks, ... }:
let
extractVersion =
(x: if x == [ ] then info.base.version else builtins.head x)
<| builtins.match "([^-]*)-.*"
<| info.base.version;
in
{
policies.ExtensionSettings =
builtins.listToAttrs
<| builtins.map (lang: {
name = "langpack-${lang}@firefox.mozilla.org";
value = {
installation_mode = "force_installed";
install_url = "https://releases.mozilla.org/pub/firefox/releases/${extractVersion}/linux-x86_64/xpi/${lang}.xpi";
};
})
<| languagePacks;
};
assertions = profiles: [
{
assertion = profiles != { };
message = "No profiles are defined.";
}
{
assertion =
(x: x == 1)
<| builtins.length
<| builtins.filter (x: profiles.${x}.isDefault)
<| builtins.attrNames
<| profiles;
message = "One, and only one, profile must be marked as 'isDefault'. If you have not set this option on any profile, then one and only one profile must have id '0'.";
}
];
warnings = _: [ ];
evaluateAllProfiles =
profiles:
let
processLevel =
level: userConfig:
sLib.recursiveMergeAll
<| (x: [ userConfig ] ++ x)
<| builtins.map (myLib: (myLib.${level} or (_: { })) userConfig)
<| allLibs';
# Internal libraries can create their own configuration that needs to be merged
# into the user configuration (the user's takes priority). Generate that config.
finalConfig = builtins.mapAttrs (
_: v: processLevel "postprocess" <| processLevel "process" <| processLevel "preprocess" <| v
) profiles;
in
{
assertions =
mapLibAndProfile (x: x.assertions or (_: [ ])) finalConfig
++ geckoLib.profiles.assertions finalConfig;
warnings =
mapLibAndProfile (x: x.warnings or (_: [ ])) finalConfig ++ geckoLib.profiles.warnings finalConfig;
profiles = lib.mapAttrs (_: v: {
policies = v.policies;
prefs = v.prefs;
files = builtins.concatMap (x: (x.files or (_: [ ])) v) allLibs';
extFiles = builtins.concatMap (x: (x.extFiles or (_: [ ])) v) allLibs';
}) finalConfig;
files = [
(writeProfilesIni info.configPath finalConfig)
]
++ mapLibAndProfile (x: x.files or (_: [ ])) finalConfig;
};
}