diff --git a/nixos/modules/services/desktops/pipewire/wireplumber.nix b/nixos/modules/services/desktops/pipewire/wireplumber.nix index 6ab62eb03c25..c924801bcd8b 100644 --- a/nixos/modules/services/desktops/pipewire/wireplumber.nix +++ b/nixos/modules/services/desktops/pipewire/wireplumber.nix @@ -1,18 +1,40 @@ { config, lib, pkgs, ... }: let - inherit (builtins) attrNames concatMap length; + inherit (builtins) concatMap; inherit (lib) maintainers; - inherit (lib.attrsets) attrByPath filterAttrs; + inherit (lib.attrsets) attrByPath mapAttrsToList; inherit (lib.lists) flatten optional; inherit (lib.modules) mkIf; inherit (lib.options) literalExpression mkOption; - inherit (lib.strings) hasPrefix; - inherit (lib.types) bool listOf package; + inherit (lib.strings) concatStringsSep makeSearchPath; + inherit (lib.types) bool listOf attrsOf package lines; + inherit (lib.path) subpath; pwCfg = config.services.pipewire; cfg = pwCfg.wireplumber; pwUsedForAudio = pwCfg.audio.enable; + + json = pkgs.formats.json { }; + + configSectionsToConfFile = path: value: + pkgs.writeTextDir + path + (concatStringsSep "\n" ( + mapAttrsToList + (section: content: "${section} = " + (builtins.toJSON content)) + value + )); + + mapConfigToFiles = config: + mapAttrsToList + (name: value: configSectionsToConfFile "share/wireplumber/wireplumber.conf.d/${name}.conf" value) + config; + + mapScriptsToFiles = scripts: + mapAttrsToList + (relativePath: value: pkgs.writeTextDir (subpath.join ["share/wireplumber/scripts" relativePath]) value) + scripts; in { meta.maintainers = [ maintainers.k900 ]; @@ -33,6 +55,114 @@ in description = "The WirePlumber derivation to use."; }; + extraConfig = mkOption { + # Two layer attrset is necessary before using JSON, because of the whole + # config file not being a JSON object, but a concatenation of JSON objects + # in sections. + type = attrsOf (attrsOf json.type); + default = { }; + example = literalExpression ''{ + "log-level-debug" = { + "context.properties" = { + # Output Debug log messages as opposed to only the default level (Notice) + "log.level" = "D"; + }; + }; + "wh-1000xm3-ldac-hq" = { + "monitor.bluez.rules" = [ + { + matches = [ + { + # Match any bluetooth device with ids equal to that of a WH-1000XM3 + "device.name" = "~bluez_card.*"; + "device.product.id" = "0x0cd3"; + "device.vendor.id" = "usb:054c"; + } + ]; + actions = { + update-props = { + # Set quality to high quality instead of the default of auto + "bluez5.a2dp.ldac.quality" = "hq"; + }; + }; + } + ]; + }; + }''; + description = '' + Additional configuration for the WirePlumber daemon when run in + single-instance mode (the default in nixpkgs and currently the only + supported way to run WirePlumber configured via `extraConfig`). + + See also: + - [The configuration file][docs-the-conf-file] + - [Modifying configuration][docs-modifying-config] + - [Locations of files][docs-file-locations] + - and the [configuration section][docs-config-section] of the docs in general + + Note that WirePlumber (and PipeWire) use dotted attribute names like + `device.product.id`. These are not nested, but flat objects for WirePlumber/PipeWire, + so to write these in nix expressions, remember to quote them like `"device.product.id"`. + Have a look at the example for this. + + [docs-the-conf-file]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/conf_file.html + [docs-modifying-config]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/modifying_configuration.html + [docs-file-locations]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/locations.html + [docs-config-section]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration.html + ''; + }; + + extraScripts = mkOption { + type = attrsOf lines; + default = { }; + example = { + "test/hello-world.lua" = '' + print("Hello, world!") + ''; + }; + description = '' + Additional scripts for WirePlumber to be used by configuration files. + + Every item in this attrset becomes a separate lua file with the path + relative to the `scripts` directory specified in the name of the item. + The scripts get passed to the WirePlumber service via the `XDG_DATA_DIRS` + variable. Scripts specified here are preferred over those shipped with + WirePlumber if they occupy the same relative path. + + For a script to be loaded, it needs to be specified as part of a component, + and that component needs to be required by an active profile (e.g. `main`). + Components can be defined in config files either via `extraConfig` or `configPackages`. + + For the hello-world example, you'd have to add the following `extraConfig`: + ```nix + services.pipewire.wireplumber.extraConfig."99-hello-world" = { + "wireplumber.components" = [ + { + name = "test/hello-world.lua"; + type = "script/lua"; + provides = "custom.hello-world"; + } + ]; + + "wireplumber.profiles" = { + main = { + "custom.hello-world" = "required"; + }; + }; + }; + ``` + + See also: + - [Location of scripts][docs-file-locations-scripts] + - [Components & Profiles][docs-components-profiles] + - [Migration - Loading custom scripts][docs-migration-loading-custom-scripts] + + [docs-file-locations-scripts]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/locations.html#location-of-scripts + [docs-components-profiles]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/components_and_profiles.html + [docs-migration-loading-custom-scripts]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/migration.html#loading-custom-scripts + ''; + }; + configPackages = mkOption { type = listOf package; default = [ ]; @@ -57,7 +187,7 @@ in extraLv2Packages = mkOption { type = listOf package; - default = []; + default = [ ]; example = literalExpression "[ pkgs.lsp-plugins ]"; description = '' List of packages that provide LV2 plugins in `lib/lv2` that should @@ -96,9 +226,22 @@ in } ''; + extraConfigPkg = pkgs.buildEnv { + name = "wireplumber-extra-config"; + paths = mapConfigToFiles cfg.extraConfig; + pathsToLink = [ "/share/wireplumber/wireplumber.conf.d" ]; + }; + + extraScriptsPkg = pkgs.buildEnv { + name = "wireplumber-extra-scrips"; + paths = mapScriptsToFiles cfg.extraScripts; + pathsToLink = [ "/share/wireplumber/scripts" ]; + }; + configPackages = cfg.configPackages - ++ optional (!pwUsedForAudio) pwNotForAudioConfigPkg - ++ optional pwCfg.systemWide systemwideConfigPkg; + ++ [ extraConfigPkg extraScriptsPkg ] + ++ optional (!pwUsedForAudio) pwNotForAudioConfigPkg + ++ optional pwCfg.systemWide systemwideConfigPkg; configs = pkgs.buildEnv { name = "wireplumber-configs"; @@ -110,7 +253,7 @@ in ( concatMap (p: - attrByPath ["passthru" "requiredLv2Packages"] [] p + attrByPath [ "passthru" "requiredLv2Packages" ] [ ] p ) configPackages ); @@ -127,24 +270,10 @@ in assertion = !config.hardware.bluetooth.hsphfpd.enable; message = "Using WirePlumber conflicts with hsphfpd, as it provides the same functionality. `hardware.bluetooth.hsphfpd.enable` needs be set to false"; } - { - assertion = length - (attrNames - ( - filterAttrs - (name: value: - hasPrefix "wireplumber/" name || name == "wireplumber" - ) - config.environment.etc - )) == 1; - message = "Using `environment.etc.\"wireplumber<...>\"` directly is no longer supported in 24.05. Use `services.pipewire.wireplumber.configPackages` instead."; - } ]; environment.systemPackages = [ cfg.package ]; - environment.etc.wireplumber.source = "${configs}/share/wireplumber"; - systemd.packages = [ cfg.package ]; systemd.services.wireplumber.enable = pwCfg.systemWide; @@ -156,10 +285,16 @@ in systemd.services.wireplumber.environment = mkIf pwCfg.systemWide { # Force WirePlumber to use system dbus. DBUS_SESSION_BUS_ADDRESS = "unix:path=/run/dbus/system_bus_socket"; + + # Make WirePlumber find our config/script files and lv2 plugins required by those + # (but also the configs/scripts shipped with WirePlumber) + XDG_DATA_DIRS = makeSearchPath "share" [ configs cfg.package ]; LV2_PATH = "${lv2Plugins}/lib/lv2"; }; - systemd.user.services.wireplumber.environment.LV2_PATH = - mkIf (!pwCfg.systemWide) "${lv2Plugins}/lib/lv2"; + systemd.user.services.wireplumber.environment = mkIf (!pwCfg.systemWide) { + XDG_DATA_DIRS = makeSearchPath "share" [ configs cfg.package ]; + LV2_PATH = "${lv2Plugins}/lib/lv2"; + }; }; }