add input-remapper-switcher script and readme

This commit is contained in:
2026-07-12 01:04:29 -04:00
parent ba428d591c
commit 2eced0627e
4 changed files with 299 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
# audio_toggle.sh
Switch PulseAudio/PipeWire audio output sinks.
## Usage
```bash
./audio_toggle.sh [OPTION]
```
## Options
| Flag | Description |
|------|-------------|
| *(no args)* | Switch to the next available sink |
| `--to <pattern>` | Switch to a sink whose name matches `<pattern>` (case-insensitive) |
| `--prev`, `--previous` | Switch back to the previously active sink |
| `--list` | List all available sinks with descriptions |
| `--current` | Show the current default sink |
| `--move-streams` | Also move existing audio streams to the new sink |
| `--help`, `-h` | Show help message |
## Examples
```bash
audio_toggle.sh # next sink
audio_toggle.sh --to hdmi # switch to HDMI sink
audio_toggle.sh --prev # back to previous sink
audio_toggle.sh --list # see what's available
```
## Notes
- Sinks named `easyeffects` are automatically filtered out.
- The previous sink is stored in `~/.cache/audio-toggle-prev-sink`.
- Streams are not moved by default; use `--move-streams` to relocate them.
+136
View File
@@ -0,0 +1,136 @@
#!/bin/bash
# Description: Switch PulseAudio/PipeWire audio output sinks
STATE_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/audio-toggle-prev-sink"
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTION]
Toggle or switch PulseAudio/PipeWire audio output sinks.
Options:
--to <pattern> Switch to a sink whose name matches <pattern>
--prev, --previous Switch back to the previously active sink
--list List available sinks
--current Show the current default sink
--move-streams Also move existing audio streams to the new sink
--help, -h Show this help message
EOF
exit 0
}
get_current_sink() {
pactl info | sed -En 's/Default Sink: (.*)/\1/p'
}
get_sink_description() {
local sink="$1"
pactl list sinks | awk -v s="$sink" '
/^[[:space:]]*Sink #[0-9]+/ { in_block=""; name="" }
$1 == "Name:" { name = $2 }
name == s && /Description:/ { $1=""; sub(/^[[:space:]]+/, ""); print; exit }
'
}
MOVE_STREAMS=0
MODE="toggle"
TARGET_SINK=""
while [ $# -gt 0 ]; do
case "$1" in
--help|-h) usage ;;
--list)
pactl list short sinks | grep -v easyeffects | while read -r line; do
name=$(echo "$line" | awk '{print $2}')
desc=$(get_sink_description "$name")
echo "$name ($desc)"
done
exit 0
;;
--current)
sink=$(get_current_sink)
desc=$(get_sink_description "$sink")
echo "$sink ($desc)"
exit 0
;;
--prev|--previous)
if [ ! -f "$STATE_FILE" ]; then
notify-send "Audio Toggle" "No previous sink saved"
exit 1
fi
prev=$(cat "$STATE_FILE")
if ! pactl list short sinks | awk '{print $2}' | grep -qxF "$prev"; then
notify-send "Audio Toggle" "Previous sink no longer available"
exit 1
fi
current=$(get_current_sink)
echo "$current" > "$STATE_FILE"
TARGET_SINK="$prev"
MODE="switch"
;;
--to)
shift
[ $# -eq 0 ] && echo "Error: --to requires a pattern" >&2 && exit 1
matched=$(pactl list short sinks | grep -v easyeffects | awk '{print $2}' | grep -i "$1" | head -1)
[ -z "$matched" ] && echo "Error: no sink matching '$1'" >&2 && exit 1
current=$(get_current_sink)
echo "$current" > "$STATE_FILE"
TARGET_SINK="$matched"
MODE="switch"
;;
--move-streams) MOVE_STREAMS=1 ;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
shift
done
SINKS=$(pactl list short sinks | grep -v easyeffects)
SINK_COUNT=$(echo "$SINKS" | wc -l)
if [ "$SINK_COUNT" -eq 0 ]; then
notify-send -u critical "Audio Toggle" "No audio sinks available"
exit 1
fi
CURRENT_SINK=$(get_current_sink)
if [ "$MODE" = "switch" ] && [ -n "$TARGET_SINK" ]; then
NEW_SINK="$TARGET_SINK"
else
SINK_INDEX=$(echo "$SINKS" | awk -v s="$CURRENT_SINK" '$2 == s { print NR; exit }')
SINK_INDEX=${SINK_INDEX:-0}
NEXT_INDEX=$((SINK_INDEX % SINK_COUNT + 1))
NEW_SINK=$(echo "$SINKS" | awk "NR == $NEXT_INDEX { print \$2; exit }")
echo "$CURRENT_SINK" > "$STATE_FILE"
fi
NEW_DESC=$(get_sink_description "$NEW_SINK")
notify-send "Audio Output" "Switched to $NEW_DESC" --icon=audio-speakers
MAX_RETRIES=6
RETRIES=0
while true; do
[ "$RETRIES" -ge "$MAX_RETRIES" ] && notify-send -u critical "Audio Toggle" "Failed after $MAX_RETRIES attempts" && break
pactl set-default-sink "$NEW_SINK"
[ "$(get_current_sink)" = "$NEW_SINK" ] && break
RETRIES=$((RETRIES + 1))
NEXT_INDEX=$(((SINK_INDEX + RETRIES) % SINK_COUNT + 1))
NEW_SINK=$(echo "$SINKS" | awk "NR == $NEXT_INDEX { print \$2; exit }")
NEW_DESC=$(get_sink_description "$NEW_SINK")
notify-send "Audio Toggle" "Sink unavailable, trying $NEW_DESC..."
done
if [ "$MOVE_STREAMS" = 1 ]; then
pactl list short sink-inputs | grep -Eo '^[0-9]+' | while read -r id; do
pactl move-sink-input "$id" "$NEW_SINK"
done
fi
+45
View File
@@ -0,0 +1,45 @@
# input-remapper-switcher
Automatically switches [input-remapper](https://github.com/sezanzeb/input-remapper) presets on a per-game (or per-app) basis when used with the **Hyprland** window manager.
## How It Works
1. Connects to the Hyprland socket (`.socket2.sock`) and listens for `activewindowv2` events.
2. On each window change, it grabs the focused window's **class** and **title** via `hyprctl activewindow -j | jq`.
3. If either value matches a configured entry in the `PROFILES` associative array, it activates the corresponding input-remapper preset on your device.
4. When an unknown window is focused, it deactivates the last active preset.
> Uses `input-remapper-control` under the hood to start/stop presets.
## Adding a Game Profile
1. Find the window class or title of your game:
- Run `hyprctl clients` and locate the game in the output (class and title are listed for each window)
2. Get the exact name of your input device:
- `sudo input-remapper-control --list-devices`
3. Edit `input-remapper-switcher.sh` and add an entry to the `PROFILES` array (line 6):
```bash
declare -A PROFILES=(
["Grand Theft Auto V"]="GTA"
["steam_app_1151340"]="Fallout"
["steam_app_1203620"]="Enshrouded"
# Add yours here:
["Your Window Class or Title"]="Your Preset Name"
)
```
4. If your device is different, update `DEVICE_NAME` on line 13:
```bash
DEVICE_NAME="Your Device Name"
```
## Usage
Run the script manually or launch it at Hyprland startup (e.g., in `hyprland.conf`):
```conf
exec-once = ~/.config/hypr/scripts/input-remapper-switcher.sh
```
The script runs in the foreground listening for events — it does not daemonize on its own.
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# ~/.config/hypr/scripts/input-remapper-switcher.sh
# Define your mappings: "Window Class" -> "Preset Name"
# Use 'hyprctl clients' to find the 'class' of your windows.
declare -A PROFILES=(
["Grand Theft Auto V"]="GTA"
["steam_app_1151340"]="Fallout"
["steam_app_1203620"]="Enshrouded"
)
# Get exact name via: sudo input-remapper-control --list-devices
DEVICE_NAME="Logitech G502 LIGHTSPEED Wireless Gaming Mouse"
last_class=""
last_title=""
class(){
local target_preset_class="${PROFILES[$active_class]}"
echo "Activating preset '$target_preset_class' for class '$active_class'"
input-remapper-control --command start --device "$DEVICE_NAME" --preset "$target_preset_class" > /dev/null 2>&1
last_class="${target_preset_class}"
}
title(){
local target_preset_title="${PROFILES[$active_title]}"
echo "Activating preset '$target_preset_title' for title '$active_title'"
input-remapper-control --command start --device "$DEVICE_NAME" --preset "$target_preset_title" > /dev/null 2>&1
last_title="${target_preset_title}"
}
none(){
if [[ -n $last_class ]]; then
echo "Deactivating preset for '${last_class}'"
input-remapper-control --command stop --device "$DEVICE_NAME" --preset "${active_class}" > /dev/null 2>&1
last_class=""
elif [[ -n $last_title ]]; then
echo "Deactivating preset for '${last_title}'"
input-remapper-control --command stop --device "$DEVICE_NAME" --preset "${active_title}" > /dev/null 2>&1
last_title=""
fi
}
doit(){
# local active_class
active_class=$(hyprctl activewindow -j | jq -r '.class')
# local active_title
active_title=$(hyprctl activewindow -j | jq -r '.title')
if [[ -v PROFILES["$active_class"] ]]; then
# echo "found class ${active_class}"
class
elif [[ -v PROFILES["$active_title"] ]]; then
# echo "found title ${active_title}"
title
else
none
fi
}
cleanup() {
echo ""
echo "Performing cleanup on exit..."
# Cleanup logic here
}
# Trap EXIT covers normal exit, errors, SIGINT, and SIGTERM
trap cleanup EXIT
# Listen to the Hyprland socket for active window changes
# 'activewindowv2' sends events when the active window changes
socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" | \
while read -r line; do
if [[ "$line" =~ ^activewindowv2 ]]; then
doit
fi
done