feat(installer): replace number-input checklist with scrollable TUI

Arrow keys navigate a viewport-bounded list, Space toggles items,
Enter/n confirms — fixes overflow on the app selection screen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
Amir Alexander Abdelbaki 2026-05-21 22:58:02 +02:00
parent cdccc7634a
commit 379dfc4885
1 changed files with 131 additions and 32 deletions

View File

@ -75,54 +75,153 @@ tui_input() {
# tui_checklist TITLE PROMPT tag desc state tag desc state ...
# state: "on" | "off" | "header" (header = section label, tag is ignored)
# Prints space-separated selected tags to stdout.
# Arrow keys navigate · Space toggles · a selects all · Enter/n confirms
tui_checklist() {
local title="$1" prompt="$2"; shift 2
local -a _T _D _S
while [[ $# -ge 3 ]]; do _T+=("$1"); _D+=("$2"); _S+=("$3"); shift 3; done
local n=${#_T[@]}
local _tv _vp _cur _scr _VIS _k _k2 _k3 _k4 _i _j _f _l _chk _res _toti _posi i
while true; do
_tv=$(tput lines 2>/dev/null || echo 24)
_vp=$(( _tv - 11 ))
(( _vp < 4 )) && _vp=4
_toti=0
for (( i=0; i<n; i++ )); do [[ "${_S[$i]}" != "header" ]] && (( _toti++ )); done
_cur=0
for (( i=0; i<n; i++ )); do
[[ "${_S[$i]}" != "header" ]] && { _cur=$i; break; }
done
_scr=0; _VIS=0
# Count visual lines from index _s up to (not including) _e; result in _VIS
__cl_vis() {
local _s=$1 _e=$2; _VIS=0
for (( i=_s; i<_e && i<n; i++ )); do
if [[ "${_S[$i]}" == "header" ]]; then
(( i == _s )) && (( _VIS++ )) || (( _VIS+=2 ))
else
(( _VIS++ ))
fi
done
}
# Ensure _cur is within the visible viewport; adjust _scr if needed
__cl_sync() {
(( _cur < _scr )) && _scr=$_cur
while true; do
__cl_vis $_scr $(( _cur + 1 ))
(( _VIS <= _vp )) && break
(( _scr < n-1 )) || break
(( _scr++ ))
done
}
__cl_draw() {
_hdr
printf " ${M}${B}%s${R}\n" "$title" >/dev/tty; _sep
printf " ${M}${B}%s${R}\n" "$title" >/dev/tty
_sep
printf " ${D}%s${R}\n\n" "$prompt" >/dev/tty
local -a sel=()
local i num=0
for (( i=0; i<n; i++ )); do
_l=0
for (( i=_scr; i<n; i++ )); do
if [[ "${_S[$i]}" == "header" ]]; then
printf "\n ${M}── %s${R}\n" "${_D[$i]}" >/dev/tty
continue
fi
sel+=("$i"); (( num++ ))
if [[ "${_S[$i]}" == "on" ]]; then
printf " ${CY}%3d) [*] %-22s${R} %s\n" "$num" "${_T[$i]}" "${_D[$i]}" >/dev/tty
if (( i == _scr )); then
(( _l + 1 > _vp )) && break
printf " ${M}── %s${R}\n" "${_D[$i]}" >/dev/tty
(( _l++ ))
else
(( _l + 2 > _vp )) && break
printf "\n ${M}── %s${R}\n" "${_D[$i]}" >/dev/tty
(( _l+=2 ))
fi
else
printf " %3d) [ ] %-22s %s\n" "$num" "${_T[$i]}" "${_D[$i]}" >/dev/tty
(( _l + 1 > _vp )) && break
_chk=" "; [[ "${_S[$i]}" == "on" ]] && _chk="*"
if (( i == _cur )); then
printf " ${CY}▶ [%s] %-22s${R} %s\n" "$_chk" "${_T[$i]}" "${_D[$i]}" >/dev/tty
else
printf " [%s] %-22s %s\n" "$_chk" "${_T[$i]}" "${_D[$i]}" >/dev/tty
fi
(( _l++ ))
fi
done
local total_sel=${#sel[@]}
printf "\n ${D}Number(s) to toggle · 'a' all · 'n' none · Enter confirm:${R}\n > " >/dev/tty
local inp; read -r inp </dev/tty
_posi=0
for (( i=0; i<_cur; i++ )); do [[ "${_S[$i]}" != "header" ]] && (( _posi++ )); done
(( _posi++ ))
printf "\n ${D}↑↓ navigate Space toggle a all Enter/n confirm [%d/%d]${R}\n" \
"$_posi" "$_toti" >/dev/tty
}
[[ -z "$inp" ]] && break
case "$inp" in
a) for (( i=0; i<n; i++ )); do [[ "${_S[$i]}" != "header" ]] && _S[$i]="on"; done; continue ;;
n) for (( i=0; i<n; i++ )); do [[ "${_S[$i]}" != "header" ]] && _S[$i]="off"; done; continue ;;
while true; do
__cl_sync
__cl_draw
IFS= read -rsn1 _k </dev/tty
if [[ "$_k" == $'\e' ]]; then
IFS= read -rsn1 -t 0.05 _k2 </dev/tty
if [[ "$_k2" == '[' ]]; then
IFS= read -rsn1 -t 0.05 _k3 </dev/tty
if [[ "$_k3" =~ [0-9] ]]; then
IFS= read -rsn1 -t 0.05 _k4 </dev/tty
_k="${_k}${_k2}${_k3}${_k4}"
else
_k="${_k}${_k2}${_k3}"
fi
else
_k="${_k}${_k2}"
fi
fi
case "$_k" in
$'\e[A') # Up arrow
for (( _i=_cur-1; _i>=0; _i-- )); do
[[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; }
done ;;
$'\e[B') # Down arrow
for (( _i=_cur+1; _i<n; _i++ )); do
[[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; }
done ;;
$'\e[5~') # Page Up
for (( _j=0; _j < _vp/2; _j++ )); do
_f=0
for (( _i=_cur-1; _i>=0; _i-- )); do
[[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; _f=1; break; }
done
(( _f == 0 )) && break
done ;;
$'\e[6~') # Page Down
for (( _j=0; _j < _vp/2; _j++ )); do
_f=0
for (( _i=_cur+1; _i<n; _i++ )); do
[[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; _f=1; break; }
done
(( _f == 0 )) && break
done ;;
$'\e[H') # Home
for (( _i=0; _i<n; _i++ )); do
[[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; }
done; _scr=0 ;;
$'\e[F') # End
for (( _i=n-1; _i>=0; _i-- )); do
[[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; }
done ;;
' ') # Space — toggle current item
[[ "${_S[$_cur]}" == "on" ]] && _S[$_cur]="off" || _S[$_cur]="on" ;;
'a') # Select all
for (( _i=0; _i<n; _i++ )); do
[[ "${_S[$_i]}" != "header" ]] && _S[$_i]="on"
done ;;
''|'n') break ;; # Enter or n — confirm
esac
for tok in $inp; do
if [[ "$tok" =~ ^[0-9]+$ ]]; then
local k=$(( tok - 1 ))
(( k >= 0 && k < total_sel )) || continue
local idx="${sel[$k]}"
[[ "${_S[$idx]}" == "on" ]] && _S[$idx]="off" || _S[$idx]="on"
fi
done
done
local res=""
for (( i=0; i<n; i++ )); do [[ "${_S[$i]}" == "on" ]] && res+="${_T[$i]} "; done
printf '%s' "${res% }"
_res=""
for (( i=0; i<n; i++ )); do [[ "${_S[$i]}" == "on" ]] && _res+="${_T[$i]} "; done
printf '%s' "${_res% }"
}
# tui_menu TITLE PROMPT tag desc tag desc ...
@ -348,7 +447,7 @@ if $ANSWERFILE_MODE; then
COMPONENTS="$AF_COMPONENTS"
else
COMPONENTS=$(tui_checklist " Select Components " \
"Number to toggle · Enter to confirm" \
"Select system components to install" \
"pkg" "Package managers yay · nvm · rust" on \
"core" "Core packages 100+ base system packages" on \
"svc" "Core services NetworkManager · cronie · fail2ban" on \
@ -377,7 +476,7 @@ if $ANSWERFILE_MODE; then
SELECTED_APPS="$AF_APPS"
else
SELECTED_APPS=$(tui_checklist " Applications " \
"Optional applications — number to toggle, Enter to confirm:" \
"Select optional applications to install" \
\
"" "AI / LLM" header \
"ollama" "Ollama local LLM runner + API server" off \