This commit is contained in:
2025-05-22 13:28:23 +03:00
commit 5a1b44e4b1
6 changed files with 394 additions and 0 deletions

203
scripts/.local/bin/mute_toggle.sh Executable file
View File

@@ -0,0 +1,203 @@
#!/bin/bash
set -euo pipefail # Exit on error, undefined variable, or pipe failure
# Set STATE_FILE according to XDG Base Directory Specification
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
readonly STATE_DIR="$XDG_CACHE_HOME/mute_toggle"
readonly STATE_FILE="$STATE_DIR/muted_sources"
# Store command paths or empty strings if not found
WPCTL_CMD=""
NOTIFY_SEND_CMD=""
CANBERRA_GTK_PLAY_CMD=""
# Function to play KDE notification sound
play_sound() {
local event="$1"
if [[ -n "$CANBERRA_GTK_PLAY_CMD" ]]; then
"$CANBERRA_GTK_PLAY_CMD" -i "$event" --description="$event" --volume -10 2>/dev/null & disown
fi
}
# Function to send desktop notification and play sound
# Arguments:
# $1: Status message part (e.g., "Muted", "Unmuted")
# $2: Icon name
# $3: Sound event name
notify_user() {
local status_msg="$1"
local icon_name="$2"
local sound_event="$3"
play_sound "$sound_event"
if [[ -n "$NOTIFY_SEND_CMD" ]]; then
"$NOTIFY_SEND_CMD" -a "MicrophoneToggle" -i "$icon_name" \
"Microphone" "Microphone is $status_msg" -t 500
fi
}
# Function to check for required commands
check_requirements() {
if command -v wpctl &>/dev/null; then
WPCTL_CMD="wpctl"
else
echo "Error: wpctl not found. Please install WirePlumber (package wireplumber)." >&2
exit 1
fi
if command -v notify-send &>/dev/null; then
NOTIFY_SEND_CMD="notify-send"
else
echo "Warning: notify-send not found. Desktop notifications will be skipped." >&2
fi
if command -v canberra-gtk-play &>/dev/null; then
CANBERRA_GTK_PLAY_CMD="canberra-gtk-play"
else
echo "Warning: canberra-gtk-play not found. Notification sounds will be skipped." >&2
fi
}
# Function to extract audio source IDs and their mute states
# Output: Lines of "<id> <0_or_1_for_muted_status>"
extract_sources() {
# The awk script:
# 1. /Audio/,/Video/: Operates only within the 'Audio' section up to 'Video'.
# 2. If /Sources:/: Sets flag 's=1' indicating we are in the Sources sub-section.
# 3. Else if /Filters:|Sinks:|Streams:/: Sets 's=0' if another sub-section starts.
# This handles cases where Sources is not the last sub-section.
# 4. Else if (s && match(...)): If in Sources sub-section and line matches pattern for a source:
# - Extracts ID.
# - Prints ID and 1 if "MUTED]" is found, 0 otherwise.
"$WPCTL_CMD" status 2>/dev/null | awk '
/Audio/,/Video/ {
if ($0 ~ /Sources:/) s=1
else if ($0 ~ /Filters:|Sinks:|Streams:/) s=0
else if (s && match($0, /^[[:space:]]*│[[:space:]]*[* ]?[[:space:]]*([0-9]+)\. (.*)/, a)) {
id = a[1]
print id, ($0 ~ /MUTED\]/ ? 1 : 0) # Check for "MUTED]" for more specificity
}
}'
}
main() {
local FORCE_MODE=false
if [[ "$#" -gt 0 && "$1" == "--force" ]]; then
FORCE_MODE=true
shift # Consume the --force argument
echo "Running in --force mode: will not save/use saved state."
fi
check_requirements
# Ensure state file directory exists (only if we might use it)
if ! "$FORCE_MODE"; then
mkdir -p "$STATE_DIR" || { echo "Error: Cannot create directory for $STATE_DIR" >&2; exit 1; }
fi
declare -A current_source_states # Stores ID -> current_mute_status (0=unmuted, 1=muted)
declare -A ids_muted_by_script # Stores IDs of sources this script muted previously (from STATE_FILE)
local -a ids_to_mute_now=() # Array of UNMUTED source IDs to be muted
local -a ids_to_unmute_based_on_script_state=() # Array of MUTED source IDs (that script muted) to be unmuted
local -a ids_currently_muted=() # Array of ALL currently MUTED source IDs (used by --force)
# Step 1: Load IDs of sources previously muted by this script (if not in force mode)
if ! "$FORCE_MODE"; then
if [[ -f "$STATE_FILE" ]]; then
while IFS= read -r id_from_file || [[ -n "$id_from_file" ]]; do # Handle last line without newline
if [[ "$id_from_file" =~ ^[0-9]+$ ]]; then # Basic validation
ids_muted_by_script["$id_from_file"]=1
fi
done < "$STATE_FILE"
fi
fi
# Step 2: Get current state of all audio sources
local no_sources_found=true
while IFS=' ' read -r id muted_status; do
no_sources_found=false
current_source_states["$id"]=$muted_status
if [[ "$muted_status" -eq 0 ]]; then # If currently unmuted
ids_to_mute_now+=("$id")
else # If currently muted
ids_currently_muted+=("$id") # Store all muted IDs for --force unmute
# If not in force mode, and it was muted by this script
if ! "$FORCE_MODE" && [[ "${ids_muted_by_script[$id]+exists}" ]]; then
ids_to_unmute_based_on_script_state+=("$id")
fi
fi
done < <(extract_sources)
if "$no_sources_found"; then
echo "No active audio sources found."
notify_user "Unavailable (No Sources)" "dialog-error" "dialog-error" # Or some other icon/sound
exit 0
fi
# Step 3: Determine action based on force mode or state
local target_ids=()
local action_type="" # "mute" or "unmute"
if "$FORCE_MODE"; then
if [[ ${#ids_to_mute_now[@]} -gt 0 ]]; then # If any sources are unmuted, force mute
target_ids=("${ids_to_mute_now[@]}")
action_type="mute"
elif [[ ${#ids_currently_muted[@]} -gt 0 ]]; then # If all are muted, force unmute
target_ids=("${ids_currently_muted[@]}")
action_type="unmute"
fi
else # Not force mode, use stateful logic
if [[ ${#ids_to_mute_now[@]} -gt 0 ]]; then # If any sources are unmuted, mute them all
target_ids=("${ids_to_mute_now[@]}")
action_type="mute"
elif [[ ${#ids_to_unmute_based_on_script_state[@]} -gt 0 ]]; then # If all are muted, and some by script, unmute those
target_ids=("${ids_to_unmute_based_on_script_state[@]}")
action_type="unmute"
fi
fi
# Step 4: Perform the determined action
if [[ "$action_type" == "mute" ]]; then
echo "Muting sources: ${target_ids[*]}"
if ! "$FORCE_MODE"; then # Clear state file if not in force mode
>"$STATE_FILE" || { echo "Error: Cannot clear $STATE_FILE" >&2; exit 1; }
fi
for id in "${target_ids[@]}"; do
if ! "$WPCTL_CMD" set-mute "$id" 1; then
echo "Warning: Failed to mute source $id. Continuing..." >&2
fi
if ! "$FORCE_MODE"; then # Add to state file if not in force mode
echo "$id" >> "$STATE_FILE"
fi
done
notify_user "Muted" "microphone-sensitivity-muted" "dialog-warning"
elif [[ "$action_type" == "unmute" ]]; then
echo "Unmuting sources: ${target_ids[*]}"
for id in "${target_ids[@]}"; do
if ! "$WPCTL_CMD" set-mute "$id" 0; then
echo "Warning: Failed to unmute source $id. Continuing..." >&2
fi
done
if ! "$FORCE_MODE"; then # Remove state file if not in force mode
rm -f "$STATE_FILE" || { echo "Error: Cannot remove $STATE_FILE" >&2; exit 1; }
fi
notify_user "Unmuted" "microphone-sensitivity-high" "dialog-information"
else
# This case means:
# 1. No sources are unmuted.
# 2. AND (if not force mode) no sources currently muted were previously muted by this script.
# This can happen if all sources are manually muted, or no relevant action was determined.
echo "No microphone status change needed."
if [[ -f "$STATE_FILE" ]]; then
echo "(State file $STATE_FILE exists, but relevant sources are not currently active or already handled)."
fi
notify_user "Unchanged" "dialog-information" "dialog-information"
fi
}
# Ensure script is not sourced
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi