gaming: arctis-sound-manager + quickshell ANC switcher

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-07-28 19:21:37 +01:00
parent 1df7ebb302
commit 027858ff1e
4 changed files with 143 additions and 1 deletions

23
flake.lock generated
View file

@ -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",

View file

@ -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" [];
};

View file

@ -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

View file

@ -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