A simple yet powerful UI overlay made for Wayland WMs built with Quickshell
wayland
qs
linux
ui
ux
1#!/usr/bin/env bash
2set -euo pipefail
3
4wpctl_bin="${WPCTL_BIN:-wpctl}"
5
6fail() { echo "$*" >&2; exit 1; }
7
8inspect() {
9 "$wpctl_bin" inspect -a "$1" 2>/dev/null || true
10}
11
12field_quoted() {
13 local insp="$1" key="$2"
14 printf '%s\n' "$insp" | sed -n "s/.*${key}[[:space:]]*=[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n1
15}
16
17media_class() {
18 field_quoted "$1" "media\.class"
19}
20
21display_name() {
22 local insp="$1" n
23 n="$(field_quoted "$insp" "node\.description")"
24 [[ -z "$n" ]] && n="$(field_quoted "$insp" "node\.nick")"
25 [[ -z "$n" ]] && n="$(field_quoted "$insp" "node\.name")"
26 printf '%s' "${n:-}"
27}
28
29candidates_from_status() {
30 "$wpctl_bin" status --name | awk '
31 function clean(s) {
32 gsub(/[│├└─]/, " ", s)
33 sub(/^[ \t]+/, "", s)
34 return s
35 }
36
37 BEGIN { in_audio=0; sec="" }
38
39 { line = clean($0) }
40
41 line == "Audio" { in_audio=1; next }
42 in_audio && line == "Video" { exit }
43
44 in_audio && line == "Sinks:" { sec="SINKS"; next }
45 in_audio && line == "Sources:" { sec="SOURCES"; next }
46 in_audio && line == "Filters:" { sec="FILTERS"; next }
47
48 in_audio && sec != "" && line ~ /^[A-Z][A-Za-z ]*:/ && line != "Sinks:" && line != "Sources:" && line != "Filters:" {
49 sec=""
50 next
51 }
52
53 (sec=="SINKS" || sec=="SOURCES") && match(line, /^(\*)?[[:space:]]*([0-9]+)\.[[:space:]]+(.+)$/, m) {
54 def = (m[1] == "*" ? 1 : 0)
55 id = m[2]
56 raw = m[3]
57
58 sub(/[[:space:]]+\[vol:[^]]+\][[:space:]]*$/, "", raw)
59
60 kind = (sec=="SINKS" ? "SINK" : "SOURCE")
61 printf "%s\t%d\t%s\t%s\n", kind, def, id, raw
62 next
63 }
64
65 sec=="FILTERS" && match(line, /^(\*)?[[:space:]]*([0-9]+)\.[[:space:]]+([^[]+)[[:space:]]+\[([^]]+)\][[:space:]]*$/, f) {
66 def = (f[1] == "*" ? 1 : 0)
67 id = f[2]
68 raw = f[3]
69 cls = f[4]
70
71 if (cls == "Audio/Sink") printf "SINK\t%d\t%s\t%s\n", def, id, raw
72 if (cls == "Audio/Source") printf "SOURCE\t%d\t%s\t%s\n", def, id, raw
73 next
74 }
75 '
76}
77
78list() {
79 local line kind def id raw
80 local any=0
81
82 while IFS=$'\t' read -r kind def id raw; do
83 [[ -z "${id:-}" ]] && continue
84
85 local insp cls name
86 insp="$(inspect "$id")"
87 [[ -z "$insp" ]] && continue
88
89 cls="$(media_class "$insp")"
90 if [[ "$kind" == "SINK" ]]; then
91 [[ "$cls" == "Audio/Sink" ]] || continue
92 else
93 [[ "$cls" == "Audio/Source" ]] || continue
94 fi
95
96 name="$(display_name "$insp")"
97 [[ -z "$name" ]] && name="$raw"
98 [[ -z "$name" ]] && name="Node $id"
99
100 printf "%s\t%s\t%s\t%s\n" "$kind" "$def" "$id" "$name"
101 any=1
102 done < <(candidates_from_status)
103
104 if [[ "$any" -eq 0 ]]; then
105 echo "No audio nodes returned by audioctl" >&2
106 exit 0
107 fi
108}
109
110case "${1:-}" in
111 list)
112 list
113 ;;
114
115 set-default)
116 [[ $# -eq 3 ]] || fail "usage: $0 set-default sink|source <id>"
117 "$wpctl_bin" set-default "$3"
118 ;;
119
120 volume)
121 [[ $# -eq 2 ]] || fail "usage: $0 volume sink|source"
122 if [[ "$2" == "sink" ]]; then
123 "$wpctl_bin" get-volume @DEFAULT_AUDIO_SINK@ 2>/dev/null || true
124 else
125 "$wpctl_bin" get-volume @DEFAULT_AUDIO_SOURCE@ 2>/dev/null || true
126 fi
127 ;;
128
129 *)
130 fail "usage: $0 list | set-default sink|source <id> | volume sink|source"
131 ;;
132esac