fontconfig: add RFC42-style config file settings

This commit is contained in:
Benedikt Rips 2026-04-26 18:29:16 +02:00 committed by Austin Horstman
parent 4441446672
commit c8f0b8ed44
3 changed files with 178 additions and 86 deletions

View file

@ -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 `<fontconfig>` tag, i.e. as
`{ fontconfig = <settings>; }`.
''
+ 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:

View file

@ -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.<name>.settings` option to
define Fontconfig configuration files via a structured attrs in the
format of `pkgs.formats.xml {}`.
'';
}

View file

@ -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" ''
<?xml version="1.0" encoding="utf-8"?>
<fontconfig>
<a attr="value">
<b>1</b>
</a>
<a>
<c>string</c>
</a>
</fontconfig>
'';
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}
'';