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
parent
cdccc7634a
commit
379dfc4885
|
|
@ -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 \
|
||||
|
|
|
|||
Loading…
Reference in New Issue