diff --git a/waybar/config.jsonc b/waybar/config.jsonc index eb6c609..2179bb6 100644 --- a/waybar/config.jsonc +++ b/waybar/config.jsonc @@ -12,8 +12,8 @@ "custom/mem", "custom/cpu", "custom/tlp", - "wireplumber#sink", - "wireplumber#source", + "custom/volume", + "custom/mic", "tray", "clock" ], @@ -47,27 +47,27 @@ "format-disconnected": "Disconnected", "tooltip-format": "{ifname} via {gwaddr}" }, - "wireplumber#sink": { - "format": "{node_name} {volume}% {icon}", - "format-muted": "{node_name} ", - "format-icons": { - "headphone": "", - "hands-free": "", - "default": ["", "", ""] - }, + "custom/volume": { + "format": "{}", + "return-type": "json", + "exec": "python3 ~/.config/waybar/scripts/volume_combined.py sink", "on-click": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle", - "on-click-right": "~/.config/waybar/scripts/audio.sh cycle", - "on-click-middle": "pavucontrol", - "scroll-step": 5 + "on-scroll-up": "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+", + "on-scroll-down": "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-", + "on-click-right": "~/.config/waybar/scripts/audio.sh cycle", + "on-click-middle": "pavucontrol", + "interval": 1 }, - "wireplumber#source": { - "node-type": "Audio/Source", - "format": "{node_name} {volume}% ", - "format-muted": "", + "custom/mic": { + "format": "{}", + "return-type": "json", + "exec": "python3 ~/.config/waybar/scripts/volume_combined.py source", "on-click": "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle", - "on-click-right": "~/.config/waybar/scripts/cycle_input.sh cycle", - "on-click-middle": "pavucontrol", - "scroll-step": 5 + "on-scroll-up": "wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 5%+", + "on-scroll-down": "wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 5%-", + "on-click-right": "~/.config/waybar/scripts/cycle_input.sh cycle", + "on-click-middle": "pavucontrol", + "interval": 1 }, "custom/bluetooth-audio": { "format": "{}", @@ -76,21 +76,6 @@ "interval": 3, "on-click": "~/.config/waybar/scripts/bluetooth_audio.sh disconnect & disown" }, - - "pulseaudio": { - "format": "{icon} {volume}%", - "format-muted": " Muted", - "format-icons": { - "headphone": "", - "hands-free": "", - "headset": "", - "phone": "", - "portable": "", - "car": "", - "default": ["", ""] - }, - "on-click": "pavucontrol & disown" - }, "tray": { "icon-size": 18, "spacing": 6 @@ -126,13 +111,6 @@ "on-click": "~/.config/waybar/scripts/pixelbuds_pro_control.sh connect & disown", "on-click-right": "~/.config/waybar/scripts/pixelbuds_pro_control.sh cycle_anc & disown" }, - "custom/audio-output": { - "format": "{}", - "return-type": "json", - "exec": "/home/narl/.config/waybar/scripts/audio.sh show", - "on-click": "/home/narl/.config/waybar/scripts/audio.sh cycle & disown", - "interval": 1 - }, "custom/gamemode": { "format": "{}", "return-type": "json", diff --git a/waybar/scripts/cycle_input.sh b/waybar/scripts/cycle_input.sh index c058966..858227f 100755 --- a/waybar/scripts/cycle_input.sh +++ b/waybar/scripts/cycle_input.sh @@ -5,7 +5,8 @@ DESCRIPTION=$(pactl list sources | grep -A2 "Name: $DEFAULT_SOURCE" | grep "Desc case $1 in cycle) - SOURCES=($(pactl list short sources | awk '{print $2}')) + # Filter out monitor sources + SOURCES=($(pactl list short sources | awk '{print $2}' | grep -v ".monitor")) NUM_SOURCES=${#SOURCES[@]} CURRENT_SOURCE=$(pactl info | grep 'Default Source' | cut -d ' ' -f3) @@ -16,6 +17,10 @@ case $1 in exit 0 fi done + # If current source was a monitor or not in list, just pick the first one + if [ $NUM_SOURCES -gt 0 ]; then + pactl set-default-source "${SOURCES[0]}" + fi ;; show) TEXT=$(echo "$DESCRIPTION" | cut -c -20) @@ -35,6 +40,6 @@ case $1 in printf '{"text": "%s", "tooltip": "%s", "class": "%s"}' "$TEXT" "$DESCRIPTION" "$CLASS" ;; *) - echo "usage audio.sh {cycle|show}" + echo "usage cycle_input.sh {cycle|show}" ;; esac diff --git a/waybar/scripts/volume.sh b/waybar/scripts/volume.sh new file mode 100755 index 0000000..1de7c85 --- /dev/null +++ b/waybar/scripts/volume.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +TYPE=$1 # "mic" for source, empty for sink + +if [ "$TYPE" == "mic" ]; then + TARGET="@DEFAULT_AUDIO_SOURCE@" +else + TARGET="@DEFAULT_AUDIO_SINK@" +fi + +# Get volume and mute status from wpctl +OUTPUT=$(wpctl get-volume $TARGET) +VOLUME_RAW=$(echo "$OUTPUT" | awk '{print $2}') +VOLUME=$(echo "$VOLUME_RAW * 100 / 1" | bc) +MUTE=$(echo "$OUTPUT" | grep -c "MUTED") + +if [ "$MUTE" -eq 1 ]; then + if [ "$TYPE" == "mic" ]; then + ICON="" + else + ICON="" + fi + printf '{"text": "%s", "class": "muted", "percentage": %d}' "$ICON" "$VOLUME" +else + if [ "$TYPE" == "mic" ]; then + ICON="" + else + if [ "$VOLUME" -le 30 ]; then + ICON="" + elif [ "$VOLUME" -le 60 ]; then + ICON="" + else + ICON="" + fi + fi + printf '{"text": "%d%% %s", "class": "unmuted", "percentage": %d}' "$VOLUME" "$ICON" "$VOLUME" +fi diff --git a/waybar/scripts/volume_combined.py b/waybar/scripts/volume_combined.py new file mode 100644 index 0000000..dc51b42 --- /dev/null +++ b/waybar/scripts/volume_combined.py @@ -0,0 +1,64 @@ +import subprocess +import json +import sys + +def get_wpctl_status(target): + try: + output = subprocess.check_output(["wpctl", "get-volume", target], text=True) + parts = output.strip().split() + if len(parts) < 2: + return 0, False + vol = int(float(parts[1]) * 100) + muted = "[MUTED]" in output + return vol, muted + except: + return 0, False + +def get_pactl_description(target_type): + try: + cmd_info = ["pactl", "info"] + info = subprocess.check_output(cmd_info, text=True) + search_key = "Default Sink" if target_type == "sink" else "Default Source" + default_dev = next(line.split(": ")[1] for line in info.splitlines() if search_key in line) + + cmd_list = ["pactl", "list", "sinks" if target_type == "sink" else "sources"] + output = subprocess.check_output(cmd_list, text=True) + + blocks = output.split("\n\n") + for block in blocks: + if f"Name: {default_dev}" in block: + for line in block.splitlines(): + if "Description:" in line: + desc = line.split(": ")[1].strip() + # Add a small hint if it is a monitor, though cycle_input should prevent it + if ".monitor" in default_dev: + return "Monitor: " + desc[:11] + return desc[:20] + except: + pass + return "Unknown" + +try: + target_type = sys.argv[1] if len(sys.argv) > 1 else "sink" + target = "@DEFAULT_AUDIO_SINK@" if target_type == "sink" else "@DEFAULT_AUDIO_SOURCE@" + + vol, muted = get_wpctl_status(target) + name = get_pactl_description(target_type) + + if muted: + icon = "" if target_type == "sink" else "" + text = f"{name} {icon}" + cls = "muted" + else: + if target_type == "sink": + if vol <= 30: icon = "" + elif vol <= 60: icon = "" + else: icon = "" + else: + icon = "" + text = f"{name} {vol}% {icon}" + cls = "unmuted" + + print(json.dumps({"text": text, "class": cls, "percentage": vol})) +except Exception as e: + print(json.dumps({"text": "Error", "class": "error"})) diff --git a/waybar/style.css b/waybar/style.css index eee596e..249823c 100644 --- a/waybar/style.css +++ b/waybar/style.css @@ -62,6 +62,8 @@ window#waybar.hidden { #battery, #backlight, #wireplumber, +#custom-volume, +#custom-mic, #custom-network, #network, #clock, @@ -72,7 +74,6 @@ window#waybar.hidden { #custom-disk-data, #custom-pixelbuds_pro, #custom-bluetooth-audio, -#custom-audio-output, #custom-btrfs, #custom-gpu-screen-recorder { border-radius: 10px; @@ -82,7 +83,7 @@ window#waybar.hidden { color: @text; } -#wireplumber.muted, #custom-pixelbuds_pro, #custom-audio-output.muted { +#wireplumber.muted, #custom-pixelbuds_pro, #custom-volume.muted, #custom-mic.muted { background-color: @base; color: @subtext1; border-bottom: 3px solid @subtext1; @@ -226,11 +227,11 @@ window#waybar.hidden { color: @mauve; } -#wireplumber.muted { +#custom-volume.muted, #custom-mic.muted { padding-right: 15px; } -#custom-audio-output.unmuted, #wireplumber { +#custom-volume.unmuted, #custom-mic.unmuted, #wireplumber { color: @mauve; border-bottom: 3px solid @mauve; }