diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/desktop/eww/eww.mustache.scss | 4 | ||||
| -rw-r--r-- | modules/desktop/eww/system-stats.py | 73 | ||||
| -rw-r--r-- | modules/desktop/eww/yuck.nix | 8 |
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) |
