diff --git a/laptop_full_setup.sh b/laptop_full_setup.sh index 5508450..55e037f 100755 --- a/laptop_full_setup.sh +++ b/laptop_full_setup.sh @@ -94,24 +94,38 @@ POSTINSTALL_SCRIPT="$SCRIPT_DIR/ubuntu-post-install.sh" HAS_POSTINSTALL=false [[ -f "$POSTINSTALL_SCRIPT" ]] && HAS_POSTINSTALL=true -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 +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 "" - $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 + 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 $INSTALL_AI || $INSTALL_POSTINSTALL \ || 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 } - 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 + 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 "" - 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 + 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 # 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 2bd1b91..b260202 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -137,6 +137,7 @@ run_disaster_recovery() { tree \ zip \ unzip \ + whiptail \ 2>/dev/null || echo " ⚠ Some packages may have failed" # Install Kopia for backup management @@ -324,31 +325,55 @@ run_disaster_recovery() { echo "Found ${#SERVICES_FOUND[@]} services in backup." echo "" - # 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 "" - for i in "${!SERVICES_FOUND[@]}"; do - printf " [%s] %d. %s\n" "$( ${_SEL_FLAGS[$i]} && echo '*' || echo ' ')" "$((i+1))" "${SERVICES_FOUND[$i]}" + # 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" 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 + 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 # Restore selected services echo "" @@ -610,34 +635,54 @@ run_migration() { SELECTED_CONTAINERS=() - # 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 "" - for i in "${!CONTAINERS_FOUND[@]}"; do - printf " [%s] %d. %s\n" "$( ${_CONT_FLAGS[$i]} && echo '*' || echo ' ')" "$((i+1))" "${CONTAINERS_FOUND[$i]}" + 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 - 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 + + 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 - done - for i in "${!CONTAINERS_FOUND[@]}"; do - ${_CONT_FLAGS[$i]} && SELECTED_CONTAINERS+=("${CONTAINERS_FOUND[$i]}") - done - unset _CONT_FLAGS _T + else + # Text-based selection + echo "Select containers to migrate:" + echo " [A] All containers" + 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 echo "No containers selected." @@ -3748,228 +3793,184 @@ else # SERVICE SELECTION MENU # ============================================================================ - # 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" + # 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" - # 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}" + # 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) - 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 + 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") 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 _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." + ;; + 3|*) + # Cancel SELECTED_SERVICES="" MENU_MODE="CANCEL" - 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|*) + ;; + esac + + # Check if user cancelled + if [ $? -ne 0 ]; then + echo "Service selection cancelled. Skipping Docker applications." SELECTED_SERVICES="" - MENU_MODE="CANCEL" - ;; - esac + fi - # 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"} + # 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"} - 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 + # 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 [ "$MENU_MODE" = "UNINSTALL" ]; then + if [ "$MENU_MODE" = "UNINSTALL" ]; then echo "" echo "Services selected for uninstallation:" echo "$SELECTED_SERVICES" | tr '"' '\n' | grep -v '^$' | sed 's/^/ - /' @@ -4061,12 +4062,25 @@ 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