Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Configs/.config/hypr/keybindings.conf
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ $d=[$ut]
binddl = $mainMod, K, $d toggle keyboard layout , exec, hyde-shell keyboardswitch # switch keyboard layout
bindd = $mainMod Alt, G, $d game mode , exec, hyde-shell gamemode # disable hypr effects for gamemode
bindd = $mainMod Shift, G, $d open game launcher , exec, hyde-shell gamelauncher # run game launcher for steam and lutris
bindd = $mainMod Shift, H, $d toggle mute/unmute for active-window, exec, hyde-shell wl-togglesink

$d=[$ut|Screen Capture]
bindd = $mainMod Shift, P, $d color picker, exec, hyprpicker -an # Pick color (Hex) >> clipboard#
Expand Down
143 changes: 143 additions & 0 deletions Configs/.local/lib/hyde/wl-togglesink.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env bash
set -eo pipefail

scrDir="$(dirname "$(realpath "${0}")")"
source "${scrDir}/globalcontrol.sh"
[[ -n "${iconsDir}" ]] || { echo "globalcontrol.sh did not set iconsDir" >&2; iconsDir="${XDG_DATA_HOME}/icons"; }
dunstDir="${iconsDir}/Wallbash-Icon"

#// Credits to sl1ng for the orginal script. Rewritten by Vyle.
ctlcheck=("pactl" "jq" "notify-send" "awk" "pgrep" "hyprctl" "iconv")
missing=()

for ctl in "${ctlcheck[@]}"; do
command -v "${ctl}" >/dev/null || missing+=("${ctl}")
done

