From c8f0b8ed441c95bfeaf33555e7631044bb09f384 Mon Sep 17 00:00:00 2001 From: Benedikt Rips Date: Sun, 26 Apr 2026 18:29:16 +0200 Subject: [PATCH] fontconfig: add RFC42-style config file settings --- modules/misc/fontconfig.nix | 194 ++++++++++-------- .../misc/news/2026/04/2026-04-26_14-42-03.nix | 18 ++ .../misc/fontconfig/extra-config-files.nix | 52 +++++ 3 files changed, 178 insertions(+), 86 deletions(-) create mode 100644 modules/misc/news/2026/04/2026-04-26_14-42-03.nix diff --git a/modules/misc/fontconfig.nix b/modules/misc/fontconfig.nix index 6ff10cfab..e77c4a936 100644 --- a/modules/misc/fontconfig.nix +++ b/modules/misc/fontconfig.nix @@ -14,13 +14,15 @@ let cfg = config.fonts.fontconfig; + xml = pkgs.formats.xml { }; + globalConfig = config; fontConfigFileType = lib.types.submodule ( let noteContentOptionsExclusivity = '' - Note that {option}`source` will be derived from {option}`text` if it is set; with override - priorities being preserved. As a consequence, exactly one of {option}`text`, and - {option}`source` must be set with highest priority. + Note that {option}`source` will be derived from {option}`settings` and {option}`text` if they are + set; with override priorities being preserved. As a consequence, exactly + one of {option}`settings`, {option}`text`, and {option}`source` must be set with highest priority. ''; in { @@ -80,6 +82,27 @@ let defaultText = ''"''${toString config.priority}-hm-''${config.label}.conf"''; type = lib.types.nonEmptyStr; }; + settings = lib.mkOption { + inherit (xml) type; + default = { }; + example = { + description = "Enable antialiasing"; + match = { + "@target" = "font"; + edit = { + "@mode" = "assign"; + "@name" = "antialias"; + bool = "true"; + }; + }; + }; + description = '' + Structured attrs of settings in the format of `pkgs.format.xml {}`. The + settings are implicitly wrapped inside the `` tag, i.e. as + `{ fontconfig = ; }`. + '' + + noteContentOptionsExclusivity; + }; text = lib.mkOption { description = '' Verbatim contents of the config file. @@ -119,9 +142,13 @@ let config.source = let fileName = lib.hm.strings.storeFileName "hm-fontconfig-${config.label}.xml"; + mkSettingsFile = settings: xml.generate fileName { fontconfig = settings; }; mkTextFile = pkgs.writeText fileName; in - lib.mkIf (config.text != null) (lib.mkDerivedConfig options.text mkTextFile); + lib.mkMerge [ + (lib.mkIf (config.settings != { }) (lib.mkDerivedConfig options.settings mkSettingsFile)) + (lib.mkIf (config.text != null) (lib.mkDerivedConfig options.text mkTextFile)) + ]; } ); @@ -322,93 +349,88 @@ in fi ''; - fonts.fontconfig.configFile = - let - xml = pkgs.formats.xml { }; - mkFontconfigConf = name: conf: xml.generate name { fontconfig = conf; }; - in - { - fonts = - let - mkInclude = path: { - "@ignore_missing" = "yes"; - "#text" = path; + fonts.fontconfig.configFile = { + fonts = + let + mkInclude = path: { + "@ignore_missing" = "yes"; + "#text" = path; + }; + in + { + enable = true; + priority = 10; + settings = { + description = "Add fonts in the Nix user profile"; + include = map mkInclude [ + "${config.home.path}/etc/fonts/conf.d" + "${config.home.path}/etc/fonts/fonts.conf" + ]; + dir = [ + "${config.home.path}/lib/X11/fonts" + "${config.home.path}/share/fonts" + "${config.home.profileDirectory}/lib/X11/fonts" + "${config.home.profileDirectory}/share/fonts" + ]; + cachedir = "${config.home.path}/lib/fontconfig/cache"; + }; + }; + rendering = + let + toXmlValue = + value: + if lib.isBool value then + { bool = lib.boolToString value; } + else if lib.isString value then + { const = value; } + else + throw "expected bool or string but got ${lib.typeOf value}: ${toString value}"; + assign = + name: value: + toXmlValue value + // { + "@mode" = "assign"; + "@name" = name; }; - in - { - enable = true; - priority = 10; - source = mkFontconfigConf "fonts" { - description = "Add fonts in the Nix user profile"; - include = map mkInclude [ - "${config.home.path}/etc/fonts/conf.d" - "${config.home.path}/etc/fonts/fonts.conf" - ]; - dir = [ - "${config.home.path}/lib/X11/fonts" - "${config.home.path}/share/fonts" - "${config.home.profileDirectory}/lib/X11/fonts" - "${config.home.profileDirectory}/share/fonts" - ]; - cachedir = "${config.home.path}/lib/fontconfig/cache"; + content = + lib.optional (cfg.antialiasing != null) (assign "antialias" cfg.antialiasing) + ++ lib.optionals (cfg.hinting != null) [ + (assign "hinting" true) + (assign "hintstyle" ("hint" + cfg.hinting)) + ] + ++ lib.optional (cfg.subpixelRendering != null) ( + assign "rgba" (lib.replaceStrings [ "ertical-" ] [ "" ] cfg.subpixelRendering) + ); + in + { + enable = lib.length content > 0; + priority = 10; + settings = { + description = "Set the rendering mode"; + match = { + "@target" = "font"; + edit = content; }; }; - rendering = - let - toXmlValue = - value: - if lib.isBool value then - { bool = lib.boolToString value; } - else if lib.isString value then - { const = value; } - else - throw "expected bool or string but got ${lib.typeOf value}: ${toString value}"; - assign = - name: value: - toXmlValue value - // { - "@mode" = "assign"; - "@name" = name; - }; - content = - lib.optional (cfg.antialiasing != null) (assign "antialias" cfg.antialiasing) - ++ lib.optionals (cfg.hinting != null) [ - (assign "hinting" true) - (assign "hintstyle" ("hint" + cfg.hinting)) - ] - ++ lib.optional (cfg.subpixelRendering != null) ( - assign "rgba" (lib.replaceStrings [ "ertical-" ] [ "" ] cfg.subpixelRendering) - ); - in - { - enable = lib.length content > 0; - priority = 10; - source = mkFontconfigConf "rendering" { - description = "Set the rendering mode"; - match = { - "@target" = "font"; - edit = content; - }; - }; + }; + default-fonts = + let + filterNonEmpty = lib.filterAttrs (_: fonts: fonts != [ ]); + mkAlias = name: fonts: { + "@binding" = "same"; + family = if name == "sansSerif" then "sans-serif" else name; + prefer.family = fonts; }; - default-fonts = - let - filterNonEmpty = lib.filterAttrs (_: fonts: fonts != [ ]); - mkAlias = name: fonts: { - "@binding" = "same"; - family = if name == "sansSerif" then "sans-serif" else name; - prefer.family = fonts; - }; - in - { - enable = true; - priority = 52; - source = mkFontconfigConf "default-fonts" { - description = "Set default fonts"; - alias = lib.mapAttrsToList mkAlias (filterNonEmpty cfg.defaultFonts); - }; + in + { + enable = true; + priority = 52; + settings = { + description = "Set default fonts"; + alias = lib.mapAttrsToList mkAlias (filterNonEmpty cfg.defaultFonts); }; - }; + }; + }; xdg.configFile = lib.mapAttrs' ( _name: config: diff --git a/modules/misc/news/2026/04/2026-04-26_14-42-03.nix b/modules/misc/news/2026/04/2026-04-26_14-42-03.nix new file mode 100644 index 000000000..8831f3531 --- /dev/null +++ b/modules/misc/news/2026/04/2026-04-26_14-42-03.nix @@ -0,0 +1,18 @@ +{ config, lib, ... }: +{ + time = "2026-04-26T12:42:03+00:00"; + condition = + let + extraConfigFiles = lib.removeAttrs config.fonts.fontconfig.configFile [ + "fonts" + "rendering" + "default-fonts" + ]; + in + config.fonts.fontconfig.enable && extraConfigFiles != { }; + message = '' + There is a new `fonts.fontconfig.configFile..settings` option to + define Fontconfig configuration files via a structured attrs in the + format of `pkgs.formats.xml {}`. + ''; +} diff --git a/tests/modules/misc/fontconfig/extra-config-files.nix b/tests/modules/misc/fontconfig/extra-config-files.nix index 8c63598e5..9a4020694 100644 --- a/tests/modules/misc/fontconfig/extra-config-files.nix +++ b/tests/modules/misc/fontconfig/extra-config-files.nix @@ -2,6 +2,26 @@ let fcConfD = "home-files/.config/fontconfig/conf.d"; + sampleSettings = { + a = [ + { + "@attr" = "value"; + b = 1; + } + { c = "string"; } + ]; + }; + sampleSettingsFile = builtins.toFile "sample-settings-config" '' + + + + 1 + + + string + + + ''; sampleText = "hello world"; sampleTextFile = builtins.toFile "sample-text-config" sampleText; sampleSource = builtins.toFile "fontconfig-source" '' @@ -34,14 +54,31 @@ in target = "target"; text = ""; }; + settings.settings = sampleSettings; text.text = sampleText; source.source = sampleSource; # Check that priorities are propagated + settings-override-text = { + settings = lib.mkForce sampleSettings; + text = sampleText; + }; + settings-override-source = { + settings = lib.mkForce sampleSettings; + source = sampleSource; + }; text-overrides-source = { text = lib.mkForce sampleText; source = sampleSource; }; + text-overrides-settings = { + settings = sampleSettings; + text = lib.mkForce sampleText; + }; + source-overrides-settings = { + settings = sampleSettings; + source = lib.mkForce sampleSource; + }; source-overrides-text = { text = sampleText; source = lib.mkForce sampleSource; @@ -60,15 +97,30 @@ in assertFileExists ${fcConfD}/target + assertFileExists ${fcConfD}/90-hm-settings.conf + assertFileContent ${fcConfD}/90-hm-settings.conf ${sampleSettingsFile} + assertFileExists ${fcConfD}/90-hm-text.conf assertFileContent ${fcConfD}/90-hm-text.conf ${sampleTextFile} assertFileExists ${fcConfD}/90-hm-source.conf assertFileContent ${fcConfD}/90-hm-source.conf ${sampleSource} + assertFileExists ${fcConfD}/90-hm-settings-override-text.conf + assertFileContent ${fcConfD}/90-hm-settings-override-text.conf ${sampleSettingsFile} + + assertFileExists ${fcConfD}/90-hm-settings-override-source.conf + assertFileContent ${fcConfD}/90-hm-settings-override-source.conf ${sampleSettingsFile} + assertFileExists ${fcConfD}/90-hm-text-overrides-source.conf assertFileContent ${fcConfD}/90-hm-text-overrides-source.conf ${sampleTextFile} + assertFileExists ${fcConfD}/90-hm-text-overrides-settings.conf + assertFileContent ${fcConfD}/90-hm-text-overrides-settings.conf ${sampleTextFile} + + assertFileExists ${fcConfD}/90-hm-source-overrides-settings.conf + assertFileContent ${fcConfD}/90-hm-source-overrides-settings.conf ${sampleSource} + assertFileExists ${fcConfD}/90-hm-source-overrides-text.conf assertFileContent ${fcConfD}/90-hm-source-overrides-text.conf ${sampleSource} '';