From fb52d5df8669aa7ca64f3cb83c42c118cd244603 Mon Sep 17 00:00:00 2001 From: linsui Date: Tue, 15 Aug 2023 17:58:02 +0800 Subject: [PATCH] nixos/dconf: add settings support --- lib/generators.nix | 8 ++ nixos/modules/programs/dconf.nix | 155 ++++++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 5 deletions(-) diff --git a/lib/generators.nix b/lib/generators.nix index c37be1942d82..0368577d5a51 100644 --- a/lib/generators.nix +++ b/lib/generators.nix @@ -230,6 +230,14 @@ rec { in toINI_ (gitFlattenAttrs attrs); + # mkKeyValueDefault wrapper that handles dconf INI quirks. + # The main differences of the format is that it requires strings to be quoted. + mkDconfKeyValue = mkKeyValueDefault { mkValueString = v: toString (lib.gvariant.mkValue v); } "="; + + # Generates INI in dconf keyfile style. See https://help.gnome.org/admin/system-admin-guide/stable/dconf-keyfiles.html.en + # for details. + toDconfINI = toINI { mkKeyValue = mkDconfKeyValue; }; + /* Generates JSON from an arbitrary (non-function) value. * For more information see the documentation of the builtin. */ diff --git a/nixos/modules/programs/dconf.nix b/nixos/modules/programs/dconf.nix index b5bfb79be200..567d42a4a41b 100644 --- a/nixos/modules/programs/dconf.nix +++ b/nixos/modules/programs/dconf.nix @@ -3,12 +3,138 @@ let cfg = config.programs.dconf; + # Compile keyfiles to dconf DB + compileDconfDb = dir: pkgs.runCommand "dconf-db" + { + nativeBuildInputs = [ (lib.getBin pkgs.dconf) ]; + } "dconf compile $out ${dir}"; + + # Check if dconf keyfiles are valid + checkDconfKeyfiles = dir: pkgs.runCommand "check-dconf-keyfiles" + { + nativeBuildInputs = [ (lib.getBin pkgs.dconf) ]; + } '' + if [[ -f ${dir} ]]; then + echo "dconf keyfiles should be a directory but a file is provided: ${dir}" + exit 1 + fi + + dconf compile db ${dir} || ( + echo "The dconf keyfiles are invalid: ${dir}" + exit 1 + ) + cp -R ${dir} $out + ''; + + # Generate dconf DB from dconfDatabase and keyfiles + mkDconfDb = val: compileDconfDb (pkgs.symlinkJoin { + name = "nixos-generated-dconf-keyfiles"; + paths = [ + (pkgs.writeTextDir "nixos-generated-dconf-keyfiles" (lib.generators.toDconfINI val.settings)) + ] ++ (map checkDconfKeyfiles val.keyfiles); + }); + + # Check if a dconf DB file is valid. The dconf cli doesn't return 1 when it can't + # open the database file so we have to check if the output is empty. + checkDconfDb = file: pkgs.runCommand "check-dconf-db" + { + nativeBuildInputs = [ (lib.getBin pkgs.dconf) ]; + } '' + if [[ -d ${file} ]]; then + echo "dconf DB should be a file but a directory is provided: ${file}" + exit 1 + fi + + echo "file-db:${file}" > profile + DCONF_PROFILE=$(pwd)/profile dconf dump / > output 2> error + if [[ ! -s output ]] && [[ -s error ]]; then + cat error + echo "The dconf DB file is invalid: ${file}" + exit 1 + fi + + cp ${file} $out + ''; + # Generate dconf profile mkDconfProfile = name: value: - pkgs.runCommand "dconf-profile" { } '' - mkdir -p $out/etc/dconf/profile/ - cp ${value} $out/etc/dconf/profile/${name} - ''; + if lib.isDerivation value || lib.isPath value then + pkgs.runCommand "dconf-profile" { } '' + if [[ -d ${value} ]]; then + echo "Dconf profile should be a file but a directory is provided." + exit 1 + fi + mkdir -p $out/etc/dconf/profile/ + cp ${value} $out/etc/dconf/profile/${name} + '' + else + pkgs.writeTextDir "etc/dconf/profile/${name}" ( + lib.concatMapStrings (x: "${x}\n") (( + lib.optional value.enableUserDb "user-db:user" + ) ++ ( + map + (value: + let + db = if lib.isAttrs value && !lib.isDerivation value then mkDconfDb value else checkDconfDb value; + in + "file-db:${db}") + value.databases + )) + ); + + dconfDatabase = with lib.types; submodule { + options = { + keyfiles = lib.mkOption { + type = listOf (oneOf [ path package ]); + default = [ ]; + description = lib.mdDoc "A list of dconf keyfile directories."; + }; + settings = lib.mkOption { + type = attrs; + default = { }; + description = lib.mdDoc "An attrset used to generate dconf keyfile."; + example = literalExpression '' + with lib.gvariant; + { + "com/raggesilver/BlackBox" = { + scrollback-lines = mkUint32 10000; + theme-dark = "Tommorow Night"; + }; + } + ''; + }; + }; + }; + + dconfProfile = with lib.types; submodule { + options = { + enableUserDb = lib.mkOption { + type = bool; + default = true; + description = lib.mdDoc "Add `user-db:user` at the beginning of the profile."; + }; + + databases = lib.mkOption { + type = with lib.types; listOf (oneOf [ + path + package + dconfDatabase + ]); + default = [ ]; + description = lib.mdDoc '' + List of data sources for the profile. An element can be an attrset, + or the path of an already compiled database. Each element is converted + to a file-db. + + A key is searched from up to down and the first result takes the + priority. If a lock for a particular key is installed then the value from + the last database in the profile where the key is locked will be used. + This can be used to enforce mandatory settings. + ''; + }; + }; + }; + in { options = { @@ -19,8 +145,27 @@ in type = with lib.types; attrsOf (oneOf [ path package + dconfProfile ]); - description = lib.mdDoc "Attrset of dconf profiles."; + default = { }; + description = lib.mdDoc '' + Attrset of dconf profiles. By default the `user` profile is used which + ends up in `/etc/dconf/profile/user`. + ''; + example = lib.literalExpression '' + { + # A "user" profile with a database + user.databases = [ + { + settings = { }; + } + ]; + # A "bar" profile from a package + bar = pkgs.bar-dconf-profile; + # A "foo" profile from a path + foo = ''${./foo} + }; + ''; }; packages = lib.mkOption {