From af11a877f238208676b596a5b6c3f3d8dcf5a86b Mon Sep 17 00:00:00 2001 From: mikaeladev Date: Wed, 17 Jun 2026 15:10:43 +0100 Subject: [PATCH] nvibrant: add module --- .../misc/news/2026/06/2026-06-17_15-09-44.nix | 12 ++ modules/services/nvibrant.nix | 197 ++++++++++++++++++ tests/modules/services/nvibrant/default.nix | 8 + tests/modules/services/nvibrant/dithering.nix | 40 ++++ tests/modules/services/nvibrant/merged.nix | 46 ++++ tests/modules/services/nvibrant/multigpu.nix | 54 +++++ tests/modules/services/nvibrant/vibrancy.nix | 39 ++++ 7 files changed, 396 insertions(+) create mode 100644 modules/misc/news/2026/06/2026-06-17_15-09-44.nix create mode 100644 modules/services/nvibrant.nix create mode 100644 tests/modules/services/nvibrant/default.nix create mode 100644 tests/modules/services/nvibrant/dithering.nix create mode 100644 tests/modules/services/nvibrant/merged.nix create mode 100644 tests/modules/services/nvibrant/multigpu.nix create mode 100644 tests/modules/services/nvibrant/vibrancy.nix diff --git a/modules/misc/news/2026/06/2026-06-17_15-09-44.nix b/modules/misc/news/2026/06/2026-06-17_15-09-44.nix new file mode 100644 index 000000000..a020b997a --- /dev/null +++ b/modules/misc/news/2026/06/2026-06-17_15-09-44.nix @@ -0,0 +1,12 @@ +{ pkgs, ... }: +{ + time = "2026-06-17T14:09:44+00:00"; + condition = pkgs.stdenv.hostPlatform.isLinux; + message = '' + A new module is available: `services.nvibrant`. + + [nvibrant] is used for configuring NVIDIA's "Digital Vibrance" on Wayland. + + [nvibrant]: https://github.com/tremeschin/nvibrant + ''; +} diff --git a/modules/services/nvibrant.nix b/modules/services/nvibrant.nix new file mode 100644 index 000000000..6cf16afb7 --- /dev/null +++ b/modules/services/nvibrant.nix @@ -0,0 +1,197 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + assertMsg + concatLines + escapeShellArgs + floor + getExe + head + hm + imap + isList + literalExpression + maintainers + match + mkEnableOption + mkIf + mkOption + mkOptionType + mkPackageOption + optionals + platforms + removeSuffix + toInt + types + ; + inherit (pkgs) + writeShellScript + ; + + listOrListOfListOf = + t: + types.addCheck (with types; listOf (either t (listOf t))) ( + values: + values == [ ] + || ( + let + isMultiGpu = isList (head values); + in + lib.all (value: isList value == isMultiGpu) values + ) + ); + + percentStr = mkOptionType { + inherit (types.str) merge; + name = "percentStr"; + description = "percentage"; + descriptionClass = "noun"; + check = x: types.str.check x && match "[0-9]([0-9]+)?%" x != null; + }; + + percentStrToInt = x: toInt (removeSuffix "%" x); + + percentStrBetween = + lowest: highest: + assert assertMsg (lowest <= highest) "percentStrBetween: lowest must be smaller than highest"; + types.addCheck percentStr ( + v: + let + i = percentStrToInt v; + in + i >= lowest && i <= highest + ) + // { + name = "percentStrBetween"; + description = "percentage between ${toString lowest}% and ${toString highest}% (both inclusive)"; + }; + + cfg = config.services.nvibrant; +in + +{ + meta.maintainers = with maintainers; [ mikaeladev ]; + + options.services.nvibrant = { + enable = mkEnableOption "nvibrant"; + + package = mkPackageOption pkgs "nvibrant" { }; + + dithering = mkOption { + type = with types; listOrListOfListOf (nullOr (either bool (enum [ "auto" ]))); + default = [ ]; + example = literalExpression '' + [ + true # HDMI + null # DP1 + false # DP2 + ] + ''; + description = '' + Whether to enable or disable dithering for your monitor(s). + + Values should match the order of physical ports on your GPU. If a null + value is passed, nvibrant will default to false (even if a device is + connected at that port). + + If you have multiple GPUs, you can pass a list of lists in order of + device. + ''; + }; + + vibrancy = mkOption { + type = with types; listOrListOfListOf (nullOr (percentStrBetween 0 200)); + default = [ ]; + example = literalExpression '' + [ + "0%" # HDMI + null # DP1 + "200%" # DP2 + ] + ''; + description = '' + The vibrancy level for your monitor(s). + + Values should match the order of physical ports on your GPU. If a null + value is passed, nvibrant will default to 100% (even if a device is + connected at that port). + + If you have multiple GPUs, you can pass a list of lists in order of + device. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + (hm.assertions.assertPlatform "services.nvibrant" pkgs platforms.linux) + { + assertion = !(cfg.dithering == [ ] && cfg.vibrancy == [ ]); + message = "Either `services.nvibrant.dithering` or `services.nvibrant.vibrancy` must be set"; + } + ]; + + systemd.user.services.nvibrant = { + Unit = { + Description = "Applies nvibrant"; + After = [ "graphical-session.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + Service = { + Type = "oneshot"; + ExecStart = + let + clampMax = x: y: if x > y then y else x; + percentIntToValue = x: clampMax (floor (10.24 * x - 1024)) 1023; + + mkDitheringArgs = + value: + escapeShellArgs ( + map ( + v: + if v == "auto" then + 0 + else if v == true then + 1 + else + 2 + ) value + ); + + mkVibrancyArgs = + value: + escapeShellArgs (map (v: percentIntToValue (if v == null then 100 else percentStrToInt v)) value); + + mkLines = + fn: list: + if isList (head list) then + imap (i: v: "NVIDIA_GPU=${toString (i - 1)} ${fn v}") list + else + [ (fn list) ]; + + binPath = getExe cfg.package; + + ditheringLines = optionals (cfg.dithering != [ ]) ( + mkLines (value: "ATTRIBUTE=dithering ${binPath} ${mkDitheringArgs value}") cfg.dithering + ); + + vibrancyLines = optionals (cfg.vibrancy != [ ]) ( + mkLines (value: "${binPath} ${mkVibrancyArgs value}") cfg.vibrancy + ); + + scriptLines = concatLines (ditheringLines ++ vibrancyLines); + in + writeShellScript "apply-nvibrant" scriptLines; + }; + Install = { + WantedBy = [ "graphical-session.target" ]; + }; + }; + }; +} diff --git a/tests/modules/services/nvibrant/default.nix b/tests/modules/services/nvibrant/default.nix new file mode 100644 index 000000000..0b94627b9 --- /dev/null +++ b/tests/modules/services/nvibrant/default.nix @@ -0,0 +1,8 @@ +{ lib, pkgs, ... }: + +lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { + nvibrant-dithering = ./dithering.nix; + nvibrant-merged = ./merged.nix; + nvibrant-multigpu = ./multigpu.nix; + nvibrant-vibrancy = ./vibrancy.nix; +} diff --git a/tests/modules/services/nvibrant/dithering.nix b/tests/modules/services/nvibrant/dithering.nix new file mode 100644 index 000000000..f26fea699 --- /dev/null +++ b/tests/modules/services/nvibrant/dithering.nix @@ -0,0 +1,40 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) getExe head; + inherit (pkgs) writeShellScript; + + cfg = config.services.nvibrant; + service = config.systemd.user.services.nvibrant; + + scriptFile = head service.Service.ExecStart; + + expectedFile = writeShellScript "apply-nvibrant" '' + ATTRIBUTE=dithering ${getExe cfg.package} 1 2 2 0 + ''; +in + +{ + services.nvibrant = { + enable = true; + dithering = [ + true + null + false + "auto" + ]; + }; + + nmt.script = '' + local script_file=${scriptFile} + local expected_file=${expectedFile} + + assertFileExists "$script_file" + assertFileContent "$script_file" "$expected_file" + ''; +} diff --git a/tests/modules/services/nvibrant/merged.nix b/tests/modules/services/nvibrant/merged.nix new file mode 100644 index 000000000..b65324b34 --- /dev/null +++ b/tests/modules/services/nvibrant/merged.nix @@ -0,0 +1,46 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) getExe head; + inherit (pkgs) writeShellScript; + + cfg = config.services.nvibrant; + service = config.systemd.user.services.nvibrant; + + scriptFile = head service.Service.ExecStart; + + expectedFile = writeShellScript "apply-nvibrant" '' + ATTRIBUTE=dithering ${getExe cfg.package} 1 2 2 0 + ${getExe cfg.package} -1024 0 1023 + ''; +in + +{ + services.nvibrant = { + enable = true; + dithering = [ + true + null + false + "auto" + ]; + vibrancy = [ + "0%" + null + "200%" + ]; + }; + + nmt.script = '' + local script_file=${scriptFile} + local expected_file=${expectedFile} + + assertFileExists "$script_file" + assertFileContent "$script_file" "$expected_file" + ''; +} diff --git a/tests/modules/services/nvibrant/multigpu.nix b/tests/modules/services/nvibrant/multigpu.nix new file mode 100644 index 000000000..e9f234d86 --- /dev/null +++ b/tests/modules/services/nvibrant/multigpu.nix @@ -0,0 +1,54 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) getExe head; + inherit (pkgs) writeShellScript; + + cfg = config.services.nvibrant; + service = config.systemd.user.services.nvibrant; + + scriptFile = head service.Service.ExecStart; + + expectedFile = writeShellScript "apply-nvibrant" '' + NVIDIA_GPU=0 ATTRIBUTE=dithering ${getExe cfg.package} 1 2 2 0 + NVIDIA_GPU=1 ATTRIBUTE=dithering ${getExe cfg.package} 2 + NVIDIA_GPU=0 ${getExe cfg.package} -1024 0 1023 + NVIDIA_GPU=1 ${getExe cfg.package} 0 + ''; +in + +{ + services.nvibrant = { + enable = true; + dithering = [ + [ + true + null + false + "auto" + ] + [ null ] + ]; + vibrancy = [ + [ + "0%" + null + "200%" + ] + [ null ] + ]; + }; + + nmt.script = '' + local script_file=${scriptFile} + local expected_file=${expectedFile} + + assertFileExists "$script_file" + assertFileContent "$script_file" "$expected_file" + ''; +} diff --git a/tests/modules/services/nvibrant/vibrancy.nix b/tests/modules/services/nvibrant/vibrancy.nix new file mode 100644 index 000000000..c0ece7df2 --- /dev/null +++ b/tests/modules/services/nvibrant/vibrancy.nix @@ -0,0 +1,39 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) getExe head; + inherit (pkgs) writeShellScript; + + cfg = config.services.nvibrant; + service = config.systemd.user.services.nvibrant; + + scriptFile = head service.Service.ExecStart; + + expectedFile = writeShellScript "apply-nvibrant" '' + ${getExe cfg.package} -1024 0 1023 + ''; +in + +{ + services.nvibrant = { + enable = true; + vibrancy = [ + "0%" + null + "200%" + ]; + }; + + nmt.script = '' + local script_file=${scriptFile} + local expected_file=${expectedFile} + + assertFileExists "$script_file" + assertFileContent "$script_file" "$expected_file" + ''; +}