Revert "Replace whiptail menus with numbered text toggle selection"

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