c454b1085466 | atagen | 2026-05-27 15:49 UTC | 1 | # NixOS module — system-wide install. headroom is a user-scope daemon |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 2 | # (talks to the user's pipewire session), so this module just: |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 3 | # 1. binary on every login's PATH |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 4 | # 2. systemd user unit in the system-wide location (`systemctl --user enable --now headroom`) |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 5 | # 3. bundled profiles into the system profile's share/ (daemon finds via $XDG_DATA_DIRS) |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 6 | # 4. assert pipewire+wireplumber are enabled |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 7 | self: |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 8 | { |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 9 | config, |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 10 | lib, |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 11 | pkgs, |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 12 | ... |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 13 | }: |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 14 | |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 15 | let |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 16 | inherit (lib) |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 17 | mkEnableOption |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 18 | mkOption |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 19 | mkIf |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 20 | types |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 21 | literalExpression |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 22 | ; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 23 | |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 24 | cfg = config.programs.headroom; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 25 | in |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 26 | { |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 27 | options.programs.headroom = { |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 28 | enable = mkEnableOption "Headroom — automatic loudness and per-app volume control (pipewire)"; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 29 | |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 30 | package = mkOption { |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 31 | type = types.package; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 32 | default = self.packages.${pkgs.stdenv.hostPlatform.system}.headroom; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 33 | defaultText = literalExpression "headroom.packages.\${pkgs.system}.headroom"; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 34 | description = '' |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 35 | The headroom package to install system-wide. |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 36 | ''; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 37 | }; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 38 | |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 39 | gui = { |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 40 | enable = mkEnableOption "the Headroom GUI (iced monitor + control) and its application-launcher entry"; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 41 | |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 42 | package = mkOption { |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 43 | type = types.package; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 44 | default = self.packages.${pkgs.stdenv.hostPlatform.system}.headroom-gui; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 45 | defaultText = literalExpression "headroom.packages.\${pkgs.system}.headroom-gui"; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 46 | description = '' |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 47 | headroom-gui package installed when `programs.headroom.gui.enable` |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 48 | is set. ships the `headroom-gui` binary + `.desktop` entry, so it |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 49 | appears in application launchers automatically. |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 50 | ''; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 51 | }; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 52 | }; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 53 | |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 54 | autoStart = mkOption { |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 55 | type = types.bool; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 56 | default = true; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 57 | description = '' |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 58 | pull the headroom user service into every login's |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 59 | `default.target` so it autostarts without `systemctl |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 60 | --user enable headroom`. |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 61 | |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 62 | `systemd.packages` installs the unit but doesn't process its |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 63 | `[Install]` section, so the `default.target.wants` |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 64 | symlink is never created (unit sits `linked-runtime`). this |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 65 | materialises that symlink at the system level. false = unit |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 66 | install-only, manage activation yourself. |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 67 | ''; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 68 | }; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 69 | }; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 70 | |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 71 | config = mkIf cfg.enable { |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 72 | # binary on PATH; gui package too when enabled (drops a |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 73 | # share/applications/.desktop entry NixOS aggregates into the menu) |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 74 | environment.systemPackages = [ cfg.package ] ++ lib.optional cfg.gui.enable cfg.gui.package; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 75 | |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 76 | # make the user unit discoverable by `systemctl --user`; materialises |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 77 | # /etc/systemd/user/headroom.service → package's lib/systemd/user/headroom.service |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 78 | systemd.packages = [ cfg.package ]; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 79 | |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 80 | # systemd.packages doesn't process [Install] → add a `wantedBy` so NixOS |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 81 | # emits the default.target.wants symlink. unit body comes from the |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 82 | # package, so we must NOT author headroom.service here (duplicate-unit error); |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 83 | # overrideStrategy="asDropin" makes this a drop-in (empty bar the wants link). |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 84 | # not via environment.etc: /etc/systemd/user is a read-only store symlink. |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 85 | systemd.user.services.headroom = mkIf cfg.autoStart { |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 86 | overrideStrategy = "asDropin"; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 87 | wantedBy = [ "default.target" ]; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 88 | }; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 89 | |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 90 | # daemon scans each $XDG_DATA_DIRS entry's headroom/profiles/. NixOS only |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 91 | # links an allowlist of share/ subdirs into the system profile |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 92 | # (environment.pathsToLink), so opt share/headroom in or the shipped |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 93 | # profiles get dropped and the daemon sees only the built-in `default`. |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 94 | environment.pathsToLink = [ "/share/headroom" ]; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 95 | |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 96 | # fail eval (not a confusing runtime error) if headroom is on but pipewire isn't |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 97 | assertions = [ |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 98 | { |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 99 | assertion = config.services.pipewire.enable; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 100 | message = '' |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 101 | programs.headroom.enable requires services.pipewire.enable = true; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 102 | headroom is a PipeWire-only daemon. |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 103 | ''; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 104 | } |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 105 | ]; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 106 | }; |
c454b1085466 | atagen | 2026-05-27 15:49 UTC | 107 | } |