#!/usr/bin/env bash set -e # Best-effort script to control volume of the focused application, # falling back to the default sink. # # MODIFIED: Now checks for Spotify and uses 'playerctl' if found. # MODIFIED: Pactl branch now matches on process binary name, not PID. USAGE="Usage: $(basename "$0") WINDOW_ID [--up | --down | --set <0-100>]" CHANGE_PCT="5" # Percent to change # Get the focused window ID FOCUSED_WIN_ID="$1" shift; ACTION=$1 ARGUMENT=$2 # --- Check if the window is Spotify --- IS_SPOTIFY=0 if [ -n "$FOCUSED_WIN_ID" ] && [ "$FOCUSED_WIN_ID" != "0" ]; then # xprop output is: WM_CLASS(STRING) = "spotify", "Spotify" # We check for "spotify" case-insensitive if xprop -id "$FOCUSED_WIN_ID" WM_CLASS | grep -q -i "spotify"; then IS_SPOTIFY=1 fi fi # --- BRANCH 1: SPOTIFY LOGIC (use playerctl) --- if [ "$IS_SPOTIFY" -eq 1 ]; then echo "Spotify window detected. Using playerctl." >&2 # playerctl volume is a float from 0.0 to 1.0 # We use awk to convert our 5% to 0.05 CHANGE_FLOAT=$(awk "BEGIN {print $CHANGE_PCT / 100}") case $ACTION in --up) playerctl --player=spotify volume "${CHANGE_FLOAT}+" ;; --down) playerctl --player=spotify volume "${CHANGE_FLOAT}-" ;; --set) # Convert 0-100 input to 0.0-1.0 float TARGET_VOL=$(awk "BEGIN {print $ARGUMENT / 100}") playerctl --player=spotify volume "$TARGET_VOL" ;; *) echo "$USAGE" >&2 exit 1 ;; esac # We are done. Exit successfully. exit 0 fi # --- BRANCH 2: DEFAULT PACTL LOGIC (if not Spotify) --- echo "Non-Spotify window. Using pactl." >&2 SINK_INPUT_INDEX="" if [ -n "$FOCUSED_WIN_ID" ] && [ "$FOCUSED_WIN_ID" != "0" ]; then # Get the PID of the focused window PID=$(xprop -id "$FOCUSED_WIN_ID" _NET_WM_PID | awk '{print $NF}') if [ -n "$PID" ]; then # NEW: Get the binary name from the PID BINARY_NAME=$(ps -p "$PID" -o comm= | tr -d '[:space:]') echo "Looking for binary $BINARY_NAME" if [ -n "$BINARY_NAME" ]; then # Find PulseAudio sink input index for this BINARY_NAME # This is more robust than PID for apps like browsers SINK_INPUT_INDEX=$(pactl list sink-inputs | perl -ne ' BEGIN { our $sink_id = -1; our $maybe_sink_id = -1; } if (/Sink Input #(\d+)/) { $maybe_sink_id = $1; } # Match the binary name, e.g., application.process.binary = "firefox" if (/application\.process\.binary = "'"$BINARY_NAME"'"/) { $sink_id = $maybe_sink_id; } END { print "$sink_id\n"; } ') fi fi fi CHANGE="${CHANGE_PCT}%" set_volume() { local target=$1 local diff=$2 if [[ "$target" == *#* ]]; then echo "Setting volume for Sink Input $target ($diff)" >&2 pactl set-sink-input-volume "${target#SINK INPUT #}" "$diff" else echo "Setting default sink volume ($diff)" >&2 pactl set-sink-volume "$target" "$diff" fi } TARGET="@DEFAULT_SINK@" if [ -n "$SINK_INPUT_INDEX" ] && [ "$SINK_INPUT_INDEX" -ne -1 ]; then TARGET="SINK INPUT #$SINK_INPUT_INDEX" fi case $ACTION in --up) set_volume "$TARGET" "+$CHANGE" ;; --down) set_volume "$TARGET" "-$CHANGE" ;; --set) # pactl can take a percentage directly set_volume "$TARGET" "${ARGUMENT}%" ;; *) echo "$USAGE" >&2 exit 1 ;; esac