fix(install): read prompts from /dev/tty so curl|sh can answer them#213
Conversation
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 <enter> as consent while still refusing to sudo, install a
package and mask HHD unattended. Phase 2 and InputPlumber now default to
yes on <enter>: 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 <noreply@anthropic.com>
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 <enter> 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 <enter>. 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 <noreply@anthropic.com>
The
|
…it to yes" This reverts commit cbbd596.
…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 <enter>. 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 <noreply@anthropic.com>
|
…ler is the upgrade path The prompt is reachable now (/dev/tty), but an <enter> 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 <noreply@anthropic.com>
Problem
The README documents install and uninstall as
curl -fsSL … | sh. Under apipe the shell reads the script itself from stdin, so
[ -t 0 ]is falseand
prompt_yntook its default every time. Every prompt in both scripts wasunanswerable via the exact command we tell people to run.
Three bugs fell out of that, all silent:
curl|shinstall always printedSkipping Phase 2— noinputgroup, no InputPlumber — leaving the overlayunable to capture controllers. The user only ever sees one
[INFO]lineabout it in a wall of output.
curl|shupgrades silently kept the old binary. Re-running the installhit
Loadout binary already exists. Overwrite? (y/N)→ default no →INSTALL_BINARY=0. The documented upgrade path never upgraded the binary.defaulted to no.
The intent was there but unreachable:
phase2_input_groupcarried a branchcommented "Non-interactive (curl|sh): default to yes since the overlay is
useless without it" — dead code for the exact case it named, because the outer
Phase 2 gate defaulted to no and
phase2never ran.Fix
Read the controlling terminal rather than stdin.
/dev/ttyexists underboth
curl … | shandsh install.sh, so prompts reach the user in both. Withgenuinely no terminal (CI, systemd,
docker run -d)PROMPT_TTYis empty andprompts fall back to defaults rather than hanging.
prompt_yngrows a separate non-interactive default ($3), kept distinctfrom the
<enter>default ($2). This lets the Phase 2 gate treat<enter>asconsent while still refusing to sudo, install a package and mask HHD
unattended — the gate is the single place that declines headless runs, so the
inner prompts don't each need their own guard.
Phase 2 and InputPlumber now default to yes on
<enter>: the overlay can'tcapture controllers without them, so the single documented command should
produce a working install. Both remain declinable with
n.Also drops
phase2_input_group's bespokereadin favour of the sharedhelper, deleting the unreachable branch described above.
Verification
prompt_ynextracted frominstall.sh(the real code, not a copy) and drivenunder a real PTY with the script piped into
sh— i.e. the exactcurl|shshape, where stdin is the pipe and
/dev/ttyis the terminal:curl|shin a terminalycurl|shin a terminal<enter>curl|shin a terminalnsetsid, stdin/dev/null)Phase 2 gate specifically (
"y" "n"):<enter>→ runs,n→ skips,no terminal → skips (no unattended sudo).
Confirmed reading
/dev/ttydoes not disturb the shell consuming the scriptfrom the pipe — the script runs to completion.
Both scripts pass
sh -n.Not covered: a full end-to-end
curl|shinstall against a live releasewasn't re-run, since the prompt path is exercised directly above.
Notes
Independent of #211 — that fixes an overlay GL segfault and touches the overlay
.serviceheredoc; this touchesprompt_yn/phase2~800 lines away. Noconflict expected.
🤖 Generated with Claude Code