From a913ae61bf3b9f4312f6097b68cdf0a0fa699279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Kugland?= Date: Fri, 13 Feb 2026 18:40:30 -0300 Subject: [PATCH] firefox: add handlers.json configuration Adds support for configuring Firefox's handlers.json file to manage MIME type and URL scheme handlers declaratively (at `programs.firefox.profiles..handlers`). Handlers control how Firefox opens files and protocols (e.g., PDF viewers, `mailto:` handlers). --- .../misc/news/2025/12/2025-12-10_04-15-59.nix | 18 ++ modules/programs/firefox/mkFirefoxModule.nix | 24 ++ .../programs/firefox/profiles/handlers.nix | 209 ++++++++++++++++++ tests/modules/programs/firefox/common.nix | 1 + .../firefox/profiles/handlers/default.nix | 69 ++++++ .../profiles/handlers/expected-handlers.json | 43 ++++ 6 files changed, 364 insertions(+) create mode 100644 modules/misc/news/2025/12/2025-12-10_04-15-59.nix create mode 100644 modules/programs/firefox/profiles/handlers.nix create mode 100644 tests/modules/programs/firefox/profiles/handlers/default.nix create mode 100644 tests/modules/programs/firefox/profiles/handlers/expected-handlers.json diff --git a/modules/misc/news/2025/12/2025-12-10_04-15-59.nix b/modules/misc/news/2025/12/2025-12-10_04-15-59.nix new file mode 100644 index 00000000..256bc824 --- /dev/null +++ b/modules/misc/news/2025/12/2025-12-10_04-15-59.nix @@ -0,0 +1,18 @@ +{ config, ... }: +{ + time = "2025-12-10T07:15:59+00:00"; + condition = config.programs.firefox.enable; + message = '' + The Firefox module now provides a + 'programs.firefox.profiles..handlers' option. + + It allows declarative configuration of MIME type and URL scheme handlers + through Firefox's handlers.json file, controlling how Firefox opens files + and protocols (e.g., PDF viewers, mailto handlers). + + Configure handlers with: + + programs.firefox.profiles..handlers.mimeTypes + programs.firefox.profiles..handlers.schemes + ''; +} diff --git a/modules/programs/firefox/mkFirefoxModule.nix b/modules/programs/firefox/mkFirefoxModule.nix index 13e61c89..d5291e72 100644 --- a/modules/programs/firefox/mkFirefoxModule.nix +++ b/modules/programs/firefox/mkFirefoxModule.nix @@ -548,6 +548,25 @@ in description = "Declarative search engine configuration."; }; + handlers = mkOption { + type = types.submodule ( + args: + import ./profiles/handlers.nix { + inherit (args) config; + inherit lib pkgs appName; + package = cfg.finalPackage; + modulePath = modulePath ++ [ + "profiles" + name + "handlers" + ]; + profilePath = config.path; + } + ); + default = { }; + description = "Declarative handlers configuration for MIME types and URL schemes."; + }; + containersForce = mkOption { type = types.bool; default = false; @@ -1048,6 +1067,11 @@ in source = profile.search.file; }; + "${cfg.profilesPath}/${profile.path}/handlers.json" = mkIf (profile.handlers.enable) { + source = profile.handlers.configFile; + force = profile.handlers.force; + }; + "${cfg.profilesPath}/${profile.path}/extensions" = mkIf (profile.extensions.packages != [ ]) { source = let diff --git a/modules/programs/firefox/profiles/handlers.nix b/modules/programs/firefox/profiles/handlers.nix new file mode 100644 index 00000000..9bd13985 --- /dev/null +++ b/modules/programs/firefox/profiles/handlers.nix @@ -0,0 +1,209 @@ +{ + config, + lib, + pkgs, + appName, + ... +}: +let + jsonFormat = pkgs.formats.json { }; + + # Process configuration, remove null values and empty handlers arrays. + genCfg = + cfg: + lib.mapAttrs ( + _: item: + (removeAttrs item [ "handlers" ]) + // (lib.optionalAttrs (item.handlers != [ ]) { + handlers = map (handler: lib.filterAttrsRecursive (_: v: v != null) handler) item.handlers; + }) + ) cfg; + + # Common options shared between mimeTypes and schemes + commonHandlerOptions = { + action = lib.mkOption { + type = lib.types.enum [ + 0 + 1 + 2 + 3 + 4 + ]; + default = 1; + description = '' + The action to take for this MIME type / URL scheme. Possible values: + - 0: Save file + - 1: Always ask + - 2: Use helper app + - 3: Open in ${appName} + - 4: Use system default + ''; + }; + + ask = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + If true, the user is asked what they want to do with the file. + If false, the action is taken without user intervention. + ''; + }; + + handlers = lib.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Display name of the handler. + ''; + }; + + path = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Path to the executable to be used. + + Only one of 'path' or 'uriTemplate' should be set. + ''; + }; + + uriTemplate = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + URI for the application handler. + + Only one of 'path' or 'uriTemplate' should be set. + ''; + }; + }; + } + ); + default = [ ]; + description = '' + An array of handlers with the first one being the default. + If you don't want to have a default handler, use an empty object for the first handler. + Only valid when action is set to 2 (Use helper app). + ''; + }; + }; +in +{ + imports = [ (pkgs.path + "/nixos/modules/misc/meta.nix") ]; + + meta.maintainers = with lib.maintainers; [ kugland ]; + + options = { + enable = lib.mkOption { + type = lib.types.bool; + default = config.schemes != { } || config.mimeTypes != { }; + internal = true; + }; + + force = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to force replace the existing handlers configuration. + ''; + }; + + mimeTypes = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + options = commonHandlerOptions // { + extensions = lib.mkOption { + type = lib.types.listOf (lib.types.strMatching "^[^\\.].+$"); + default = [ ]; + example = [ + "jpg" + "jpeg" + ]; + description = '' + List of file extensions associated with this MIME type. + ''; + }; + }; + } + ); + default = { }; + example = lib.literalExpression '' + { + "application/pdf" = { + action = 2; + ask = false; + handlers = [ + { + name = "Okular"; + path = "''${pkgs.okular}/bin/okular"; + } + ]; + extensions = [ "pdf" ]; + }; + } + ''; + description = '' + Attribute set mapping MIME types to their handler configurations. + + For a configuration example, see [this file on Firefox’s source code](https://github.com/mozilla-firefox/firefox/blob/c3797cdebac1316dd7168e995e3468c5a597e8d1/uriloader/exthandler/tests/unit/handlers.json). + ''; + }; + + schemes = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + options = commonHandlerOptions; + } + ); + default = { }; + example = lib.literalExpression '' + { + mailto = { + action = 2; + ask = false; + handlers = [ + { + name = "Gmail"; + uriTemplate = "https://mail.google.com/mail/?extsrc=mailto&url=%s"; + } + ]; + }; + } + ''; + description = '' + Attribute set mapping URL schemes to their handler configurations. + + For a configuration example, see [this file on Firefox’s source code](https://github.com/mozilla-firefox/firefox/blob/c3797cdebac1316dd7168e995e3468c5a597e8d1/uriloader/exthandler/tests/unit/handlers.json). + ''; + }; + + finalSettings = lib.mkOption { + type = jsonFormat.type; + internal = true; + readOnly = true; + default = { + defaultHandlersVersion = { }; + isDownloadsImprovementsAlreadyMigrated = false; + mimeTypes = genCfg config.mimeTypes; + schemes = genCfg config.schemes; + }; + description = '' + Resulting handlers.json settings. + ''; + }; + + configFile = lib.mkOption { + type = lib.types.path; + internal = true; + readOnly = true; + default = jsonFormat.generate "handlers.json" config.finalSettings; + description = '' + JSON representation of the handlers configuration. + ''; + }; + }; +} diff --git a/tests/modules/programs/firefox/common.nix b/tests/modules/programs/firefox/common.nix index 14218705..fbd3637d 100644 --- a/tests/modules/programs/firefox/common.nix +++ b/tests/modules/programs/firefox/common.nix @@ -22,6 +22,7 @@ builtins.mapAttrs "${name}-profiles-extensions-assertions" = ./profiles/extensions/assertions.nix; "${name}-profiles-extensions-exhaustive" = ./profiles/extensions/exhaustive.nix; "${name}-profiles-extensions-exact" = ./profiles/extensions/exact.nix; + "${name}-profiles-handlers" = ./profiles/handlers; "${name}-profiles-overwrite" = ./profiles/overwrite; "${name}-profiles-search" = ./profiles/search; "${name}-profiles-settings" = ./profiles/settings; diff --git a/tests/modules/programs/firefox/profiles/handlers/default.nix b/tests/modules/programs/firefox/profiles/handlers/default.nix new file mode 100644 index 00000000..9ce5013f --- /dev/null +++ b/tests/modules/programs/firefox/profiles/handlers/default.nix @@ -0,0 +1,69 @@ +modulePath: +{ + config, + lib, + pkgs, + ... +}: +let + cfg = lib.getAttrFromPath modulePath config; + firefoxMockOverlay = import ../../setup-firefox-mock-overlay.nix modulePath; +in +{ + imports = [ firefoxMockOverlay ]; + + config = lib.mkIf config.test.enableBig ( + lib.setAttrByPath modulePath { + enable = true; + profiles.handlers = { + id = 0; + handlers = { + mimeTypes = { + "application/pdf" = { + action = 2; + ask = false; + handlers = [ + { + name = "Hello App"; + path = "${pkgs.hello}/bin/hello"; + } + ]; + extensions = [ "pdf" ]; + }; + "text/html" = { + action = 4; + ask = true; + extensions = [ + "html" + "htm" + ]; + }; + }; + schemes = { + mailto = { + action = 2; + ask = false; + handlers = [ + { + name = "Gmail"; + uriTemplate = "https://mail.google.com/mail/?extsrc=mailto&url=%s"; + } + ]; + }; + http = { + action = 3; + ask = true; + }; + }; + }; + }; + } + // { + nmt.script = '' + assertFileContent \ + home-files/${cfg.configPath}/handlers/handlers.json \ + ${./expected-handlers.json} + ''; + } + ); +} diff --git a/tests/modules/programs/firefox/profiles/handlers/expected-handlers.json b/tests/modules/programs/firefox/profiles/handlers/expected-handlers.json new file mode 100644 index 00000000..17c5303d --- /dev/null +++ b/tests/modules/programs/firefox/profiles/handlers/expected-handlers.json @@ -0,0 +1,43 @@ +{ + "defaultHandlersVersion": {}, + "isDownloadsImprovementsAlreadyMigrated": false, + "mimeTypes": { + "application/pdf": { + "action": 2, + "ask": false, + "extensions": [ + "pdf" + ], + "handlers": [ + { + "name": "Hello App", + "path": "@hello@/bin/hello" + } + ] + }, + "text/html": { + "action": 4, + "ask": true, + "extensions": [ + "html", + "htm" + ] + } + }, + "schemes": { + "http": { + "action": 3, + "ask": true + }, + "mailto": { + "action": 2, + "ask": false, + "handlers": [ + { + "name": "Gmail", + "uriTemplate": "https://mail.google.com/mail/?extsrc=mailto&url=%s" + } + ] + } + } +}