atagen

headroom

public
Star0
Watch0

Commit c454b1085466

tarball

atagen <boss@atagen.co>2026-06-04 03:38 UTC

3 files changed+156-0
packaging: systemd unit, nixos module, desktop entry
Added file: contrib/desktop/headroom-gui.desktop+13 additions
@@ -0,0 +1,13 @@
1+[Desktop Entry]
2+Type=Application
3+Name=Headroom
4+GenericName=Audio Loudness Control
5+Comment=automatic loudness and per-app volume control (pipewire)
6+# @bindir@ rewritten to this build's bin dir at install time (headroom-gui nix package)
7+Exec=@bindir@/headroom-gui
8+# no icon shipped yet; stock freedesktop name
9+Icon=multimedia-volume-control
10+Terminal=false
11+Categories=AudioVideo;Audio;Mixer;
12+Keywords=audio;volume;loudness;pipewire;normalization;compressor;limiter;
13+StartupNotify=true
Added file: contrib/systemd/headroom.service+36 additions
@@ -0,0 +1,36 @@
1+[Unit]
2+Description=automatic loudness and per-app volume control (pipewire)
3+Documentation=https://github.com/manic-systems/headroom
4+# pipewire is a hard dep — can't start before pw-mainloop. ConditionUser pins user-scope
5+After=pipewire.service pipewire-pulse.service wireplumber.service
6+Requires=pipewire.service
7+Wants=wireplumber.service
8+# start with the user's service manager, but do not stop just because
9+# graphical-session.target is restarted during idle/session churn.
10+ConditionUser=!@system
11+
12+[Service]
13+Type=simple
14+ExecStart=@bindir@/headroom daemon
15+# restart after unexpected daemon exits, including clean main-loop exits.
16+Restart=always
17+RestartSec=2s
18+# sigterm is the clean shutdown path; explicit for clarity
19+KillMode=control-group
20+TimeoutStopSec=5s
21+# logs to journald (journalctl --user -u headroom)
22+StandardOutput=journal
23+StandardError=journal
24+SyslogIdentifier=headroom
25+# Allow realtime/nice elevation where the user manager has permission
26+# to grant it. Do not request CPUSchedulingPolicy= here: on systems
27+# without CAP_SYS_NICE / realtime user limits, systemd fails the unit
28+# before headroom can start. Users who have configured rt privileges can
29+# add a drop-in with CPUSchedulingPolicy=rr, CPUSchedulingPriority=20,
30+# and Nice=-11.
31+LimitRTPRIO=20
32+LimitRTTIME=200000
33+LimitNICE=-11
34+
35+[Install]
36+WantedBy=default.target
Added file: nix/nixos-module.nix+107 additions
@@ -0,0 +1,107 @@
1+# NixOS module — system-wide install. headroom is a user-scope daemon
2+# (talks to the user's pipewire session), so this module just:
3+# 1. binary on every login's PATH
4+# 2. systemd user unit in the system-wide location (`systemctl --user enable --now headroom`)
5+# 3. bundled profiles into the system profile's share/ (daemon finds via $XDG_DATA_DIRS)
6+# 4. assert pipewire+wireplumber are enabled
7+self:
8+{
9+ config,
10+ lib,
11+ pkgs,
12+ ...
13+}:
14+
15+let
16+ inherit (lib)
17+ mkEnableOption
18+ mkOption
19+ mkIf
20+ types
21+ literalExpression
22+ ;
23+
24+ cfg = config.programs.headroom;
25+in
26+{
27+ options.programs.headroom = {
28+ enable = mkEnableOption "Headroom — automatic loudness and per-app volume control (pipewire)";
29+
30+ package = mkOption {
31+ type = types.package;
32+ default = self.packages.${pkgs.stdenv.hostPlatform.system}.headroom;
33+ defaultText = literalExpression "headroom.packages.\${pkgs.system}.headroom";
34+ description = ''
35+ The headroom package to install system-wide.
36+ '';
37+ };
38+
39+ gui = {
40+ enable = mkEnableOption "the Headroom GUI (iced monitor + control) and its application-launcher entry";
41+
42+ package = mkOption {
43+ type = types.package;
44+ default = self.packages.${pkgs.stdenv.hostPlatform.system}.headroom-gui;
45+ defaultText = literalExpression "headroom.packages.\${pkgs.system}.headroom-gui";
46+ description = ''
47+ headroom-gui package installed when `programs.headroom.gui.enable`
48+ is set. ships the `headroom-gui` binary + `.desktop` entry, so it
49+ appears in application launchers automatically.
50+ '';
51+ };
52+ };
53+
54+ autoStart = mkOption {
55+ type = types.bool;
56+ default = true;
57+ description = ''
58+ pull the headroom user service into every login's
59+ `default.target` so it autostarts without `systemctl
60+ --user enable headroom`.
61+
62+ `systemd.packages` installs the unit but doesn't process its
63+ `[Install]` section, so the `default.target.wants`
64+ symlink is never created (unit sits `linked-runtime`). this
65+ materialises that symlink at the system level. false = unit
66+ install-only, manage activation yourself.
67+ '';
68+ };
69+ };
70+
71+ config = mkIf cfg.enable {
72+ # binary on PATH; gui package too when enabled (drops a
73+ # share/applications/.desktop entry NixOS aggregates into the menu)
74+ environment.systemPackages = [ cfg.package ] ++ lib.optional cfg.gui.enable cfg.gui.package;
75+
76+ # make the user unit discoverable by `systemctl --user`; materialises
77+ # /etc/systemd/user/headroom.service → package's lib/systemd/user/headroom.service
78+ systemd.packages = [ cfg.package ];
79+
80+ # systemd.packages doesn't process [Install] → add a `wantedBy` so NixOS
81+ # emits the default.target.wants symlink. unit body comes from the
82+ # package, so we must NOT author headroom.service here (duplicate-unit error);
83+ # overrideStrategy="asDropin" makes this a drop-in (empty bar the wants link).
84+ # not via environment.etc: /etc/systemd/user is a read-only store symlink.
85+ systemd.user.services.headroom = mkIf cfg.autoStart {
86+ overrideStrategy = "asDropin";
87+ wantedBy = [ "default.target" ];
88+ };
89+
90+ # daemon scans each $XDG_DATA_DIRS entry's headroom/profiles/. NixOS only
91+ # links an allowlist of share/ subdirs into the system profile
92+ # (environment.pathsToLink), so opt share/headroom in or the shipped
93+ # profiles get dropped and the daemon sees only the built-in `default`.
94+ environment.pathsToLink = [ "/share/headroom" ];
95+
96+ # fail eval (not a confusing runtime error) if headroom is on but pipewire isn't
97+ assertions = [
98+ {
99+ assertion = config.services.pipewire.enable;
100+ message = ''
101+ programs.headroom.enable requires services.pipewire.enable = true;
102+ headroom is a PipeWire-only daemon.
103+ '';
104+ }
105+ ];
106+ };
107+}