diff --git a/system/audio-toggle/README.md b/system/audio-toggle/README.md new file mode 100644 index 0000000..cea2d8f --- /dev/null +++ b/system/audio-toggle/README.md @@ -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 ` | Switch to a sink whose name matches `` (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. diff --git a/system/audio-toggle/audio_toggle.sh b/system/audio-toggle/audio_toggle.sh new file mode 100755 index 0000000..7220066 --- /dev/null +++ b/system/audio-toggle/audio_toggle.sh @@ -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 < Switch to a sink whose name matches + --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 diff --git a/system/input-remapper-switcher/README.md b/system/input-remapper-switcher/README.md new file mode 100644 index 0000000..93c9ad7 --- /dev/null +++ b/system/input-remapper-switcher/README.md @@ -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. diff --git a/system/input-remapper-switcher/input-remapper-switcher.sh b/system/input-remapper-switcher/input-remapper-switcher.sh new file mode 100755 index 0000000..bd68df2 --- /dev/null +++ b/system/input-remapper-switcher/input-remapper-switcher.sh @@ -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