From aa58cce1bb98b2eaf0ae0e81db722ddc331cab69 Mon Sep 17 00:00:00 2001 From: srsholmes Date: Tue, 14 Jul 2026 14:17:18 +0100 Subject: [PATCH 1/5] fix(install): read prompts from /dev/tty so curl|sh can answer them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README install/uninstall are documented as `curl -fsSL … | sh`. Under a pipe the shell's stdin is the script text, not the user, so `[ -t 0 ]` was false and prompt_yn silently returned its default every time — the prompts were unanswerable via the one command we tell people to run. Three bugs fell out of that, all invisible: - Phase 2 (input group + InputPlumber) could never run, so a curl|sh install left the overlay unable to capture controllers. - Re-running the install to upgrade always took the default on "binary already exists. Overwrite?" — i.e. no, silently keeping the old binary forever. - The uninstaller could never ask about removing plugin data or config. Read the controlling terminal instead, which exists under both `curl|sh` and `sh install.sh`. With genuinely no terminal (CI, systemd) prompts still fall back to defaults rather than hanging. prompt_yn grows a separate non-interactive default ($3) so the Phase 2 gate can treat as consent while still refusing to sudo, install a package and mask HHD unattended. Phase 2 and InputPlumber now default to yes on : the overlay can't capture controllers without them, so the single documented command should get a working install. This also drops phase2_input_group's bespoke read, whose "Non-interactive (curl|sh): default to yes" branch was unreachable for the exact case it named — the outer gate defaulted to no and phase2 never ran. Co-Authored-By: Claude Opus 4.8 --- scripts/install.sh | 74 ++++++++++++++++++++++++++++---------------- scripts/uninstall.sh | 20 +++++++++--- 2 files changed, 64 insertions(+), 30 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index 45e90b1c..f013e547 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -237,19 +237,44 @@ verify_sha256() { return 0 } -# Prompt user for yes/no (defaults to $2 if provided, otherwise no) +# Where prompts read from. Under the documented `curl … | sh` install, +# stdin is the script text itself, so `[ -t 0 ]` is false and a bare +# `read` would eat the script rather than the user's answer — every +# prompt silently took its default (Phase 2 could never run). Read the +# controlling terminal instead, which exists in both the piped and the +# `sh install.sh` cases. Empty only when there's genuinely no terminal +# (CI, systemd, `docker run -d`), where prompts fall back to defaults. +PROMPT_TTY="" +if { true < /dev/tty; } 2>/dev/null; then + PROMPT_TTY=/dev/tty +fi + +# Prompt user for yes/no. y/Y accepts, anything else declines. +# $1 — prompt text +# $2 — default taken on a bare (default: n) +# $3 — default taken when there is no terminal to ask on, e.g. CI or +# systemd (default: $2). Kept separate from $2 so a prompt can +# treat as consent while still refusing to take a +# sudo/system-modifying action unattended. prompt_yn() { - if [ ! -t 0 ]; then - # Non-interactive: use default - case "${2:-n}" in + if [ -z "$PROMPT_TTY" ]; then + # No terminal to ask on: use the non-interactive default + case "${3:-${2:-n}}" in [Yy]*) return 0 ;; *) return 1 ;; esac fi - printf "%s " "$1" - read -r answer + printf "%s " "$1" > "$PROMPT_TTY" + # EOF on the terminal is treated as the default rather than hanging. + read -r answer < "$PROMPT_TTY" || answer="" case "$answer" in [Yy]*) return 0 ;; + "") + case "${2:-n}" in + [Yy]*) return 0 ;; + *) return 1 ;; + esac + ;; *) return 1 ;; esac } @@ -1130,25 +1155,14 @@ phase2_input_group() { info "controller buttons. That requires membership in the 'input' group." echo "" - # Default-yes: empty answer (just ) accepts. Honors the (Y/n) - # hint without needing to restructure the shared prompt_yn helper. - INPUT_GROUP_ANSWER="" - if [ -t 0 ]; then - printf "Add %s to the 'input' group? (needed for the overlay to capture controller buttons) (Y/n) " "$USER" - read -r INPUT_GROUP_ANSWER - else - # Non-interactive (curl|sh): default to yes since the overlay is - # useless without it. - INPUT_GROUP_ANSWER="y" + # Default-yes: a bare accepts, since the overlay can't capture + # controllers without it. + if ! prompt_yn "Add $USER to the 'input' group? (needed for the overlay to capture controller buttons) (Y/n)" "y"; then + warn "Skipped. You'll need to add yourself to 'input' manually before the overlay can grab controllers:" + warn " sudo usermod -aG input \"\$USER\"" + warn " # then log out and back in" + return fi - case "$INPUT_GROUP_ANSWER" in - [Nn]*) - warn "Skipped. You'll need to add yourself to 'input' manually before the overlay can grab controllers:" - warn " sudo usermod -aG input \"\$USER\"" - warn " # then log out and back in" - return - ;; - esac info "Adding $USER to the input group (requires sudo)..." if sudo usermod -aG input "$USER"; then @@ -1215,7 +1229,11 @@ phase2_inputplumber() { info "it conflicts with IP over controller HID ownership." echo "" - if ! prompt_yn "Install / enable InputPlumber now? (y/N)"; then + # Default-yes on to match the Phase 2 gate: controller wake + # in-game doesn't work without IP, and the guard above already made + # this a no-op when IP is installed and uncontested. The gate is what + # refuses to run any of this unattended, so no separate default here. + if ! prompt_yn "Install / enable InputPlumber now? (Y/n)" "y"; then info "Skipping. You can do this later from the welcome wizard." return fi @@ -1243,7 +1261,11 @@ main() { phase1 echo "" - if prompt_yn "Run Phase 2 (input group + InputPlumber)? May require sudo. (y/N)"; then + # Default-yes on : without Phase 2 the overlay can't capture + # controllers, so the common single-command install should get it. + # Still declined with no terminal — this shells out to sudo, installs a + # package and masks HHD, none of which should happen unattended. + if prompt_yn "Run Phase 2 (input group + InputPlumber)? May require sudo. (Y/n)" "y" "n"; then phase2 else info "Skipping Phase 2." diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 033ba663..453d1da6 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -64,14 +64,26 @@ error() { printf "${RED}[ERROR]${NC} %s\n" "$1" } +# Where prompts read from. Under the documented `curl … | sh` uninstall, +# stdin is the script text itself, so `[ -t 0 ]` is false and a bare +# `read` would eat the script rather than the user's answer — the +# plugin-data and config prompts below could never be answered. Read the +# controlling terminal instead. Empty only when there's genuinely no +# terminal (CI, systemd), where prompts fall back to the default (no). +PROMPT_TTY="" +if { true < /dev/tty; } 2>/dev/null; then + PROMPT_TTY=/dev/tty +fi + # Prompt user for yes/no (defaults to no) prompt_yn() { - if [ ! -t 0 ]; then - # Non-interactive: use default (no) + if [ -z "$PROMPT_TTY" ]; then + # No terminal to ask on: use default (no) return 1 fi - printf "%s " "$1" - read -r answer + printf "%s " "$1" > "$PROMPT_TTY" + # EOF on the terminal is treated as the default rather than hanging. + read -r answer < "$PROMPT_TTY" || answer="" case "$answer" in [Yy]*) return 0 ;; *) return 1 ;; From cbbd596f635b2ea0d970b99c642766e4854c1b26 Mon Sep 17 00:00:00 2001 From: Simon Holmes Date: Tue, 14 Jul 2026 14:57:28 +0100 Subject: [PATCH 2/5] fix(install): make Phase 2's promises true before defaulting it to yes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /dev/tty fix is what makes these prompts reachable at all, which is right -- but it also promotes Phase 2 from dead code to default-yes, and Phase 2 was making two claims that were not true. Reachable + untrue is a much worse combination than unreachable + untrue, so fix the claims. 1. SteamOS: the text promised "IP ships disabled -- this step leaves it untouched ... so it stays opt-in", while both installer paths ended in `systemctl enable --now inputplumber.service`. On a stock Deck IP is installed but inactive, so the short-circuit guard doesn't fire, and would enable IP and claim the built-in controller -- exactly what the text said it wouldn't, and what the design says belongs to the user in-app when they pick a wake button. Add IP_ENABLE (default 1) to the IP installer and route every enable/start path through a single ip_enable(). install.sh passes IP_ENABLE=0 on ID=steamos, so the promise is now real. The plugin backend keeps the default and still enables IP for the in-app flow. Passed via `sudo env IP_ENABLE=…`, not `sudo IP_ENABLE=…`: the latter is subject to sudoers' env_reset and can be silently dropped, which would default back to 1 and re-enable IP on a Deck -- the exact bug this fixes, reintroduced by a stripped variable. 2. HHD: install.sh claimed the installer "stops + masks any conflicting hhd*.service units". Nothing in the tree has ever done that -- the only hhd code is the guard that checks whether it's active. On Bazzite (rpm-ostree -> tarball path -> enable) HHD keeps running and IP comes up alongside it, both contesting controller HID: the precise conflict the text claimed was handled. Refuse instead of implementing it. Bazzite ships HHD as a base package, and masking it takes the user's TDP/RGB/controller stack down with it -- not something to do because someone pressed . Detect active HHD, explain the conflict, point at the one-liner, and skip. CachyOS is unaffected and still gets what it needs: no HHD, ID != steamos => IP_ENABLE=1 => pacman install + enable, exactly as before. Verified on SteamOS: ID=steamos resolves to IP_ENABLE=0; ip_enable() skips enable/restart at 0 and runs both at 1; no enable path remains outside the helper; no HHD active so the normal prompt path is unchanged. Both scripts pass syntax checks. Bazzite is reasoned, not measured -- I have no Bazzite device here. Co-Authored-By: Claude Opus 4.8 --- .../scripts/install-inputplumber.sh | 41 ++++++++++---- scripts/install.sh | 56 +++++++++++++++---- 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/plugins/input-plumber/scripts/install-inputplumber.sh b/plugins/input-plumber/scripts/install-inputplumber.sh index 3ebde8e4..663a8e35 100755 --- a/plugins/input-plumber/scripts/install-inputplumber.sh +++ b/plugins/input-plumber/scripts/install-inputplumber.sh @@ -20,6 +20,13 @@ # and short-circuits when InputPlumber is already on disk. # # Usage: bash scripts/install-inputplumber.sh +# IP_ENABLE=0 bash scripts/install-inputplumber.sh # install only +# +# IP_ENABLE=0 installs InputPlumber but leaves the unit disabled and +# stopped. SteamOS needs this: IP ships with the image but deliberately +# disabled, and enabling it claims the Deck's built-in controller — a +# choice that belongs to the user in-app when they pick a wake button, +# not to the installer. Defaults to 1 (install + enable) everywhere else. # # The loadout backend runs as root, so no sudo / id-check is needed — # the script is invoked directly by the plugin backend. @@ -30,6 +37,25 @@ # behaviour. Real failure paths use explicit `exit 1`. set -eu +# Install-only mode (see the header). Every path that would start the +# daemon goes through ip_enable() so there is one place to honour this. +IP_ENABLE="${IP_ENABLE:-1}" + +# Enable + start inputplumber.service, unless we were asked to install +# only. Called at the end of each install path. +ip_enable() { + systemctl daemon-reload + if [ "$IP_ENABLE" = "0" ]; then + log "IP_ENABLE=0 — installed but leaving inputplumber.service disabled" + log "enable it later with: systemctl enable --now inputplumber.service" + return 0 + fi + log "enabling and starting inputplumber.service" + systemctl enable inputplumber.service + systemctl restart inputplumber.service + log "done — inputplumber.service is running" +} + REPO="ShadowBlip/InputPlumber" TARBALL="inputplumber-x86_64.tar.gz" SHA256_FILE="${TARBALL}.sha256.txt" @@ -98,9 +124,7 @@ if [ "$IS_RPM_OSTREE" -eq 0 ] && command -v pacman >/dev/null 2>&1 && [[ "$HAYST else pacman -S --noconfirm --needed inputplumber || die "pacman install failed" fi - systemctl daemon-reload - systemctl enable --now inputplumber.service - log "done — inputplumber.service is running" + ip_enable exit 0 fi @@ -108,9 +132,7 @@ if [ "$IS_RPM_OSTREE" -eq 0 ] && command -v dnf >/dev/null 2>&1 && [[ "$HAYSTACK log "── dnf install inputplumber ──" # `dnf install` is non-interactive with -y and no-op if installed. if dnf install -y inputplumber 2>&1; then - systemctl daemon-reload - systemctl enable --now inputplumber.service - log "done — inputplumber.service is running" + ip_enable exit 0 fi warn "dnf could not install inputplumber from configured repos — falling through to tarball" @@ -282,11 +304,6 @@ EOF # Reload dbus so the new policy is in effect before the daemon starts. systemctl reload dbus.service 2>/dev/null || systemctl reload dbus-broker.service 2>/dev/null || true -log "systemctl daemon-reload" -systemctl daemon-reload -log "enabling and starting inputplumber.service" -systemctl enable inputplumber.service -systemctl restart inputplumber.service +ip_enable -log "done — inputplumber.service is running" log "verify with: systemctl status inputplumber.service" diff --git a/scripts/install.sh b/scripts/install.sh index f013e547..eb3088cd 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1189,8 +1189,12 @@ phase2_input_group() { # Delegates to the input-plumber plugin's own installer # (./plugins/input-plumber/scripts/install-inputplumber.sh), which: # - Detects + installs IP via pacman / dnf / upstream tarball -# - Enables + starts inputplumber.service -# - Stops + masks any conflicting hhd*.service units +# - Enables + starts inputplumber.service, unless IP_ENABLE=0 (SteamOS, +# where IP ships disabled and enabling it is an in-app choice) +# +# It does NOT touch HHD. An earlier version of this comment claimed it +# stopped and masked conflicting hhd*.service units; nothing ever did. +# We refuse to install alongside an active HHD instead — see below. # Idempotent — re-running is safe. phase2_inputplumber() { echo "" @@ -1212,21 +1216,45 @@ phase2_inputplumber() { return fi + # Refuse rather than fight. IP and HHD both want ownership of the + # controller HID, so enabling IP under a live HHD breaks input on the + # very devices this is meant to help. Bazzite ships HHD as a base + # package, so this is its normal state, not an edge case. Masking HHD + # would take the user's TDP / RGB / controller stack down with it — + # not something to do because someone pressed . Their call. + if systemctl list-units --plain --no-legend --type=service --state=active 'hhd*' 2>/dev/null | grep -q hhd; then + warn "Handheld Daemon (HHD) is active. It contests controller HID ownership" + warn "with InputPlumber, so Loadout won't install IP alongside it." + warn "If you want IP, disable HHD yourself first:" + warn " sudo systemctl mask --now hhd@\$USER.service" + warn "Then re-run this step from the welcome wizard." + return + fi + + # SteamOS ships IP with the image but deliberately disabled, and + # enabling it claims the Deck's built-in controller. That's the user's + # choice in-app when they pick a wake button, not ours at install time. + # Everywhere else, install + enable is the useful default. + case "$(. /etc/os-release 2>/dev/null && printf '%s' "${ID:-}")" in + steamos) IP_ENABLE=0 ;; + *) IP_ENABLE=1 ;; + esac + info "Loadout uses InputPlumber to route a controller button to the" info "overlay when you're in a game (Steam Input owns the controller in" info "big-picture / gamescope, so a daemon below it is the only way to" info "produce an event the overlay can see). Required on handhelds;" info "optional on a plain desktop." info "" - info "On Bazzite IP is already installed — this is a no-op." - info "On SteamOS Deck IP is already installed but ships disabled — this" - info "step leaves it untouched. Enabling it (and claiming the Deck's" - info "built-in controller) happens in-app when you choose an overlay wake" - info "button, so it stays opt-in." - info "On CachyOS/Arch this installs the pacman package." - info "" - info "If Handheld Daemon (HHD) is running it'll be stopped and masked —" - info "it conflicts with IP over controller HID ownership." + if [ "$IP_ENABLE" = "0" ]; then + info "On SteamOS Deck IP is already installed but ships disabled — this" + info "step leaves it disabled. Enabling it (and claiming the Deck's" + info "built-in controller) happens in-app when you choose an overlay" + info "wake button, so it stays opt-in." + else + info "On CachyOS/Arch this installs the pacman package, then enables" + info "and starts inputplumber.service." + fi echo "" # Default-yes on to match the Phase 2 gate: controller wake @@ -1239,7 +1267,11 @@ phase2_inputplumber() { fi info "Running $(basename "$IP_SCRIPT") (requires sudo)..." - if sudo bash "$IP_SCRIPT"; then + # `sudo env VAR=…` rather than `sudo VAR=… `: the latter is subject to + # sudoers' env_reset and can be silently dropped, which would leave the + # script defaulting to IP_ENABLE=1 and enabling IP on a Deck anyway. + # Running env(1) as the command sets it unconditionally. + if sudo env IP_ENABLE="$IP_ENABLE" bash "$IP_SCRIPT"; then success "InputPlumber setup complete." else warn "InputPlumber setup script returned non-zero — check the log above." From 3696c2810f29ac00ea6b4cd3d379ed59cf6baaf2 Mon Sep 17 00:00:00 2001 From: Simon Holmes Date: Wed, 15 Jul 2026 09:55:08 +0100 Subject: [PATCH 3/5] Revert "fix(install): make Phase 2's promises true before defaulting it to yes" This reverts commit cbbd596f635b2ea0d970b99c642766e4854c1b26. --- .../scripts/install-inputplumber.sh | 41 ++++---------- scripts/install.sh | 56 ++++--------------- 2 files changed, 24 insertions(+), 73 deletions(-) diff --git a/plugins/input-plumber/scripts/install-inputplumber.sh b/plugins/input-plumber/scripts/install-inputplumber.sh index 663a8e35..3ebde8e4 100755 --- a/plugins/input-plumber/scripts/install-inputplumber.sh +++ b/plugins/input-plumber/scripts/install-inputplumber.sh @@ -20,13 +20,6 @@ # and short-circuits when InputPlumber is already on disk. # # Usage: bash scripts/install-inputplumber.sh -# IP_ENABLE=0 bash scripts/install-inputplumber.sh # install only -# -# IP_ENABLE=0 installs InputPlumber but leaves the unit disabled and -# stopped. SteamOS needs this: IP ships with the image but deliberately -# disabled, and enabling it claims the Deck's built-in controller — a -# choice that belongs to the user in-app when they pick a wake button, -# not to the installer. Defaults to 1 (install + enable) everywhere else. # # The loadout backend runs as root, so no sudo / id-check is needed — # the script is invoked directly by the plugin backend. @@ -37,25 +30,6 @@ # behaviour. Real failure paths use explicit `exit 1`. set -eu -# Install-only mode (see the header). Every path that would start the -# daemon goes through ip_enable() so there is one place to honour this. -IP_ENABLE="${IP_ENABLE:-1}" - -# Enable + start inputplumber.service, unless we were asked to install -# only. Called at the end of each install path. -ip_enable() { - systemctl daemon-reload - if [ "$IP_ENABLE" = "0" ]; then - log "IP_ENABLE=0 — installed but leaving inputplumber.service disabled" - log "enable it later with: systemctl enable --now inputplumber.service" - return 0 - fi - log "enabling and starting inputplumber.service" - systemctl enable inputplumber.service - systemctl restart inputplumber.service - log "done — inputplumber.service is running" -} - REPO="ShadowBlip/InputPlumber" TARBALL="inputplumber-x86_64.tar.gz" SHA256_FILE="${TARBALL}.sha256.txt" @@ -124,7 +98,9 @@ if [ "$IS_RPM_OSTREE" -eq 0 ] && command -v pacman >/dev/null 2>&1 && [[ "$HAYST else pacman -S --noconfirm --needed inputplumber || die "pacman install failed" fi - ip_enable + systemctl daemon-reload + systemctl enable --now inputplumber.service + log "done — inputplumber.service is running" exit 0 fi @@ -132,7 +108,9 @@ if [ "$IS_RPM_OSTREE" -eq 0 ] && command -v dnf >/dev/null 2>&1 && [[ "$HAYSTACK log "── dnf install inputplumber ──" # `dnf install` is non-interactive with -y and no-op if installed. if dnf install -y inputplumber 2>&1; then - ip_enable + systemctl daemon-reload + systemctl enable --now inputplumber.service + log "done — inputplumber.service is running" exit 0 fi warn "dnf could not install inputplumber from configured repos — falling through to tarball" @@ -304,6 +282,11 @@ EOF # Reload dbus so the new policy is in effect before the daemon starts. systemctl reload dbus.service 2>/dev/null || systemctl reload dbus-broker.service 2>/dev/null || true -ip_enable +log "systemctl daemon-reload" +systemctl daemon-reload +log "enabling and starting inputplumber.service" +systemctl enable inputplumber.service +systemctl restart inputplumber.service +log "done — inputplumber.service is running" log "verify with: systemctl status inputplumber.service" diff --git a/scripts/install.sh b/scripts/install.sh index eb3088cd..f013e547 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1189,12 +1189,8 @@ phase2_input_group() { # Delegates to the input-plumber plugin's own installer # (./plugins/input-plumber/scripts/install-inputplumber.sh), which: # - Detects + installs IP via pacman / dnf / upstream tarball -# - Enables + starts inputplumber.service, unless IP_ENABLE=0 (SteamOS, -# where IP ships disabled and enabling it is an in-app choice) -# -# It does NOT touch HHD. An earlier version of this comment claimed it -# stopped and masked conflicting hhd*.service units; nothing ever did. -# We refuse to install alongside an active HHD instead — see below. +# - Enables + starts inputplumber.service +# - Stops + masks any conflicting hhd*.service units # Idempotent — re-running is safe. phase2_inputplumber() { echo "" @@ -1216,45 +1212,21 @@ phase2_inputplumber() { return fi - # Refuse rather than fight. IP and HHD both want ownership of the - # controller HID, so enabling IP under a live HHD breaks input on the - # very devices this is meant to help. Bazzite ships HHD as a base - # package, so this is its normal state, not an edge case. Masking HHD - # would take the user's TDP / RGB / controller stack down with it — - # not something to do because someone pressed . Their call. - if systemctl list-units --plain --no-legend --type=service --state=active 'hhd*' 2>/dev/null | grep -q hhd; then - warn "Handheld Daemon (HHD) is active. It contests controller HID ownership" - warn "with InputPlumber, so Loadout won't install IP alongside it." - warn "If you want IP, disable HHD yourself first:" - warn " sudo systemctl mask --now hhd@\$USER.service" - warn "Then re-run this step from the welcome wizard." - return - fi - - # SteamOS ships IP with the image but deliberately disabled, and - # enabling it claims the Deck's built-in controller. That's the user's - # choice in-app when they pick a wake button, not ours at install time. - # Everywhere else, install + enable is the useful default. - case "$(. /etc/os-release 2>/dev/null && printf '%s' "${ID:-}")" in - steamos) IP_ENABLE=0 ;; - *) IP_ENABLE=1 ;; - esac - info "Loadout uses InputPlumber to route a controller button to the" info "overlay when you're in a game (Steam Input owns the controller in" info "big-picture / gamescope, so a daemon below it is the only way to" info "produce an event the overlay can see). Required on handhelds;" info "optional on a plain desktop." info "" - if [ "$IP_ENABLE" = "0" ]; then - info "On SteamOS Deck IP is already installed but ships disabled — this" - info "step leaves it disabled. Enabling it (and claiming the Deck's" - info "built-in controller) happens in-app when you choose an overlay" - info "wake button, so it stays opt-in." - else - info "On CachyOS/Arch this installs the pacman package, then enables" - info "and starts inputplumber.service." - fi + info "On Bazzite IP is already installed — this is a no-op." + info "On SteamOS Deck IP is already installed but ships disabled — this" + info "step leaves it untouched. Enabling it (and claiming the Deck's" + info "built-in controller) happens in-app when you choose an overlay wake" + info "button, so it stays opt-in." + info "On CachyOS/Arch this installs the pacman package." + info "" + info "If Handheld Daemon (HHD) is running it'll be stopped and masked —" + info "it conflicts with IP over controller HID ownership." echo "" # Default-yes on to match the Phase 2 gate: controller wake @@ -1267,11 +1239,7 @@ phase2_inputplumber() { fi info "Running $(basename "$IP_SCRIPT") (requires sudo)..." - # `sudo env VAR=…` rather than `sudo VAR=… `: the latter is subject to - # sudoers' env_reset and can be silently dropped, which would leave the - # script defaulting to IP_ENABLE=1 and enabling IP on a Deck anyway. - # Running env(1) as the command sets it unconditionally. - if sudo env IP_ENABLE="$IP_ENABLE" bash "$IP_SCRIPT"; then + if sudo bash "$IP_SCRIPT"; then success "InputPlumber setup complete." else warn "InputPlumber setup script returned non-zero — check the log above." From ef17e36e744d8f3a1011ec5c63b3c500c626e8e0 Mon Sep 17 00:00:00 2001 From: Simon Holmes Date: Wed, 15 Jul 2026 09:55:51 +0100 Subject: [PATCH 4/5] docs(install): stop claiming Phase 2 masks HHD, and record why IP is safe Text and comments only -- no behaviour change. install.sh told the user "If Handheld Daemon (HHD) is running it'll be stopped and masked", and the function comment claimed the installer "stops + masks any conflicting hhd*.service units". Nothing in the tree has ever done either; the only hhd code is the guard that checks whether it's active. Drop both claims rather than implement them -- masking HHD takes a user's TDP/RGB/controller stack down with it, which is not something to do because someone pressed . Also record what actually makes the surrounding promises true, because it isn't obvious and I got it wrong reading this the first time: install-inputplumber.sh's detect_installed() exits "nothing to do" before the package-manager branch whenever IP is on PATH. SteamOS ships IP with the image (disabled) and Bazzite ships it too, so on both the installer short-circuits before it can enable anything. That -- not the prose -- is what keeps "leaves it untouched" honest on a Deck and what stops IP being enabled alongside a live HHD. Noted so the gate doesn't get removed as a redundant optimisation. Co-Authored-By: Claude Opus 4.8 --- scripts/install.sh | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index f013e547..1fa36ea5 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1188,9 +1188,19 @@ phase2_input_group() { # # Delegates to the input-plumber plugin's own installer # (./plugins/input-plumber/scripts/install-inputplumber.sh), which: -# - Detects + installs IP via pacman / dnf / upstream tarball -# - Enables + starts inputplumber.service -# - Stops + masks any conflicting hhd*.service units +# - Exits early ("nothing to do") when IP is already on PATH +# - Otherwise installs IP via pacman / dnf / upstream tarball, then +# enables + starts inputplumber.service +# +# It does NOT touch HHD. This comment used to claim it "stops + masks any +# conflicting hhd*.service units"; nothing here has ever done that. +# +# That early exit is load-bearing, not an optimisation: SteamOS ships IP +# with the image (disabled), and Bazzite ships it too, so on both the +# installer short-circuits before it can enable anything. That is what +# makes the "leaves it untouched" promise below true and what keeps IP +# from being enabled alongside a live HHD. Don't remove detect_installed +# without replacing the guarantee. # Idempotent — re-running is safe. phase2_inputplumber() { echo "" @@ -1224,9 +1234,6 @@ phase2_inputplumber() { info "built-in controller) happens in-app when you choose an overlay wake" info "button, so it stays opt-in." info "On CachyOS/Arch this installs the pacman package." - info "" - info "If Handheld Daemon (HHD) is running it'll be stopped and masked —" - info "it conflicts with IP over controller HID ownership." echo "" # Default-yes on to match the Phase 2 gate: controller wake From 7f930b8a8620243d3d1eb253b47599e9aadbc903 Mon Sep 17 00:00:00 2001 From: Simon Holmes Date: Wed, 15 Jul 2026 11:34:17 +0100 Subject: [PATCH 5/5] =?UTF-8?q?fix(install):=20default-yes=20on=20binary?= =?UTF-8?q?=20overwrite=20=E2=80=94=20re-running=20the=20installer=20is=20?= =?UTF-8?q?the=20upgrade=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prompt is reachable now (/dev/tty), but an or headless re-run still kept the old binary. The binary lives in ~/.local/bin (no sudo) and the fresh-install path installs it without asking, so overwrite defaults to yes in both modes. Still declinable with n. Co-Authored-By: Claude Fable 5 --- scripts/install.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/install.sh b/scripts/install.sh index 1fa36ea5..2f89d1be 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -387,7 +387,11 @@ phase1() { # user to delete the binary first. INSTALL_BINARY=1 if [ -f "$BINARY_PATH" ]; then - if prompt_yn "Loadout binary already exists. Overwrite? (y/N)"; then + # Default-yes everywhere: re-running the documented install one-liner + # IS the upgrade path, so both a bare and a headless re-run + # should refresh the binary. It lives in ~/.local/bin (no sudo), same + # as the fresh-install path just below, which never asked. + if prompt_yn "Loadout binary already exists. Overwrite? (Y/n)" "y"; then info "Overwriting existing binary..." else info "Keeping existing binary (overlay + plugins will still be refreshed)."