diff --git a/flake.lock b/flake.lock index 4ab7717..1dafe8a 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,27 @@ { "nodes": { + "arctis-sound-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "dir": "nix", + "lastModified": 1785229953, + "narHash": "sha256-5oezrSj8OxNeMUN0IxjdKh4t2D9jn1BOainmBRNeXgE=", + "owner": "loteran", + "repo": "Arctis-Sound-Manager", + "rev": "aaa05fd62b7ac037d59802ce9c46b69c5bc84a33", + "type": "github" + }, + "original": { + "dir": "nix", + "owner": "loteran", + "repo": "Arctis-Sound-Manager", + "type": "github" + } + }, "base16": { "inputs": { "fromYaml": "fromYaml" @@ -372,6 +394,7 @@ }, "root": { "inputs": { + "arctis-sound-manager": "arctis-sound-manager", "home-manager": "home-manager", "nix-cachyos-kernel": "nix-cachyos-kernel", "nixpkgs": "nixpkgs_2", diff --git a/flake.nix b/flake.nix index ce78b5e..ab400ff 100644 --- a/flake.nix +++ b/flake.nix @@ -20,6 +20,11 @@ proton-cachyos-nix.url = "github:powerofthe69/proton-cachyos-nix"; + arctis-sound-manager = { + url = "github:loteran/Arctis-Sound-Manager?dir=nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + stylix.url = "github:nix-community/stylix/release-26.05"; }; outputs = @@ -49,7 +54,7 @@ in { nixosConfigurations = { - FredOS-Gaming = mkHost "FredOS-Gaming" []; + FredOS-Gaming = mkHost "FredOS-Gaming" [ inputs.arctis-sound-manager.nixosModules.default ]; FredOS-Mediaserver = mkHost "FredOS-Mediaserver" []; FredOS-Macbook = mkHost "FredOS-Macbook" []; }; diff --git a/hosts/FredOS-Gaming.nix b/hosts/FredOS-Gaming.nix index 713aefd..b22e5f0 100644 --- a/hosts/FredOS-Gaming.nix +++ b/hosts/FredOS-Gaming.nix @@ -24,6 +24,10 @@ programs.geary.enable = true; + # SteelSeries Nova Pro control (ANC switching via quickshell talks to its + # asm-daemon over D-Bus) + services.arctis-sound-manager.enable = true; + # Force heavy builds off the gaming PC onto the media server so nix updates # never compete with games for CPU/RAM. Pairs with the distributed build # config in common.nix. builders-use-substitutes lets the server pull from diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 25cec59..f2833da 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -418,6 +418,7 @@ in readonly property string systemctl: "${pkgs.systemd}/bin/systemctl" readonly property string weatherFetch: "${weatherFetchScript}" readonly property string ssh: "${pkgs.openssh}/bin/ssh" + readonly property string gdbus: "${pkgs.glib}/bin/gdbus" // Unprivileged ping works via ICMP dgram sockets — NixOS sets // net.ipv4.ping_group_range for everyone (no wrapper exists). readonly property string ping: "${pkgs.iputils}/bin/ping" @@ -2932,6 +2933,115 @@ in } } + ${lib.optionalString (!isMacbook) '' + // Nova Pro noise cancelling — talks to asm-daemon + // (Arctis Sound Manager) over the session bus. + // SetSetting("noise_cancelling", "0|1|2"), status read + // back as off/transparent/on. Card hides when the + // daemon or headset is gone. + Card { + id: ancCard + visible: ancMode !== "" + width: parent.width + property string ancMode: "" + + function setMode(key, value) { + if (key === ancMode) return; + ancMode = key; // optimistic; next poll corrects if the device disagrees + ancSet.mode = value; + ancSet.running = true; + } + + Process { + id: ancSet + property string mode: "0" + command: [Commands.gdbus, "call", "--session", + "--dest", "name.giacomofurlan.ArctisManager.Next", + "--object-path", "/name/giacomofurlan/ArctisManager/Next/Settings", + "--method", "name.giacomofurlan.ArctisManager.Next.Settings.SetSetting", + "noise_cancelling", mode] + } + + Process { + id: ancStatus + command: [Commands.gdbus, "call", "--session", + "--dest", "name.giacomofurlan.ArctisManager.Next", + "--object-path", "/name/giacomofurlan/ArctisManager/Next/Status", + "--method", "name.giacomofurlan.ArctisManager.Next.Status.GetStatus"] + stdout: StdioCollector { + onStreamFinished: { + // gdbus prints a GVariant tuple: ('{"headset": ...}',) + const a = text.indexOf("('"); + const b = text.lastIndexOf("',"); + if (a < 0 || b <= a) { ancCard.ancMode = ""; return; } + try { + const st = JSON.parse(text.slice(a + 2, b)); + const nc = st.headset ? st.headset.noise_cancelling : undefined; + ancCard.ancMode = (nc && nc.value) ? nc.value : ""; + } catch (e) { ancCard.ancMode = ""; } + } + } + } + + // Poll only while the dropdown is up; keeps ticking + // with the card hidden so a headset appearing + // mid-open reveals it. + Timer { + interval: 3000 + repeat: true + triggeredOnStart: true + running: volDropdown.visible + onTriggered: ancStatus.running = true + } + + Row { + spacing: 6 + SIcon { anchors.verticalCenter: parent.verticalCenter; text: "headphones" } + SText { + anchors.verticalCenter: parent.verticalCenter + text: "Noise Cancelling" + font.weight: Font.Medium + } + } + + Row { + width: parent.width + spacing: 6 + + Repeater { + model: [ + { key: "off", label: "Off", val: "0" }, + { key: "transparent", label: "Transparent", val: "1" }, + { key: "on", label: "ANC", val: "2" } + ] + + Rectangle { + required property var modelData + property bool active: ancCard.ancMode === modelData.key + width: (parent.width - 12) / 3 + height: 26 + radius: 6 + color: active ? Theme.base0D : "transparent" + border.color: active ? Theme.base0D : Theme.base03 + border.width: 1 + + SText { + anchors.centerIn: parent + text: modelData.label + font.pixelSize: 11 + color: active ? Theme.base00 : Theme.base05 + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: ancCard.setMode(modelData.key, modelData.val) + } + } + } + } + } + ''} // Applications card Card { visible: appStreamsCol.childrenRect.height > 0