add input-remapper-switcher script and readme
This commit is contained in:
@@ -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.
|
||||
Executable
+136
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user