diff --git a/modules/programs/firefoxpwa.nix b/modules/programs/firefoxpwa.nix new file mode 100644 index 00000000..ed9d1bae --- /dev/null +++ b/modules/programs/firefoxpwa.nix @@ -0,0 +1,254 @@ +{ + pkgs, + config, + lib, + ... +}: +let + cfg = config.programs.firefoxpwa; + + jsonFmt = pkgs.formats.json { }; + + inherit (pkgs.stdenv.hostPlatform) isLinux; + + sites = lib.concatMapAttrs (_: profile: profile.sites) cfg.profiles; + + mkUlidAssertions = + path: + lib.concatMap ( + { name, value }: + let + length = 26; + allowed = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + disallowed = lib.subtractLists (lib.stringToCharacters allowed) (lib.stringToCharacters name); + in + [ + { + assertion = builtins.stringLength name == length; + message = '' + ULID '${name}' at 'programs.firefoxpwa.${lib.showOption path}' must be 26 characters, but is + ${toString (builtins.stringLength name)} characters long. + ''; + } + { + assertion = disallowed == [ ]; + message = '' + ULID '${name}' at 'programs.firefoxpwa.profiles' must only contain characters + '${allowed}', but contains '${lib.concatStrings disallowed}'. + ''; + } + ] + ) (lib.attrsToList (lib.attrByPath path null cfg)); +in +{ + meta.maintainers = [ lib.maintainers.bricked ]; + + options.programs.firefoxpwa = { + enable = lib.mkEnableOption "Progressive Web Apps for Firefox"; + + package = lib.mkPackageOption pkgs "firefoxpwa" { nullable = true; }; + + settings = lib.mkOption { + type = jsonFmt.type; + default = { }; + description = '' + Settings to be written to the configuration file. See + + for a list of available options. + ''; + }; + + profiles = lib.mkOption { + default = { }; + description = '' + Attribute set of profile options. The keys of that attribute set consist of + ULIDs. A ULID is made of 16 characters, each of which is one of + '0123456789ABCDEFGHJKMNPQRSTVWXYZ' (Excluding I, L, O and U). See + . + ''; + example = lib.literalExpression '' + { + "0123456789ABCDEFGHJKMNPQRSTVWXYZ".sites."ZYXWVTSRQPNMKJHGFEDCBA9876543210" = { + name = "MDN Web Docs"; + url = "https://developer.mozilla.org/"; + manifestUrl = "https://developer.mozilla.org/manifest.f42880861b394dd4dc9b.json"; + desktopEntry.icon = pkgs.fetchurl { + url = "https://developer.mozilla.org/favicon-192x192.png"; + sha256 = "0p8zgf2ba48l2pq1gjcffwzmd9kfmj9qc0v7zpwf2qd54fndifxr"; + }; + }; + } + ''; + + type = lib.types.attrsOf ( + lib.types.submodule ( + profile@{ config, name, ... }: + + { + options = { + name = lib.mkOption { + type = lib.types.str; + default = name; + description = "Name of the profile."; + }; + sites = lib.mkOption { + default = { }; + description = '' + Attribute set of site options for this profile. The keys of that attribute set + consist of ULIDs. A ULID is made of 16 characters, each of which is one of + '0123456789ABCDEFGHJKMNPQRSTVWXYZ' (Excluding I, L, O and U). See + . + Site ULIDs must be unique across profiles. + ''; + + type = lib.types.attrsOf ( + lib.types.submodule ( + { config, name, ... }: + + { + options = { + name = lib.mkOption { + type = lib.types.str; + default = name; + description = "Name of the site."; + example = "MDN Web Docs"; + }; + url = lib.mkOption { + type = lib.types.str; + description = "Start URL of the site."; + example = "https://developer.mozilla.org/"; + }; + manifestUrl = lib.mkOption { + type = lib.types.str; + description = "URL of the site's web app manifest."; + example = "https://developer.mozilla.org/manifest.f42880861b394dd4dc9b.json"; + }; + desktopEntry = { + enable = lib.mkOption { + type = lib.types.bool; + defaultText = "true if host platform is Linux"; + description = "Whether to enable the desktop entry for this site."; + }; + icon = lib.mkOption { + type = with lib.types; nullOr (either str path); + default = null; + description = "Icon to display in file manager, menus, etc."; + example = lib.literalExpression '' + pkgs.fetchurl { + url = "https://developer.mozilla.org/favicon-192x192.png"; + sha256 = "0p8zgf2ba48l2pq1gjcffwzmd9kfmj9qc0v7zpwf2qd54fndifxr"; + } + ''; + }; + categories = lib.mkOption { + type = with lib.types; nullOr (listOf str); + default = null; + description = "Categories in which the entry should be shown in a menu."; + }; + }; + settings = lib.mkOption { + type = jsonFmt.type; + default = { }; + description = '' + Settings for this site. See + + for a list of available options. + ''; + example = { + config.manifest_url = "https://developer.mozilla.org/manifest.f42880861b394dd4dc9b.json"; + }; + }; + }; + + config = { + desktopEntry.enable = lib.mkDefault isLinux; + settings = { + ulid = name; + profile = profile.name; + config = { + name = config.name; + document_url = config.url; + manifest_url = config.manifestUrl; + }; + manifest = { + name = config.name; + start_url = config.url; + }; + }; + }; + } + ) + ); + }; + settings = lib.mkOption { + type = jsonFmt.type; + default = { }; + description = '' + Settings for this profile. See + + for a list of available options. + ''; + }; + }; + + config.settings = { + ulid = name; + name = config.name; + sites = builtins.attrNames config.sites; + }; + } + ) + ); + }; + }; + + config = lib.mkIf cfg.enable { + assertions = + mkUlidAssertions [ "profiles" ] + ++ lib.concatMap ( + name: + mkUlidAssertions [ + "profiles" + name + "sites" + ] + ) (builtins.attrNames cfg.profiles) + ++ map ( + name: + let + profiles = builtins.attrNames (lib.filterAttrs (_: profile: profile.sites ? ${name}) cfg.profiles); + in + { + assertion = builtins.length profiles == 1; + message = '' + Site with ULID '${name}' must be present in exactly one profile, but is present + in ${toString (builtins.length profiles)} profiles, namely ${ + lib.concatMapStringsSep ", " (x: "'${x}'") profiles + }. + ''; + } + ) (builtins.attrNames sites); + + home.packages = lib.mkIf (cfg.package != null) [ cfg.package ]; + + xdg.dataFile."firefoxpwa/config.json" = lib.mkIf (cfg.settings != { }) { + text = lib.generators.toJSON { } cfg.settings; + }; + + xdg.desktopEntries = lib.mkMerge ( + lib.mapAttrsToList (name: site: { + "FFPWA-${name}" = lib.mkIf site.desktopEntry.enable { + inherit (site.desktopEntry) icon categories; + name = site.settings.manifest.name; + exec = "firefoxpwa site launch ${name} --protocol %u"; + terminal = false; + }; + }) sites + ); + + programs.firefoxpwa.settings = { + profiles = builtins.mapAttrs (_: profile: profile.settings) cfg.profiles; + sites = builtins.mapAttrs (_: site: site.settings) sites; + }; + }; +} diff --git a/tests/modules/programs/firefoxpwa/assertions.nix b/tests/modules/programs/firefoxpwa/assertions.nix new file mode 100644 index 00000000..bdcbace9 --- /dev/null +++ b/tests/modules/programs/firefoxpwa/assertions.nix @@ -0,0 +1,33 @@ +{ + programs.firefoxpwa = { + enable = true; + package = null; + profiles = { + "INVALID" = { }; + "0123456789ABCDEFGHJKMNPQRS".sites."00000000000000000000000000" = { + name = "MDN Web Docs"; + url = "https://developer.mozilla.org/"; + manifestUrl = "https://developer.mozilla.org/manifest.f42880861b394dd4dc9b.json"; + }; + "ZYXWVTSRQPNMKJHGFEDCBA9876".sites."00000000000000000000000000" = { + name = "MDN Web Docs"; + url = "https://developer.mozilla.org/"; + manifestUrl = "https://developer.mozilla.org/manifest.f42880861b394dd4dc9b.json"; + }; + }; + }; + test.asserts.assertions.expected = [ + '' + ULID 'INVALID' at 'programs.firefoxpwa.profiles' must be 26 characters, but is + 7 characters long. + '' + '' + ULID 'INVALID' at 'programs.firefoxpwa.profiles' must only contain characters + '0123456789ABCDEFGHJKMNPQRSTVWXYZ', but contains 'ILI'. + '' + '' + Site with ULID '00000000000000000000000000' must be present in exactly one profile, but is present + in 2 profiles, namely '0123456789ABCDEFGHJKMNPQRS', 'ZYXWVTSRQPNMKJHGFEDCBA9876'. + '' + ]; +} diff --git a/tests/modules/programs/firefoxpwa/config-expected.json b/tests/modules/programs/firefoxpwa/config-expected.json new file mode 100644 index 00000000..36bd8405 --- /dev/null +++ b/tests/modules/programs/firefoxpwa/config-expected.json @@ -0,0 +1 @@ +{"profiles":{"0123456789ABCDEFGHJKMNPQRS":{"name":"0123456789ABCDEFGHJKMNPQRS","sites":["ZYXWVTSRQPNMKJHGFEDCBA9876"],"ulid":"0123456789ABCDEFGHJKMNPQRS"}},"sites":{"ZYXWVTSRQPNMKJHGFEDCBA9876":{"config":{"document_url":"https://developer.mozilla.org/","manifest_url":"https://developer.mozilla.org/manifest.f42880861b394dd4dc9b.json","name":"MDN Web Docs"},"manifest":{"name":"MDN Web Docs","start_url":"https://developer.mozilla.org/"},"profile":"0123456789ABCDEFGHJKMNPQRS","ulid":"ZYXWVTSRQPNMKJHGFEDCBA9876"}}} \ No newline at end of file diff --git a/tests/modules/programs/firefoxpwa/config.nix b/tests/modules/programs/firefoxpwa/config.nix new file mode 100644 index 00000000..e88217f1 --- /dev/null +++ b/tests/modules/programs/firefoxpwa/config.nix @@ -0,0 +1,17 @@ +{ + programs.firefoxpwa = { + enable = true; + package = null; + profiles."0123456789ABCDEFGHJKMNPQRS".sites."ZYXWVTSRQPNMKJHGFEDCBA9876" = { + name = "MDN Web Docs"; + url = "https://developer.mozilla.org/"; + manifestUrl = "https://developer.mozilla.org/manifest.f42880861b394dd4dc9b.json"; + }; + }; + + nmt.script = '' + configFile=home-files/.local/share/firefoxpwa/config.json + assertFileExists $configFile + assertFileContent $configFile ${./config-expected.json} + ''; +} diff --git a/tests/modules/programs/firefoxpwa/default.nix b/tests/modules/programs/firefoxpwa/default.nix new file mode 100644 index 00000000..a15c05ae --- /dev/null +++ b/tests/modules/programs/firefoxpwa/default.nix @@ -0,0 +1,4 @@ +{ + firefoxpwa-config = ./config.nix; + firefoxpwa-assertions = ./assertions.nix; +}