aboutsummaryrefslogtreecommitdiffstats
path: root/modules/desktop/eww
diff options
context:
space:
mode:
authorArpit Chakladar <arpitchakladar+git@gmail.com>2026-08-28 03:54:49 +0530
committerArpit Chakladar <arpitchakladar+git@gmail.com>2026-08-29 03:50:31 +0530
commitec0606538442fc10f9f57c67f235176f139a1408 (patch)
treec723ecbe29c3b3fe4d22e6d08f77600b0c0ff565 /modules/desktop/eww
parent112d93af69e922b779b5dd3f1be9fb407211bb0a (diff)
downloadhome-manager-config-ec0606538442fc10f9f57c67f235176f139a1408.tar.gz
home-manager-config-ec0606538442fc10f9f57c67f235176f139a1408.zip
desktop: added audio section to eww system section
Diffstat (limited to 'modules/desktop/eww')
-rw-r--r--modules/desktop/eww/eww.mustache.scss4
-rw-r--r--modules/desktop/eww/system-stats.py73
-rw-r--r--modules/desktop/eww/yuck.nix8
3 files changed, 81 insertions, 4 deletions
diff --git a/modules/desktop/eww/eww.mustache.scss b/modules/desktop/eww/eww.mustache.scss
index 9fe8573..7b409f3 100644
--- a/modules/desktop/eww/eww.mustache.scss
+++ b/modules/desktop/eww/eww.mustache.scss
@@ -19,7 +19,7 @@ window#eww-bar {
}
.windows {
- margin: 15px 5px;
+ margin: 2.5px 5px;
}
.workspaces, .windows {
@@ -50,7 +50,7 @@ window#eww-bar {
font-size: 12px;
}
-.system-item:last-child {
+.system > .system-item:last-child {
border: none;
}
diff --git a/modules/desktop/eww/system-stats.py b/modules/desktop/eww/system-stats.py
index b85ffd4..fdc1503 100644
--- a/modules/desktop/eww/system-stats.py
+++ b/modules/desktop/eww/system-stats.py
@@ -35,13 +35,22 @@ net is an object: {
]
}
-Requires `iw` (for wifi link speed) and `iproute2` (for route/ip lookup) on PATH.
+sound is an object: {
+ "percent": 58, # default sink volume, 0-100
+ "mute": false, # whether the default sink is muted
+ "sink": "Rockerz 430", # description of the default sink, or null
+ "tooltip": "Volume: 58%\\nSink: Rockerz 430"
+}
+
+Requires `iw` (for wifi link speed), `iproute2` (for route/ip lookup)
+and `pamixer` (for volume/mute) on PATH.
RAM speed detection requires `dmidecode`, which typically needs root privileges
(run as root, grant CAP_SYS_RAWIO, or add a passwordless sudo rule) — if it's
unavailable or unreadable, "speed_mhz" is simply reported as null.
"""
import json
import os
+import re
import subprocess
import threading
import time
@@ -71,6 +80,12 @@ STATE = {
"tooltip": "Network Offline",
"routes": [],
},
+ "sound": {
+ "percent": None,
+ "mute": False,
+ "sink": None,
+ "tooltip": "Volume: unknown",
+ },
}
# Module-level tracking for throughput calculations
@@ -244,6 +259,52 @@ def build_ram_tooltip(ram_pct, used, total, available, swap_pct, swap_used, swap
return "\n".join(lines)
+def build_sound_tooltip(percent, mute, sink):
+ vol = f"{percent}%" if percent is not None else "unknown"
+ lines = [f"Muted ({vol})" if mute else f"Volume: {vol}"]
+ if sink:
+ lines.append(f"Sink: {sink}")
+ return "\n".join(lines)
+
+
+def get_sound_state():
+ """Query the default sink's volume, mute state, and description via
+ pamixer. Any unavailable field is reported as None/False."""
+ def pamixer_flag(flag):
+ try:
+ return subprocess.check_output(
+ ["pamixer", flag], text=True, stderr=subprocess.DEVNULL
+ ).strip()
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ return None
+
+ percent, mute, sink = None, None, None
+
+ out = pamixer_flag("--get-volume")
+ if out:
+ try:
+ percent = max(0, min(100, int(out)))
+ except ValueError:
+ percent = None
+
+ out = pamixer_flag("--get-mute")
+ if out is not None:
+ mute = out.lower() == "true"
+
+ out = pamixer_flag("--get-default-sink")
+ if out:
+ # Output ends with: INDEX "name" "description"
+ names = re.findall(r'"([^"]*)"', out)
+ sink = names[-1] if names else out.splitlines()[-1]
+
+ return {
+ "percent": percent,
+ "mute": mute,
+ "sink": sink,
+ "tooltip": build_sound_tooltip(percent, mute, sink),
+ }
+
+
def cpu_ram_loop(interval=2.0):
prev_times = read_cpu_times()
# Static for the life of the process — no need to shell out every tick.
@@ -497,6 +558,15 @@ def net_loop():
emit()
+def sound_loop(interval=1.0):
+ """Poll the default audio sink for volume/mute changes."""
+ while True:
+ time.sleep(interval)
+ with STATE_LOCK:
+ STATE["sound"] = get_sound_state()
+ emit()
+
+
def emit():
with STATE_LOCK:
print(json.dumps(STATE), flush=True)
@@ -509,6 +579,7 @@ def main():
threading.Thread(target=cpu_ram_loop, daemon=True).start()
threading.Thread(target=net_loop, daemon=True).start()
+ threading.Thread(target=sound_loop, daemon=True).start()
threading.Event().wait()
diff --git a/modules/desktop/eww/yuck.nix b/modules/desktop/eww/yuck.nix
index 1f2f993..6f33ee1 100644
--- a/modules/desktop/eww/yuck.nix
+++ b/modules/desktop/eww/yuck.nix
@@ -18,6 +18,7 @@
nativeBuildInputs = [
pkgs.iw
+ pkgs.pamixer
pkgs.wrapGAppsHook3
pkgs.gobject-introspection
];
@@ -95,7 +96,7 @@
(deflisten workspaces :initial "[]" "${lib.getExe workspaces}")
(deflisten active-windows :initial "[]" "${lib.getExe activate-windows}")
(deflisten system_stats
- :initial "{\"cpu\":{\"percent\":0,\"per_core\":[],\"freq_mhz\":null,\"load_avg\":null,\"tooltip\":\"CPU: 0%\"},\"ram\":{\"percent\":0,\"used\":\"0B\",\"total\":\"0B\",\"available\":\"0B\",\"swap_percent\":0,\"swap_used\":\"0B\",\"swap_total\":\"0B\",\"speed_mhz\":null,\"tooltip\":\"RAM: 0%\"},\"net\":{\"status\":\"down\",\"tooltip\":\"Network Offline\",\"routes\":[]}}"
+ :initial "{\"cpu\":{\"percent\":0,\"per_core\":[],\"freq_mhz\":null,\"load_avg\":null,\"tooltip\":\"CPU: 0%\"},\"ram\":{\"percent\":0,\"used\":\"0B\",\"total\":\"0B\",\"available\":\"0B\",\"swap_percent\":0,\"swap_used\":\"0B\",\"swap_total\":\"0B\",\"speed_mhz\":null,\"tooltip\":\"RAM: 0%\"},\"net\":{\"status\":\"down\",\"tooltip\":\"Network Offline\",\"routes\":[]},\"sound\":{\"percent\":0,\"mute\":true,\"sink\":null,\"tooltip\":\"Volume: unknown\"}}"
"${lib.getExe system-stats}")
(defwindow bar
@@ -162,6 +163,11 @@
:tooltip "''${system_stats.ram.tooltip}"
(image :class "system-icon" :path "${../../../assets/icons/bar/ram.svg}" :image-width 24 :image-height 24)
(label :class "system-label" :text "''${system_stats.ram.percent}%"))
+ (eventbox :onclick "pamixer --toggle-mute" :cursor "pointer" :halign "fill"
+ (box :orientation "v" :space-evenly false :spacing 4 :class "system-item"
+ :tooltip "''${system_stats.sound.tooltip}"
+ (image :class "system-icon" :path "${../../../assets/icons/bar/audio.svg}" :image-width 24 :image-height 24)
+ (label :class "system-label" :text "''${system_stats.sound.mute ? '-' : (system_stats.sound.percent + '%')}")))
(box :orientation "v" :class "system-item" :space-evenly false :spacing 4
:tooltip "''${full_date} ''${time} (''${date})"
(image :class "system-icon" :path "${../../../assets/icons/bar/calendar.svg}" :image-width 24 :image-height 24)