From 7169720bcc5e080cefcd6034d0fc4ac8ea3e3dfb Mon Sep 17 00:00:00 2001 From: codepuncher Date: Mon, 22 Jun 2026 21:44:28 +0100 Subject: [PATCH 1/6] refactor(install): add aur helper detection and install wrappers --- shell/functions | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/shell/functions b/shell/functions index 7f3cf23..2cdbd08 100644 --- a/shell/functions +++ b/shell/functions @@ -8,6 +8,53 @@ section_start() { echo "${1}" } +# Detect the AUR helper once; prefer paru, then pamac, then yay. +detect_aur_helper() { + local h + for h in paru pamac yay; do + if command -v "${h}" &>/dev/null; then + AUR_HELPER="${h}" + return 0 + fi + done + echo 'No supported AUR helper (paru/pamac/yay) found' >&2 + return 1 +} + +# Install packages via the detected helper. paru/yay must not run under sudo. +# Set DRY=1 to print the resolved command instead of running it. +arch_install() { + [[ -n "${AUR_HELPER:-}" ]] || detect_aur_helper || return 1 + local cmd + case "${AUR_HELPER}" in + paru | yay) cmd=("${AUR_HELPER}" -S --noconfirm --needed "$@") ;; + pamac) cmd=(sudo pamac install --no-confirm --no-upgrade "$@") ;; + *) echo "Unknown AUR helper: ${AUR_HELPER}" >&2; return 1 ;; + esac + if [[ -n "${DRY:-}" ]]; then + echo "${cmd[*]}" + return 0 + fi + "${cmd[@]}" +} + +# Enable AUR support where the helper needs it (pamac only). +enable_aur() { + [[ -n "${AUR_HELPER:-}" ]] || detect_aur_helper || return 1 + [[ "${AUR_HELPER}" == pamac ]] || return 0 + [[ -n "${DRY:-}" ]] && return 0 + sudo sed -Ei '/EnableAUR/s/^#//' /etc/pamac.conf +} + +# True on Arch and derivatives. Sourced in a subshell so os-release vars don't leak. +# shellcheck disable=SC1091 +is_arch() { + local id id_like + id="$(. /etc/os-release 2>/dev/null && echo "${ID:-}")" + id_like="$(. /etc/os-release 2>/dev/null && echo "${ID_LIKE:-}")" + [[ "${id}" == arch ]] || [[ " ${id_like} " == *' arch '* ]] +} + reinstall_dotfiles() { rm -rf ~/.{local/bin,npm,nvm,zinit,go/bin,config/composer/vendor,nvim,config/nvim} cd ~/.dotfiles && ./setup.sh From e16e8570746ddb698c8ed84c79a60563d4952cc5 Mon Sep 17 00:00:00 2001 From: codepuncher Date: Mon, 22 Jun 2026 21:48:02 +0100 Subject: [PATCH 2/6] refactor(install): route arch installs through helper and broaden detection --- setup.sh | 2 +- shell/functions | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/setup.sh b/setup.sh index 0585019..5286ac2 100755 --- a/setup.sh +++ b/setup.sh @@ -67,7 +67,7 @@ install_packages() { return fi - if grep --quiet 'ID=manjaro' /etc/os-release; then + if is_arch; then # shellcheck disable=2119 install_arch_packages post_install_packages diff --git a/shell/functions b/shell/functions index 2cdbd08..0d45c71 100644 --- a/shell/functions +++ b/shell/functions @@ -62,7 +62,7 @@ reinstall_dotfiles() { install_arch_packages() { echo 'Enabling the AUR...' - sudo sed -Ei '/EnableAUR/s/^#//' /etc/pamac.conf + enable_aur PACKAGES=( # General @@ -148,11 +148,7 @@ install_arch_packages() { bat ) - # shellcheck disable=2046 - sudo pamac install --no-confirm --no-upgrade $( - IFS=' ' - echo "${PACKAGES[*]}" - ) "$@" + arch_install "${PACKAGES[@]}" "$@" } install_wsl_packages() { @@ -440,7 +436,7 @@ post_install_packages() { echo 'Installing `spotify-tui' mkdir "${HOME}/.rustup" rustup default stable - pamac install --no-confirm --no-upgrade spotify-tui + arch_install spotify-tui } post_setup_arch_services() { @@ -450,7 +446,7 @@ post_setup_arch_services() { } setup_arch_system_emoji_support() { - sudo pamac install noto-fonts-emoji + arch_install noto-fonts-emoji sudo cp ~/.dotfiles/templates/fonts-local.conf /etc/fonts/local.conf } From e754af354e36ec11730579d42e03fc566d4f7b7c Mon Sep 17 00:00:00 2001 From: codepuncher Date: Mon, 22 Jun 2026 21:49:48 +0100 Subject: [PATCH 3/6] chore(install): add headsetcontrol package --- shell/functions | 1 + 1 file changed, 1 insertion(+) diff --git a/shell/functions b/shell/functions index 0d45c71..62a4b5d 100644 --- a/shell/functions +++ b/shell/functions @@ -137,6 +137,7 @@ install_arch_packages() { tldr git jq + headsetcontrol ripgrep exa fd From c677d300b7a4a830caa14c0d356127c9db6a7ecb Mon Sep 17 00:00:00 2001 From: codepuncher Date: Mon, 22 Jun 2026 21:58:48 +0100 Subject: [PATCH 4/6] feat(audio): add corsair headset auto-switch daemon --- bin/corsair-headset-switch | 81 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100755 bin/corsair-headset-switch diff --git a/bin/corsair-headset-switch b/bin/corsair-headset-switch new file mode 100755 index 0000000..21b4723 --- /dev/null +++ b/bin/corsair-headset-switch @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -uo pipefail + +readonly VOID_SINK_PATTERN='VOID_ELITE' +readonly POLL_INTERVAL=2 + +# Map headsetcontrol JSON (stdin) to on|off|unknown. Unknown covers empty or +# unparseable input so a transient query failure never flips the default sink. +parse_headset_state() { + local json status + json="$(cat)" + [[ -n "${json}" ]] || { echo unknown; return; } + status="$(jq -r '.devices[0].battery.status // empty' <<<"${json}" 2>/dev/null)" + case "${status}" in + BATTERY_AVAILABLE | BATTERY_CHARGING) echo on ;; + BATTERY_UNAVAILABLE | BATTERY_TIMEOUT) echo off ;; + *) echo unknown ;; + esac +} + +# Current headset power state, or "unknown" if headsetcontrol fails. +headset_state() { + local json + json="$(headsetcontrol -o json 2>/dev/null)" || { echo unknown; return; } + parse_headset_state <<<"${json}" +} + +# PipeWire sink name for the VOID dongle, empty if not present. +void_sink() { + pactl list short sinks | awk -v p="${VOID_SINK_PATTERN}" '$0 ~ p { print $2; exit }' +} + +log() { printf '%s %s\n' "$(date '+%H:%M:%S')" "$*"; } + +main() { + command -v headsetcontrol >/dev/null || { echo 'headsetcontrol not found' >&2; exit 1; } + command -v jq >/dev/null || { echo 'jq not found' >&2; exit 1; } + command -v pactl >/dev/null || { echo 'pactl not found' >&2; exit 1; } + + local prev current sink saved_default='' + prev="$(headset_state)" + [[ "${prev}" == unknown ]] && prev=off + log "baseline headset state: ${prev}" + + while true; do + current="$(headset_state)" + case "${current}" in + on) + if [[ "${prev}" == off ]]; then + sink="$(void_sink)" + if [[ -n "${sink}" ]]; then + saved_default="$(pactl get-default-sink)" + pactl set-default-sink "${sink}" + log "headset on -> default sink = ${sink} (was ${saved_default})" + else + log 'headset on but VOID sink not found; skipping' + fi + fi + prev=on + ;; + off) + if [[ "${prev}" == on ]]; then + if [[ -n "${saved_default}" ]] \ + && pactl list short sinks | awk '{print $2}' | grep -qxF "${saved_default}"; then + pactl set-default-sink "${saved_default}" + log "headset off -> restored default sink = ${saved_default}" + else + log 'headset off but no valid previous sink to restore' + fi + fi + prev=off + ;; + *) : ;; # unknown: hold state, do nothing + esac + sleep "${POLL_INTERVAL}" + done +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main +fi From e56543963759a695c2ec0e8ac577c966d863e3f5 Mon Sep 17 00:00:00 2001 From: codepuncher Date: Mon, 22 Jun 2026 22:02:47 +0100 Subject: [PATCH 5/6] feat(audio): add corsair headset systemd unit and installer wiring --- README.md | 14 ++++++++++++++ setup.sh | 1 + systemd/corsair-headset.service | 12 ++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 systemd/corsair-headset.service diff --git a/README.md b/README.md index 615545c..0f4684d 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,20 @@ reinstall_dotfiles - ✅ **WSL** (Windows Subsystem for Linux) - ✅ **macOS** (shell and tool configs, package installation not automated) +## Corsair headset auto-switch + +`bin/corsair-headset-switch` is a systemd user service that switches the default +PipeWire sink to the Corsair VOID Elite when the headset powers on and restores +the previous sink when it powers off. It depends on `headsetcontrol` (installed +by `setup.sh`). After symlinking, enable it once: + +```bash +systemctl --user daemon-reload +systemctl --user enable --now corsair-headset.service +``` + +Logs: `journalctl --user -u corsair-headset -f`. + ## 📝 License Personal dotfiles - use at your own discretion. diff --git a/setup.sh b/setup.sh index 5286ac2..417f6ed 100755 --- a/setup.sh +++ b/setup.sh @@ -47,6 +47,7 @@ init_links() { move_link .config/spotifyd/spotifyd.conf media/spotifyd.conf move_link .config/fontconfig fontconfig move_link .config/ngrok/ngrok.yml web/ngrok.yml + move_link .config/systemd/user/corsair-headset.service systemd/corsair-headset.service echo "Symlinking complete. Backups stored in ${SCRIPT_PATH}/backups." return diff --git a/systemd/corsair-headset.service b/systemd/corsair-headset.service new file mode 100644 index 0000000..ed18837 --- /dev/null +++ b/systemd/corsair-headset.service @@ -0,0 +1,12 @@ +[Unit] +Description=Corsair VOID Elite auto-switch +After=pipewire.service + +[Service] +Type=simple +ExecStart=%h/.dotfiles/bin/corsair-headset-switch +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=default.target From ad057bcc496cd73648769e50860d047a4adca18a Mon Sep 17 00:00:00 2001 From: codepuncher Date: Mon, 22 Jun 2026 22:25:45 +0100 Subject: [PATCH 6/6] refactor: remove comments --- bin/corsair-headset-switch | 6 +----- shell/functions | 15 ++++++--------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/bin/corsair-headset-switch b/bin/corsair-headset-switch index 21b4723..e071164 100755 --- a/bin/corsair-headset-switch +++ b/bin/corsair-headset-switch @@ -4,8 +4,6 @@ set -uo pipefail readonly VOID_SINK_PATTERN='VOID_ELITE' readonly POLL_INTERVAL=2 -# Map headsetcontrol JSON (stdin) to on|off|unknown. Unknown covers empty or -# unparseable input so a transient query failure never flips the default sink. parse_headset_state() { local json status json="$(cat)" @@ -18,14 +16,12 @@ parse_headset_state() { esac } -# Current headset power state, or "unknown" if headsetcontrol fails. headset_state() { local json json="$(headsetcontrol -o json 2>/dev/null)" || { echo unknown; return; } parse_headset_state <<<"${json}" } -# PipeWire sink name for the VOID dongle, empty if not present. void_sink() { pactl list short sinks | awk -v p="${VOID_SINK_PATTERN}" '$0 ~ p { print $2; exit }' } @@ -70,7 +66,7 @@ main() { fi prev=off ;; - *) : ;; # unknown: hold state, do nothing + *) : ;; esac sleep "${POLL_INTERVAL}" done diff --git a/shell/functions b/shell/functions index 62a4b5d..b0a8a74 100644 --- a/shell/functions +++ b/shell/functions @@ -8,7 +8,6 @@ section_start() { echo "${1}" } -# Detect the AUR helper once; prefer paru, then pamac, then yay. detect_aur_helper() { local h for h in paru pamac yay; do @@ -21,15 +20,16 @@ detect_aur_helper() { return 1 } -# Install packages via the detected helper. paru/yay must not run under sudo. -# Set DRY=1 to print the resolved command instead of running it. arch_install() { [[ -n "${AUR_HELPER:-}" ]] || detect_aur_helper || return 1 local cmd case "${AUR_HELPER}" in - paru | yay) cmd=("${AUR_HELPER}" -S --noconfirm --needed "$@") ;; - pamac) cmd=(sudo pamac install --no-confirm --no-upgrade "$@") ;; - *) echo "Unknown AUR helper: ${AUR_HELPER}" >&2; return 1 ;; + paru | yay) cmd=("${AUR_HELPER}" -S --noconfirm --needed "$@") ;; + pamac) cmd=(sudo pamac install --no-confirm --no-upgrade "$@") ;; + *) + echo "Unknown AUR helper: ${AUR_HELPER}" >&2 + return 1 + ;; esac if [[ -n "${DRY:-}" ]]; then echo "${cmd[*]}" @@ -38,7 +38,6 @@ arch_install() { "${cmd[@]}" } -# Enable AUR support where the helper needs it (pamac only). enable_aur() { [[ -n "${AUR_HELPER:-}" ]] || detect_aur_helper || return 1 [[ "${AUR_HELPER}" == pamac ]] || return 0 @@ -46,8 +45,6 @@ enable_aur() { sudo sed -Ei '/EnableAUR/s/^#//' /etc/pamac.conf } -# True on Arch and derivatives. Sourced in a subshell so os-release vars don't leak. -# shellcheck disable=SC1091 is_arch() { local id id_like id="$(. /etc/os-release 2>/dev/null && echo "${ID:-}")"