From c83439f576c7413030551bec2767166751acd493 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 02:05:08 +0000 Subject: [PATCH] Replace whiptail menus with numbered text toggle selection All interactive prompts now use numbered toggles (type a number to toggle on/off, Enter to confirm) instead of requiring whiptail. Removed whiptail from apt dependencies in ubuntu-post-install.sh. https://claude.ai/code/session_012gDnantBmFTWZGCiKyjazx --- laptop_full_setup.sh | 133 ++++------ ubuntu-post-install.sh | 544 ++++++++++++++++++++--------------------- 2 files changed, 312 insertions(+), 365 deletions(-) diff --git a/laptop_full_setup.sh b/laptop_full_setup.sh index 55e037f..5508450 100755 --- a/laptop_full_setup.sh +++ b/laptop_full_setup.sh @@ -94,38 +94,24 @@ POSTINSTALL_SCRIPT="$SCRIPT_DIR/ubuntu-post-install.sh" HAS_POSTINSTALL=false [[ -f "$POSTINSTALL_SCRIPT" ]] && HAS_POSTINSTALL=true -if command -v whiptail &>/dev/null; then - WHIP_ARGS=() - $HAS_POSTINSTALL && WHIP_ARGS+=( - "postinstall" "Full system setup — Ubuntu apps, security, backups" OFF - ) - WHIP_ARGS+=( "aistack" "AI stack — select services on next screen" ON ) - SELECTED=$(whiptail --title "Local AI Setup" \ - --checklist "SPACE = toggle TAB = move ENTER = confirm" \ - 12 68 "${#WHIP_ARGS[@]}" "${WHIP_ARGS[@]}" 3>&1 1>&2 2>&3) || die "Cancelled." - INSTALL_AI=false - [[ "$SELECTED" == *"postinstall"* ]] && INSTALL_POSTINSTALL=true - [[ "$SELECTED" == *"aistack"* ]] && INSTALL_AI=true -else +echo "" +echo -e " ${BOLD}[1] Select what to run${NC}" +echo " (Type a number to toggle, Enter to confirm)" +SEL_POST=false; SEL_AI=true +while true; do echo "" - echo -e " ${BOLD}[1] Select what to run${NC}" - echo " (Type a number to toggle, Enter to confirm)" - SEL_POST=false; SEL_AI=true - while true; do - echo "" - $HAS_POSTINSTALL && printf " [%s] 1. Full system setup — Ubuntu apps, security, backups\n" "$($SEL_POST && echo '*' || echo ' ')" - printf " [%s] 2. AI stack — select services on next screen\n" "$($SEL_AI && echo '*' || echo ' ')" - echo "" - read -rp " Toggle [number] or Enter to confirm: " T - case "$T" in - 1) $HAS_POSTINSTALL && { $SEL_POST && SEL_POST=false || SEL_POST=true; } ;; - 2) $SEL_AI && SEL_AI=false || SEL_AI=true ;; - "") break ;; - esac - done - INSTALL_POSTINSTALL=$SEL_POST - INSTALL_AI=$SEL_AI -fi + $HAS_POSTINSTALL && printf " [%s] 1. Full system setup — Ubuntu apps, security, backups\n" "$($SEL_POST && echo '*' || echo ' ')" + printf " [%s] 2. AI stack — select services on next screen\n" "$($SEL_AI && echo '*' || echo ' ')" + echo "" + read -rp " Toggle [number] or Enter to confirm: " T + case "$T" in + 1) $HAS_POSTINSTALL && { $SEL_POST && SEL_POST=false || SEL_POST=true; } ;; + 2) $SEL_AI && SEL_AI=false || SEL_AI=true ;; + "") break ;; + esac +done +INSTALL_POSTINSTALL=$SEL_POST +INSTALL_AI=$SEL_AI $INSTALL_AI || $INSTALL_POSTINSTALL \ || die "Nothing selected — run again and select at least one option." @@ -137,62 +123,37 @@ if $INSTALL_AI; then docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^$1$" && echo ON || echo OFF } - if command -v whiptail &>/dev/null; then - SELECTED_SVCS=$(whiptail --title "AI Stack — Select Services" \ - --checklist "Ollama always installed (core). SPACE = toggle ENTER = confirm" \ - 20 72 9 \ - "open-webui" "Open WebUI — Chat interface (like ChatGPT, uses Ollama)" "$(existing_svc open-webui)" \ - "rag" "RAG + ChromaDB— Code & document search (needed by MCP)" "$(existing_svc rag-server)" \ - "mcp" "MCP Server — Claude Code tools (bash, files, git, search)" "$(existing_svc mcp-server)" \ - "searxng" "SearXNG — Private local web search engine" "$(existing_svc searxng)" \ - "gitea" "Gitea — Self-hosted Git server" "$(existing_svc gitea)" \ - "invokeai" "InvokeAI — Image generation (Stable Diffusion)" "$(existing_svc invokeai)" \ - "portainer" "Portainer — Docker management web UI" "$(existing_svc portainer)" \ - "kiwix" "Kiwix — Offline Wikipedia, Stack Overflow, Arch Wiki" "$(existing_svc kiwix)" \ - 3>&1 1>&2 2>&3) || die "Cancelled." - - SVC_WEBUI=false; [[ "$SELECTED_SVCS" == *'"open-webui"'* ]] && SVC_WEBUI=true - SVC_RAG=false; [[ "$SELECTED_SVCS" == *'"rag"'* ]] && SVC_RAG=true - SVC_MCP=false; [[ "$SELECTED_SVCS" == *'"mcp"'* ]] && SVC_MCP=true - SVC_SEARXNG=false; [[ "$SELECTED_SVCS" == *'"searxng"'* ]] && SVC_SEARXNG=true - SVC_GITEA=false; [[ "$SELECTED_SVCS" == *'"gitea"'* ]] && SVC_GITEA=true - SVC_INVOKEAI=false; [[ "$SELECTED_SVCS" == *'"invokeai"'* ]] && SVC_INVOKEAI=true - SVC_PORTAINER=false;[[ "$SELECTED_SVCS" == *'"portainer"'* ]] && SVC_PORTAINER=true - SVC_KIWIX=false; [[ "$SELECTED_SVCS" == *'"kiwix"'* ]] && SVC_KIWIX=true - else - # Text toggle fallback + echo "" + echo -e " ${BOLD}[1a] AI Stack — select services to install${NC}" + echo " Ollama always included (required core). Type number to toggle, Enter to confirm." + _W=$SVC_WEBUI; _R=$SVC_RAG; _M=$SVC_MCP; _S=$SVC_SEARXNG + _G=$SVC_GITEA; _I=$SVC_INVOKEAI; _P=$SVC_PORTAINER; _K=$SVC_KIWIX + while true; do echo "" - echo -e " ${BOLD}[1a] AI Stack — select services to install${NC}" - echo " Ollama always included (required core). Type number to toggle, Enter to confirm." - _W=$SVC_WEBUI; _R=$SVC_RAG; _M=$SVC_MCP; _S=$SVC_SEARXNG - _G=$SVC_GITEA; _I=$SVC_INVOKEAI; _P=$SVC_PORTAINER; _K=$SVC_KIWIX - while true; do - echo "" - printf " [%s] 1. Open WebUI — Chat interface\n" "$($_W && echo '*' || echo ' ')" - printf " [%s] 2. RAG+ChromaDB — Code & document search\n" "$($_R && echo '*' || echo ' ')" - printf " [%s] 3. MCP Server — Claude Code tools (needs RAG)\n" "$($_M && echo '*' || echo ' ')" - printf " [%s] 4. SearXNG — Private web search\n" "$($_S && echo '*' || echo ' ')" - printf " [%s] 5. Gitea — Self-hosted Git\n" "$($_G && echo '*' || echo ' ')" - printf " [%s] 6. InvokeAI — Image generation\n" "$($_I && echo '*' || echo ' ')" - printf " [%s] 7. Portainer — Docker management UI\n" "$($_P && echo '*' || echo ' ')" - printf " [%s] 8. Kiwix — Offline Wikipedia, Stack Overflow\n" "$($_K && echo '*' || echo ' ')" - echo "" - read -rp " Toggle [number] or Enter to confirm: " T - case "$T" in - 1) $_W && _W=false || _W=true ;; - 2) $_R && _R=false || _R=true ;; - 3) $_M && _M=false || _M=true ;; - 4) $_S && _S=false || _S=true ;; - 5) $_G && _G=false || _G=true ;; - 6) $_I && _I=false || _I=true ;; - 7) $_P && _P=false || _P=true ;; - 8) $_K && _K=false || _K=true ;; - "") break ;; - esac - done - SVC_WEBUI=$_W; SVC_RAG=$_R; SVC_MCP=$_M; SVC_SEARXNG=$_S - SVC_GITEA=$_G; SVC_INVOKEAI=$_I; SVC_PORTAINER=$_P; SVC_KIWIX=$_K - fi + printf " [%s] 1. Open WebUI — Chat interface\n" "$($_W && echo '*' || echo ' ')" + printf " [%s] 2. RAG+ChromaDB — Code & document search\n" "$($_R && echo '*' || echo ' ')" + printf " [%s] 3. MCP Server — Claude Code tools (needs RAG)\n" "$($_M && echo '*' || echo ' ')" + printf " [%s] 4. SearXNG — Private web search\n" "$($_S && echo '*' || echo ' ')" + printf " [%s] 5. Gitea — Self-hosted Git\n" "$($_G && echo '*' || echo ' ')" + printf " [%s] 6. InvokeAI — Image generation\n" "$($_I && echo '*' || echo ' ')" + printf " [%s] 7. Portainer — Docker management UI\n" "$($_P && echo '*' || echo ' ')" + printf " [%s] 8. Kiwix — Offline Wikipedia, Stack Overflow\n" "$($_K && echo '*' || echo ' ')" + echo "" + read -rp " Toggle [number] or Enter to confirm: " T + case "$T" in + 1) $_W && _W=false || _W=true ;; + 2) $_R && _R=false || _R=true ;; + 3) $_M && _M=false || _M=true ;; + 4) $_S && _S=false || _S=true ;; + 5) $_G && _G=false || _G=true ;; + 6) $_I && _I=false || _I=true ;; + 7) $_P && _P=false || _P=true ;; + 8) $_K && _K=false || _K=true ;; + "") break ;; + esac + done + SVC_WEBUI=$_W; SVC_RAG=$_R; SVC_MCP=$_M; SVC_SEARXNG=$_S + SVC_GITEA=$_G; SVC_INVOKEAI=$_I; SVC_PORTAINER=$_P; SVC_KIWIX=$_K # Enforce dependencies: MCP needs RAG $SVC_MCP && ! $SVC_RAG && { warn "MCP Server requires RAG — enabling RAG+ChromaDB"; SVC_RAG=true; } diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index b260202..2bd1b91 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -137,7 +137,6 @@ run_disaster_recovery() { tree \ zip \ unzip \ - whiptail \ 2>/dev/null || echo " ⚠ Some packages may have failed" # Install Kopia for backup management @@ -325,55 +324,31 @@ run_disaster_recovery() { echo "Found ${#SERVICES_FOUND[@]} services in backup." echo "" - # Check if whiptail is available for nice UI - if command -v whiptail &> /dev/null; then - # Build whiptail checklist arguments - CHECKLIST_ARGS=() - for svc in "${SERVICES_FOUND[@]}"; do - CHECKLIST_ARGS+=("$svc" "" "ON") - done - - # Show checklist - returns selected items - SELECTED=$(whiptail --title "Select Services to Restore" \ - --checklist "Use SPACE to select/deselect, ENTER to confirm:" \ - 20 60 12 \ - "${CHECKLIST_ARGS[@]}" \ - 3>&1 1>&2 2>&3) - - # Parse selected services (whiptail returns quoted strings) - SELECTED_SERVICES=() - for item in $SELECTED; do - # Remove quotes - svc=$(echo "$item" | tr -d '"') - SELECTED_SERVICES+=("$svc") - done - else - # Fallback: text-based selection - echo "Select services to restore:" - echo " [A] All services" - echo " [N] None (skip restore)" - echo " [S] Select individually" + # Numbered toggle selection for services to restore + echo "Select services to restore (all selected by default):" + echo " Type a number to toggle, Enter to confirm." + echo "" + declare -a _SEL_FLAGS + for i in "${!SERVICES_FOUND[@]}"; do _SEL_FLAGS[$i]=true; done + while true; do echo "" - read -p "Choice (A/N/S) [A]: " RESTORE_CHOICE - - case "${RESTORE_CHOICE^^}" in - N) - SELECTED_SERVICES=() - ;; - S) - SELECTED_SERVICES=() - for svc in "${SERVICES_FOUND[@]}"; do - read -p " Restore $svc? (y/n) [y]: " RESTORE_SVC - if [ "$RESTORE_SVC" != "n" ] && [ "$RESTORE_SVC" != "N" ]; then - SELECTED_SERVICES+=("$svc") - fi - done - ;; - *) - SELECTED_SERVICES=("${SERVICES_FOUND[@]}") - ;; - esac - fi + for i in "${!SERVICES_FOUND[@]}"; do + printf " [%s] %d. %s\n" "$( ${_SEL_FLAGS[$i]} && echo '*' || echo ' ')" "$((i+1))" "${SERVICES_FOUND[$i]}" + done + echo "" + read -p " Toggle [number] or Enter to confirm: " _T + if [[ -z "$_T" ]]; then + break + elif [[ "$_T" =~ ^[0-9]+$ ]] && [[ "$_T" -ge 1 ]] && [[ "$_T" -le "${#SERVICES_FOUND[@]}" ]]; then + idx=$((_T-1)) + ${_SEL_FLAGS[$idx]} && _SEL_FLAGS[$idx]=false || _SEL_FLAGS[$idx]=true + fi + done + SELECTED_SERVICES=() + for i in "${!SERVICES_FOUND[@]}"; do + ${_SEL_FLAGS[$i]} && SELECTED_SERVICES+=("${SERVICES_FOUND[$i]}") + done + unset _SEL_FLAGS _T # Restore selected services echo "" @@ -635,54 +610,34 @@ run_migration() { SELECTED_CONTAINERS=() - if command -v whiptail &> /dev/null && [ ${#CONTAINERS_FOUND[@]} -gt 3 ]; then - # Use whiptail for many containers - CHECKLIST_ARGS=() - for container in "${CONTAINERS_FOUND[@]}"; do - CHECKLIST_ARGS+=("$container" "" "ON") - done - - SELECTED=$(whiptail --title "Select Containers to Migrate" \ - --checklist "Use SPACE to select/deselect, ENTER to confirm:" \ - 20 60 12 \ - "${CHECKLIST_ARGS[@]}" \ - 3>&1 1>&2 2>&3) - - if [ $? -eq 0 ] && [ -n "$SELECTED" ]; then - # Parse whiptail output - for container in $SELECTED; do - # Remove quotes - container="${container//\"/}" - SELECTED_CONTAINERS+=("$container") - done - fi - else - # Text-based selection - echo "Select containers to migrate:" - echo " [A] All containers" - echo " [S] Select individually" - echo " [N] None (cancel)" + # Numbered toggle selection for containers to migrate (all selected by default) + echo "Select containers to migrate (all selected by default):" + echo " Type a number to toggle, Enter to confirm, or 0 to cancel." + echo "" + declare -a _CONT_FLAGS + for i in "${!CONTAINERS_FOUND[@]}"; do _CONT_FLAGS[$i]=true; done + while true; do echo "" - read -p "Choice (A/S/N) [A]: " MIGRATE_CHOICE - - case "${MIGRATE_CHOICE^^}" in - N) - echo "Migration cancelled." - return 0 - ;; - S) - for container in "${CONTAINERS_FOUND[@]}"; do - read -p " Migrate $container? (y/n) [y]: " MIGRATE_THIS - if [ "$MIGRATE_THIS" != "n" ] && [ "$MIGRATE_THIS" != "N" ]; then - SELECTED_CONTAINERS+=("$container") - fi - done - ;; - *) - SELECTED_CONTAINERS=("${CONTAINERS_FOUND[@]}") - ;; - esac - fi + for i in "${!CONTAINERS_FOUND[@]}"; do + printf " [%s] %d. %s\n" "$( ${_CONT_FLAGS[$i]} && echo '*' || echo ' ')" "$((i+1))" "${CONTAINERS_FOUND[$i]}" + done + echo "" + read -p " Toggle [number], 0=cancel, Enter to confirm: " _T + if [[ -z "$_T" ]]; then + break + elif [[ "$_T" == "0" ]]; then + echo "Migration cancelled." + unset _CONT_FLAGS _T + return 0 + elif [[ "$_T" =~ ^[0-9]+$ ]] && [[ "$_T" -ge 1 ]] && [[ "$_T" -le "${#CONTAINERS_FOUND[@]}" ]]; then + idx=$((_T-1)) + ${_CONT_FLAGS[$idx]} && _CONT_FLAGS[$idx]=false || _CONT_FLAGS[$idx]=true + fi + done + for i in "${!CONTAINERS_FOUND[@]}"; do + ${_CONT_FLAGS[$i]} && SELECTED_CONTAINERS+=("${CONTAINERS_FOUND[$i]}") + done + unset _CONT_FLAGS _T if [ ${#SELECTED_CONTAINERS[@]} -eq 0 ]; then echo "No containers selected." @@ -3793,184 +3748,228 @@ else # SERVICE SELECTION MENU # ============================================================================ - # Use whiptail for service selection if available - if command -v whiptail &> /dev/null; then - # Detect existing services - echo "Detecting existing services..." - declare -A EXISTING_SERVICES - [ -d "$DOCKER_DIR/immich" ] && EXISTING_SERVICES[IMMICH]="ON" - [ -d "$DOCKER_DIR/audiobookshelf" ] && EXISTING_SERVICES[AUDIOBOOKSHELF]="ON" - [ -d "$DOCKER_DIR/emby" ] && EXISTING_SERVICES[EMBY]="ON" - [ -d "$DOCKER_DIR/arm" ] && EXISTING_SERVICES[ARM]="ON" - [ -d "$DOCKER_DIR/filebrowser" ] && EXISTING_SERVICES[FILEBROWSER]="ON" - [ -d "$DOCKER_DIR/magicmirror" ] && EXISTING_SERVICES[MAGICMIRROR]="ON" - [ -d "$DOCKER_DIR/actualbudget" ] && EXISTING_SERVICES[ACTUALBUDGET]="ON" - [ -d "$DOCKER_DIR/keycloak" ] && EXISTING_SERVICES[KEYCLOAK]="ON" - [ -d "$DOCKER_DIR/caddy" ] && EXISTING_SERVICES[CADDY]="ON" - [ -d "$DOCKER_DIR/lms" ] && EXISTING_SERVICES[LYRION]="ON" - [ -d "$DOCKER_DIR/mealie" ] && EXISTING_SERVICES[MEALIE]="ON" - [ -d "$DOCKER_DIR/minecraft" ] && EXISTING_SERVICES[MINECRAFT]="ON" - [ -d "$DOCKER_DIR/jellyfin" ] && EXISTING_SERVICES[JELLYFIN]="ON" - [ -d "$DOCKER_DIR/frigate" ] && EXISTING_SERVICES[FRIGATE]="ON" - [ -d "$DOCKER_DIR/ntfy" ] && EXISTING_SERVICES[NTFY]="ON" - [ -d "$DOCKER_DIR/uptime-kuma" ] && EXISTING_SERVICES[UPTIMEKUMA]="ON" - [ -d "$DOCKER_DIR/wg-easy" ] && EXISTING_SERVICES[WGEASY]="ON" - [ -d "$DOCKER_DIR/traccar" ] && EXISTING_SERVICES[TRACCAR]="ON" - [ -d "$DOCKER_DIR/portainer" ] && EXISTING_SERVICES[PORTAINER]="ON" - [ -d "$DOCKER_DIR/meshcentral" ] && EXISTING_SERVICES[MESHCENTRAL]="ON" - [ -d "$DOCKER_DIR/fmd" ] && EXISTING_SERVICES[FINDMYDEVICE]="ON" - [ -d "$DOCKER_DIR/frigate-notify" ] && EXISTING_SERVICES[FRIGATE_NOTIFY]="ON" - [ -d "$DOCKER_DIR/watchtower" ] && EXISTING_SERVICES[WATCHTOWER]="ON" - command -v fail2ban-client &> /dev/null && EXISTING_SERVICES[FAIL2BAN]="ON" + # Detect existing services + echo "Detecting existing services..." + declare -A EXISTING_SERVICES + [ -d "$DOCKER_DIR/immich" ] && EXISTING_SERVICES[IMMICH]="ON" + [ -d "$DOCKER_DIR/audiobookshelf" ] && EXISTING_SERVICES[AUDIOBOOKSHELF]="ON" + [ -d "$DOCKER_DIR/emby" ] && EXISTING_SERVICES[EMBY]="ON" + [ -d "$DOCKER_DIR/arm" ] && EXISTING_SERVICES[ARM]="ON" + [ -d "$DOCKER_DIR/filebrowser" ] && EXISTING_SERVICES[FILEBROWSER]="ON" + [ -d "$DOCKER_DIR/magicmirror" ] && EXISTING_SERVICES[MAGICMIRROR]="ON" + [ -d "$DOCKER_DIR/actualbudget" ] && EXISTING_SERVICES[ACTUALBUDGET]="ON" + [ -d "$DOCKER_DIR/keycloak" ] && EXISTING_SERVICES[KEYCLOAK]="ON" + [ -d "$DOCKER_DIR/caddy" ] && EXISTING_SERVICES[CADDY]="ON" + [ -d "$DOCKER_DIR/lms" ] && EXISTING_SERVICES[LYRION]="ON" + [ -d "$DOCKER_DIR/mealie" ] && EXISTING_SERVICES[MEALIE]="ON" + [ -d "$DOCKER_DIR/minecraft" ] && EXISTING_SERVICES[MINECRAFT]="ON" + [ -d "$DOCKER_DIR/jellyfin" ] && EXISTING_SERVICES[JELLYFIN]="ON" + [ -d "$DOCKER_DIR/frigate" ] && EXISTING_SERVICES[FRIGATE]="ON" + [ -d "$DOCKER_DIR/ntfy" ] && EXISTING_SERVICES[NTFY]="ON" + [ -d "$DOCKER_DIR/uptime-kuma" ] && EXISTING_SERVICES[UPTIMEKUMA]="ON" + [ -d "$DOCKER_DIR/wg-easy" ] && EXISTING_SERVICES[WGEASY]="ON" + [ -d "$DOCKER_DIR/traccar" ] && EXISTING_SERVICES[TRACCAR]="ON" + [ -d "$DOCKER_DIR/portainer" ] && EXISTING_SERVICES[PORTAINER]="ON" + [ -d "$DOCKER_DIR/meshcentral" ] && EXISTING_SERVICES[MESHCENTRAL]="ON" + [ -d "$DOCKER_DIR/fmd" ] && EXISTING_SERVICES[FINDMYDEVICE]="ON" + [ -d "$DOCKER_DIR/frigate-notify" ] && EXISTING_SERVICES[FRIGATE_NOTIFY]="ON" + [ -d "$DOCKER_DIR/watchtower" ] && EXISTING_SERVICES[WATCHTOWER]="ON" + command -v fail2ban-client &> /dev/null && EXISTING_SERVICES[FAIL2BAN]="ON" - # Ask user what action to perform - ACTION=$(whiptail --title "Service Management" --menu \ - "Choose an action:" 15 60 3 \ - "1" "Install new services" \ - "2" "Uninstall existing services" \ - "3" "Cancel and skip" \ - 3>&1 1>&2 2>&3) + # Action selection + echo "" + echo " Service Management" + echo " ──────────────────" + echo " 1. Install new services" + echo " 2. Uninstall existing services" + echo " 3. Cancel and skip" + echo "" + read -p " Choose action [1]: " ACTION + ACTION="${ACTION:-1}" - case "$ACTION" in - 1) - # INSTALL MODE - MENU_MODE="INSTALL" - # Build checklist - mark existing services as ON - SELECTED_SERVICES=$(whiptail --title "Select Docker Services to Install" \ - --checklist "Use SPACE to select, ENTER to confirm. [*] = already installed" 25 78 17 \ - "IMMICH" "Photo & video backup (like Google Photos)" ${EXISTING_SERVICES[IMMICH]:-OFF} \ - "AUDIOBOOKSHELF" "Audiobook & podcast server" ${EXISTING_SERVICES[AUDIOBOOKSHELF]:-OFF} \ - "EMBY" "Media server for movies, TV, music" ${EXISTING_SERVICES[EMBY]:-OFF} \ - "ARM" "Automatic Ripping Machine for DVDs/Blu-rays" ${EXISTING_SERVICES[ARM]:-OFF} \ - "FILEBROWSER" "Web-based file manager" ${EXISTING_SERVICES[FILEBROWSER]:-OFF} \ - "MAGICMIRROR" "Smart mirror / dashboard display" ${EXISTING_SERVICES[MAGICMIRROR]:-OFF} \ - "ACTUALBUDGET" "Personal finance management with bank sync" ${EXISTING_SERVICES[ACTUALBUDGET]:-OFF} \ - "KEYCLOAK" "Identity & Access Management (SSO)" ${EXISTING_SERVICES[KEYCLOAK]:-OFF} \ - "CADDY" "Reverse proxy with automatic HTTPS" ${EXISTING_SERVICES[CADDY]:-OFF} \ - "FAIL2BAN" "Intrusion prevention system" ${EXISTING_SERVICES[FAIL2BAN]:-OFF} \ - "LYRION" "Music streaming server (LMS)" ${EXISTING_SERVICES[LYRION]:-OFF} \ - "MEALIE" "Recipe manager & meal planner" ${EXISTING_SERVICES[MEALIE]:-OFF} \ - "MINECRAFT" "Minecraft game server" ${EXISTING_SERVICES[MINECRAFT]:-OFF} \ - "JELLYFIN" "Free media server (Emby alternative)" ${EXISTING_SERVICES[JELLYFIN]:-OFF} \ - "FRIGATE" "AI-powered NVR for security cameras" ${EXISTING_SERVICES[FRIGATE]:-OFF} \ - "NTFY" "Push notifications server" ${EXISTING_SERVICES[NTFY]:-OFF} \ - "UPTIMEKUMA" "Service monitoring dashboard" ${EXISTING_SERVICES[UPTIMEKUMA]:-OFF} \ - "WGEASY" "WireGuard VPN with web UI" ${EXISTING_SERVICES[WGEASY]:-OFF} \ - "TRACCAR" "GPS tracking server" ${EXISTING_SERVICES[TRACCAR]:-OFF} \ - "PORTAINER" "Docker management web UI" ${EXISTING_SERVICES[PORTAINER]:-OFF} \ - "MESHCENTRAL" "Remote management server" ${EXISTING_SERVICES[MESHCENTRAL]:-OFF} \ - "FINDMYDEVICE" "Device tracking (like Find My)" ${EXISTING_SERVICES[FINDMYDEVICE]:-OFF} \ - "FRIGATE_NOTIFY" "Push notifications for Frigate" ${EXISTING_SERVICES[FRIGATE_NOTIFY]:-OFF} \ - "WATCHTOWER" "Automatic container updates" ${EXISTING_SERVICES[WATCHTOWER]:-OFF} \ - 3>&1 1>&2 2>&3) - ;; - 2) - # UNINSTALL MODE - MENU_MODE="UNINSTALL" - # Only show services that exist - UNINSTALL_OPTIONS="" - [ -n "${EXISTING_SERVICES[IMMICH]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS IMMICH \"Photo & video backup\" ON" - [ -n "${EXISTING_SERVICES[AUDIOBOOKSHELF]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS AUDIOBOOKSHELF \"Audiobook server\" ON" - [ -n "${EXISTING_SERVICES[EMBY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS EMBY \"Media server\" ON" - [ -n "${EXISTING_SERVICES[ARM]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS ARM \"Automatic Ripping Machine\" ON" - [ -n "${EXISTING_SERVICES[FILEBROWSER]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FILEBROWSER \"Web file manager\" ON" - [ -n "${EXISTING_SERVICES[MAGICMIRROR]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MAGICMIRROR \"Smart mirror\" ON" - [ -n "${EXISTING_SERVICES[ACTUALBUDGET]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS ACTUALBUDGET \"Personal finance\" ON" - [ -n "${EXISTING_SERVICES[KEYCLOAK]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS KEYCLOAK \"Identity management\" ON" - [ -n "${EXISTING_SERVICES[CADDY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS CADDY \"Reverse proxy\" ON" - [ -n "${EXISTING_SERVICES[FAIL2BAN]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FAIL2BAN \"Intrusion prevention\" ON" - [ -n "${EXISTING_SERVICES[LYRION]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS LYRION \"Music server\" ON" - [ -n "${EXISTING_SERVICES[MEALIE]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MEALIE \"Recipe manager\" ON" - [ -n "${EXISTING_SERVICES[MINECRAFT]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MINECRAFT \"Game server\" ON" - [ -n "${EXISTING_SERVICES[JELLYFIN]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS JELLYFIN \"Media server\" ON" - [ -n "${EXISTING_SERVICES[FRIGATE]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FRIGATE \"NVR cameras\" ON" - [ -n "${EXISTING_SERVICES[NTFY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS NTFY \"Push notifications\" ON" - [ -n "${EXISTING_SERVICES[UPTIMEKUMA]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS UPTIMEKUMA \"Service monitoring\" ON" - [ -n "${EXISTING_SERVICES[WGEASY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS WGEASY \"WireGuard VPN\" ON" - [ -n "${EXISTING_SERVICES[TRACCAR]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS TRACCAR \"GPS tracking\" ON" - [ -n "${EXISTING_SERVICES[PORTAINER]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS PORTAINER \"Docker management\" ON" - [ -n "${EXISTING_SERVICES[MESHCENTRAL]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MESHCENTRAL \"Remote management\" ON" - [ -n "${EXISTING_SERVICES[FINDMYDEVICE]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FINDMYDEVICE \"Device tracking\" ON" - [ -n "${EXISTING_SERVICES[FRIGATE_NOTIFY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FRIGATE_NOTIFY \"Frigate notifications\" ON" - [ -n "${EXISTING_SERVICES[WATCHTOWER]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS WATCHTOWER \"Container updates\" ON" - - if [ -z "$UNINSTALL_OPTIONS" ]; then - whiptail --title "No Services Found" --msgbox "No services detected to uninstall." 8 50 - SELECTED_SERVICES="" - else - SELECTED_SERVICES=$(eval "whiptail --title 'Select Services to Uninstall' \ - --checklist 'WARNING: Data will be backed up but services will be removed' 25 78 17 \ - $UNINSTALL_OPTIONS \ - 3>&1 1>&2 2>&3") + SELECTED_SERVICES="" + case "$ACTION" in + 1) + MENU_MODE="INSTALL" + # Build ordered list of services; mark existing ones as selected + _SVC_KEYS=(IMMICH AUDIOBOOKSHELF EMBY ARM FILEBROWSER MAGICMIRROR ACTUALBUDGET + KEYCLOAK CADDY FAIL2BAN LYRION MEALIE MINECRAFT JELLYFIN FRIGATE + NTFY UPTIMEKUMA WGEASY TRACCAR PORTAINER MESHCENTRAL FINDMYDEVICE + FRIGATE_NOTIFY WATCHTOWER) + _SVC_LABELS=( + "IMMICH — Photo & video backup (like Google Photos)" + "AUDIOBOOKSHELF— Audiobook & podcast server" + "EMBY — Media server for movies, TV, music" + "ARM — Automatic Ripping Machine for DVDs/Blu-rays" + "FILEBROWSER — Web-based file manager" + "MAGICMIRROR — Smart mirror / dashboard display" + "ACTUALBUDGET — Personal finance management with bank sync" + "KEYCLOAK — Identity & Access Management (SSO)" + "CADDY — Reverse proxy with automatic HTTPS" + "FAIL2BAN — Intrusion prevention system" + "LYRION — Music streaming server (LMS)" + "MEALIE — Recipe manager & meal planner" + "MINECRAFT — Minecraft game server" + "JELLYFIN — Free media server (Emby alternative)" + "FRIGATE — AI-powered NVR for security cameras" + "NTFY — Push notifications server" + "UPTIMEKUMA — Service monitoring dashboard" + "WGEASY — WireGuard VPN with web UI" + "TRACCAR — GPS tracking server" + "PORTAINER — Docker management web UI" + "MESHCENTRAL — Remote management server" + "FINDMYDEVICE — Device tracking (like Find My)" + "FRIGATE_NOTIFY— Push notifications for Frigate" + "WATCHTOWER — Automatic container updates" + ) + declare -a _SVC_FLAGS + for i in "${!_SVC_KEYS[@]}"; do + [ -n "${EXISTING_SERVICES[${_SVC_KEYS[$i]}]:-}" ] && _SVC_FLAGS[$i]=true || _SVC_FLAGS[$i]=false + done + echo "" + echo " Select services to install ([*] = already installed)" + echo " Type number to toggle, Enter to confirm." + while true; do + echo "" + for i in "${!_SVC_KEYS[@]}"; do + _mark="$( ${_SVC_FLAGS[$i]} && echo '*' || echo ' ')" + _exist="$( [ -n "${EXISTING_SERVICES[${_SVC_KEYS[$i]}]:-}" ] && echo ' [installed]' || echo '')" + printf " [%s] %2d. %s%s\n" "$_mark" "$((i+1))" "${_SVC_LABELS[$i]}" "$_exist" + done + echo "" + read -p " Toggle [number] or Enter to confirm: " _T + if [[ -z "$_T" ]]; then + break + elif [[ "$_T" =~ ^[0-9]+$ ]] && [[ "$_T" -ge 1 ]] && [[ "$_T" -le "${#_SVC_KEYS[@]}" ]]; then + idx=$((_T-1)) + ${_SVC_FLAGS[$idx]} && _SVC_FLAGS[$idx]=false || _SVC_FLAGS[$idx]=true fi - ;; - 3|*) - # Cancel + done + for i in "${!_SVC_KEYS[@]}"; do + ${_SVC_FLAGS[$i]} && SELECTED_SERVICES="$SELECTED_SERVICES ${_SVC_KEYS[$i]}" + done + unset _SVC_FLAGS _SVC_KEYS _SVC_LABELS _T _mark _exist + ;; + 2) + MENU_MODE="UNINSTALL" + # Build list from only installed services + _SVC_KEYS=(); _SVC_LABELS=() + _add_if_exists() { [ -n "${EXISTING_SERVICES[$1]:-}" ] && _SVC_KEYS+=("$1") && _SVC_LABELS+=("$2"); } + _add_if_exists IMMICH "IMMICH — Photo & video backup" + _add_if_exists AUDIOBOOKSHELF "AUDIOBOOKSHELF — Audiobook server" + _add_if_exists EMBY "EMBY — Media server" + _add_if_exists ARM "ARM — Automatic Ripping Machine" + _add_if_exists FILEBROWSER "FILEBROWSER — Web file manager" + _add_if_exists MAGICMIRROR "MAGICMIRROR — Smart mirror" + _add_if_exists ACTUALBUDGET "ACTUALBUDGET — Personal finance" + _add_if_exists KEYCLOAK "KEYCLOAK — Identity management" + _add_if_exists CADDY "CADDY — Reverse proxy" + _add_if_exists FAIL2BAN "FAIL2BAN — Intrusion prevention" + _add_if_exists LYRION "LYRION — Music server" + _add_if_exists MEALIE "MEALIE — Recipe manager" + _add_if_exists MINECRAFT "MINECRAFT — Game server" + _add_if_exists JELLYFIN "JELLYFIN — Media server" + _add_if_exists FRIGATE "FRIGATE — NVR cameras" + _add_if_exists NTFY "NTFY — Push notifications" + _add_if_exists UPTIMEKUMA "UPTIMEKUMA — Service monitoring" + _add_if_exists WGEASY "WGEASY — WireGuard VPN" + _add_if_exists TRACCAR "TRACCAR — GPS tracking" + _add_if_exists PORTAINER "PORTAINER — Docker management" + _add_if_exists MESHCENTRAL "MESHCENTRAL — Remote management" + _add_if_exists FINDMYDEVICE "FINDMYDEVICE — Device tracking" + _add_if_exists FRIGATE_NOTIFY "FRIGATE_NOTIFY — Frigate notifications" + _add_if_exists WATCHTOWER "WATCHTOWER — Container updates" + unset -f _add_if_exists + + if [ ${#_SVC_KEYS[@]} -eq 0 ]; then + echo "No installed services detected to uninstall." SELECTED_SERVICES="" MENU_MODE="CANCEL" - ;; - esac - - # Check if user cancelled - if [ $? -ne 0 ]; then - echo "Service selection cancelled. Skipping Docker applications." + else + declare -a _SVC_FLAGS + for i in "${!_SVC_KEYS[@]}"; do _SVC_FLAGS[$i]=true; done + echo "" + echo " WARNING: selected services will be removed (data backed up first)." + echo " Type number to toggle, Enter to confirm." + while true; do + echo "" + for i in "${!_SVC_KEYS[@]}"; do + printf " [%s] %2d. %s\n" "$( ${_SVC_FLAGS[$i]} && echo '*' || echo ' ')" "$((i+1))" "${_SVC_LABELS[$i]}" + done + echo "" + read -p " Toggle [number] or Enter to confirm: " _T + if [[ -z "$_T" ]]; then + break + elif [[ "$_T" =~ ^[0-9]+$ ]] && [[ "$_T" -ge 1 ]] && [[ "$_T" -le "${#_SVC_KEYS[@]}" ]]; then + idx=$((_T-1)) + ${_SVC_FLAGS[$idx]} && _SVC_FLAGS[$idx]=false || _SVC_FLAGS[$idx]=true + fi + done + for i in "${!_SVC_KEYS[@]}"; do + ${_SVC_FLAGS[$i]} && SELECTED_SERVICES="$SELECTED_SERVICES ${_SVC_KEYS[$i]}" + done + unset _SVC_FLAGS _SVC_KEYS _SVC_LABELS _T + fi + ;; + 3|*) SELECTED_SERVICES="" - fi + MENU_MODE="CANCEL" + ;; + esac - # Parse selections (whiptail returns quoted strings) - # Only set to "n" if not already set (preserve any earlier choices) - : ${INSTALL_IMMICH:="n"} - : ${INSTALL_AUDIOBOOKSHELF:="n"} - : ${INSTALL_EMBY:="n"} - : ${INSTALL_ARM:="n"} - : ${INSTALL_FILEBROWSER:="n"} - : ${INSTALL_MAGICMIRROR:="n"} - : ${INSTALL_ACTUALBUDGET:="n"} - : ${INSTALL_KEYCLOAK:="n"} - : ${INSTALL_CADDY:="n"} - : ${INSTALL_FAIL2BAN:="n"} - : ${INSTALL_LMS:="n"} - : ${INSTALL_MEALIE:="n"} - : ${INSTALL_MINECRAFT:="n"} - : ${INSTALL_JELLYFIN:="n"} - : ${INSTALL_FRIGATE:="n"} - : ${INSTALL_NTFY:="n"} - : ${INSTALL_UPTIMEKUMA:="n"} - : ${INSTALL_WGEASY:="n"} - : ${INSTALL_TRACCAR:="n"} - : ${INSTALL_PORTAINER:="n"} - : ${INSTALL_MESHCENTRAL_SERVER:="n"} - : ${INSTALL_FMD:="n"} - : ${INSTALL_FRIGATE_NOTIFY:="n"} - : ${INSTALL_WATCHTOWER:="n"} + # Set installation flags from SELECTED_SERVICES + WHIPTAIL_USED=true + : ${INSTALL_IMMICH:="n"} + : ${INSTALL_AUDIOBOOKSHELF:="n"} + : ${INSTALL_EMBY:="n"} + : ${INSTALL_ARM:="n"} + : ${INSTALL_FILEBROWSER:="n"} + : ${INSTALL_MAGICMIRROR:="n"} + : ${INSTALL_ACTUALBUDGET:="n"} + : ${INSTALL_KEYCLOAK:="n"} + : ${INSTALL_CADDY:="n"} + : ${INSTALL_FAIL2BAN:="n"} + : ${INSTALL_LMS:="n"} + : ${INSTALL_MEALIE:="n"} + : ${INSTALL_MINECRAFT:="n"} + : ${INSTALL_JELLYFIN:="n"} + : ${INSTALL_FRIGATE:="n"} + : ${INSTALL_NTFY:="n"} + : ${INSTALL_UPTIMEKUMA:="n"} + : ${INSTALL_WGEASY:="n"} + : ${INSTALL_TRACCAR:="n"} + : ${INSTALL_PORTAINER:="n"} + : ${INSTALL_MESHCENTRAL_SERVER:="n"} + : ${INSTALL_FMD:="n"} + : ${INSTALL_FRIGATE_NOTIFY:="n"} + : ${INSTALL_WATCHTOWER:="n"} - # Set installation flags based on selections - if echo "$SELECTED_SERVICES" | grep -q "IMMICH"; then INSTALL_IMMICH="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "AUDIOBOOKSHELF"; then INSTALL_AUDIOBOOKSHELF="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "EMBY"; then INSTALL_EMBY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "ARM"; then INSTALL_ARM="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FILEBROWSER"; then INSTALL_FILEBROWSER="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "MAGICMIRROR"; then INSTALL_MAGICMIRROR="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "ACTUALBUDGET"; then INSTALL_ACTUALBUDGET="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "KEYCLOAK"; then INSTALL_KEYCLOAK="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "CADDY"; then INSTALL_CADDY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FAIL2BAN"; then INSTALL_FAIL2BAN="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "LYRION"; then INSTALL_LMS="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "MEALIE"; then INSTALL_MEALIE="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "MINECRAFT"; then INSTALL_MINECRAFT="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "JELLYFIN"; then INSTALL_JELLYFIN="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FRIGATE\""; then INSTALL_FRIGATE="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "NTFY"; then INSTALL_NTFY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "UPTIMEKUMA"; then INSTALL_UPTIMEKUMA="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "WGEASY"; then INSTALL_WGEASY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "TRACCAR"; then INSTALL_TRACCAR="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "PORTAINER"; then INSTALL_PORTAINER="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "MESHCENTRAL"; then INSTALL_MESHCENTRAL_SERVER="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FINDMYDEVICE"; then INSTALL_FMD="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FRIGATE_NOTIFY"; then INSTALL_FRIGATE_NOTIFY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "WATCHTOWER"; then INSTALL_WATCHTOWER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "IMMICH"; then INSTALL_IMMICH="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "AUDIOBOOKSHELF"; then INSTALL_AUDIOBOOKSHELF="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "EMBY"; then INSTALL_EMBY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "ARM"; then INSTALL_ARM="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FILEBROWSER"; then INSTALL_FILEBROWSER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MAGICMIRROR"; then INSTALL_MAGICMIRROR="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "ACTUALBUDGET"; then INSTALL_ACTUALBUDGET="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "KEYCLOAK"; then INSTALL_KEYCLOAK="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "CADDY"; then INSTALL_CADDY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FAIL2BAN"; then INSTALL_FAIL2BAN="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "LYRION"; then INSTALL_LMS="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MEALIE"; then INSTALL_MEALIE="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MINECRAFT"; then INSTALL_MINECRAFT="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "JELLYFIN"; then INSTALL_JELLYFIN="y"; fi + if echo "$SELECTED_SERVICES" | grep -q " FRIGATE "; then INSTALL_FRIGATE="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "NTFY"; then INSTALL_NTFY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "UPTIMEKUMA"; then INSTALL_UPTIMEKUMA="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "WGEASY"; then INSTALL_WGEASY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "TRACCAR"; then INSTALL_TRACCAR="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "PORTAINER"; then INSTALL_PORTAINER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MESHCENTRAL"; then INSTALL_MESHCENTRAL_SERVER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FINDMYDEVICE"; then INSTALL_FMD="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FRIGATE_NOTIFY"; then INSTALL_FRIGATE_NOTIFY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "WATCHTOWER"; then INSTALL_WATCHTOWER="y"; fi - if [ "$MENU_MODE" = "UNINSTALL" ]; then + if [ "$MENU_MODE" = "UNINSTALL" ]; then echo "" echo "Services selected for uninstallation:" echo "$SELECTED_SERVICES" | tr '"' '\n' | grep -v '^$' | sed 's/^/ - /' @@ -4062,25 +4061,12 @@ else fi # After uninstall, exit the Docker apps section - WHIPTAIL_USED=true - elif [ "$MENU_MODE" = "INSTALL" ]; then echo "" echo "Selected services:" - echo "$SELECTED_SERVICES" | tr '"' '\n' | grep -v '^$' | sed 's/^/ - /' + echo "$SELECTED_SERVICES" | tr ' ' '\n' | grep -v '^$' | sed 's/^/ - /' echo "" - - # Mark that whiptail was used - skip all individual prompts - WHIPTAIL_USED=true - else - # Cancel or other mode - WHIPTAIL_USED=false fi - else - # Whiptail not available - will use individual prompts - WHIPTAIL_USED=false - MENU_MODE="INSTALL" - fi # Skip installation section if we just did uninstalls if [ "$MENU_MODE" = "UNINSTALL" ]; then