if (( ${#missing[@]} )); then
if printf '%s\n' "${missing[@]}" | grep -qx "pactl"; then
printf '%s\n' "${missing[@]}" | grep -qx "notify-send" || notify-send -a "t1" -r 91190 -t 2000 -i "${dunstDir}/hyprdots.svg" "Pactl Not Installed!"
fi
echo "Missing required dependencies: \"${missing[*]}\"" >&2
exit 1
fi

#// Parse .pid, .class, .title to __pid, __class, __title.
active_json="$(hyprctl -j activewindow 2>/dev/null)" || { echo "Did hyprctl fail to run? [EXIT-CODE:-1]" >&2; exit 1; }
PID="$(jq -r '"\(.pid)\t\(.class)\t\(.title)\t\(.initialTitle)"' <<< "${active_json}")" || { echo "Did jq fail to run? [EXIT-CODE:-1]" >&2; exit 1; }

IFS=$'\t' read -r __pid __class __title __initialTitle <<< "${PID}"

[[ -z "${__pid}" || "${__pid}" == "null" || "${__pid}" -le 0 ]] 2>/dev/null && { echo "Could not resolve PID for focused window." >&2; exit 1; }
sink_json="$(pactl -f json list sink-inputs 2>/dev/null | iconv -f utf-8 -t utf-8 -c)" || { echo "Did pactl or iconv fail to run? Required manual intervention." >&2; exit 1; }

#// Collect all descendant PIDs for the active window (Chrome/Wayland audio often runs in child processes).
declare -A seen_pids=()
queue=("${__pid}")
all_pids=()
while ((${#queue[@]})); do
pid="${queue[0]}"
queue=("${queue[@]:1}")
[[ -n "${seen_pids[$pid]:-}" ]] && continue
seen_pids["$pid"]=1
all_pids+=("$pid")
mapfile -t children < <(pgrep -P "$pid" || true)
for child in "${children[@]}"; do
[[ -n "${seen_pids[$child]:-}" ]] || queue+=("$child")
done
done
idsJson="$(printf '%s\n' "${all_pids[@]}" | jq -s 'map(tonumber)')"

#// Check if any descendant PID matches application.process.id or else verify other statements.
+mapfile -t sink_ids < <(jq -r --argjson pids "${idsJson}" --arg class "${__class}" --arg title "${__title}" '
.[] |
def lc(x): (x // "" | ascii_downcase);
def normalize(x): x | gsub("[-_~.]+";" ") ;
select(
(.properties["application.process.id"] | tostring | tonumber? as $p | $p != null and ($pids | index($p) != null))
or
($class != "" and (lc(.properties["application.name"]) | contains(lc($class))))
or
($class != "" and (lc(.properties["application.id"]) | contains(lc($class))))
or
($class != "" and (lc(.properties["application.process.binary"]) | contains(lc($class))))
or
($title != "" and (normalize(lc(.properties["media.name"])) | contains(normalize(lc($title)))))
) | .index' <<< "${sink_json}"
)

if [[ "${#sink_ids[@]}" -eq 0 ]]; then
mapfile -t fallback_pids < <(pgrep -x "${__class}" || true)
if [[ "${#fallback_pids[@]}" -gt 0 ]]; then
declare -A seen_fallback=()
queue=("${fallback_pids[@]}")
all_fallback=()
while ((${#queue[@]})); do
pid="${queue[0]}"
queue=("${queue[@]:1}")
[[ -n "${seen_fallback[$pid]:-}" ]] && continue
seen_fallback["$pid"]=1
all_fallback+=("$pid")
mapfile -t children < <(pgrep -P "$pid" || true)
for child in "${children[@]}"; do
[[ -n "${seen_fallback[$child]:-}" ]] || queue+=("$child")
done
done
fallbackJson="$(printf '%s\n' "${all_fallback[@]}" | jq -s 'map(tonumber)')"
mapfile -t sink_ids < <( jq -r --argjson pids "${fallbackJson}" '.[] |
select((.properties["application.process.id"] | tostring | tonumber? as $p | $p != null and ($pids | index($p)))) | .index' <<< "${sink_json}" )
fi
fi

#// Auto-Detect if the environment is on Hyprland or $HYPRLAND_INSTANCE_SIGNATURE.
if [[ ${#sink_ids[@]} -eq 0 ]]; then
if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE}" ]]; then
# If even the fallback_pid remains empty, we will dispatch exit code based on $HYPRLAND_INSTANCE_SIGNATURE.
notify-send -a "t1" -r 91190 -t 1200 -i "${dunstDir}/hyprdots.svg" "No sink input available."
echo "No sink input for focused window: ${__class}" >&2
exit 1
else
echo "No sink input for focused active_window ${__class}" >&2
exit 1
fi
fi

idsJson=$(printf '%s\n' "${sink_ids[@]}" | jq -s 'map(tonumber)')

#// Get the available option from pactl.
want_mute=$(jq -r --argjson ids "$idsJson" '
[ .[] | select(.index as $i | $ids | index($i)) | .mute ] as $m |
if all($m[]; . == true) then "no"
else "yes"
end' <<< "${sink_json}"
)

if [[ "${want_mute}" == "no" ]]; then
state_msg="Unmuted"
swayIcon="${dunstDir}/media/unmuted-speaker.svg"
else
state_msg="Muted"
swayIcon="${dunstDir}/media/muted-speaker.svg"
fi

[[ -f "${swayIcon}" ]] || { echo -e "Missing swaync icons." >&2; swayIcon=""; }

errors=0
for id in "${sink_ids[@]}"; do
pactl set-sink-input-mute "$id" "$want_mute" || ((++errors))
done
if ((errors)); then
echo -e "pactl failed to set \"${id}\" to be \"${state_msg}\"! Manual intervention required." >&2
notify-send -a "t1" -r 91190 -t 1200 -i "${dunstDir}/hyprdots.svg" "Failed to set \"${id}\" to be \"${state_msg}\"!"
else
if ((errors)); then
echo -e "pactl failed to set \"${id}\" to be \"${state_msg}\"! Manual intervention required." >&2
notify-send -a "t1" -r 91190 -t 1200 -i "${dunstDir}/hyprdots.svg" "Failed to set \"${id}\" to be \"${state_msg}\"!"
else
# // Append paxmier to get a nice result. Pamixer is complete optional here.
if command -v pamixer >/dev/null; then
notify-send -a "t2" -r 91190 -t 800 -i "${swayIcon}" "${state_msg} ${__initialTitle}" "$(pamixer --get-default-sink | awk -F '"' 'END{print $(NF - 1)}' || true)"
else
notify-send -a "t2" -r 91190 -t 800 -i "${swayIcon}" "${state_msg} ${__initialTitle}"
fi
fi