3927 lines
130 KiB
Bash
Executable File
3927 lines
130 KiB
Bash
Executable File
#!/bin/bash
|
||
# ==============================================================================
|
||
# ULTRABUNT ULTIMATE BUNTSTALLER v4.2.0
|
||
# Professional Ubuntu/Mint setup & buntage manager
|
||
# - Full system scan (APT, Snap, Flatpak, binaries)
|
||
# - Granular per-buntage control with visual status indicators
|
||
# - Safe uninstall with multiple confirmation layers
|
||
# - Stock-compatible Python (system python3 only, no custom builds)
|
||
# - Comprehensive logging and error handling
|
||
# - Repository management for third-party software
|
||
# ==============================================================================
|
||
|
||
# Check for bash version 4.0+ (required for associative arrays)
|
||
if [ "${BASH_VERSION%%.*}" -lt 4 ]; then
|
||
echo "Error: This script requires Bash 4.0 or higher for associative arrays."
|
||
echo "Current version: $BASH_VERSION"
|
||
echo "Please run with: bash $0"
|
||
exit 1
|
||
fi
|
||
|
||
set -euo pipefail
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
|
||
# Configuration
|
||
LOGFILE="/var/log/ultrabunt.log"
|
||
BACKUP_DIR="/var/backups/ultrabunt"
|
||
PHP_VER="8.3"
|
||
NODE_LTS="20"
|
||
|
||
# Cache for installed buntages - populated once at startup
|
||
declare -A INSTALLED_CACHE
|
||
|
||
# Color codes for terminal output
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Initialize logging and buntage cache
|
||
init_logging() {
|
||
sudo mkdir -p "$(dirname "$LOGFILE")" "$BACKUP_DIR" 2>/dev/null || true
|
||
sudo touch "$LOGFILE" 2>/dev/null || true
|
||
sudo chown "$USER:$USER" "$LOGFILE" "$BACKUP_DIR" 2>/dev/null || true
|
||
}
|
||
|
||
# Build cache of all installed buntages for fast lookups
|
||
build_package_cache() {
|
||
log "Building buntage installation cache..."
|
||
|
||
# Clear existing cache
|
||
INSTALLED_CACHE=()
|
||
|
||
# Cache APT buntages
|
||
log "Caching APT buntages..."
|
||
while IFS= read -r pkg; do
|
||
INSTALLED_CACHE["apt:$pkg"]=1
|
||
done < <(dpkg-query -W -f='${Package}\n' 2>/dev/null | sort)
|
||
|
||
# Cache Snap buntages
|
||
if command -v snap &>/dev/null; then
|
||
log "Caching Snap buntages..."
|
||
while IFS= read -r pkg; do
|
||
INSTALLED_CACHE["snap:$pkg"]=1
|
||
done < <(snap list 2>/dev/null | awk 'NR>1 {print $1}' | sort)
|
||
fi
|
||
|
||
# Cache Flatpak buntages
|
||
if command -v flatpak &>/dev/null; then
|
||
log "Caching Flatpak buntages..."
|
||
while IFS= read -r pkg; do
|
||
INSTALLED_CACHE["flatpak:$pkg"]=1
|
||
done < <(flatpak list --app --columns=application 2>/dev/null | sort)
|
||
fi
|
||
|
||
local total_cached=${#INSTALLED_CACHE[@]}
|
||
log "Buntage cache built with $total_cached entries"
|
||
}
|
||
|
||
# Function to update cache for a specific buntage
|
||
update_package_cache() {
|
||
local name="$1"
|
||
|
||
if [[ -z "$name" ]]; then
|
||
log "ERROR: No buntage name provided to update_package_cache"
|
||
return 1
|
||
fi
|
||
|
||
if [[ -z "${PACKAGES[$name]:-}" ]]; then
|
||
log "WARNING: Buntage '$name' not found in PACKAGES array"
|
||
return 1
|
||
fi
|
||
|
||
local pkg="${PACKAGES[$name]}"
|
||
local method="${PKG_METHOD[$name]:-}"
|
||
|
||
log "Updating cache for buntage: $name (method: $method, pkg: $pkg)"
|
||
|
||
# Update the specific buntage in the cache
|
||
case "$method" in
|
||
apt)
|
||
if dpkg -l "$pkg" 2>/dev/null | grep -q "^ii"; then
|
||
INSTALLED_CACHE["apt:$pkg"]="1"
|
||
log "Cache updated: $name is installed (APT)"
|
||
else
|
||
unset INSTALLED_CACHE["apt:$pkg"]
|
||
# Removed verbose logging for not installed packages
|
||
fi
|
||
;;
|
||
snap)
|
||
if snap list "$pkg" &>/dev/null; then
|
||
INSTALLED_CACHE["snap:$pkg"]="1"
|
||
log "Cache updated: $name is installed (Snap)"
|
||
else
|
||
unset INSTALLED_CACHE["snap:$pkg"]
|
||
# Removed verbose logging for not installed packages
|
||
fi
|
||
;;
|
||
flatpak)
|
||
if flatpak list --app 2>/dev/null | grep -q "$pkg"; then
|
||
INSTALLED_CACHE["flatpak:$pkg"]="1"
|
||
log "Cache updated: $name is installed (Flatpak)"
|
||
else
|
||
unset INSTALLED_CACHE["flatpak:$pkg"]
|
||
# Removed verbose logging for not installed packages
|
||
fi
|
||
;;
|
||
custom)
|
||
# For custom buntages, we can't easily cache them, so we'll just log
|
||
log "Cache update skipped for custom buntage: $name"
|
||
;;
|
||
*)
|
||
log "WARNING: Unknown method '$method' for buntage '$name'"
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
return 0
|
||
}
|
||
|
||
log() {
|
||
local msg="[$(date +'%Y-%m-%d %H:%M:%S')] $*"
|
||
echo "$msg" | tee -a "$LOGFILE"
|
||
}
|
||
|
||
log_error() {
|
||
local msg="[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*"
|
||
echo "$msg" | tee -a "$LOGFILE" >&2
|
||
}
|
||
|
||
# Ensure whiptail is available
|
||
ensure_deps() {
|
||
log "Checking dependencies..."
|
||
if ! command -v whiptail &>/dev/null; then
|
||
log "Installing whiptail..."
|
||
if sudo apt-get update -qq 2>&1 | tee -a "$LOGFILE"; then
|
||
log "APT cache updated for whiptail installation"
|
||
else
|
||
log "WARNING: APT update failed during whiptail installation"
|
||
fi
|
||
|
||
if sudo apt-get install -y whiptail 2>&1 | tee -a "$LOGFILE"; then
|
||
log "Successfully installed whiptail"
|
||
else
|
||
log "ERROR: Failed to install whiptail - UI may not work properly"
|
||
exit 1
|
||
fi
|
||
else
|
||
log "whiptail is already available"
|
||
fi
|
||
}
|
||
|
||
# Whiptail wrapper functions
|
||
ui_msg() {
|
||
whiptail --title "$1" --msgbox "$2" 15 70
|
||
}
|
||
|
||
ui_info() {
|
||
whiptail --title "$1" --msgbox "$2" 20 80
|
||
}
|
||
|
||
ui_yesno() {
|
||
whiptail --title "$1" --yesno "$2" 12 70
|
||
}
|
||
|
||
ui_input() {
|
||
whiptail --title "$1" --inputbox "$2" 10 70 "${3:-}" 3>&1 1>&2 2>&3
|
||
}
|
||
|
||
# UI Functions
|
||
ui_menu() {
|
||
local title="$1"
|
||
local text="$2"
|
||
local height="$3"
|
||
local width="$4"
|
||
local menu_height="$5"
|
||
shift 5
|
||
|
||
if ! command -v whiptail &>/dev/null; then
|
||
log "ERROR: whiptail not available for ui_menu"
|
||
return 1
|
||
fi
|
||
|
||
# Execute whiptail and capture the result properly
|
||
local choice
|
||
choice=$(whiptail --title "$title" --menu "$text" "$height" "$width" "$menu_height" "$@" 3>&1 1>&2 2>&3)
|
||
local exit_code=$?
|
||
|
||
# Handle ESC key (exit code 255) as back navigation in nested menus
|
||
if [[ $exit_code -eq 255 ]]; then
|
||
log "ESC key pressed - treating as back navigation"
|
||
echo "back"
|
||
return 0
|
||
fi
|
||
|
||
# Only log if there's an error (but not for ESC)
|
||
if [[ $exit_code -ne 0 ]]; then
|
||
log "ui_menu failed with exit code: $exit_code"
|
||
fi
|
||
|
||
if [[ $exit_code -eq 0 ]]; then
|
||
echo "$choice"
|
||
fi
|
||
|
||
return $exit_code
|
||
}
|
||
|
||
ui_checklist() {
|
||
local title="$1"
|
||
local text="$2"
|
||
local height="$3"
|
||
local width="$4"
|
||
local menu_height="$5"
|
||
shift 5
|
||
whiptail --title "$title" --checklist "$text" "$height" "$width" "$menu_height" "$@" 3>&1 1>&2 2>&3
|
||
}
|
||
|
||
# ==============================================================================
|
||
# INSTALLATION METHOD SELECTION
|
||
# ==============================================================================
|
||
|
||
# Function to get all available installation methods for a base app name
|
||
get_available_methods() {
|
||
local base_name="$1"
|
||
local methods=()
|
||
|
||
# Check if base package exists
|
||
if [[ -n "${PACKAGES[$base_name]:-}" ]]; then
|
||
local method="${PKG_METHOD[$base_name]}"
|
||
local desc="${PKG_DESC[$base_name]}"
|
||
methods+=("$base_name" "$desc")
|
||
fi
|
||
|
||
# Check for alternative methods
|
||
for variant in "${base_name}-snap" "${base_name}-flatpak" "${base_name}-deb"; do
|
||
if [[ -n "${PACKAGES[$variant]:-}" ]]; then
|
||
local desc="${PKG_DESC[$variant]}"
|
||
methods+=("$variant" "$desc")
|
||
fi
|
||
done
|
||
|
||
# Special cases for apps with different naming patterns
|
||
case "$base_name" in
|
||
"vscode")
|
||
if [[ -n "${PACKAGES[vscode-snap]:-}" ]]; then
|
||
methods+=("vscode-snap" "${PKG_DESC[vscode-snap]}")
|
||
fi
|
||
if [[ -n "${PACKAGES[vscode-flatpak]:-}" ]]; then
|
||
methods+=("vscode-flatpak" "${PKG_DESC[vscode-flatpak]}")
|
||
fi
|
||
;;
|
||
"discord")
|
||
if [[ -n "${PACKAGES[discord-deb]:-}" ]]; then
|
||
methods+=("discord-deb" "${PKG_DESC[discord-deb]}")
|
||
fi
|
||
if [[ -n "${PACKAGES[discord-flatpak]:-}" ]]; then
|
||
methods+=("discord-flatpak" "${PKG_DESC[discord-flatpak]}")
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
printf '%s\n' "${methods[@]}"
|
||
}
|
||
|
||
# Function to offer installation method choice
|
||
choose_installation_method() {
|
||
local base_name="$1"
|
||
local available_methods
|
||
|
||
# Get available methods as array
|
||
mapfile -t available_methods < <(get_available_methods "$base_name")
|
||
|
||
# If only one method available, return it
|
||
if [[ ${#available_methods[@]} -eq 2 ]]; then
|
||
echo "${available_methods[0]}"
|
||
return 0
|
||
fi
|
||
|
||
# If multiple methods available, show selection menu
|
||
if [[ ${#available_methods[@]} -gt 2 ]]; then
|
||
local choice
|
||
choice=$(ui_menu "Installation Method" "Multiple installation methods available for $base_name.\nChoose your preferred method:" 20 80 10 "${available_methods[@]}")
|
||
|
||
if [[ -n "$choice" && "$choice" != "back" ]]; then
|
||
echo "$choice"
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# No methods found
|
||
return 1
|
||
}
|
||
|
||
# Enhanced install function that checks for multiple methods
|
||
install_package_with_choice() {
|
||
local name="$1"
|
||
|
||
# First check if this is already a specific variant (e.g., vscode-snap)
|
||
if [[ -n "${PACKAGES[$name]:-}" ]]; then
|
||
install_package "$name"
|
||
return $?
|
||
fi
|
||
|
||
# Check for multiple installation methods
|
||
local chosen_method
|
||
chosen_method=$(choose_installation_method "$name")
|
||
|
||
if [[ -n "$chosen_method" ]]; then
|
||
install_package "$chosen_method"
|
||
return $?
|
||
else
|
||
ui_msg "Installation Cancelled" "No installation method selected for $name."
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# BUNTAGE DETECTION FUNCTIONS
|
||
# ==============================================================================
|
||
|
||
is_apt_installed() {
|
||
local pkg="$1"
|
||
if [[ -z "$pkg" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
# Use cache for fast lookup
|
||
if [[ -n "${INSTALLED_CACHE["apt:$pkg"]:-}" ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
is_snap_installed() {
|
||
local pkg="$1"
|
||
if [[ -z "$pkg" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
if ! command -v snap &>/dev/null; then
|
||
return 1
|
||
fi
|
||
|
||
# Use cache for fast lookup
|
||
if [[ -n "${INSTALLED_CACHE["snap:$pkg"]:-}" ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
is_flatpak_installed() {
|
||
local pkg="$1"
|
||
if [[ -z "$pkg" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
if ! command -v flatpak &>/dev/null; then
|
||
return 1
|
||
fi
|
||
|
||
# Use cache for fast lookup
|
||
if [[ -n "${INSTALLED_CACHE["flatpak:$pkg"]:-}" ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
is_binary_available() {
|
||
command -v "$1" &>/dev/null
|
||
}
|
||
|
||
# Check if buntage exists in repos
|
||
apt_package_exists() {
|
||
apt-cache show "$1" &>/dev/null
|
||
}
|
||
|
||
# Get buntage status with icon
|
||
get_status() {
|
||
local pkg="$1"
|
||
local method="${2:-apt}" # apt, snap, flatpak, binary
|
||
|
||
case "$method" in
|
||
apt)
|
||
if is_apt_installed "$pkg"; then
|
||
echo "✓"
|
||
else
|
||
echo "✗"
|
||
fi
|
||
;;
|
||
snap)
|
||
if is_snap_installed "$pkg"; then
|
||
echo "✓"
|
||
else
|
||
echo "✗"
|
||
fi
|
||
;;
|
||
flatpak)
|
||
if is_flatpak_installed "$pkg"; then
|
||
echo "✓"
|
||
else
|
||
echo "✗"
|
||
fi
|
||
;;
|
||
binary)
|
||
if is_binary_available "$pkg"; then
|
||
echo "✓"
|
||
else
|
||
echo "✗"
|
||
fi
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ==============================================================================
|
||
# BUNTAGE DEFINITIONS
|
||
# Each buntage has: name, description, install method, dependencies
|
||
# ==============================================================================
|
||
|
||
declare -A PACKAGES
|
||
declare -A PKG_DESC
|
||
declare -A PKG_METHOD
|
||
declare -A PKG_CATEGORY
|
||
declare -A PKG_DEPS
|
||
|
||
# CORE UTILITIES
|
||
PACKAGES[git]="git"
|
||
PKG_DESC[git]="Version control system [APT]"
|
||
PKG_METHOD[git]="apt"
|
||
PKG_CATEGORY[git]="core"
|
||
|
||
PACKAGES[curl]="curl"
|
||
PKG_DESC[curl]="Command line tool for transferring data [APT]"
|
||
PKG_METHOD[curl]="apt"
|
||
PKG_CATEGORY[curl]="core"
|
||
|
||
PACKAGES[wget]="wget"
|
||
PKG_DESC[wget]="Network downloader [APT]"
|
||
PKG_METHOD[wget]="apt"
|
||
PKG_CATEGORY[wget]="core"
|
||
|
||
PACKAGES[build-essential]="build-essential"
|
||
PKG_DESC[build-essential]="Compilation tools (gcc, make, etc) [APT]"
|
||
PKG_METHOD[build-essential]="apt"
|
||
PKG_CATEGORY[build-essential]="core"
|
||
|
||
PACKAGES[tree]="tree"
|
||
PKG_DESC[tree]="Directory listing in tree format [APT]"
|
||
PKG_METHOD[tree]="apt"
|
||
PKG_CATEGORY[tree]="core"
|
||
|
||
PACKAGES[ncdu]="ncdu"
|
||
PKG_DESC[ncdu]="NCurses Disk Usage analyzer [APT]"
|
||
PKG_METHOD[ncdu]="apt"
|
||
PKG_CATEGORY[ncdu]="core"
|
||
|
||
PACKAGES[jq]="jq"
|
||
PKG_DESC[jq]="JSON processor [APT]"
|
||
PKG_METHOD[jq]="apt"
|
||
PKG_CATEGORY[jq]="core"
|
||
|
||
PACKAGES[tmux]="tmux"
|
||
PKG_DESC[tmux]="Terminal multiplexer [APT]"
|
||
PKG_METHOD[tmux]="apt"
|
||
PKG_CATEGORY[tmux]="core"
|
||
|
||
PACKAGES[fzf]="fzf"
|
||
PKG_DESC[fzf]="Fuzzy finder [APT]"
|
||
PKG_METHOD[fzf]="apt"
|
||
PKG_CATEGORY[fzf]="core"
|
||
|
||
PACKAGES[ripgrep]="ripgrep"
|
||
PKG_DESC[ripgrep]="Fast grep alternative (rg) [APT]"
|
||
PKG_METHOD[ripgrep]="apt"
|
||
PKG_CATEGORY[ripgrep]="core"
|
||
|
||
PACKAGES[bat]="bat"
|
||
PKG_DESC[bat]="Cat clone with syntax highlighting [APT]"
|
||
PKG_METHOD[bat]="apt"
|
||
PKG_CATEGORY[bat]="core"
|
||
|
||
PACKAGES[eza]="eza"
|
||
PKG_DESC[eza]="Modern ls replacement [APT]"
|
||
PKG_METHOD[eza]="apt"
|
||
PKG_CATEGORY[eza]="core"
|
||
|
||
PACKAGES[tldr]="tldr"
|
||
PKG_DESC[tldr]="Simplified man pages [APT]"
|
||
PKG_METHOD[tldr]="apt"
|
||
PKG_CATEGORY[tldr]="core"
|
||
|
||
# DEVELOPMENT TOOLS
|
||
PACKAGES[neovim]="neovim"
|
||
PKG_DESC[neovim]="Modern Vim-based editor [APT]"
|
||
PKG_METHOD[neovim]="apt"
|
||
PKG_CATEGORY[neovim]="dev"
|
||
|
||
PACKAGES[python3-pip]="python3-pip"
|
||
PKG_DESC[python3-pip]="Python buntage installer [APT]"
|
||
PKG_METHOD[python3-pip]="apt"
|
||
PKG_CATEGORY[python3-pip]="dev"
|
||
|
||
PACKAGES[python3-venv]="python3-venv"
|
||
PKG_DESC[python3-venv]="Python virtual environments [APT]"
|
||
PKG_METHOD[python3-venv]="apt"
|
||
PKG_CATEGORY[python3-venv]="dev"
|
||
|
||
PACKAGES[default-jdk]="default-jdk"
|
||
PKG_DESC[default-jdk]="Java Development Kit [APT]"
|
||
PKG_METHOD[default-jdk]="apt"
|
||
PKG_CATEGORY[default-jdk]="dev"
|
||
|
||
PACKAGES[golang]="golang-go"
|
||
PKG_DESC[golang]="Go programming language [APT]"
|
||
PKG_METHOD[golang]="apt"
|
||
PKG_CATEGORY[golang]="dev"
|
||
|
||
# CONTAINERS
|
||
PACKAGES[docker]="docker-ce"
|
||
PKG_DESC[docker]="Docker container platform [DEB]"
|
||
PKG_METHOD[docker]="custom"
|
||
PKG_CATEGORY[docker]="containers"
|
||
|
||
PACKAGES[docker-compose]="docker-compose-plugin"
|
||
PKG_DESC[docker-compose]="Docker Compose plugin [APT]"
|
||
PKG_METHOD[docker-compose]="apt"
|
||
PKG_CATEGORY[docker-compose]="containers"
|
||
PKG_DEPS[docker-compose]="docker"
|
||
|
||
# WEB STACK
|
||
PACKAGES[nginx]="nginx"
|
||
PKG_DESC[nginx]="High-performance web server [APT]"
|
||
PKG_METHOD[nginx]="apt"
|
||
PKG_CATEGORY[nginx]="web"
|
||
|
||
PACKAGES[apache2]="apache2"
|
||
PKG_DESC[apache2]="Apache HTTP Server [APT]"
|
||
PKG_METHOD[apache2]="apt"
|
||
PKG_CATEGORY[apache2]="web"
|
||
|
||
PACKAGES[php-fpm]="php${PHP_VER}-fpm"
|
||
PKG_DESC[php-fpm]="PHP FastCGI Process Manager"
|
||
PKG_METHOD[php-fpm]="apt"
|
||
PKG_CATEGORY[php-fpm]="web"
|
||
|
||
PACKAGES[libapache2-mod-php]="libapache2-mod-php${PHP_VER}"
|
||
PKG_DESC[libapache2-mod-php]="PHP module for Apache [APT]"
|
||
PKG_METHOD[libapache2-mod-php]="apt"
|
||
PKG_CATEGORY[libapache2-mod-php]="web"
|
||
|
||
PACKAGES[php-mysql]="php${PHP_VER}-mysql"
|
||
PKG_DESC[php-mysql]="PHP MySQL extension [APT]"
|
||
PKG_METHOD[php-mysql]="apt"
|
||
PKG_CATEGORY[php-mysql]="web"
|
||
|
||
PACKAGES[php-curl]="php${PHP_VER}-curl"
|
||
PKG_DESC[php-curl]="PHP cURL extension [APT]"
|
||
PKG_METHOD[php-curl]="apt"
|
||
PKG_CATEGORY[php-curl]="web"
|
||
|
||
PACKAGES[php-gd]="php${PHP_VER}-gd"
|
||
PKG_DESC[php-gd]="PHP GD graphics extension [APT]"
|
||
PKG_METHOD[php-gd]="apt"
|
||
PKG_CATEGORY[php-gd]="web"
|
||
|
||
PACKAGES[php-xml]="php${PHP_VER}-xml"
|
||
PKG_DESC[php-xml]="PHP XML extension [APT]"
|
||
PKG_METHOD[php-xml]="apt"
|
||
PKG_CATEGORY[php-xml]="web"
|
||
|
||
PACKAGES[php-mbstring]="php${PHP_VER}-mbstring"
|
||
PKG_DESC[php-mbstring]="PHP multibyte string extension [APT]"
|
||
PKG_METHOD[php-mbstring]="apt"
|
||
PKG_CATEGORY[php-mbstring]="web"
|
||
|
||
PACKAGES[php-zip]="php${PHP_VER}-zip"
|
||
PKG_DESC[php-zip]="PHP ZIP extension [APT]"
|
||
PKG_METHOD[php-zip]="apt"
|
||
PKG_CATEGORY[php-zip]="web"
|
||
|
||
PACKAGES[mariadb]="mariadb-server"
|
||
PKG_DESC[mariadb]="MariaDB database server [APT]"
|
||
PKG_METHOD[mariadb]="apt"
|
||
PKG_CATEGORY[mariadb]="web"
|
||
|
||
PACKAGES[certbot]="certbot"
|
||
PKG_DESC[certbot]="Let's Encrypt SSL certificate tool [APT]"
|
||
PKG_METHOD[certbot]="apt"
|
||
PKG_CATEGORY[certbot]="web"
|
||
|
||
PACKAGES[python3-certbot-nginx]="python3-certbot-nginx"
|
||
PKG_DESC[python3-certbot-nginx]="Certbot Nginx plugin [APT]"
|
||
PKG_METHOD[python3-certbot-nginx]="apt"
|
||
PKG_CATEGORY[python3-certbot-nginx]="web"
|
||
|
||
PACKAGES[python3-certbot-apache]="python3-certbot-apache"
|
||
PKG_DESC[python3-certbot-apache]="Certbot Apache plugin [APT]"
|
||
PKG_METHOD[python3-certbot-apache]="apt"
|
||
PKG_CATEGORY[python3-certbot-apache]="web"
|
||
|
||
PACKAGES[redis]="redis-server"
|
||
PKG_DESC[redis]="Redis in-memory data store [APT]"
|
||
PKG_METHOD[redis]="apt"
|
||
PKG_CATEGORY[redis]="web"
|
||
|
||
# NODE.JS
|
||
PACKAGES[nodejs]="nodejs"
|
||
PKG_DESC[nodejs]="Node.js JavaScript runtime [DEB]"
|
||
PKG_METHOD[nodejs]="custom"
|
||
PKG_CATEGORY[nodejs]="dev"
|
||
|
||
PACKAGES[npm]="npm"
|
||
PKG_DESC[npm]="Node buntage manager [APT]"
|
||
PKG_METHOD[npm]="apt"
|
||
PKG_CATEGORY[npm]="dev"
|
||
|
||
# SHELLS & UI
|
||
PACKAGES[zsh]="zsh"
|
||
PKG_DESC[zsh]="Z shell [APT]"
|
||
PKG_METHOD[zsh]="apt"
|
||
PKG_CATEGORY[zsh]="shell"
|
||
|
||
PACKAGES[fonts-powerline]="fonts-powerline"
|
||
PKG_DESC[fonts-powerline]="Powerline fonts [APT]"
|
||
PKG_METHOD[fonts-powerline]="apt"
|
||
PKG_CATEGORY[fonts-powerline]="shell"
|
||
|
||
# EDITORS & IDEs
|
||
PACKAGES[vscode]="code"
|
||
PKG_DESC[vscode]="Visual Studio Code [DEB]"
|
||
PKG_METHOD[vscode]="custom"
|
||
PKG_CATEGORY[vscode]="editors"
|
||
|
||
PACKAGES[sublime-text]="sublime-text"
|
||
PKG_DESC[sublime-text]="Sublime Text editor [DEB]"
|
||
PKG_METHOD[sublime-text]="custom"
|
||
PKG_CATEGORY[sublime-text]="editors"
|
||
|
||
# BROWSERS
|
||
PACKAGES[brave]="brave-browser"
|
||
PKG_DESC[brave]="Brave web browser [DEB]"
|
||
PKG_METHOD[brave]="custom"
|
||
PKG_CATEGORY[brave]="browsers"
|
||
|
||
PACKAGES[firefox]="firefox"
|
||
PKG_DESC[firefox]="Mozilla Firefox browser [APT]"
|
||
PKG_METHOD[firefox]="apt"
|
||
PKG_CATEGORY[firefox]="browsers"
|
||
|
||
PACKAGES[chromium]="chromium"
|
||
PKG_DESC[chromium]="Chromium web browser [APT]"
|
||
PKG_METHOD[chromium]="apt"
|
||
PKG_CATEGORY[chromium]="browsers"
|
||
|
||
# MONITORING
|
||
PACKAGES[htop]="htop"
|
||
PKG_DESC[htop]="Interactive process viewer [APT]"
|
||
PKG_METHOD[htop]="apt"
|
||
PKG_CATEGORY[htop]="monitoring"
|
||
|
||
PACKAGES[btop]="btop"
|
||
PKG_DESC[btop]="Resource monitor with better graphs [APT]"
|
||
PKG_METHOD[btop]="apt"
|
||
PKG_CATEGORY[btop]="monitoring"
|
||
|
||
PACKAGES[glances]="glances"
|
||
PKG_DESC[glances]="Cross-platform system monitor [APT]"
|
||
PKG_METHOD[glances]="apt"
|
||
PKG_CATEGORY[glances]="monitoring"
|
||
|
||
PACKAGES[nethogs]="nethogs"
|
||
PKG_DESC[nethogs]="Network bandwidth monitor per process [APT]"
|
||
PKG_METHOD[nethogs]="apt"
|
||
PKG_CATEGORY[nethogs]="monitoring"
|
||
|
||
PACKAGES[iotop]="iotop"
|
||
PKG_DESC[iotop]="I/O monitor [APT]"
|
||
PKG_METHOD[iotop]="apt"
|
||
PKG_CATEGORY[iotop]="monitoring"
|
||
|
||
# SECURITY
|
||
PACKAGES[ufw]="ufw"
|
||
PKG_DESC[ufw]="Uncomplicated Firewall [APT]"
|
||
PKG_METHOD[ufw]="apt"
|
||
PKG_CATEGORY[ufw]="security"
|
||
|
||
PACKAGES[fail2ban]="fail2ban"
|
||
PKG_DESC[fail2ban]="Intrusion prevention system [APT]"
|
||
PKG_METHOD[fail2ban]="apt"
|
||
PKG_CATEGORY[fail2ban]="security"
|
||
|
||
# FLATPAK & APPS
|
||
PACKAGES[flatpak]="flatpak"
|
||
PKG_DESC[flatpak]="Flatpak buntage manager [APT]"
|
||
PKG_METHOD[flatpak]="apt"
|
||
PKG_CATEGORY[flatpak]="system"
|
||
|
||
PACKAGES[localsend]="localsend"
|
||
PKG_DESC[localsend]="LocalSend file sharing [SNP]"
|
||
PKG_METHOD[localsend]="snap"
|
||
PKG_CATEGORY[localsend]="communication"
|
||
PKG_DEPS[localsend]="snapd"
|
||
|
||
# SNAP APPS
|
||
PACKAGES[spotify]="spotify"
|
||
PKG_DESC[spotify]="Music streaming service [SNP]"
|
||
PKG_METHOD[spotify]="snap"
|
||
PKG_CATEGORY[spotify]="multimedia"
|
||
|
||
PACKAGES[postman]="postman"
|
||
PKG_DESC[postman]="API development platform [SNP]"
|
||
PKG_METHOD[postman]="snap"
|
||
PKG_CATEGORY[postman]="development"
|
||
|
||
# DESKTOP APPS
|
||
PACKAGES[obs-studio]="obs-studio"
|
||
PKG_DESC[obs-studio]="OBS Studio streaming/recording [APT]"
|
||
PKG_METHOD[obs-studio]="apt"
|
||
PKG_CATEGORY[obs-studio]="multimedia"
|
||
|
||
PACKAGES[vlc]="vlc"
|
||
PKG_DESC[vlc]="VLC media player [APT]"
|
||
PKG_METHOD[vlc]="apt"
|
||
PKG_CATEGORY[vlc]="multimedia"
|
||
|
||
# CLOUD & SYNC
|
||
PACKAGES[rclone]="rclone"
|
||
PKG_DESC[rclone]="Cloud storage sync tool [APT]"
|
||
PKG_METHOD[rclone]="apt"
|
||
PKG_CATEGORY[rclone]="cloud"
|
||
|
||
# TERMINALS
|
||
PACKAGES[warp-terminal]="warp-terminal"
|
||
PKG_DESC[warp-terminal]="Modern terminal with AI features [DEB]"
|
||
PKG_METHOD[warp-terminal]="custom"
|
||
PKG_CATEGORY[warp-terminal]="terminals"
|
||
|
||
# GAMING
|
||
PACKAGES[steam]="steam"
|
||
PKG_DESC[steam]="Steam gaming platform [APT]"
|
||
PKG_METHOD[steam]="apt"
|
||
PKG_CATEGORY[steam]="gaming"
|
||
|
||
PACKAGES[heroic-launcher]="heroic"
|
||
PKG_DESC[heroic-launcher]="Open-source Epic Games/GOG launcher [SNP]"
|
||
PKG_METHOD[heroic-launcher]="snap"
|
||
PKG_CATEGORY[heroic-launcher]="gaming"
|
||
PKG_DEPS[heroic-launcher]="snapd"
|
||
|
||
PACKAGES[gimp]="gimp"
|
||
PKG_DESC[gimp]="GIMP image editor [APT]"
|
||
PKG_METHOD[gimp]="apt"
|
||
PKG_CATEGORY[gimp]="multimedia"
|
||
|
||
# OFFICE & PRODUCTIVITY
|
||
PACKAGES[libreoffice]="libreoffice"
|
||
PKG_DESC[libreoffice]="LibreOffice office suite [APT]"
|
||
PKG_METHOD[libreoffice]="apt"
|
||
PKG_CATEGORY[libreoffice]="office"
|
||
|
||
PACKAGES[thunderbird]="thunderbird"
|
||
PKG_DESC[thunderbird]="Thunderbird email client [APT]"
|
||
PKG_METHOD[thunderbird]="apt"
|
||
PKG_CATEGORY[thunderbird]="office"
|
||
|
||
# COMMUNICATION
|
||
PACKAGES[discord]="discord"
|
||
PKG_DESC[discord]="Discord voice and text chat [SNP]"
|
||
PKG_METHOD[discord]="snap"
|
||
PKG_CATEGORY[discord]="communication"
|
||
|
||
PACKAGES[telegram-desktop]="telegram-desktop"
|
||
PKG_DESC[telegram-desktop]="Telegram messaging app [APT]"
|
||
PKG_METHOD[telegram-desktop]="apt"
|
||
PKG_CATEGORY[telegram-desktop]="communication"
|
||
|
||
PACKAGES[zoom]="zoom-client"
|
||
PKG_DESC[zoom]="Zoom video conferencing [SNP]"
|
||
PKG_METHOD[zoom]="snap"
|
||
PKG_CATEGORY[zoom]="communication"
|
||
|
||
# MULTIMEDIA
|
||
PACKAGES[audacity]="audacity"
|
||
PKG_DESC[audacity]="Audacity audio editor [APT]"
|
||
PKG_METHOD[audacity]="apt"
|
||
PKG_CATEGORY[audacity]="multimedia"
|
||
|
||
PACKAGES[blender]="blender"
|
||
PKG_DESC[blender]="Blender 3D creation suite [SNP]"
|
||
PKG_METHOD[blender]="snap"
|
||
PKG_CATEGORY[blender]="multimedia"
|
||
|
||
PACKAGES[inkscape]="inkscape"
|
||
PKG_DESC[inkscape]="Inkscape vector graphics editor [APT]"
|
||
PKG_METHOD[inkscape]="apt"
|
||
PKG_CATEGORY[inkscape]="multimedia"
|
||
|
||
# AI & MODERN TOOLS
|
||
PACKAGES[ollama]="ollama"
|
||
PKG_DESC[ollama]="Local AI model runner (Llama, Mistral, etc.) [DEB]"
|
||
PKG_METHOD[ollama]="custom"
|
||
PKG_CATEGORY[ollama]="ai"
|
||
|
||
PACKAGES[gollama]="gollama"
|
||
PKG_DESC[gollama]="Advanced LLM model management and interaction tool [DEB]"
|
||
PKG_METHOD[gollama]="custom"
|
||
PKG_CATEGORY[gollama]="ai"
|
||
|
||
PACKAGES[ffmpeg]="ffmpeg"
|
||
PKG_DESC[ffmpeg]="Complete multimedia processing toolkit [APT]"
|
||
PKG_METHOD[ffmpeg]="apt"
|
||
PKG_CATEGORY[ffmpeg]="multimedia"
|
||
|
||
PACKAGES[yt-dlp]="yt-dlp"
|
||
PKG_DESC[yt-dlp]="Modern YouTube/media downloader (youtube-dl fork) [PIP]"
|
||
PKG_METHOD[yt-dlp]="custom"
|
||
PKG_CATEGORY[yt-dlp]="multimedia"
|
||
|
||
PACKAGES[n8n]="n8n"
|
||
PKG_DESC[n8n]="Workflow automation tool (self-hosted Zapier alternative) [NPM]"
|
||
PKG_METHOD[n8n]="custom"
|
||
PKG_CATEGORY[n8n]="development"
|
||
|
||
# ==============================================================================
|
||
# ALTERNATIVE INSTALLATION METHODS
|
||
# ==============================================================================
|
||
# Apps that have multiple installation options - users can choose their preferred method
|
||
|
||
# VS Code alternatives
|
||
PACKAGES[vscode-snap]="code"
|
||
PKG_DESC[vscode-snap]="Visual Studio Code [SNP] - Alternative to DEB version"
|
||
PKG_METHOD[vscode-snap]="snap"
|
||
PKG_CATEGORY[vscode-snap]="editors"
|
||
|
||
PACKAGES[vscode-flatpak]="com.visualstudio.code"
|
||
PKG_DESC[vscode-flatpak]="Visual Studio Code [FLT] - Alternative to DEB version"
|
||
PKG_METHOD[vscode-flatpak]="flatpak"
|
||
PKG_CATEGORY[vscode-flatpak]="editors"
|
||
PKG_DEPS[vscode-flatpak]="flatpak"
|
||
|
||
# Firefox alternatives
|
||
PACKAGES[firefox-snap]="firefox"
|
||
PKG_DESC[firefox-snap]="Mozilla Firefox browser [SNP] - Alternative to APT version"
|
||
PKG_METHOD[firefox-snap]="snap"
|
||
PKG_CATEGORY[firefox-snap]="browsers"
|
||
|
||
PACKAGES[firefox-flatpak]="org.mozilla.firefox"
|
||
PKG_DESC[firefox-flatpak]="Mozilla Firefox browser [FLT] - Alternative to APT version"
|
||
PKG_METHOD[firefox-flatpak]="flatpak"
|
||
PKG_CATEGORY[firefox-flatpak]="browsers"
|
||
PKG_DEPS[firefox-flatpak]="flatpak"
|
||
|
||
# Chromium alternatives
|
||
PACKAGES[chromium-snap]="chromium"
|
||
PKG_DESC[chromium-snap]="Chromium web browser [SNP] - Alternative to APT version"
|
||
PKG_METHOD[chromium-snap]="snap"
|
||
PKG_CATEGORY[chromium-snap]="browsers"
|
||
|
||
PACKAGES[chromium-flatpak]="org.chromium.Chromium"
|
||
PKG_DESC[chromium-flatpak]="Chromium web browser [FLT] - Alternative to APT version"
|
||
PKG_METHOD[chromium-flatpak]="flatpak"
|
||
PKG_CATEGORY[chromium-flatpak]="browsers"
|
||
PKG_DEPS[chromium-flatpak]="flatpak"
|
||
|
||
# VLC alternatives
|
||
PACKAGES[vlc-snap]="vlc"
|
||
PKG_DESC[vlc-snap]="VLC media player [SNP] - Alternative to APT version"
|
||
PKG_METHOD[vlc-snap]="snap"
|
||
PKG_CATEGORY[vlc-snap]="multimedia"
|
||
|
||
PACKAGES[vlc-flatpak]="org.videolan.VLC"
|
||
PKG_DESC[vlc-flatpak]="VLC media player [FLT] - Alternative to APT version"
|
||
PKG_METHOD[vlc-flatpak]="flatpak"
|
||
PKG_CATEGORY[vlc-flatpak]="multimedia"
|
||
PKG_DEPS[vlc-flatpak]="flatpak"
|
||
|
||
# GIMP alternatives
|
||
PACKAGES[gimp-snap]="gimp"
|
||
PKG_DESC[gimp-snap]="GIMP image editor [SNP] - Alternative to APT version"
|
||
PKG_METHOD[gimp-snap]="snap"
|
||
PKG_CATEGORY[gimp-snap]="multimedia"
|
||
|
||
PACKAGES[gimp-flatpak]="org.gimp.GIMP"
|
||
PKG_DESC[gimp-flatpak]="GIMP image editor [FLT] - Alternative to APT version"
|
||
PKG_METHOD[gimp-flatpak]="flatpak"
|
||
PKG_CATEGORY[gimp-flatpak]="multimedia"
|
||
PKG_DEPS[gimp-flatpak]="flatpak"
|
||
|
||
# OBS Studio alternatives
|
||
PACKAGES[obs-studio-flatpak]="com.obsproject.Studio"
|
||
PKG_DESC[obs-studio-flatpak]="OBS Studio streaming/recording [FLT] - Alternative to APT version"
|
||
PKG_METHOD[obs-studio-flatpak]="flatpak"
|
||
PKG_CATEGORY[obs-studio-flatpak]="multimedia"
|
||
PKG_DEPS[obs-studio-flatpak]="flatpak"
|
||
|
||
# Discord alternatives (APT version)
|
||
PACKAGES[discord-deb]="discord"
|
||
PKG_DESC[discord-deb]="Discord voice and text chat [DEB] - Alternative to SNP version"
|
||
PKG_METHOD[discord-deb]="custom"
|
||
PKG_CATEGORY[discord-deb]="communication"
|
||
|
||
PACKAGES[discord-flatpak]="com.discordapp.Discord"
|
||
PKG_DESC[discord-flatpak]="Discord voice and text chat [FLT] - Alternative to SNP version"
|
||
PKG_METHOD[discord-flatpak]="flatpak"
|
||
PKG_CATEGORY[discord-flatpak]="communication"
|
||
PKG_DEPS[discord-flatpak]="flatpak"
|
||
|
||
# Audacity alternatives
|
||
PACKAGES[audacity-snap]="audacity"
|
||
PKG_DESC[audacity-snap]="Audacity audio editor [SNP] - Alternative to APT version"
|
||
PKG_METHOD[audacity-snap]="snap"
|
||
PKG_CATEGORY[audacity-snap]="multimedia"
|
||
|
||
PACKAGES[audacity-flatpak]="org.audacityteam.Audacity"
|
||
PKG_DESC[audacity-flatpak]="Audacity audio editor [FLT] - Alternative to APT version"
|
||
PKG_METHOD[audacity-flatpak]="flatpak"
|
||
PKG_CATEGORY[audacity-flatpak]="multimedia"
|
||
PKG_DEPS[audacity-flatpak]="flatpak"
|
||
|
||
# Inkscape alternatives
|
||
PACKAGES[inkscape-snap]="inkscape"
|
||
PKG_DESC[inkscape-snap]="Inkscape vector graphics [SNP] - Alternative to APT version"
|
||
PKG_METHOD[inkscape-snap]="snap"
|
||
PKG_CATEGORY[inkscape-snap]="multimedia"
|
||
|
||
PACKAGES[inkscape-flatpak]="org.inkscape.Inkscape"
|
||
PKG_DESC[inkscape-flatpak]="Inkscape vector graphics [FLT] - Alternative to APT version"
|
||
PKG_METHOD[inkscape-flatpak]="flatpak"
|
||
PKG_CATEGORY[inkscape-flatpak]="multimedia"
|
||
PKG_DEPS[inkscape-flatpak]="flatpak"
|
||
|
||
# ==============================================================================
|
||
# CATEGORY DEFINITIONS
|
||
# ==============================================================================
|
||
|
||
CATEGORIES=(
|
||
"core:Core Utilities"
|
||
"dev:Development Tools"
|
||
"ai:AI & LLM Tools"
|
||
"containers:Containers & Orchestration"
|
||
"web:Web Stack"
|
||
"shell:Shells & Customization"
|
||
"editors:Editors & IDEs"
|
||
"browsers:Web Browsers"
|
||
"monitoring:System Monitoring"
|
||
"security:Security Tools"
|
||
"system:System Tools"
|
||
"office:Office & Productivity"
|
||
"communication:Communication"
|
||
"multimedia:Multimedia & Graphics"
|
||
"cloud:Cloud & Sync"
|
||
"terminals:Terminals"
|
||
"gaming:Gaming"
|
||
)
|
||
|
||
# ==============================================================================
|
||
# INSTALLATION FUNCTIONS
|
||
# ==============================================================================
|
||
|
||
apt_update() {
|
||
log "Updating APT cache..."
|
||
if sudo apt-get update -qq 2>&1 | tee -a "$LOGFILE"; then
|
||
log "APT cache updated successfully"
|
||
else
|
||
local exit_code=$?
|
||
if [ $exit_code -eq 0 ]; then
|
||
log "APT cache updated with warnings (non-critical)"
|
||
else
|
||
log "WARNING: APT cache update failed with exit code $exit_code"
|
||
log "Continuing anyway - some packages may not be available"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
install_apt_package() {
|
||
local pkg="$1"
|
||
log "Installing $pkg via APT..."
|
||
if sudo apt-get install -y --no-install-recommends "$pkg" 2>&1 | tee -a "$LOGFILE"; then
|
||
log "Successfully installed $pkg"
|
||
return 0
|
||
else
|
||
log "ERROR: Failed to install $pkg"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
remove_apt_package() {
|
||
local pkg="$1"
|
||
log "Removing $pkg via APT..."
|
||
if sudo apt-get remove --purge -y "$pkg" 2>&1 | tee -a "$LOGFILE"; then
|
||
log "Successfully removed $pkg"
|
||
return 0
|
||
else
|
||
log "ERROR: Failed to remove $pkg"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
install_snap_package() {
|
||
local pkg="$1"
|
||
log "Installing $pkg via Snap..."
|
||
sudo snap install "$pkg" 2>&1 | tee -a "$LOGFILE"
|
||
}
|
||
|
||
remove_snap_package() {
|
||
local pkg="$1"
|
||
log "Removing $pkg via Snap..."
|
||
sudo snap remove "$pkg" 2>&1 | tee -a "$LOGFILE"
|
||
}
|
||
|
||
install_flatpak_package() {
|
||
local pkg="$1"
|
||
log "Installing $pkg via Flatpak..."
|
||
flatpak install -y flathub "$pkg" 2>&1 | tee -a "$LOGFILE"
|
||
}
|
||
|
||
remove_flatpak_package() {
|
||
local pkg="$1"
|
||
log "Removing $pkg via Flatpak..."
|
||
flatpak uninstall -y "$pkg" 2>&1 | tee -a "$LOGFILE"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# CUSTOM INSTALLERS (for buntages requiring special setup)
|
||
# ==============================================================================
|
||
|
||
install_docker() {
|
||
log "Installing Docker from official repository..."
|
||
|
||
# Remove old versions
|
||
sudo apt-get remove -y docker docker-engine docker.io containerd runc 2>/dev/null || true
|
||
|
||
# Add Docker repository
|
||
sudo install -m 0755 -d /etc/apt/keyrings
|
||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
||
|
||
echo \
|
||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
|
||
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
|
||
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||
|
||
apt_update
|
||
install_apt_package "docker-ce"
|
||
install_apt_package "docker-ce-cli"
|
||
install_apt_package "containerd.io"
|
||
install_apt_package "docker-buildx-plugin"
|
||
install_apt_package "docker-compose-plugin"
|
||
|
||
# Add user to docker group
|
||
sudo usermod -aG docker "$USER" || true
|
||
|
||
ui_msg "Docker Installed" "Docker installed successfully. Log out and back in for group changes to take effect."
|
||
}
|
||
|
||
remove_docker() {
|
||
ui_yesno "Remove Docker?" "This will remove Docker and optionally its data. Continue?" || return 1
|
||
|
||
remove_apt_package "docker-ce"
|
||
remove_apt_package "docker-ce-cli"
|
||
remove_apt_package "containerd.io"
|
||
remove_apt_package "docker-buildx-plugin"
|
||
remove_apt_package "docker-compose-plugin"
|
||
|
||
if ui_yesno "Remove Data?" "Remove Docker data directories (/var/lib/docker)?"; then
|
||
sudo rm -rf /var/lib/docker /var/lib/containerd
|
||
log "Docker data removed"
|
||
fi
|
||
|
||
sudo rm -f /etc/apt/sources.list.d/docker.list
|
||
sudo rm -f /etc/apt/keyrings/docker.gpg
|
||
}
|
||
|
||
install_nodejs() {
|
||
log "Installing Node.js ${NODE_LTS} from NodeSource..."
|
||
|
||
curl -fsSL https://deb.nodesource.com/setup_${NODE_LTS}.x | sudo -E bash -
|
||
apt_update
|
||
install_apt_package "nodejs"
|
||
|
||
ui_msg "Node.js Installed" "Node.js ${NODE_LTS} and npm installed successfully."
|
||
}
|
||
|
||
remove_nodejs() {
|
||
remove_apt_package "nodejs"
|
||
sudo rm -f /etc/apt/sources.list.d/nodesource.list
|
||
}
|
||
|
||
install_vscode() {
|
||
log "Installing VS Code from Microsoft repository..."
|
||
|
||
wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /usr/share/keyrings/packages.microsoft.gpg > /dev/null
|
||
echo "deb [arch=amd64,arm64,armhf signed-by=/usr/share/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" | sudo tee /etc/apt/sources.list.d/vscode.list
|
||
|
||
apt_update
|
||
install_apt_package "code"
|
||
|
||
ui_msg "VS Code Installed" "Visual Studio Code installed successfully."
|
||
}
|
||
|
||
remove_vscode() {
|
||
remove_apt_package "code"
|
||
sudo rm -f /etc/apt/sources.list.d/vscode.list
|
||
sudo rm -f /usr/share/keyrings/packages.microsoft.gpg
|
||
}
|
||
|
||
install_brave() {
|
||
log "Installing Brave browser..."
|
||
|
||
sudo curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg
|
||
echo "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg] https://brave-browser-apt-release.s3.brave.com/ stable main" | sudo tee /etc/apt/sources.list.d/brave-browser-release.list
|
||
|
||
apt_update
|
||
install_apt_package "brave-browser"
|
||
|
||
ui_msg "Brave Installed" "Brave browser installed successfully."
|
||
}
|
||
|
||
remove_brave() {
|
||
remove_apt_package "brave-browser"
|
||
sudo rm -f /etc/apt/sources.list.d/brave-browser-release.list
|
||
sudo rm -f /usr/share/keyrings/brave-browser-archive-keyring.gpg
|
||
}
|
||
|
||
install_sublime() {
|
||
log "Installing Sublime Text..."
|
||
|
||
wget -qO - https://download.sublimetext.com/sublimehq-pub.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/sublimehq-archive.gpg > /dev/null
|
||
echo "deb [signed-by=/usr/share/keyrings/sublimehq-archive.gpg] https://download.sublimetext.com/ apt/stable/" | sudo tee /etc/apt/sources.list.d/sublime-text.list
|
||
|
||
apt_update
|
||
install_apt_package "sublime-text"
|
||
|
||
ui_msg "Sublime Text Installed" "Sublime Text installed successfully."
|
||
}
|
||
|
||
remove_sublime() {
|
||
remove_apt_package "sublime-text"
|
||
sudo rm -f /etc/apt/sources.list.d/sublime-text.list
|
||
sudo rm -f /usr/share/keyrings/sublimehq-archive.gpg
|
||
}
|
||
|
||
install_warp() {
|
||
log "Installing Warp terminal..."
|
||
|
||
# Add Warp's official GPG key and repository
|
||
sudo mkdir -p /usr/share/keyrings
|
||
if ! wget -qO- https://releases.warp.dev/linux/keys/warp.asc | sudo gpg --dearmor -o /usr/share/keyrings/warp-archive-keyring.gpg; then
|
||
log_error "Failed to add Warp GPG key"
|
||
return 1
|
||
fi
|
||
|
||
# Add Warp repository
|
||
echo "deb [signed-by=/usr/share/keyrings/warp-archive-keyring.gpg] https://releases.warp.dev/linux/deb stable main" | sudo tee /etc/apt/sources.list.d/warp-terminal.list > /dev/null
|
||
|
||
# Update package list and install
|
||
sudo apt-get update -qq
|
||
if install_apt_package "warp-terminal"; then
|
||
ui_msg "Warp Installed" "Warp terminal installed successfully."
|
||
else
|
||
log_error "Failed to install Warp terminal from repository"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
remove_warp() {
|
||
remove_apt_package "warp-terminal"
|
||
sudo rm -f /etc/apt/sources.list.d/warp-terminal.list
|
||
sudo rm -f /usr/share/keyrings/warp-archive-keyring.gpg
|
||
}
|
||
|
||
install_ollama() {
|
||
log "Installing Ollama..."
|
||
|
||
# Download and install Ollama
|
||
if curl -fsSL https://ollama.ai/install.sh | sh; then
|
||
ui_msg "Ollama Installed" "Ollama AI model runner installed successfully."
|
||
else
|
||
log_error "Failed to install Ollama"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
remove_ollama() {
|
||
log "Removing Ollama..."
|
||
sudo systemctl stop ollama 2>/dev/null || true
|
||
sudo systemctl disable ollama 2>/dev/null || true
|
||
sudo rm -f /usr/local/bin/ollama
|
||
sudo rm -rf /usr/share/ollama
|
||
sudo rm -f /etc/systemd/system/ollama.service
|
||
sudo systemctl daemon-reload
|
||
}
|
||
|
||
install_yt-dlp() {
|
||
log "Installing yt-dlp..."
|
||
|
||
# Install via pip for latest version
|
||
if command -v pip3 >/dev/null 2>&1; then
|
||
if pip3 install --user yt-dlp; then
|
||
ui_msg "yt-dlp Installed" "yt-dlp YouTube downloader installed successfully."
|
||
else
|
||
log_error "Failed to install yt-dlp via pip"
|
||
return 1
|
||
fi
|
||
else
|
||
log_error "pip3 not found. Please install Python3 and pip first."
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
remove_yt-dlp() {
|
||
log "Removing yt-dlp..."
|
||
pip3 uninstall -y yt-dlp 2>/dev/null || true
|
||
}
|
||
|
||
install_n8n() {
|
||
log "Installing n8n..."
|
||
|
||
# Check if Node.js is installed
|
||
if ! command -v node >/dev/null 2>&1; then
|
||
log_error "Node.js is required for n8n. Please install Node.js first."
|
||
return 1
|
||
fi
|
||
|
||
# Install n8n globally via npm
|
||
if sudo npm install -g n8n; then
|
||
ui_msg "n8n Installed" "n8n workflow automation tool installed successfully."
|
||
else
|
||
log_error "Failed to install n8n"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
remove_n8n() {
|
||
log "Removing n8n..."
|
||
sudo npm uninstall -g n8n 2>/dev/null || true
|
||
}
|
||
|
||
install_discord-deb() {
|
||
log "Installing Discord (DEB package)..."
|
||
|
||
# Download Discord DEB package
|
||
local discord_url="https://discord.com/api/download?platform=linux&format=deb"
|
||
local temp_file="/tmp/discord.deb"
|
||
|
||
if wget -O "$temp_file" "$discord_url" 2>/dev/null; then
|
||
if sudo dpkg -i "$temp_file" 2>/dev/null || sudo apt-get install -f -y; then
|
||
rm -f "$temp_file"
|
||
ui_msg "Discord Installed" "Discord voice and text chat installed successfully."
|
||
else
|
||
log_error "Failed to install Discord DEB package"
|
||
rm -f "$temp_file"
|
||
return 1
|
||
fi
|
||
else
|
||
log_error "Failed to download Discord DEB package"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
remove_discord-deb() {
|
||
log "Removing Discord (DEB)..."
|
||
remove_apt_package "discord"
|
||
}
|
||
|
||
install_gollama() {
|
||
log "Installing Gollama..."
|
||
|
||
# Download and install Gollama from GitHub releases
|
||
local latest_url=$(curl -s https://api.github.com/repos/sammcj/gollama/releases/latest | grep "browser_download_url.*linux.*amd64" | cut -d '"' -f 4)
|
||
|
||
if [[ -z "$latest_url" ]]; then
|
||
log_error "Failed to get Gollama download URL"
|
||
return 1
|
||
fi
|
||
|
||
local temp_file="/tmp/gollama"
|
||
|
||
if wget -O "$temp_file" "$latest_url" 2>/dev/null; then
|
||
chmod +x "$temp_file"
|
||
sudo mv "$temp_file" /usr/local/bin/gollama
|
||
ui_msg "Gollama Installed" "Gollama LLM management tool installed successfully."
|
||
else
|
||
log_error "Failed to download Gollama"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
remove_gollama() {
|
||
log "Removing Gollama..."
|
||
sudo rm -f /usr/local/bin/gollama
|
||
}
|
||
|
||
# ==============================================================================
|
||
# BUNTAGE MANAGEMENT DISPATCHER
|
||
# ==============================================================================
|
||
|
||
install_package() {
|
||
local name="$1"
|
||
local pkg="${PACKAGES[$name]}"
|
||
local method="${PKG_METHOD[$name]}"
|
||
|
||
# Check dependencies
|
||
if [[ -n "${PKG_DEPS[$name]:-}" ]]; then
|
||
local dep="${PKG_DEPS[$name]}"
|
||
if ! is_package_installed "$dep"; then
|
||
ui_msg "Dependency Required" "$name requires $dep. Install $dep first."
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
local install_result=0
|
||
|
||
case "$method" in
|
||
apt)
|
||
apt_update
|
||
install_apt_package "$pkg"
|
||
install_result=$?
|
||
;;
|
||
snap)
|
||
install_snap_package "$pkg"
|
||
install_result=$?
|
||
;;
|
||
flatpak)
|
||
if ! is_apt_installed "flatpak"; then
|
||
ui_msg "Flatpak Required" "Installing Flatpak first..."
|
||
apt_update
|
||
install_apt_package "flatpak"
|
||
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||
fi
|
||
install_flatpak_package "$pkg"
|
||
install_result=$?
|
||
;;
|
||
custom)
|
||
case "$name" in
|
||
docker) install_docker; install_result=$? ;;
|
||
nodejs) install_nodejs; install_result=$? ;;
|
||
vscode) install_vscode; install_result=$? ;;
|
||
brave) install_brave; install_result=$? ;;
|
||
sublime-text) install_sublime; install_result=$? ;;
|
||
warp-terminal) install_warp; install_result=$? ;;
|
||
ollama) install_ollama; install_result=$? ;;
|
||
yt-dlp) install_yt-dlp; install_result=$? ;;
|
||
n8n) install_n8n; install_result=$? ;;
|
||
gollama) install_gollama; install_result=$? ;;
|
||
discord-deb) install_discord-deb; install_result=$? ;;
|
||
*) log_error "Unknown custom installer: $name"; install_result=1 ;;
|
||
esac
|
||
;;
|
||
esac
|
||
|
||
# Update cache for this specific buntage if installation was successful
|
||
if [[ $install_result -eq 0 ]]; then
|
||
log "Installation successful, updating cache for buntage: $name"
|
||
update_package_cache "$name"
|
||
fi
|
||
|
||
return $install_result
|
||
}
|
||
|
||
remove_package() {
|
||
local name="$1"
|
||
local pkg="${PACKAGES[$name]}"
|
||
local method="${PKG_METHOD[$name]}"
|
||
|
||
ui_yesno "Confirm Removal" "Are you sure you want to remove $name?\n\n${PKG_DESC[$name]}" || return 1
|
||
|
||
local remove_result=0
|
||
|
||
case "$method" in
|
||
apt)
|
||
remove_apt_package "$pkg"
|
||
remove_result=$?
|
||
;;
|
||
snap)
|
||
remove_snap_package "$pkg"
|
||
remove_result=$?
|
||
;;
|
||
flatpak)
|
||
remove_flatpak_package "$pkg"
|
||
remove_result=$?
|
||
;;
|
||
custom)
|
||
case "$name" in
|
||
docker) remove_docker; remove_result=$? ;;
|
||
nodejs) remove_nodejs; remove_result=$? ;;
|
||
vscode) remove_vscode; remove_result=$? ;;
|
||
brave) remove_brave; remove_result=$? ;;
|
||
sublime-text) remove_sublime; remove_result=$? ;;
|
||
warp-terminal) remove_warp; remove_result=$? ;;
|
||
ollama) remove_ollama; remove_result=$? ;;
|
||
yt-dlp) remove_yt-dlp; remove_result=$? ;;
|
||
n8n) remove_n8n; remove_result=$? ;;
|
||
gollama) remove_gollama; remove_result=$? ;;
|
||
discord-deb) remove_discord-deb; remove_result=$? ;;
|
||
*) log_error "Unknown custom remover: $name"; remove_result=1 ;;
|
||
esac
|
||
;;
|
||
esac
|
||
|
||
# Update cache for this specific buntage if removal was successful
|
||
if [[ $remove_result -eq 0 ]]; then
|
||
log "Removal successful, updating cache for buntage: $name"
|
||
update_package_cache "$name"
|
||
ui_msg "Removed" "$name has been removed."
|
||
else
|
||
ui_msg "Error" "Failed to remove $name. Check the logs for details."
|
||
fi
|
||
|
||
return $remove_result
|
||
}
|
||
|
||
is_package_installed() {
|
||
local name="$1"
|
||
# Removed verbose logging for package checking
|
||
|
||
# Add error handling for empty or invalid buntage names
|
||
if [[ -z "$name" ]]; then
|
||
log "ERROR: Empty buntage name provided to is_package_installed"
|
||
return 1
|
||
fi
|
||
|
||
if [[ -z "${PACKAGES[$name]:-}" ]]; then
|
||
log "WARNING: Buntage '$name' not found in PACKAGES array"
|
||
return 1
|
||
fi
|
||
|
||
local pkg="${PACKAGES[$name]}"
|
||
local method="${PKG_METHOD[$name]:-}"
|
||
|
||
if [[ -z "$method" ]]; then
|
||
log "WARNING: No method defined for buntage '$name'"
|
||
return 1
|
||
fi
|
||
|
||
log "Buntage '$name' uses method '$method' with buntage name '$pkg'"
|
||
|
||
# Add error handling for each method
|
||
local result=1
|
||
case "$method" in
|
||
apt)
|
||
log "Checking APT installation for '$pkg'"
|
||
if is_apt_installed "$pkg"; then
|
||
result=0
|
||
else
|
||
result=1
|
||
fi
|
||
;;
|
||
snap)
|
||
log "Checking Snap installation for '$pkg'"
|
||
if is_snap_installed "$pkg"; then
|
||
result=0
|
||
else
|
||
result=1
|
||
fi
|
||
;;
|
||
flatpak)
|
||
log "Checking Flatpak installation for '$pkg'"
|
||
if is_flatpak_installed "$pkg"; then
|
||
result=0
|
||
else
|
||
result=1
|
||
fi
|
||
;;
|
||
custom)
|
||
log "Checking custom installation for '$name'"
|
||
case "$name" in
|
||
docker)
|
||
if is_apt_installed "docker-ce"; then
|
||
result=0
|
||
else
|
||
result=1
|
||
fi
|
||
;;
|
||
nodejs)
|
||
if is_binary_available "node"; then
|
||
result=0
|
||
else
|
||
result=1
|
||
fi
|
||
;;
|
||
vscode)
|
||
if is_apt_installed "code"; then
|
||
result=0
|
||
else
|
||
result=1
|
||
fi
|
||
;;
|
||
brave)
|
||
if is_apt_installed "brave-browser"; then
|
||
result=0
|
||
else
|
||
result=1
|
||
fi
|
||
;;
|
||
sublime-text)
|
||
if is_apt_installed "sublime-text"; then
|
||
result=0
|
||
else
|
||
result=1
|
||
fi
|
||
;;
|
||
warp-terminal)
|
||
if is_apt_installed "warp-terminal"; then
|
||
result=0
|
||
else
|
||
result=1
|
||
fi
|
||
;;
|
||
yt-dlp)
|
||
if is_binary_available "yt-dlp"; then
|
||
result=0
|
||
else
|
||
result=1
|
||
fi
|
||
;;
|
||
*)
|
||
log "WARNING: Unknown custom buntage '$name'"
|
||
result=1
|
||
;;
|
||
esac
|
||
;;
|
||
*)
|
||
log "ERROR: Unknown installation method '$method' for buntage '$name'"
|
||
result=1
|
||
;;
|
||
esac
|
||
|
||
log "Buntage '$name' installation check result: $result"
|
||
return $result
|
||
}
|
||
|
||
# ==============================================================================
|
||
# MENU SYSTEM
|
||
# ==============================================================================
|
||
|
||
show_category_menu() {
|
||
log "Entering show_category_menu function"
|
||
|
||
while true; do
|
||
log "Building category menu items..."
|
||
local menu_items=()
|
||
|
||
log "Processing categories array with ${#CATEGORIES[@]} entries"
|
||
for entry in "${CATEGORIES[@]}"; do
|
||
local cat_id="${entry%%:*}"
|
||
local cat_name="${entry#*:}"
|
||
|
||
# Count installed buntages in category
|
||
local total=0
|
||
local installed=0
|
||
|
||
# Use a safer iteration method
|
||
local package_names=()
|
||
for name in "${!PACKAGES[@]}"; do
|
||
package_names+=("$name")
|
||
done
|
||
|
||
for name in "${package_names[@]}"; do
|
||
local pkg_category="${PKG_CATEGORY[$name]:-}"
|
||
|
||
if [[ "$pkg_category" == "$cat_id" ]]; then
|
||
total=$((total + 1))
|
||
|
||
# Re-enable installation check
|
||
if is_package_installed "$name"; then
|
||
installed=$((installed + 1))
|
||
fi
|
||
fi
|
||
done
|
||
|
||
menu_items+=("$cat_id" "(.Y.) $cat_name [$installed/$total installed]")
|
||
done
|
||
|
||
log "Adding additional menu items..."
|
||
menu_items+=("" "(_*_)")
|
||
menu_items+=("system-info" "(.Y.) System Information")
|
||
menu_items+=("keyboard-layout" "(.Y.) Keyboard Layout Configuration")
|
||
menu_items+=("wordpress-setup" "(.Y.) WordPress Installation")
|
||
menu_items+=("bulk-ops" "(.Y.) Bulk Operations")
|
||
menu_items+=("quit" "(Q) Exit Installer")
|
||
|
||
log "Calling ui_menu with ${#menu_items[@]} menu items"
|
||
log "Menu items array contents: ${menu_items[*]}"
|
||
|
||
# Add error handling for ui_menu call
|
||
local choice=""
|
||
set +e # Temporarily disable exit on error
|
||
choice=$(ui_menu "Ultrabunt Ultimate Buntstaller" \
|
||
"Select a buntegory to manage buntages:" \
|
||
24 80 14 "${menu_items[@]}")
|
||
local ui_result=$?
|
||
set -e # Re-enable exit on error
|
||
|
||
if [[ $ui_result -ne 0 ]]; then
|
||
log "User cancelled or error occurred"
|
||
break
|
||
fi
|
||
|
||
case "$choice" in
|
||
quit|back|q)
|
||
# Add confirmation for quit
|
||
if [[ "$choice" == "quit" || "$choice" == "q" ]]; then
|
||
if ui_yesno "Confirm Exit" "Are you sure you want to exit the Ultrabunt installer?"; then
|
||
break
|
||
else
|
||
continue
|
||
fi
|
||
else
|
||
break
|
||
fi
|
||
;;
|
||
system-info)
|
||
show_system_info || {
|
||
log "ERROR: show_system_info failed"
|
||
ui_msg "Error" "Failed to display system information. Please check the logs."
|
||
}
|
||
;;
|
||
keyboard-layout)
|
||
show_keyboard_layout_menu || {
|
||
log "ERROR: show_keyboard_layout_menu failed"
|
||
ui_msg "Error" "Failed to display keyboard layout menu. Please check the logs."
|
||
}
|
||
;;
|
||
wordpress-setup)
|
||
show_wordpress_setup_menu || {
|
||
log "ERROR: show_wordpress_setup_menu failed"
|
||
ui_msg "Error" "Failed to display WordPress setup menu. Please check the logs."
|
||
}
|
||
;;
|
||
bulk-ops)
|
||
show_bulk_operations
|
||
;;
|
||
"")
|
||
continue
|
||
;;
|
||
*)
|
||
show_buntage_list "$choice"
|
||
;;
|
||
esac
|
||
done
|
||
log "Exiting show_category_menu function"
|
||
}
|
||
|
||
show_buntage_list() {
|
||
local category="$1"
|
||
local cat_name=""
|
||
|
||
for entry in "${CATEGORIES[@]}"; do
|
||
if [[ "${entry%%:*}" == "$category" ]]; then
|
||
cat_name="${entry#*:}"
|
||
break
|
||
fi
|
||
done
|
||
|
||
while true; do
|
||
local menu_items=()
|
||
|
||
for name in "${!PACKAGES[@]}"; do
|
||
local pkg_category="${PKG_CATEGORY[$name]:-}"
|
||
|
||
if [[ "$pkg_category" == "$category" ]]; then
|
||
local status="✗ Not Installed"
|
||
local display_text
|
||
if is_package_installed "$name"; then
|
||
status="✓ Installed"
|
||
display_text="(.Y.) ${PKG_DESC[$name]:-No description} [$status]"
|
||
else
|
||
display_text="(_*_) ${PKG_DESC[$name]:-No description} [$status]"
|
||
fi
|
||
# Use buntage key as both the selection value and display text
|
||
# This ensures whiptail returns the buntage key, not the description
|
||
menu_items+=("$name" "$display_text")
|
||
fi
|
||
done
|
||
|
||
if [[ ${#menu_items[@]} -eq 0 ]]; then
|
||
ui_msg "No Buntages" "No buntages found in category: $cat_name"
|
||
return
|
||
fi
|
||
|
||
# Sort menu items alphabetically by buntage name (keeping key-value pairs together)
|
||
# Create temporary array for sorting
|
||
local sorted_items=()
|
||
local temp_array=()
|
||
|
||
# Convert menu_items to sortable format (key:value pairs)
|
||
for ((i=0; i<${#menu_items[@]}; i+=2)); do
|
||
if [[ -n "${menu_items[i]}" ]]; then
|
||
temp_array+=("${menu_items[i]}:${menu_items[i+1]}")
|
||
fi
|
||
done
|
||
|
||
# Sort the temp array
|
||
IFS=$'\n' temp_array=($(sort <<<"${temp_array[*]}"))
|
||
unset IFS
|
||
|
||
# Convert back to menu_items format
|
||
for item in "${temp_array[@]}"; do
|
||
local key="${item%%:*}"
|
||
local value="${item#*:}"
|
||
sorted_items+=("$key" "$value")
|
||
done
|
||
|
||
menu_items=("${sorted_items[@]}")
|
||
|
||
menu_items+=("" "(_*_)")
|
||
menu_items+=("zback" "(Z) ← Back to Categories")
|
||
|
||
local choice
|
||
choice=$(ui_menu "$cat_name" \
|
||
"Select a buntage to manage:" \
|
||
24 80 14 "${menu_items[@]}") || break
|
||
|
||
case "$choice" in
|
||
back|zback|z|"") break ;;
|
||
*) show_buntage_actions "$choice" ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
show_buntage_actions() {
|
||
local name="$1"
|
||
|
||
# Validate that the buntage exists in our arrays
|
||
if [[ -z "${PACKAGES[$name]:-}" ]]; then
|
||
log "ERROR: Buntage '$name' not found in PACKAGES array"
|
||
return 1
|
||
fi
|
||
|
||
local pkg="${PACKAGES[$name]}"
|
||
local desc="${PKG_DESC[$name]:-No description available}"
|
||
local method="${PKG_METHOD[$name]:-unknown}"
|
||
|
||
local status="Not Installed"
|
||
if is_package_installed "$name"; then
|
||
status="✓ Installed"
|
||
fi
|
||
|
||
while true; do
|
||
local info="Buntage: $name\nDescription: $desc\nInstall Method: $method\nBuntage Name: $pkg\nStatus: $status"
|
||
|
||
local menu_items=()
|
||
|
||
if is_package_installed "$name"; then
|
||
menu_items+=("reinstall" "(.Y.) Reinstall Buntage")
|
||
menu_items+=("remove" "(.Y.) Remove Buntage")
|
||
menu_items+=("info" "(.Y.) Show Detailed Info")
|
||
else
|
||
menu_items+=("install" "(.Y.) Install Buntage")
|
||
menu_items+=("info" "(.Y.) Show Detailed Info")
|
||
fi
|
||
|
||
menu_items+=("zback" "(Z) ← Back to Buntage List")
|
||
|
||
local choice
|
||
choice=$(ui_menu "Manage: $name" "$info" 20 80 10 "${menu_items[@]}")
|
||
|
||
case "$choice" in
|
||
install)
|
||
install_package_with_choice "$name"
|
||
ui_msg "Success" "$name installed successfully!"
|
||
status="✓ Installed"
|
||
;;
|
||
reinstall)
|
||
remove_package "$name"
|
||
install_package_with_choice "$name"
|
||
ui_msg "Success" "$name reinstalled successfully!"
|
||
;;
|
||
remove)
|
||
remove_package "$name"
|
||
status="Not Installed"
|
||
;;
|
||
info)
|
||
show_buntage_info "$name"
|
||
;;
|
||
""|back|zback|z)
|
||
return
|
||
;;
|
||
*)
|
||
return
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
show_buntage_info() {
|
||
local name="$1"
|
||
local pkg="${PACKAGES[$name]}"
|
||
local desc="${PKG_DESC[$name]}"
|
||
local method="${PKG_METHOD[$name]}"
|
||
local category="${PKG_CATEGORY[$name]}"
|
||
local deps="${PKG_DEPS[$name]:-None}"
|
||
|
||
local status="Not Installed"
|
||
local details=""
|
||
|
||
if is_package_installed "$name"; then
|
||
status="✓ Installed"
|
||
|
||
case "$method" in
|
||
apt)
|
||
if is_apt_installed "$pkg"; then
|
||
local version=$(dpkg -l "$pkg" 2>/dev/null | awk '/^ii/ {print $3}')
|
||
details="Version: $version\n"
|
||
details+="Files: $(dpkg -L "$pkg" 2>/dev/null | wc -l) files installed\n"
|
||
fi
|
||
;;
|
||
snap)
|
||
if is_snap_installed "$pkg"; then
|
||
local snap_info=$(snap info "$pkg" 2>/dev/null | grep -E "^(installed|tracking)" | head -2)
|
||
details="$snap_info\n"
|
||
fi
|
||
;;
|
||
flatpak)
|
||
if is_flatpak_installed "$pkg"; then
|
||
local flat_info=$(flatpak info "$pkg" 2>/dev/null | grep -E "^(Version|Branch|Ref)")
|
||
details="$flat_info\n"
|
||
fi
|
||
;;
|
||
esac
|
||
fi
|
||
|
||
local info="BUNTAGE INFORMATION\n"
|
||
info+="═══════════════════════════════════════\n\n"
|
||
info+="Name: $name\n"
|
||
info+="Description: $desc\n"
|
||
info+="Category: $category\n"
|
||
info+="Install Method: $method\n"
|
||
info+="Buntage ID: $pkg\n"
|
||
info+="Dependencies: $deps\n"
|
||
info+="Status: $status\n\n"
|
||
|
||
if [[ -n "$details" ]]; then
|
||
info+="INSTALLED DETAILS\n"
|
||
info+="═══════════════════════════════════════\n"
|
||
info+="$details"
|
||
fi
|
||
|
||
ui_info "Buntage Info: $name" "$info"
|
||
}
|
||
|
||
show_system_info() {
|
||
log "Starting show_system_info function"
|
||
|
||
local info="SYSTEM INFORMATION\n"
|
||
info+="═══════════════════════════════════════\n\n"
|
||
|
||
# OS Info
|
||
if [[ -f /etc/os-release ]]; then
|
||
local os_name="Unknown"
|
||
local os_version="N/A"
|
||
|
||
# Safely parse os-release
|
||
while IFS='=' read -r key value 2>/dev/null; do
|
||
case "$key" in
|
||
PRETTY_NAME) os_name="${value//\"/}" ;;
|
||
VERSION) os_version="${value//\"/}" ;;
|
||
esac
|
||
done < /etc/os-release 2>/dev/null || true
|
||
|
||
info+="OS: $os_name\n"
|
||
info+="Version: $os_version\n"
|
||
fi
|
||
|
||
# Kernel
|
||
local kernel_version
|
||
kernel_version=$(uname -r 2>/dev/null) || kernel_version="Unknown"
|
||
info+="Kernel: $kernel_version\n\n"
|
||
|
||
# Hardware Info
|
||
info+="HARDWARE INFORMATION\n"
|
||
info+="─────────────────────────────────────\n"
|
||
|
||
# CPU Info
|
||
local cpu_info
|
||
if [[ -f /proc/cpuinfo ]]; then
|
||
cpu_info=$(grep "model name" /proc/cpuinfo 2>/dev/null | head -1 | cut -d: -f2 | sed 's/^ *//' 2>/dev/null) || cpu_info="Unknown"
|
||
local cpu_cores
|
||
cpu_cores=$(nproc 2>/dev/null) || cpu_cores="Unknown"
|
||
info+="CPU: $cpu_info ($cpu_cores cores)\n"
|
||
else
|
||
info+="CPU: Information not available\n"
|
||
fi
|
||
|
||
# Memory Info
|
||
if [[ -f /proc/meminfo ]]; then
|
||
local mem_total mem_available
|
||
mem_total=$(grep "MemTotal:" /proc/meminfo 2>/dev/null | awk '{print int($2/1024)}' 2>/dev/null) || mem_total="Unknown"
|
||
mem_available=$(grep "MemAvailable:" /proc/meminfo 2>/dev/null | awk '{print int($2/1024)}' 2>/dev/null) || mem_available="Unknown"
|
||
if [[ "$mem_total" != "Unknown" && "$mem_available" != "Unknown" ]]; then
|
||
local mem_used=$((mem_total - mem_available))
|
||
info+="Memory: ${mem_used}MB used / ${mem_total}MB total\n"
|
||
else
|
||
info+="Memory: Information not available\n"
|
||
fi
|
||
else
|
||
info+="Memory: Information not available\n"
|
||
fi
|
||
|
||
# Storage Information
|
||
info+="STORAGE INFORMATION\n"
|
||
info+="─────────────────────────────────────\n"
|
||
|
||
# Root filesystem
|
||
local disk_info
|
||
disk_info=$(df -h / 2>/dev/null | tail -1 2>/dev/null | awk '{print $3 " used / " $2 " total (" $5 " full)"}' 2>/dev/null) || disk_info="Information not available"
|
||
info+="Root (/): $disk_info\n"
|
||
|
||
# All mounted filesystems (excluding special filesystems)
|
||
local mount_info
|
||
mount_info=$(df -h 2>/dev/null | grep -E '^/dev/' | grep -v -E '(tmpfs|udev|overlay)' | awk '{print $6 ": " $3 " used / " $2 " total (" $5 " full)"}' 2>/dev/null | tail -n +2 2>/dev/null)
|
||
if [[ -n "$mount_info" ]]; then
|
||
while IFS= read -r line; do
|
||
[[ "$line" != *"/ :"* ]] && info+="$line\n"
|
||
done <<< "$mount_info"
|
||
fi
|
||
|
||
# Swap information
|
||
local swap_info
|
||
swap_info=$(free -h 2>/dev/null | grep "Swap:" 2>/dev/null | awk '{print $3 " used / " $2 " total"}' 2>/dev/null) || swap_info="Not available"
|
||
info+="Swap: $swap_info\n"
|
||
|
||
# Disk usage summary
|
||
local total_disk_count
|
||
total_disk_count=$(lsblk -d 2>/dev/null | grep -c "disk" 2>/dev/null) || total_disk_count="Unknown"
|
||
info+="Physical disks: $total_disk_count detected\n\n"
|
||
|
||
# System Uptime and Load
|
||
local uptime_info load_avg
|
||
uptime_info=$(uptime -p 2>/dev/null | sed 's/up //' 2>/dev/null) || uptime_info="Unknown"
|
||
load_avg=$(uptime 2>/dev/null | awk -F'load average:' '{print $2}' 2>/dev/null | sed 's/^ *//' 2>/dev/null) || load_avg="Unknown"
|
||
|
||
info+="Uptime: $uptime_info\n"
|
||
info+="Load average:$load_avg\n"
|
||
|
||
# Process count
|
||
local process_count
|
||
process_count=$(ps aux 2>/dev/null | wc -l 2>/dev/null) || process_count="Unknown"
|
||
[[ "$process_count" != "Unknown" ]] && process_count=$((process_count - 1)) # Subtract header line
|
||
info+="Running processes: $process_count\n\n"
|
||
|
||
# Buntage Managers
|
||
info+="BUNTAGE MANAGERS\n"
|
||
info+="─────────────────────────────────────\n"
|
||
|
||
# APT detailed info
|
||
local apt_count apt_upgradable apt_auto_removable
|
||
apt_count=$(dpkg -l 2>/dev/null | grep -c "^ii" 2>/dev/null) || apt_count="0"
|
||
apt_upgradable=$(apt list --upgradable 2>/dev/null | grep -c "upgradable" 2>/dev/null) || apt_upgradable="0"
|
||
apt_auto_removable=$(apt autoremove --dry-run 2>/dev/null | grep -c "^Remv" 2>/dev/null) || apt_auto_removable="0"
|
||
|
||
info+="APT: $apt_count installed"
|
||
[[ "$apt_upgradable" != "0" ]] && info+=" ($apt_upgradable upgradable)"
|
||
[[ "$apt_auto_removable" != "0" ]] && info+=" ($apt_auto_removable auto-removable)"
|
||
info+="\n"
|
||
|
||
# Snap detailed info
|
||
if command -v snap &>/dev/null; then
|
||
local snap_count snap_refreshable
|
||
snap_count=$(snap list 2>/dev/null | tail -n +2 2>/dev/null | wc -l 2>/dev/null) || snap_count="0"
|
||
snap_refreshable=$(snap refresh --list 2>/dev/null | tail -n +2 2>/dev/null | wc -l 2>/dev/null) || snap_refreshable="0"
|
||
|
||
info+="Snap: $snap_count installed"
|
||
[[ "$snap_refreshable" != "0" ]] && info+=" ($snap_refreshable refreshable)"
|
||
info+="\n"
|
||
else
|
||
info+="Snap: Not available\n"
|
||
fi
|
||
|
||
# Flatpak detailed info
|
||
if command -v flatpak &>/dev/null; then
|
||
local flat_count flat_updates flat_remotes
|
||
flat_count=$(flatpak list --app 2>/dev/null | wc -l 2>/dev/null) || flat_count="0"
|
||
flat_updates=$(flatpak remote-ls --updates 2>/dev/null | wc -l 2>/dev/null) || flat_updates="0"
|
||
flat_remotes=$(flatpak remotes 2>/dev/null | tail -n +2 2>/dev/null | wc -l 2>/dev/null) || flat_remotes="0"
|
||
|
||
info+="Flatpak: $flat_count apps"
|
||
[[ "$flat_updates" != "0" ]] && info+=" ($flat_updates updates)"
|
||
info+=" [$flat_remotes remotes]\n"
|
||
else
|
||
info+="Flatpak: Not available\n"
|
||
fi
|
||
|
||
# Additional buntage managers
|
||
if command -v pip3 &>/dev/null || command -v pip &>/dev/null; then
|
||
local pip_count
|
||
pip_count=$(pip3 list 2>/dev/null | tail -n +3 2>/dev/null | wc -l 2>/dev/null) || pip_count="0"
|
||
[[ "$pip_count" == "0" ]] && pip_count=$(pip list 2>/dev/null | tail -n +3 2>/dev/null | wc -l 2>/dev/null) || pip_count="0"
|
||
info+="Python (pip): $pip_count packages\n"
|
||
fi
|
||
|
||
if command -v npm &>/dev/null; then
|
||
local npm_global_count
|
||
npm_global_count=$(npm list -g --depth=0 2>/dev/null | grep -c "├──\|└──" 2>/dev/null) || npm_global_count="0"
|
||
info+="Node.js (npm global): $npm_global_count packages\n"
|
||
fi
|
||
|
||
info+="\n"
|
||
|
||
# Network Information
|
||
info+="NETWORK INFORMATION\n"
|
||
info+="─────────────────────────────────────\n"
|
||
|
||
# Active network interfaces
|
||
local active_interfaces
|
||
active_interfaces=$(ip -o link show 2>/dev/null | awk -F': ' '$3 !~ /lo|docker|br-/ && $3 ~ /UP/ {print $2}' 2>/dev/null | tr '\n' ' ' 2>/dev/null) || active_interfaces="Unknown"
|
||
[[ -n "$active_interfaces" ]] && info+="Active interfaces: $active_interfaces\n" || info+="Active interfaces: None detected\n"
|
||
|
||
# IP addresses
|
||
local primary_ip
|
||
primary_ip=$(ip route get 8.8.8.8 2>/dev/null | awk '{print $7; exit}' 2>/dev/null) || primary_ip="Unknown"
|
||
info+="Primary IP: $primary_ip\n"
|
||
|
||
# Internet connectivity check
|
||
local internet_status
|
||
if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then
|
||
internet_status="Connected"
|
||
else
|
||
internet_status="Disconnected"
|
||
fi
|
||
info+="Internet: $internet_status\n"
|
||
|
||
# DNS servers
|
||
local dns_servers
|
||
dns_servers=$(grep "^nameserver" /etc/resolv.conf 2>/dev/null | awk '{print $2}' 2>/dev/null | tr '\n' ' ' 2>/dev/null) || dns_servers="Unknown"
|
||
[[ -n "$dns_servers" ]] && info+="DNS servers: $dns_servers\n" || info+="DNS servers: Not configured\n"
|
||
|
||
info+="\n"
|
||
|
||
# Ultrabunt Stats
|
||
info+="ULTRABUNT STATISTICS\n"
|
||
info+="─────────────────────────────────────\n"
|
||
|
||
local total_pkgs=${#PACKAGES[@]}
|
||
local installed_count=0
|
||
|
||
# Safely count installed packages
|
||
for name in "${!PACKAGES[@]}"; do
|
||
if is_package_installed "$name" 2>/dev/null; then
|
||
((installed_count++)) 2>/dev/null || true
|
||
fi
|
||
done 2>/dev/null || true
|
||
|
||
info+="Tracked packages: $total_pkgs\n"
|
||
info+="Installed: $installed_count\n"
|
||
info+="Available: $((total_pkgs - installed_count))\n\n"
|
||
|
||
info+="Log file: $LOGFILE\n"
|
||
info+="Backup dir: $BACKUP_DIR\n"
|
||
|
||
log "Calling ui_info with system information"
|
||
ui_info "System Information" "$info" || {
|
||
log "ERROR: ui_info failed in show_system_info"
|
||
return 1
|
||
}
|
||
|
||
log "show_system_info completed successfully"
|
||
return 0
|
||
}
|
||
|
||
show_bulk_operations() {
|
||
while true; do
|
||
local choice
|
||
choice=$(ui_menu "Bulk Operations" \
|
||
"Perform operations on multiple packages:" \
|
||
20 80 10 \
|
||
"install-category" "(.Y.) Install All Packages in Category" \
|
||
"remove-category" "(.Y.) Remove All Packages in Category" \
|
||
"install-selected" "(.Y.) Install Selected Packages" \
|
||
"remove-selected" "(.Y.) Remove Selected Packages" \
|
||
"update-all" "(.Y.) Update All Installed Buntages" \
|
||
"cleanup" "(.Y.) Clean Buntage Cache & Remove Orphans" \
|
||
"export-list" "(.Y.) Export Installed Buntage List" \
|
||
"zback" "(Z) ← Back to Main Menu") || break
|
||
|
||
case "$choice" in
|
||
install-category) bulk_install_category ;;
|
||
remove-category) bulk_remove_category ;;
|
||
install-selected) bulk_install_selected ;;
|
||
remove-selected) bulk_remove_selected ;;
|
||
update-all) bulk_update_all ;;
|
||
cleanup) bulk_cleanup ;;
|
||
export-list) export_package_list ;;
|
||
back|zback|"") break ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
bulk_install_category() {
|
||
local menu_items=()
|
||
|
||
for entry in "${CATEGORIES[@]}"; do
|
||
local cat_id="${entry%%:*}"
|
||
local cat_name="${entry#*:}"
|
||
menu_items+=("$cat_id" "(.Y.) $cat_name")
|
||
done
|
||
|
||
local choice
|
||
choice=$(ui_menu "Install Category" \
|
||
"Select category to install all packages:" \
|
||
20 70 10 "${menu_items[@]}") || return
|
||
|
||
ui_yesno "Confirm Install" "Install ALL packages in this category?\n\nThis may take several minutes." || return
|
||
|
||
local installed=0
|
||
local failed=0
|
||
|
||
for name in "${!PACKAGES[@]}"; do
|
||
if [[ "${PKG_CATEGORY[$name]}" == "$choice" ]]; then
|
||
if ! is_package_installed "$name"; then
|
||
log "Bulk installing: $name"
|
||
if install_package "$name" 2>&1 | tee -a "$LOGFILE"; then
|
||
((installed++))
|
||
else
|
||
((failed++))
|
||
log_error "Failed to install: $name"
|
||
fi
|
||
fi
|
||
fi
|
||
done
|
||
|
||
ui_msg "Bulk Install Complete" "Installed: $installed\nFailed: $failed\n\nCheck $LOGFILE for details."
|
||
}
|
||
|
||
bulk_remove_category() {
|
||
local menu_items=()
|
||
|
||
for entry in "${CATEGORIES[@]}"; do
|
||
local cat_id="${entry%%:*}"
|
||
local cat_name="${entry#*:}"
|
||
menu_items+=("$cat_id" "(.Y.) $cat_name")
|
||
done
|
||
|
||
local choice
|
||
choice=$(ui_menu "Remove Category" \
|
||
"Select category to remove all packages:" \
|
||
20 70 10 "${menu_items[@]}") || return
|
||
|
||
ui_yesno "Confirm Removal" "⚠️ WARNING ⚠️\n\nRemove ALL packages in this category?\n\nThis cannot be undone!" || return
|
||
|
||
ui_yesno "Final Confirmation" "Are you ABSOLUTELY SURE?\n\nThis will remove all packages in the selected category." || return
|
||
|
||
local removed=0
|
||
|
||
for name in "${!PACKAGES[@]}"; do
|
||
if [[ "${PKG_CATEGORY[$name]}" == "$choice" ]]; then
|
||
if is_package_installed "$name"; then
|
||
log "Bulk removing: $name"
|
||
local pkg="${PACKAGES[$name]}"
|
||
local method="${PKG_METHOD[$name]}"
|
||
|
||
case "$method" in
|
||
apt) remove_apt_package "$pkg" ;;
|
||
snap) remove_snap_package "$pkg" ;;
|
||
flatpak) remove_flatpak_package "$pkg" ;;
|
||
custom)
|
||
case "$name" in
|
||
docker) remove_docker ;;
|
||
nodejs) remove_nodejs ;;
|
||
vscode) remove_vscode ;;
|
||
brave) remove_brave ;;
|
||
sublime-text) remove_sublime ;;
|
||
esac
|
||
;;
|
||
esac
|
||
((removed++))
|
||
fi
|
||
fi
|
||
done
|
||
|
||
ui_msg "Bulk Remove Complete" "Removed: $removed packages\n\nCheck $LOGFILE for details."
|
||
}
|
||
|
||
bulk_install_selected() {
|
||
local checklist_items=()
|
||
|
||
for name in "${!PACKAGES[@]}"; do
|
||
if ! is_package_installed "$name"; then
|
||
local desc="${PKG_DESC[$name]}"
|
||
local cat="${PKG_CATEGORY[$name]}"
|
||
checklist_items+=("$name" "[$cat] $desc" "OFF")
|
||
fi
|
||
done
|
||
|
||
if [[ ${#checklist_items[@]} -eq 0 ]]; then
|
||
ui_msg "Nothing to Install" "All tracked packages are already installed!"
|
||
return
|
||
fi
|
||
|
||
# Sort checklist
|
||
IFS=$'\n'
|
||
checklist_items=($(sort -t$'\t' -k1 <<<"${checklist_items[*]}"))
|
||
unset IFS
|
||
|
||
local selected
|
||
selected=$(ui_checklist "Select Packages to Install" \
|
||
"Choose packages to install (Space to select, Enter to confirm):" \
|
||
24 80 15 "${checklist_items[@]}") || return
|
||
|
||
if [[ -z "$selected" ]]; then
|
||
return
|
||
fi
|
||
|
||
local pkg_list=$(echo "$selected" | tr -d '"')
|
||
local count=$(echo "$pkg_list" | wc -w)
|
||
|
||
ui_yesno "Confirm Installation" "Install $count selected packages?" || return
|
||
|
||
local installed=0
|
||
local failed=0
|
||
|
||
for name in $pkg_list; do
|
||
log "Installing selected: $name"
|
||
if install_package "$name" 2>&1 | tee -a "$LOGFILE"; then
|
||
((installed++))
|
||
else
|
||
((failed++))
|
||
log_error "Failed to install: $name"
|
||
fi
|
||
done
|
||
|
||
ui_msg "Installation Complete" "Installed: $installed\nFailed: $failed\n\nCheck $LOGFILE for details."
|
||
}
|
||
|
||
bulk_remove_selected() {
|
||
local checklist_items=()
|
||
|
||
for name in "${!PACKAGES[@]}"; do
|
||
if is_package_installed "$name"; then
|
||
local desc="${PKG_DESC[$name]}"
|
||
local cat="${PKG_CATEGORY[$name]}"
|
||
checklist_items+=("$name" "[$cat] $desc" "OFF")
|
||
fi
|
||
done
|
||
|
||
if [[ ${#checklist_items[@]} -eq 0 ]]; then
|
||
ui_msg "Nothing to Remove" "No tracked packages are currently installed!"
|
||
return
|
||
fi
|
||
|
||
# Sort checklist
|
||
IFS=$'\n'
|
||
checklist_items=($(sort -t$'\t' -k1 <<<"${checklist_items[*]}"))
|
||
unset IFS
|
||
|
||
local selected
|
||
selected=$(ui_checklist "Select Packages to Remove" \
|
||
"Choose packages to remove (Space to select, Enter to confirm):" \
|
||
24 80 15 "${checklist_items[@]}") || return
|
||
|
||
if [[ -z "$selected" ]]; then
|
||
return
|
||
fi
|
||
|
||
local pkg_list=$(echo "$selected" | tr -d '"')
|
||
local count=$(echo "$pkg_list" | wc -w)
|
||
|
||
ui_yesno "Confirm Removal" "⚠️ Remove $count selected packages?" || return
|
||
|
||
local removed=0
|
||
|
||
for name in $pkg_list; do
|
||
log "Removing selected: $name"
|
||
local pkg="${PACKAGES[$name]}"
|
||
local method="${PKG_METHOD[$name]}"
|
||
|
||
case "$method" in
|
||
apt) remove_apt_package "$pkg" ;;
|
||
snap) remove_snap_package "$pkg" ;;
|
||
flatpak) remove_flatpak_package "$pkg" ;;
|
||
custom)
|
||
case "$name" in
|
||
docker) remove_docker ;;
|
||
nodejs) remove_nodejs ;;
|
||
vscode) remove_vscode ;;
|
||
brave) remove_brave ;;
|
||
sublime-text) remove_sublime ;;
|
||
esac
|
||
;;
|
||
esac
|
||
((removed++))
|
||
done
|
||
|
||
ui_msg "Removal Complete" "Removed: $removed packages\n\nCheck $LOGFILE for details."
|
||
}
|
||
|
||
bulk_update_all() {
|
||
ui_yesno "Update All" "Update all buntage managers and installed buntages?" || return
|
||
|
||
log "Starting system update..."
|
||
|
||
# APT
|
||
log "Updating APT buntages..."
|
||
sudo apt-get update -y 2>&1 | tee -a "$LOGFILE"
|
||
sudo apt-get upgrade -y 2>&1 | tee -a "$LOGFILE"
|
||
|
||
# Snap
|
||
if command -v snap &>/dev/null; then
|
||
log "Updating Snap buntages..."
|
||
sudo snap refresh 2>&1 | tee -a "$LOGFILE"
|
||
fi
|
||
|
||
# Flatpak
|
||
if command -v flatpak &>/dev/null; then
|
||
log "Updating Flatpak apps..."
|
||
flatpak update -y 2>&1 | tee -a "$LOGFILE"
|
||
fi
|
||
|
||
ui_msg "Update Complete" "All buntage managers updated!\n\nCheck $LOGFILE for details."
|
||
}
|
||
|
||
bulk_cleanup() {
|
||
ui_yesno "System Cleanup" "This will:\n- Remove unused buntages\n- Clean buntage cache\n- Remove old kernels\n\nContinue?" || return
|
||
|
||
log "Starting system cleanup..."
|
||
|
||
# APT cleanup
|
||
log "Cleaning APT cache..."
|
||
sudo apt-get autoremove -y 2>&1 | tee -a "$LOGFILE"
|
||
sudo apt-get autoclean -y 2>&1 | tee -a "$LOGFILE"
|
||
sudo apt-get clean 2>&1 | tee -a "$LOGFILE"
|
||
|
||
# Snap cleanup
|
||
if command -v snap &>/dev/null; then
|
||
log "Removing old snap revisions..."
|
||
sudo snap list --all | awk '/disabled/{print $1, $3}' | \
|
||
while read snapname revision; do
|
||
sudo snap remove "$snapname" --revision="$revision" 2>&1 | tee -a "$LOGFILE"
|
||
done
|
||
fi
|
||
|
||
# Flatpak cleanup
|
||
if command -v flatpak &>/dev/null; then
|
||
log "Cleaning Flatpak cache..."
|
||
flatpak uninstall --unused -y 2>&1 | tee -a "$LOGFILE"
|
||
fi
|
||
|
||
# Journal cleanup
|
||
log "Cleaning system journal..."
|
||
sudo journalctl --vacuum-time=7d 2>&1 | tee -a "$LOGFILE"
|
||
|
||
ui_msg "Cleanup Complete" "System cleanup finished!\n\nCheck $LOGFILE for details."
|
||
}
|
||
|
||
export_package_list() {
|
||
local export_file="/tmp/ultrabunt-packages-$(date +%Y%m%d-%H%M%S).txt"
|
||
|
||
{
|
||
echo "ULTRABUNT BUNTAGE LIST - $(date)"
|
||
echo "═══════════════════════════════════════════════════════════"
|
||
echo ""
|
||
|
||
for entry in "${CATEGORIES[@]}"; do
|
||
local cat_id="${entry%%:*}"
|
||
local cat_name="${entry#*:}"
|
||
|
||
echo ""
|
||
echo "[$cat_name]"
|
||
echo "─────────────────────────────────────────────────────────"
|
||
|
||
for name in "${!PACKAGES[@]}"; do
|
||
if [[ "${PKG_CATEGORY[$name]}" == "$cat_id" ]]; then
|
||
local status="NOT INSTALLED"
|
||
if is_package_installed "$name"; then
|
||
status="INSTALLED"
|
||
fi
|
||
printf "%-20s %-15s %s\n" "$name" "[$status]" "${PKG_DESC[$name]}"
|
||
fi
|
||
done
|
||
done
|
||
|
||
echo ""
|
||
echo "═══════════════════════════════════════════════════════════"
|
||
echo "Export completed: $(date)"
|
||
|
||
} > "$export_file"
|
||
|
||
ui_msg "Export Complete" "Buntage list exported to:\n\n$export_file"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# ADVANCED FEATURES
|
||
# ==============================================================================
|
||
|
||
install_oh_my_zsh() {
|
||
if [[ -d "$HOME/.oh-my-zsh" ]]; then
|
||
ui_msg "Already Installed" "Oh My Zsh is already installed!"
|
||
return
|
||
fi
|
||
|
||
ui_yesno "Install Oh My Zsh?" "Install Oh My Zsh with Powerlevel10k theme?" || return
|
||
|
||
if ! is_package_installed "zsh"; then
|
||
install_package "zsh"
|
||
fi
|
||
|
||
log "Installing Oh My Zsh..."
|
||
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended 2>&1 | tee -a "$LOGFILE"
|
||
|
||
log "Installing Powerlevel10k..."
|
||
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k" 2>&1 | tee -a "$LOGFILE"
|
||
|
||
sed -i 's/ZSH_THEME=.*/ZSH_THEME="powerlevel10k\/powerlevel10k"/' "$HOME/.zshrc"
|
||
|
||
ui_msg "Oh My Zsh Installed" "Oh My Zsh with Powerlevel10k installed!\n\nRun 'zsh' to start configuration."
|
||
}
|
||
|
||
# ==============================================================================
|
||
# WORDPRESS INSTALLATION SYSTEM
|
||
# ==============================================================================
|
||
|
||
show_wordpress_setup_menu() {
|
||
log "Entering show_wordpress_setup_menu function"
|
||
|
||
while true; do
|
||
local menu_items=(
|
||
"quick-nginx" "(.Y.) 🚀 Quick Setup (Nginx + WordPress)"
|
||
"quick-apache" "(.Y.) 🚀 Quick Setup (Apache + WordPress)"
|
||
"" "(_*_)"
|
||
"custom-nginx" "(.Y.) ⚙️ Custom Nginx Setup"
|
||
"custom-apache" "(.Y.) ⚙️ Custom Apache Setup"
|
||
"" "(_*_)"
|
||
"ssl-setup" "(.Y.) 🔒 Add SSL Certificate (Let's Encrypt)"
|
||
"wp-security" "(.Y.) 🛡️ WordPress Security Hardening"
|
||
"" "(_*_)"
|
||
"status" "(.Y.) 📊 Show WordPress Sites Status"
|
||
"zback" "(Z) ← Back to Main Menu"
|
||
)
|
||
|
||
local choice
|
||
choice=$(ui_menu "WordPress Installation" \
|
||
"Choose your WordPress installation method:" \
|
||
20 80 12 "${menu_items[@]}") || break
|
||
|
||
case "$choice" in
|
||
quick-nginx)
|
||
wordpress_quick_setup "nginx"
|
||
;;
|
||
quick-apache)
|
||
wordpress_quick_setup "apache"
|
||
;;
|
||
custom-nginx)
|
||
wordpress_custom_setup "nginx"
|
||
;;
|
||
custom-apache)
|
||
wordpress_custom_setup "apache"
|
||
;;
|
||
ssl-setup)
|
||
wordpress_ssl_setup
|
||
;;
|
||
wp-security)
|
||
wordpress_security_hardening
|
||
;;
|
||
status)
|
||
show_wordpress_status
|
||
;;
|
||
back|zback|z|"")
|
||
break
|
||
;;
|
||
esac
|
||
done
|
||
|
||
log "Exiting show_wordpress_setup_menu function"
|
||
}
|
||
|
||
wordpress_quick_setup() {
|
||
local web_server="$1"
|
||
log "Starting WordPress quick setup with $web_server"
|
||
|
||
local info="WordPress Quick Setup ($web_server)\n\n"
|
||
info+="This will install and configure:\n"
|
||
info+="• $web_server web server\n"
|
||
info+="• PHP and required extensions\n"
|
||
info+="• MariaDB database server\n"
|
||
info+="• Latest WordPress\n"
|
||
info+="• Basic security configuration\n\n"
|
||
info+="Perfect for new Ubuntu installations!\n\n"
|
||
info+="Continue with installation?"
|
||
|
||
if ! ui_yesno "Quick WordPress Setup" "$info"; then
|
||
return
|
||
fi
|
||
|
||
# Install prerequisites
|
||
install_wordpress_prerequisites "$web_server"
|
||
|
||
# Get domain name
|
||
local domain
|
||
domain=$(ui_input "Domain Name" "Enter your domain name:" "localhost") || return
|
||
domain=${domain:-localhost}
|
||
|
||
# Setup database
|
||
local db_info
|
||
db_info=$(setup_wordpress_database "$domain")
|
||
|
||
# Download and configure WordPress
|
||
setup_wordpress_files "$domain" "$db_info"
|
||
|
||
# Configure web server
|
||
if [[ "$web_server" == "nginx" ]]; then
|
||
configure_nginx_wordpress "$domain"
|
||
else
|
||
configure_apache_wordpress "$domain"
|
||
fi
|
||
|
||
# Show completion message
|
||
show_wordpress_completion "$domain" "$db_info"
|
||
}
|
||
|
||
wordpress_custom_setup() {
|
||
local web_server="$1"
|
||
log "Starting WordPress custom setup with $web_server"
|
||
|
||
local info="WordPress Custom Setup ($web_server)\n\n"
|
||
info+="This allows you to customize:\n"
|
||
info+="• Domain and directory settings\n"
|
||
info+="• Database configuration\n"
|
||
info+="• PHP settings\n"
|
||
info+="• Security options\n\n"
|
||
info+="Continue with custom setup?"
|
||
|
||
if ! ui_yesno "Custom WordPress Setup" "$info"; then
|
||
return
|
||
fi
|
||
|
||
# Check if prerequisites are installed
|
||
if ! check_wordpress_prerequisites "$web_server"; then
|
||
if ui_yesno "Install Prerequisites" "Required packages are missing. Install them now?"; then
|
||
install_wordpress_prerequisites "$web_server"
|
||
else
|
||
ui_msg "Setup Cancelled" "Cannot proceed without required packages."
|
||
return
|
||
fi
|
||
fi
|
||
|
||
# Custom configuration options
|
||
local domain
|
||
domain=$(ui_input "Domain Name" "Enter your domain name:" "localhost") || return
|
||
domain=${domain:-localhost}
|
||
|
||
local site_dir
|
||
site_dir=$(ui_input "Site Directory" "WordPress installation directory:" "/var/www/$domain") || return
|
||
site_dir=${site_dir:-/var/www/$domain}
|
||
|
||
# Database setup with custom options
|
||
local db_info
|
||
db_info=$(setup_wordpress_database_custom "$domain")
|
||
|
||
# WordPress setup
|
||
setup_wordpress_files_custom "$domain" "$site_dir" "$db_info"
|
||
|
||
# Web server configuration
|
||
if [[ "$web_server" == "nginx" ]]; then
|
||
configure_nginx_wordpress_custom "$domain" "$site_dir"
|
||
else
|
||
configure_apache_wordpress_custom "$domain" "$site_dir"
|
||
fi
|
||
|
||
# Show completion
|
||
show_wordpress_completion "$domain" "$db_info"
|
||
}
|
||
|
||
install_wordpress_prerequisites() {
|
||
local web_server="$1"
|
||
log "Installing WordPress prerequisites for $web_server"
|
||
|
||
ui_msg "Installing Prerequisites" "Installing required packages for WordPress...\n\nThis may take a few minutes."
|
||
|
||
# Common packages
|
||
local packages=("mariadb" "php-mysql" "php-curl" "php-gd" "php-xml" "php-mbstring" "php-zip")
|
||
|
||
# Web server specific packages
|
||
if [[ "$web_server" == "nginx" ]]; then
|
||
packages+=("nginx" "php-fpm")
|
||
else
|
||
packages+=("apache2" "libapache2-mod-php")
|
||
fi
|
||
|
||
# Install packages
|
||
for pkg in "${packages[@]}"; do
|
||
if ! is_package_installed "$pkg"; then
|
||
log "Installing $pkg..."
|
||
install_package "$pkg" || {
|
||
ui_msg "Installation Error" "Failed to install $pkg. Please check the logs."
|
||
return 1
|
||
}
|
||
fi
|
||
done
|
||
|
||
# Start and enable services
|
||
if [[ "$web_server" == "nginx" ]]; then
|
||
sudo systemctl enable nginx php${PHP_VER}-fpm mariadb 2>&1 | tee -a "$LOGFILE"
|
||
sudo systemctl start nginx php${PHP_VER}-fpm mariadb 2>&1 | tee -a "$LOGFILE"
|
||
else
|
||
sudo systemctl enable apache2 mariadb 2>&1 | tee -a "$LOGFILE"
|
||
sudo systemctl start apache2 mariadb 2>&1 | tee -a "$LOGFILE"
|
||
sudo a2enmod rewrite 2>&1 | tee -a "$LOGFILE"
|
||
fi
|
||
|
||
# Secure MariaDB installation
|
||
setup_mariadb_security
|
||
|
||
log "WordPress prerequisites installed successfully"
|
||
}
|
||
|
||
check_wordpress_prerequisites() {
|
||
local web_server="$1"
|
||
|
||
local required_packages=("mariadb" "php-mysql" "php-curl" "php-gd")
|
||
|
||
if [[ "$web_server" == "nginx" ]]; then
|
||
required_packages+=("nginx" "php-fpm")
|
||
else
|
||
required_packages+=("apache2" "libapache2-mod-php")
|
||
fi
|
||
|
||
for pkg in "${required_packages[@]}"; do
|
||
if ! is_package_installed "$pkg"; then
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
return 0
|
||
}
|
||
|
||
setup_mariadb_security() {
|
||
log "Setting up MariaDB security"
|
||
|
||
# Generate random root password
|
||
local root_pass
|
||
root_pass=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 20)
|
||
|
||
# Secure installation
|
||
sudo mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '${root_pass}';" 2>/dev/null || true
|
||
sudo mysql -e "DELETE FROM mysql.user WHERE User='';" 2>/dev/null || true
|
||
sudo mysql -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" 2>/dev/null || true
|
||
sudo mysql -e "DROP DATABASE IF EXISTS test;" 2>/dev/null || true
|
||
sudo mysql -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" 2>/dev/null || true
|
||
sudo mysql -e "FLUSH PRIVILEGES;" 2>/dev/null || true
|
||
|
||
# Save root password
|
||
echo "$root_pass" | sudo tee /root/.mysql_root_password > /dev/null
|
||
sudo chmod 600 /root/.mysql_root_password
|
||
|
||
log "MariaDB security setup completed"
|
||
}
|
||
|
||
setup_wordpress_database() {
|
||
local domain="$1"
|
||
local db_name="wp_${domain//[^a-zA-Z0-9]/_}_$(date +%s)"
|
||
local db_user="wpuser_$(date +%s)"
|
||
local db_pass
|
||
db_pass=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 20)
|
||
|
||
log "Creating WordPress database: $db_name"
|
||
|
||
# Create database and user
|
||
sudo mysql -e "CREATE DATABASE ${db_name} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>&1 | tee -a "$LOGFILE"
|
||
sudo mysql -e "CREATE USER '${db_user}'@'localhost' IDENTIFIED BY '${db_pass}';" 2>&1 | tee -a "$LOGFILE"
|
||
sudo mysql -e "GRANT ALL PRIVILEGES ON ${db_name}.* TO '${db_user}'@'localhost';" 2>&1 | tee -a "$LOGFILE"
|
||
sudo mysql -e "FLUSH PRIVILEGES;" 2>&1 | tee -a "$LOGFILE"
|
||
|
||
echo "${db_name}:${db_user}:${db_pass}"
|
||
}
|
||
|
||
setup_wordpress_database_custom() {
|
||
local domain="$1"
|
||
|
||
local db_name
|
||
db_name=$(ui_input "Database Name" "WordPress database name:" "wp_${domain//[^a-zA-Z0-9]/_}") || return
|
||
|
||
local db_user
|
||
db_user=$(ui_input "Database User" "WordPress database user:" "wp_user") || return
|
||
|
||
local db_pass
|
||
db_pass=$(ui_input "Database Password" "Database password (leave empty for auto-generated):" "") || return
|
||
|
||
if [[ -z "$db_pass" ]]; then
|
||
db_pass=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 20)
|
||
fi
|
||
|
||
log "Creating custom WordPress database: $db_name"
|
||
|
||
# Create database and user
|
||
sudo mysql -e "CREATE DATABASE ${db_name} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>&1 | tee -a "$LOGFILE"
|
||
sudo mysql -e "CREATE USER '${db_user}'@'localhost' IDENTIFIED BY '${db_pass}';" 2>&1 | tee -a "$LOGFILE"
|
||
sudo mysql -e "GRANT ALL PRIVILEGES ON ${db_name}.* TO '${db_user}'@'localhost';" 2>&1 | tee -a "$LOGFILE"
|
||
sudo mysql -e "FLUSH PRIVILEGES;" 2>&1 | tee -a "$LOGFILE"
|
||
|
||
echo "${db_name}:${db_user}:${db_pass}"
|
||
}
|
||
|
||
setup_wordpress_files() {
|
||
local domain="$1"
|
||
local db_info="$2"
|
||
local site_dir="/var/www/$domain"
|
||
|
||
log "Setting up WordPress files for $domain"
|
||
|
||
# Parse database info
|
||
IFS=':' read -r db_name db_user db_pass <<< "$db_info"
|
||
|
||
# Create site directory
|
||
sudo mkdir -p "$site_dir"
|
||
sudo chown "$USER:$USER" "$site_dir"
|
||
|
||
# Download WordPress
|
||
ui_msg "Downloading WordPress" "Downloading latest WordPress..."
|
||
wget -q https://wordpress.org/latest.tar.gz -O /tmp/wordpress.tar.gz || {
|
||
ui_msg "Download Error" "Failed to download WordPress. Please check your internet connection."
|
||
return 1
|
||
}
|
||
|
||
tar -xzf /tmp/wordpress.tar.gz -C /tmp
|
||
rsync -a /tmp/wordpress/ "$site_dir/"
|
||
rm -rf /tmp/wordpress /tmp/wordpress.tar.gz
|
||
|
||
# Configure WordPress
|
||
cp "$site_dir/wp-config-sample.php" "$site_dir/wp-config.php"
|
||
|
||
# Database configuration
|
||
sed -i "s/database_name_here/${db_name}/" "$site_dir/wp-config.php"
|
||
sed -i "s/username_here/${db_user}/" "$site_dir/wp-config.php"
|
||
sed -i "s/password_here/${db_pass}/" "$site_dir/wp-config.php"
|
||
|
||
# Add security keys
|
||
local salts
|
||
salts=$(curl -s https://api.wordpress.org/secret-key/1.1/salt/ 2>/dev/null) || {
|
||
log "Warning: Could not fetch WordPress security keys"
|
||
salts="// Security keys could not be fetched automatically"
|
||
}
|
||
|
||
# Replace the placeholder keys
|
||
sed -i '/AUTH_KEY/,/NONCE_SALT/d' "$site_dir/wp-config.php"
|
||
sed -i "/\/\*\* MySQL settings/i\\$salts" "$site_dir/wp-config.php"
|
||
|
||
# Set proper permissions
|
||
sudo chown -R www-data:www-data "$site_dir"
|
||
sudo find "$site_dir" -type d -exec chmod 755 {} \;
|
||
sudo find "$site_dir" -type f -exec chmod 644 {} \;
|
||
|
||
log "WordPress files setup completed"
|
||
}
|
||
|
||
setup_wordpress_files_custom() {
|
||
local domain="$1"
|
||
local site_dir="$2"
|
||
local db_info="$3"
|
||
|
||
setup_wordpress_files "$domain" "$db_info"
|
||
}
|
||
|
||
configure_nginx_wordpress() {
|
||
local domain="$1"
|
||
local site_dir="/var/www/$domain"
|
||
|
||
log "Configuring Nginx for WordPress: $domain"
|
||
|
||
local nginx_conf="/etc/nginx/sites-available/$domain"
|
||
sudo tee "$nginx_conf" > /dev/null <<EOF
|
||
server {
|
||
listen 80;
|
||
server_name $domain www.$domain;
|
||
root $site_dir;
|
||
index index.php index.html index.htm;
|
||
|
||
# Security headers
|
||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||
add_header X-XSS-Protection "1; mode=block" always;
|
||
add_header X-Content-Type-Options "nosniff" always;
|
||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||
|
||
# File upload size
|
||
client_max_body_size 100M;
|
||
|
||
# WordPress permalinks
|
||
location / {
|
||
try_files \$uri \$uri/ /index.php?\$args;
|
||
}
|
||
|
||
# PHP processing
|
||
location ~ \.php$ {
|
||
include snippets/fastcgi-php.conf;
|
||
fastcgi_pass unix:/var/run/php/php${PHP_VER}-fpm.sock;
|
||
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
|
||
include fastcgi_params;
|
||
}
|
||
|
||
# Security: Block access to sensitive files
|
||
location ~ /\.ht {
|
||
deny all;
|
||
}
|
||
|
||
location ~ /\.user\.ini {
|
||
deny all;
|
||
}
|
||
|
||
location ~ /wp-config\.php {
|
||
deny all;
|
||
}
|
||
|
||
# Cache static files
|
||
location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {
|
||
expires 1y;
|
||
add_header Cache-Control "public, immutable";
|
||
}
|
||
|
||
# Block XML-RPC attacks
|
||
location = /xmlrpc.php {
|
||
deny all;
|
||
access_log off;
|
||
log_not_found off;
|
||
}
|
||
}
|
||
EOF
|
||
|
||
# Remove default site if it exists and this is the first site
|
||
if [[ -f "/etc/nginx/sites-enabled/default" ]] && [[ $(ls -1 /etc/nginx/sites-enabled/ | wc -l) -eq 1 ]]; then
|
||
log "Removing default Nginx site"
|
||
sudo rm -f "/etc/nginx/sites-enabled/default"
|
||
fi
|
||
|
||
# Enable site
|
||
log "Enabling Nginx site: $domain"
|
||
sudo ln -sf "$nginx_conf" "/etc/nginx/sites-enabled/"
|
||
|
||
# Verify the symlink was created correctly
|
||
if [[ ! -L "/etc/nginx/sites-enabled/$domain" ]]; then
|
||
log "ERROR: Failed to create symlink for $domain"
|
||
return 1
|
||
fi
|
||
|
||
# Test Nginx configuration
|
||
log "Testing Nginx configuration..."
|
||
if sudo nginx -t 2>&1 | tee -a "$LOGFILE"; then
|
||
log "Nginx configuration test passed"
|
||
|
||
# Reload Nginx
|
||
log "Reloading Nginx..."
|
||
if sudo systemctl reload nginx 2>&1 | tee -a "$LOGFILE"; then
|
||
log "Nginx reloaded successfully"
|
||
|
||
# Verify Nginx is running
|
||
if systemctl is-active --quiet nginx; then
|
||
log "Nginx is running and site $domain is configured"
|
||
|
||
# Show site status
|
||
ui_msg "Site Configured" "✅ Nginx site '$domain' has been configured successfully!\n\n📁 Config file: /etc/nginx/sites-available/$domain\n🔗 Enabled at: /etc/nginx/sites-enabled/$domain\n🌐 Access at: http://$domain\n\n🔄 Nginx has been reloaded and is running."
|
||
else
|
||
log "WARNING: Nginx is not running after reload"
|
||
ui_msg "Nginx Issue" "⚠️ Site configured but Nginx is not running.\n\nTry: sudo systemctl start nginx"
|
||
return 1
|
||
fi
|
||
else
|
||
log "ERROR: Failed to reload Nginx"
|
||
ui_msg "Reload Failed" "❌ Nginx configuration is valid but reload failed.\n\nCheck: sudo systemctl status nginx"
|
||
return 1
|
||
fi
|
||
else
|
||
log "ERROR: Nginx configuration test failed"
|
||
ui_msg "Config Error" "❌ Nginx configuration test failed!\n\nThe site configuration has been removed.\n\nCheck the logs for details."
|
||
|
||
# Remove the broken configuration
|
||
sudo rm -f "/etc/nginx/sites-enabled/$domain"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
configure_nginx_wordpress_custom() {
|
||
local domain="$1"
|
||
local site_dir="$2"
|
||
|
||
configure_nginx_wordpress "$domain"
|
||
}
|
||
|
||
configure_apache_wordpress() {
|
||
local domain="$1"
|
||
local site_dir="/var/www/$domain"
|
||
|
||
log "Configuring Apache for WordPress: $domain"
|
||
|
||
local apache_conf="/etc/apache2/sites-available/${domain}.conf"
|
||
sudo tee "$apache_conf" > /dev/null <<EOF
|
||
<VirtualHost *:80>
|
||
ServerName $domain
|
||
ServerAlias www.$domain
|
||
DocumentRoot $site_dir
|
||
|
||
# Security headers
|
||
Header always set X-Frame-Options SAMEORIGIN
|
||
Header always set X-Content-Type-Options nosniff
|
||
Header always set X-XSS-Protection "1; mode=block"
|
||
Header always set Referrer-Policy "no-referrer-when-downgrade"
|
||
|
||
# WordPress directory configuration
|
||
<Directory $site_dir>
|
||
Options -Indexes +FollowSymLinks
|
||
AllowOverride All
|
||
Require all granted
|
||
</Directory>
|
||
|
||
# Security: Block access to sensitive files
|
||
<Files "wp-config.php">
|
||
Require all denied
|
||
</Files>
|
||
|
||
<Files ".htaccess">
|
||
Require all denied
|
||
</Files>
|
||
|
||
<Files ".user.ini">
|
||
Require all denied
|
||
</Files>
|
||
|
||
# Block XML-RPC attacks
|
||
<Files "xmlrpc.php">
|
||
Require all denied
|
||
</Files>
|
||
|
||
# Logging
|
||
ErrorLog \${APACHE_LOG_DIR}/${domain}_error.log
|
||
CustomLog \${APACHE_LOG_DIR}/${domain}_access.log combined
|
||
</VirtualHost>
|
||
EOF
|
||
|
||
# Enable required modules
|
||
sudo a2enmod rewrite headers 2>&1 | tee -a "$LOGFILE"
|
||
|
||
# Enable site
|
||
sudo a2ensite "${domain}.conf" 2>&1 | tee -a "$LOGFILE"
|
||
|
||
# Test and reload Apache
|
||
if sudo apache2ctl configtest 2>&1 | tee -a "$LOGFILE"; then
|
||
sudo systemctl reload apache2
|
||
log "Apache configuration applied successfully"
|
||
else
|
||
log "ERROR: Apache configuration test failed"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
configure_apache_wordpress_custom() {
|
||
local domain="$1"
|
||
local site_dir="$2"
|
||
|
||
configure_apache_wordpress "$domain"
|
||
}
|
||
|
||
show_wordpress_completion() {
|
||
local domain="$1"
|
||
local db_info="$2"
|
||
|
||
# Parse database info
|
||
IFS=':' read -r db_name db_user db_pass <<< "$db_info"
|
||
|
||
# Save credentials
|
||
local creds_file="$BACKUP_DIR/wordpress-${domain}-$(date +%Y%m%d-%H%M%S).txt"
|
||
cat > "$creds_file" <<EOF
|
||
WordPress Installation Complete
|
||
═══════════════════════════════════════════════════════════════
|
||
Domain: $domain
|
||
Site Directory: /var/www/$domain
|
||
Database Name: $db_name
|
||
Database User: $db_user
|
||
Database Password: $db_pass
|
||
|
||
WordPress Admin Setup:
|
||
1. Visit: http://$domain
|
||
2. Complete the WordPress installation wizard
|
||
3. Create your admin account
|
||
|
||
Next Steps:
|
||
- Consider setting up SSL with Let's Encrypt
|
||
- Configure WordPress security hardening
|
||
- Install essential plugins
|
||
- Set up backups
|
||
|
||
Installation completed: $(date)
|
||
EOF
|
||
|
||
local completion_msg="🎉 WordPress Installation Complete!\n\n"
|
||
completion_msg+="Your WordPress site is ready at:\n"
|
||
completion_msg+="http://$domain\n\n"
|
||
completion_msg+="Database Details:\n"
|
||
completion_msg+="• Name: $db_name\n"
|
||
completion_msg+="• User: $db_user\n"
|
||
completion_msg+="• Password: $db_pass\n\n"
|
||
completion_msg+="📝 Credentials saved to:\n$creds_file\n\n"
|
||
completion_msg+="🔗 Visit your site to complete the WordPress setup wizard!"
|
||
|
||
ui_info "WordPress Ready!" "$completion_msg"
|
||
|
||
log "WordPress installation completed for $domain"
|
||
}
|
||
|
||
wordpress_ssl_setup() {
|
||
log "Starting WordPress SSL setup"
|
||
|
||
# Check if certbot is installed
|
||
if ! is_package_installed "certbot"; then
|
||
if ui_yesno "Install Certbot" "Certbot is required for SSL certificates. Install it now?"; then
|
||
install_package "certbot"
|
||
else
|
||
return
|
||
fi
|
||
fi
|
||
|
||
# Get domain name
|
||
local domain
|
||
domain=$(ui_input "Domain Name" "Enter domain for SSL certificate:" "") || return
|
||
|
||
if [[ -z "$domain" || "$domain" == "localhost" ]]; then
|
||
ui_msg "Invalid Domain" "SSL certificates cannot be issued for localhost or empty domains.\n\nPlease use a real domain name."
|
||
return
|
||
fi
|
||
|
||
# Detect web server
|
||
local web_server=""
|
||
if systemctl is-active --quiet nginx; then
|
||
web_server="nginx"
|
||
elif systemctl is-active --quiet apache2; then
|
||
web_server="apache2"
|
||
else
|
||
ui_msg "No Web Server" "No active web server detected. Please start Nginx or Apache first."
|
||
return
|
||
fi
|
||
|
||
# Install appropriate certbot plugin
|
||
local certbot_plugin=""
|
||
if [[ "$web_server" == "nginx" ]]; then
|
||
if ! is_package_installed "python3-certbot-nginx"; then
|
||
install_package "python3-certbot-nginx"
|
||
fi
|
||
certbot_plugin="nginx"
|
||
else
|
||
if ! is_package_installed "python3-certbot-apache"; then
|
||
install_package "python3-certbot-apache"
|
||
fi
|
||
certbot_plugin="apache"
|
||
fi
|
||
|
||
# Get email for Let's Encrypt
|
||
local email
|
||
email=$(ui_input "Email Address" "Email for Let's Encrypt notifications:" "") || return
|
||
|
||
if [[ -z "$email" ]]; then
|
||
ui_msg "Email Required" "Email address is required for Let's Encrypt certificates."
|
||
return
|
||
fi
|
||
|
||
# Run certbot
|
||
ui_msg "Obtaining SSL Certificate" "Requesting SSL certificate from Let's Encrypt...\n\nThis may take a moment."
|
||
|
||
if sudo certbot --${certbot_plugin} -d "$domain" -d "www.$domain" --email "$email" --agree-tos --non-interactive 2>&1 | tee -a "$LOGFILE"; then
|
||
ui_msg "SSL Success" "SSL certificate installed successfully!\n\nYour site is now available at:\nhttps://$domain\n\nCertbot will automatically renew the certificate."
|
||
else
|
||
ui_msg "SSL Error" "Failed to obtain SSL certificate. Please check:\n\n• Domain DNS points to this server\n• Port 80/443 are open\n• Domain is accessible from internet\n\nCheck logs for details."
|
||
fi
|
||
}
|
||
|
||
wordpress_security_hardening() {
|
||
log "Starting WordPress security hardening"
|
||
|
||
local domain
|
||
domain=$(ui_input "Domain Name" "Enter WordPress domain to harden:" "") || return
|
||
|
||
if [[ -z "$domain" ]]; then
|
||
return
|
||
fi
|
||
|
||
local site_dir="/var/www/$domain"
|
||
|
||
if [[ ! -d "$site_dir" ]]; then
|
||
ui_msg "Site Not Found" "WordPress installation not found at $site_dir"
|
||
return
|
||
fi
|
||
|
||
local security_options=(
|
||
"file-permissions" "Fix File Permissions" on
|
||
"disable-file-editing" "Disable File Editing in Admin" on
|
||
"hide-wp-version" "Hide WordPress Version" on
|
||
"limit-login-attempts" "Install Login Security Plugin" off
|
||
"security-headers" "Add Security Headers" on
|
||
"disable-xmlrpc" "Disable XML-RPC" on
|
||
)
|
||
|
||
local selected
|
||
selected=$(ui_checklist "WordPress Security" "Select security hardening options:" 20 80 10 "${security_options[@]}") || return
|
||
|
||
if [[ -z "$selected" ]]; then
|
||
return
|
||
fi
|
||
|
||
ui_msg "Applying Security" "Applying selected security hardening options..."
|
||
|
||
# Apply selected security measures
|
||
for option in $selected; do
|
||
case "$option" in
|
||
"file-permissions")
|
||
apply_wp_file_permissions "$site_dir"
|
||
;;
|
||
"disable-file-editing")
|
||
apply_wp_disable_editing "$site_dir"
|
||
;;
|
||
"hide-wp-version")
|
||
apply_wp_hide_version "$site_dir"
|
||
;;
|
||
"security-headers")
|
||
apply_wp_security_headers "$domain"
|
||
;;
|
||
"disable-xmlrpc")
|
||
apply_wp_disable_xmlrpc "$site_dir"
|
||
;;
|
||
esac
|
||
done
|
||
|
||
ui_msg "Security Applied" "WordPress security hardening completed for $domain"
|
||
}
|
||
|
||
apply_wp_file_permissions() {
|
||
local site_dir="$1"
|
||
log "Applying WordPress file permissions to $site_dir"
|
||
|
||
sudo chown -R www-data:www-data "$site_dir"
|
||
sudo find "$site_dir" -type d -exec chmod 755 {} \;
|
||
sudo find "$site_dir" -type f -exec chmod 644 {} \;
|
||
sudo chmod 600 "$site_dir/wp-config.php"
|
||
}
|
||
|
||
apply_wp_disable_editing() {
|
||
local site_dir="$1"
|
||
log "Disabling WordPress file editing"
|
||
|
||
if ! grep -q "DISALLOW_FILE_EDIT" "$site_dir/wp-config.php"; then
|
||
sudo sed -i "/\/\* That's all, stop editing/i define('DISALLOW_FILE_EDIT', true);" "$site_dir/wp-config.php"
|
||
fi
|
||
}
|
||
|
||
apply_wp_hide_version() {
|
||
local site_dir="$1"
|
||
log "Hiding WordPress version"
|
||
|
||
local functions_php="$site_dir/wp-content/themes/*/functions.php"
|
||
for file in $functions_php; do
|
||
if [[ -f "$file" ]]; then
|
||
if ! grep -q "remove_action.*wp_head.*wp_generator" "$file"; then
|
||
echo "remove_action('wp_head', 'wp_generator');" | sudo tee -a "$file" > /dev/null
|
||
fi
|
||
fi
|
||
done
|
||
}
|
||
|
||
apply_wp_security_headers() {
|
||
local domain="$1"
|
||
log "Adding security headers for $domain"
|
||
|
||
# This is already handled in the web server configuration
|
||
# Just log that it's already applied
|
||
log "Security headers already applied in web server configuration"
|
||
}
|
||
|
||
apply_wp_disable_xmlrpc() {
|
||
local site_dir="$1"
|
||
log "Disabling XML-RPC"
|
||
|
||
if ! grep -q "xmlrpc_enabled" "$site_dir/wp-config.php"; then
|
||
sudo sed -i "/\/\* That's all, stop editing/i add_filter('xmlrpc_enabled', '__return_false');" "$site_dir/wp-config.php"
|
||
fi
|
||
}
|
||
|
||
show_wordpress_status() {
|
||
log "Showing WordPress sites status"
|
||
|
||
while true; do
|
||
local status_info="WordPress\n\n"
|
||
|
||
# Check for WordPress installations
|
||
local wp_sites=()
|
||
if [[ -d "/var/www" ]]; then
|
||
while IFS= read -r -d '' dir; do
|
||
local site_name
|
||
site_name=$(basename "$dir")
|
||
if [[ -f "$dir/wp-config.php" ]]; then
|
||
wp_sites+=("$site_name")
|
||
fi
|
||
done < <(find /var/www -maxdepth 1 -type d -print0 2>/dev/null)
|
||
fi
|
||
|
||
if [[ ${#wp_sites[@]} -eq 0 ]]; then
|
||
status_info+="No WordPress installations found in /var/www/\n\n"
|
||
status_info+="Use the WordPress Installation menu to set up new sites.\n\n"
|
||
else
|
||
status_info+="WordPress Sites (${#wp_sites[@]} found):\n"
|
||
status_info+="═══════════════════════════════════════════════════════════\n"
|
||
for site in "${wp_sites[@]}"; do
|
||
local site_dir="/var/www/$site"
|
||
|
||
# Quick status check
|
||
local status_icon="❌"
|
||
local ssl_icon="🔓"
|
||
local access_icon="❌"
|
||
|
||
# Check if site is properly configured
|
||
if [[ -f "/etc/nginx/sites-available/$site" && -L "/etc/nginx/sites-enabled/$site" ]] || \
|
||
[[ -f "/etc/apache2/sites-available/${site}.conf" ]]; then
|
||
status_icon="✅"
|
||
fi
|
||
|
||
# Check SSL
|
||
if [[ -f "/etc/letsencrypt/live/$site/fullchain.pem" ]]; then
|
||
ssl_icon="🔒"
|
||
fi
|
||
|
||
# Quick accessibility test
|
||
if command -v curl >/dev/null 2>&1; then
|
||
if curl -s -o /dev/null -w "%{http_code}" "http://$site" --connect-timeout 3 --max-time 5 | grep -q "200\|301\|302"; then
|
||
access_icon="🌍"
|
||
fi
|
||
fi
|
||
|
||
status_info+="$status_icon $ssl_icon $access_icon $site\n"
|
||
done
|
||
status_info+="\n"
|
||
fi
|
||
|
||
# Web server status
|
||
status_info+="Services:\n"
|
||
status_info+="═══════════════════════════════════════════════════════════\n"
|
||
if systemctl is-active --quiet nginx; then
|
||
status_info+="🟢 Nginx: Running\n"
|
||
else
|
||
status_info+="🔴 Nginx: Not running\n"
|
||
fi
|
||
|
||
if systemctl is-active --quiet apache2; then
|
||
status_info+="🟢 Apache: Running\n"
|
||
else
|
||
status_info+="🔴 Apache: Not running\n"
|
||
fi
|
||
|
||
if systemctl is-active --quiet mariadb; then
|
||
status_info+="🟢 MariaDB: Running\n"
|
||
else
|
||
status_info+="🔴 MariaDB: Not running\n"
|
||
fi
|
||
|
||
status_info+="\nLegend: ✅=Configured 🔒=SSL 🌍=Accessible\n"
|
||
|
||
# Create menu items for site management
|
||
local menu_items=()
|
||
if [[ ${#wp_sites[@]} -gt 0 ]]; then
|
||
menu_items+=("manage" "🔧 Manage Sites")
|
||
menu_items+=("" "(_*_)")
|
||
fi
|
||
menu_items+=("install" "➕ Install New WordPress Site")
|
||
menu_items+=("refresh" "🔄 Refresh Status")
|
||
menu_items+=("zback" "(Z) ← Back to Main Menu")
|
||
|
||
local choice
|
||
choice=$(ui_menu "WordPress" "$status_info" 25 90 15 "${menu_items[@]}") || break
|
||
|
||
case "$choice" in
|
||
manage)
|
||
show_wordpress_site_management "${wp_sites[@]}"
|
||
;;
|
||
install)
|
||
show_wordpress_setup_menu
|
||
;;
|
||
refresh)
|
||
# Just loop back to refresh the display
|
||
;;
|
||
back|zback|z|"")
|
||
break
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
show_wordpress_site_management() {
|
||
local wp_sites=("$@")
|
||
|
||
while true; do
|
||
local status_info="WordPress Site Management\n\n"
|
||
status_info+="Select a site to manage:\n"
|
||
status_info+="═══════════════════════════════════════════════════════════\n"
|
||
|
||
local menu_items=()
|
||
for site in "${wp_sites[@]}"; do
|
||
local site_dir="/var/www/$site"
|
||
local status_indicators=""
|
||
|
||
# Check configuration status
|
||
if [[ -f "/etc/nginx/sites-available/$site" ]]; then
|
||
if [[ -L "/etc/nginx/sites-enabled/$site" ]]; then
|
||
status_indicators+="✅"
|
||
else
|
||
status_indicators+="⚠️"
|
||
fi
|
||
elif [[ -f "/etc/apache2/sites-available/${site}.conf" ]]; then
|
||
if sudo a2ensite "${site}.conf" --quiet 2>/dev/null && sudo apache2ctl -S 2>/dev/null | grep -q "$site"; then
|
||
status_indicators+="✅"
|
||
else
|
||
status_indicators+="⚠️"
|
||
fi
|
||
else
|
||
status_indicators+="❌"
|
||
fi
|
||
|
||
# Check SSL status
|
||
if [[ -f "/etc/letsencrypt/live/$site/fullchain.pem" ]]; then
|
||
status_indicators+=" 🔒"
|
||
else
|
||
status_indicators+=" 🔓"
|
||
fi
|
||
|
||
# Check accessibility
|
||
if command -v curl >/dev/null 2>&1; then
|
||
if curl -s -o /dev/null -w "%{http_code}" "http://$site" --connect-timeout 5 --max-time 10 | grep -q "200\|301\|302"; then
|
||
status_indicators+=" 🌍"
|
||
else
|
||
status_indicators+=" ⚠️"
|
||
fi
|
||
fi
|
||
|
||
menu_items+=("$site" "$status_indicators $site")
|
||
done
|
||
|
||
menu_items+=("" "(_*_)")
|
||
menu_items+=("zback" "(Z) ← Back to Status View")
|
||
|
||
local choice
|
||
choice=$(ui_menu "WordPress Site Management" "$status_info" 20 80 10 "${menu_items[@]}") || break
|
||
|
||
case "$choice" in
|
||
back|zback|z|"")
|
||
break
|
||
;;
|
||
*)
|
||
if [[ " ${wp_sites[*]} " =~ " $choice " ]]; then
|
||
show_individual_site_management "$choice"
|
||
fi
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
show_individual_site_management() {
|
||
local site="$1"
|
||
local site_dir="/var/www/$site"
|
||
|
||
while true; do
|
||
local status_info="Managing Site: $site\n\n"
|
||
|
||
# Detailed site information
|
||
status_info+="Site Details:\n"
|
||
status_info+="═══════════════════════════════════════════════════════════\n"
|
||
status_info+="📁 Directory: $site_dir\n"
|
||
|
||
# WordPress version
|
||
local wp_version=""
|
||
if [[ -f "$site_dir/wp-includes/version.php" ]]; then
|
||
wp_version=$(grep "wp_version =" "$site_dir/wp-includes/version.php" | cut -d"'" -f2 2>/dev/null || echo "Unknown")
|
||
fi
|
||
status_info+="🌐 WordPress Version: $wp_version\n"
|
||
|
||
# Web server configuration
|
||
local web_server=""
|
||
local config_file=""
|
||
local config_content=""
|
||
|
||
if [[ -f "/etc/nginx/sites-available/$site" ]]; then
|
||
web_server="Nginx"
|
||
config_file="/etc/nginx/sites-available/$site"
|
||
config_content=$(head -20 "$config_file" 2>/dev/null)
|
||
elif [[ -f "/etc/apache2/sites-available/${site}.conf" ]]; then
|
||
web_server="Apache"
|
||
config_file="/etc/apache2/sites-available/${site}.conf"
|
||
config_content=$(head -20 "$config_file" 2>/dev/null)
|
||
fi
|
||
|
||
if [[ -n "$web_server" ]]; then
|
||
status_info+="🔧 Web Server: $web_server\n"
|
||
status_info+="📄 Config File: $config_file\n"
|
||
|
||
# Sites enabled status
|
||
if [[ "$web_server" == "Nginx" ]]; then
|
||
if [[ -L "/etc/nginx/sites-enabled/$site" ]]; then
|
||
status_info+="🔗 Sites Enabled: ✅ Yes\n"
|
||
else
|
||
status_info+="🔗 Sites Enabled: ❌ No\n"
|
||
fi
|
||
elif [[ "$web_server" == "Apache" ]]; then
|
||
if sudo a2ensite "${site}.conf" --quiet 2>/dev/null && sudo apache2ctl -S 2>/dev/null | grep -q "$site"; then
|
||
status_info+="🔗 Sites Enabled: ✅ Yes\n"
|
||
else
|
||
status_info+="🔗 Sites Enabled: ❌ No\n"
|
||
fi
|
||
fi
|
||
|
||
status_info+="\nConfiguration Preview:\n"
|
||
status_info+="───────────────────────────────────────────────────────────\n"
|
||
status_info+="$config_content\n"
|
||
if [[ $(echo "$config_content" | wc -l) -ge 20 ]]; then
|
||
status_info+="... (truncated, showing first 20 lines)\n"
|
||
fi
|
||
else
|
||
status_info+="⚠️ Web Server: Not configured\n"
|
||
fi
|
||
|
||
status_info+="\n"
|
||
|
||
# SSL status
|
||
if [[ -f "/etc/letsencrypt/live/$site/fullchain.pem" ]]; then
|
||
local cert_expiry=""
|
||
cert_expiry=$(openssl x509 -enddate -noout -in "/etc/letsencrypt/live/$site/fullchain.pem" 2>/dev/null | cut -d= -f2)
|
||
status_info+="🔒 SSL Certificate: ✅ Enabled (Expires: ${cert_expiry:-Unknown})\n"
|
||
else
|
||
status_info+="🔒 SSL Certificate: ❌ Not configured\n"
|
||
fi
|
||
|
||
# Database connection test
|
||
local db_status="❌"
|
||
if [[ -f "$site_dir/wp-config.php" ]]; then
|
||
local db_name db_user db_pass db_host
|
||
db_name=$(grep "DB_NAME" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null)
|
||
db_user=$(grep "DB_USER" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null)
|
||
db_pass=$(grep "DB_PASSWORD" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null)
|
||
db_host=$(grep "DB_HOST" "$site_dir/wp-config.php" | cut -d"'" -f4 2>/dev/null)
|
||
|
||
if [[ -n "$db_name" && -n "$db_user" ]]; then
|
||
if mysql -u"$db_user" -p"$db_pass" -h"${db_host:-localhost}" -e "USE $db_name;" 2>/dev/null; then
|
||
db_status="✅"
|
||
fi
|
||
fi
|
||
fi
|
||
status_info+="🗄️ Database Connection: $db_status\n"
|
||
|
||
# Site accessibility
|
||
local site_accessible="❌"
|
||
if command -v curl >/dev/null 2>&1; then
|
||
local http_code
|
||
http_code=$(curl -s -o /dev/null -w "%{http_code}" "http://$site" --connect-timeout 5 --max-time 10)
|
||
if [[ "$http_code" =~ ^(200|301|302)$ ]]; then
|
||
site_accessible="✅ (HTTP $http_code)"
|
||
else
|
||
site_accessible="❌ (HTTP ${http_code:-timeout})"
|
||
fi
|
||
fi
|
||
status_info+="🌍 Site Accessible: $site_accessible\n"
|
||
|
||
local menu_items=(
|
||
"" "(_*_)"
|
||
"view-config" "📄 View Full Web Server Config"
|
||
"edit-config" "✏️ Edit Web Server Config"
|
||
"enable-site" "🔗 Enable/Disable Site"
|
||
"test-site" "🧪 Test Site Accessibility"
|
||
"" "(_*_)"
|
||
"delete-site" "🗑️ Delete This Site (DANGER)"
|
||
"" "(_*_)"
|
||
"zback" "(Z) ← Back to Site List"
|
||
)
|
||
|
||
local choice
|
||
choice=$(ui_menu "Site Management: $site" "$status_info" 25 90 15 "${menu_items[@]}") || break
|
||
|
||
case "$choice" in
|
||
view-config)
|
||
if [[ -n "$config_file" && -f "$config_file" ]]; then
|
||
local full_config
|
||
full_config=$(cat "$config_file")
|
||
ui_info "Configuration: $config_file" "$full_config"
|
||
else
|
||
ui_error "No configuration file found for $site"
|
||
fi
|
||
;;
|
||
edit-config)
|
||
if [[ -n "$config_file" && -f "$config_file" ]]; then
|
||
sudo nano "$config_file"
|
||
if [[ "$web_server" == "Nginx" ]]; then
|
||
sudo nginx -t && sudo systemctl reload nginx
|
||
elif [[ "$web_server" == "Apache" ]]; then
|
||
sudo apache2ctl configtest && sudo systemctl reload apache2
|
||
fi
|
||
else
|
||
ui_error "No configuration file found for $site"
|
||
fi
|
||
;;
|
||
enable-site)
|
||
manage_site_enablement "$site" "$web_server"
|
||
;;
|
||
test-site)
|
||
test_site_accessibility "$site"
|
||
;;
|
||
delete-site)
|
||
delete_wordpress_site "$site"
|
||
break
|
||
;;
|
||
back|zback|z|"")
|
||
break
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
manage_site_enablement() {
|
||
local site="$1"
|
||
local web_server="$2"
|
||
|
||
if [[ "$web_server" == "Nginx" ]]; then
|
||
if [[ -L "/etc/nginx/sites-enabled/$site" ]]; then
|
||
if ui_confirm "Disable site $site?"; then
|
||
sudo rm "/etc/nginx/sites-enabled/$site"
|
||
sudo nginx -t && sudo systemctl reload nginx
|
||
ui_success "Site $site disabled"
|
||
fi
|
||
else
|
||
if ui_confirm "Enable site $site?"; then
|
||
sudo ln -sf "/etc/nginx/sites-available/$site" "/etc/nginx/sites-enabled/$site"
|
||
sudo nginx -t && sudo systemctl reload nginx
|
||
ui_success "Site $site enabled"
|
||
fi
|
||
fi
|
||
elif [[ "$web_server" == "Apache" ]]; then
|
||
if sudo a2ensite "${site}.conf" --quiet 2>/dev/null && sudo apache2ctl -S 2>/dev/null | grep -q "$site"; then
|
||
if ui_confirm "Disable site $site?"; then
|
||
sudo a2dissite "${site}.conf"
|
||
sudo apache2ctl configtest && sudo systemctl reload apache2
|
||
ui_success "Site $site disabled"
|
||
fi
|
||
else
|
||
if ui_confirm "Enable site $site?"; then
|
||
sudo a2ensite "${site}.conf"
|
||
sudo apache2ctl configtest && sudo systemctl reload apache2
|
||
ui_success "Site $site enabled"
|
||
fi
|
||
fi
|
||
else
|
||
ui_error "No web server configuration found for $site"
|
||
fi
|
||
}
|
||
|
||
test_site_accessibility() {
|
||
local site="$1"
|
||
|
||
local test_info="Testing Site Accessibility: $site\n\n"
|
||
|
||
# HTTP test
|
||
if command -v curl >/dev/null 2>&1; then
|
||
test_info+="HTTP Test:\n"
|
||
local http_result
|
||
http_result=$(curl -s -o /dev/null -w "HTTP Code: %{http_code}\nTime: %{time_total}s\nSize: %{size_download} bytes" "http://$site" --connect-timeout 10 --max-time 30 2>&1)
|
||
test_info+="$http_result\n\n"
|
||
|
||
# HTTPS test if SSL is configured
|
||
if [[ -f "/etc/letsencrypt/live/$site/fullchain.pem" ]]; then
|
||
test_info+="HTTPS Test:\n"
|
||
local https_result
|
||
https_result=$(curl -s -o /dev/null -w "HTTP Code: %{http_code}\nTime: %{time_total}s\nSize: %{size_download} bytes" "https://$site" --connect-timeout 10 --max-time 30 2>&1)
|
||
test_info+="$https_result\n\n"
|
||
fi
|
||
else
|
||
test_info+="curl not available for testing\n"
|
||
fi
|
||
|
||
# DNS resolution test
|
||
if command -v nslookup >/dev/null 2>&1; then
|
||
test_info+="DNS Resolution:\n"
|
||
local dns_result
|
||
dns_result=$(nslookup "$site" 2>&1 | head -10)
|
||
test_info+="$dns_result\n"
|
||
fi
|
||
|
||
ui_info "Site Test Results" "$test_info"
|
||
}
|
||
|
||
delete_wordpress_site() {
|
||
local site="$1"
|
||
local site_dir="/var/www/$site"
|
||
|
||
local warning_info="⚠️ WARNING: DELETE WORDPRESS SITE ⚠️\n\n"
|
||
warning_info+="You are about to permanently delete:\n"
|
||
warning_info+="• Site: $site\n"
|
||
warning_info+="• Directory: $site_dir\n"
|
||
warning_info+="• Web server configuration\n"
|
||
warning_info+="• SSL certificates (if any)\n\n"
|
||
warning_info+="This action CANNOT be undone!\n\n"
|
||
warning_info+="Type 'DELETE $site' to confirm:"
|
||
|
||
local confirmation
|
||
confirmation=$(ui_input "Confirm Site Deletion" "$warning_info")
|
||
|
||
if [[ "$confirmation" == "DELETE $site" ]]; then
|
||
# Remove web server configuration
|
||
if [[ -f "/etc/nginx/sites-available/$site" ]]; then
|
||
sudo rm -f "/etc/nginx/sites-available/$site"
|
||
sudo rm -f "/etc/nginx/sites-enabled/$site"
|
||
sudo nginx -t && sudo systemctl reload nginx
|
||
fi
|
||
|
||
if [[ -f "/etc/apache2/sites-available/${site}.conf" ]]; then
|
||
sudo a2dissite "${site}.conf" 2>/dev/null
|
||
sudo rm -f "/etc/apache2/sites-available/${site}.conf"
|
||
sudo apache2ctl configtest && sudo systemctl reload apache2
|
||
fi
|
||
|
||
# Remove SSL certificates
|
||
if [[ -d "/etc/letsencrypt/live/$site" ]]; then
|
||
sudo certbot delete --cert-name "$site" --non-interactive 2>/dev/null
|
||
fi
|
||
|
||
# Remove site directory
|
||
if [[ -d "$site_dir" ]]; then
|
||
sudo rm -rf "$site_dir"
|
||
fi
|
||
|
||
ui_success "Site $site has been completely removed"
|
||
else
|
||
ui_info "Deletion Cancelled" "Site deletion cancelled - nothing was removed"
|
||
fi
|
||
}
|
||
|
||
# Legacy function for backward compatibility
|
||
setup_wordpress() {
|
||
show_wordpress_setup_menu
|
||
}
|
||
|
||
# ==============================================================================
|
||
# KEYBOARD LAYOUT CONFIGURATION
|
||
# ==============================================================================
|
||
|
||
show_keyboard_layout_menu() {
|
||
log "Entering show_keyboard_layout_menu function"
|
||
|
||
while true; do
|
||
local menu_items=(
|
||
"macbook" "(.Y.) MacBook Layout (Cmd as Super, Cmd+Tab switching)"
|
||
"thinkpad" "(.Y.) ThinkPad Layout (Standard PC layout optimized)"
|
||
"generic" "(.Y.) Generic Laptop Layout (Standard configuration)"
|
||
"" "(_*_)"
|
||
"current-status" "(.Y.) Show Current Keyboard Configuration"
|
||
"reset" "(.Y.) Reset to Ubuntu Defaults"
|
||
"" "(_*_)"
|
||
"zback" "(Z) ← Back to Main Menu"
|
||
)
|
||
|
||
local choice
|
||
choice=$(ui_menu "Keyboard Layout Configuration" \
|
||
"Choose a keyboard layout optimized for your laptop:" \
|
||
20 80 12 "${menu_items[@]}") || break
|
||
|
||
case "$choice" in
|
||
macbook)
|
||
configure_macbook_layout
|
||
;;
|
||
thinkpad)
|
||
configure_thinkpad_layout
|
||
;;
|
||
generic)
|
||
configure_generic_layout
|
||
;;
|
||
current-status)
|
||
show_keyboard_status
|
||
;;
|
||
reset)
|
||
reset_keyboard_layout
|
||
;;
|
||
back|zback|z|"")
|
||
break
|
||
;;
|
||
esac
|
||
done
|
||
|
||
log "Exiting show_keyboard_layout_menu function"
|
||
}
|
||
|
||
configure_macbook_layout() {
|
||
log "Configuring MacBook keyboard layout"
|
||
|
||
local info="MacBook Layout Configuration:\n\n"
|
||
info+="• Command key → Super key (for shortcuts)\n"
|
||
info+="• Cmd+Tab → Switch applications\n"
|
||
info+="• Cmd+\` → Switch windows of same app\n"
|
||
info+="• Cmd+C/V/X/Z → Copy/Paste/Cut/Undo\n"
|
||
info+="• Cmd+Space → Show applications\n"
|
||
info+="• Control key remains as Control\n\n"
|
||
info+="This layout makes Ubuntu feel more like macOS.\n\n"
|
||
info+="Apply this configuration?"
|
||
|
||
if ui_yesno "MacBook Layout" "$info"; then
|
||
apply_macbook_keyboard_config
|
||
ui_msg "Success" "MacBook keyboard layout applied successfully!\n\nYou may need to log out and back in for all changes to take effect."
|
||
fi
|
||
}
|
||
|
||
configure_thinkpad_layout() {
|
||
log "Configuring ThinkPad keyboard layout"
|
||
|
||
local info="ThinkPad Layout Configuration:\n\n"
|
||
info+="• Optimized for ThinkPad keyboards\n"
|
||
info+="• Caps Lock → Additional Control key\n"
|
||
info+="• Alt+Tab → Switch applications\n"
|
||
info+="• Super key for launcher\n"
|
||
info+="• Standard PC shortcuts (Ctrl+C/V/X/Z)\n\n"
|
||
info+="This layout optimizes the ThinkPad keyboard experience.\n\n"
|
||
info+="Apply this configuration?"
|
||
|
||
if ui_yesno "ThinkPad Layout" "$info"; then
|
||
apply_thinkpad_keyboard_config
|
||
ui_msg "Success" "ThinkPad keyboard layout applied successfully!\n\nYou may need to log out and back in for all changes to take effect."
|
||
fi
|
||
}
|
||
|
||
configure_generic_layout() {
|
||
log "Configuring generic laptop keyboard layout"
|
||
|
||
local info="Generic Laptop Layout Configuration:\n\n"
|
||
info+="• Standard PC keyboard layout\n"
|
||
info+="• Alt+Tab → Switch applications\n"
|
||
info+="• Super key for launcher\n"
|
||
info+="• Standard shortcuts (Ctrl+C/V/X/Z)\n"
|
||
info+="• Optimized for most laptop keyboards\n\n"
|
||
info+="This is a safe, standard configuration.\n\n"
|
||
info+="Apply this configuration?"
|
||
|
||
if ui_yesno "Generic Layout" "$info"; then
|
||
apply_generic_keyboard_config
|
||
ui_msg "Success" "Generic keyboard layout applied successfully!\n\nYou may need to log out and back in for all changes to take effect."
|
||
fi
|
||
}
|
||
|
||
show_keyboard_status() {
|
||
log "Showing current keyboard configuration status"
|
||
|
||
local status_info=""
|
||
|
||
# Check current XKB options
|
||
local xkb_options
|
||
xkb_options=$(setxkbmap -query | grep options || echo "options: (none)")
|
||
|
||
# Check gsettings for key mappings
|
||
local switch_apps
|
||
switch_apps=$(gsettings get org.gnome.desktop.wm.keybindings switch-applications 2>/dev/null || echo "Not set")
|
||
|
||
local switch_windows
|
||
switch_windows=$(gsettings get org.gnome.desktop.wm.keybindings switch-windows 2>/dev/null || echo "Not set")
|
||
|
||
status_info="Current Keyboard Configuration:\n\n"
|
||
status_info+="XKB Options: ${xkb_options#options: }\n\n"
|
||
status_info+="Switch Applications: $switch_apps\n"
|
||
status_info+="Switch Windows: $switch_windows\n\n"
|
||
|
||
# Check for autostart keyboard mapping
|
||
if [[ -f "$HOME/.config/autostart/keyboard-mapping.desktop" ]]; then
|
||
status_info+="Autostart Mapping: ✓ Enabled\n"
|
||
else
|
||
status_info+="Autostart Mapping: ✗ Not configured\n"
|
||
fi
|
||
|
||
ui_info "Keyboard Status" "$status_info"
|
||
}
|
||
|
||
reset_keyboard_layout() {
|
||
log "Resetting keyboard layout to Ubuntu defaults"
|
||
|
||
local info="Reset Keyboard Layout:\n\n"
|
||
info+="This will restore Ubuntu's default keyboard configuration:\n\n"
|
||
info+="• Remove all custom key mappings\n"
|
||
info+="• Reset XKB options to defaults\n"
|
||
info+="• Remove autostart keyboard configurations\n"
|
||
info+="• Restore standard Ubuntu shortcuts\n\n"
|
||
info+="⚠️ This will undo any custom keyboard configurations.\n\n"
|
||
info+="Continue with reset?"
|
||
|
||
if ui_yesno "Reset Keyboard" "$info"; then
|
||
apply_keyboard_reset
|
||
ui_msg "Success" "Keyboard layout reset to Ubuntu defaults!\n\nYou may need to log out and back in for all changes to take effect."
|
||
fi
|
||
}
|
||
|
||
apply_macbook_keyboard_config() {
|
||
log "Applying MacBook keyboard configuration"
|
||
|
||
# Create autostart directory if it doesn't exist
|
||
mkdir -p "$HOME/.config/autostart"
|
||
|
||
# Create autostart file for keyboard mapping
|
||
cat > "$HOME/.config/autostart/keyboard-mapping.desktop" <<EOF
|
||
[Desktop Entry]
|
||
Type=Application
|
||
Name=Keyboard Mapping
|
||
Exec=/bin/bash -c "setxkbmap -option caps:ctrl_modifier"
|
||
X-GNOME-Autostart-enabled=true
|
||
EOF
|
||
|
||
# Apply the mapping immediately
|
||
setxkbmap -option caps:ctrl_modifier 2>/dev/null || log "Warning: Could not apply setxkbmap immediately"
|
||
|
||
# Configure GNOME shortcuts for macOS-like behavior
|
||
gsettings set org.gnome.desktop.wm.keybindings switch-applications "['<Alt>Tab']" 2>/dev/null || log "Warning: Could not set switch-applications"
|
||
gsettings set org.gnome.desktop.wm.keybindings switch-windows "['<Alt>grave']" 2>/dev/null || log "Warning: Could not set switch-windows"
|
||
|
||
# Set Super+Space for show applications (like Spotlight)
|
||
gsettings set org.gnome.shell.keybindings toggle-overview "['<Super>space']" 2>/dev/null || log "Warning: Could not set toggle-overview"
|
||
|
||
log "MacBook keyboard configuration applied"
|
||
}
|
||
|
||
apply_thinkpad_keyboard_config() {
|
||
log "Applying ThinkPad keyboard configuration"
|
||
|
||
# Create autostart directory if it doesn't exist
|
||
mkdir -p "$HOME/.config/autostart"
|
||
|
||
# Create autostart file for ThinkPad optimizations
|
||
cat > "$HOME/.config/autostart/keyboard-mapping.desktop" <<EOF
|
||
[Desktop Entry]
|
||
Type=Application
|
||
Name=Keyboard Mapping
|
||
Exec=/bin/bash -c "setxkbmap -option caps:ctrl_modifier"
|
||
X-GNOME-Autostart-enabled=true
|
||
EOF
|
||
|
||
# Apply the mapping immediately
|
||
setxkbmap -option caps:ctrl_modifier 2>/dev/null || log "Warning: Could not apply setxkbmap immediately"
|
||
|
||
# Configure standard GNOME shortcuts
|
||
gsettings set org.gnome.desktop.wm.keybindings switch-applications "['<Alt>Tab']" 2>/dev/null || log "Warning: Could not set switch-applications"
|
||
gsettings set org.gnome.desktop.wm.keybindings switch-windows "['<Alt>grave']" 2>/dev/null || log "Warning: Could not set switch-windows"
|
||
|
||
log "ThinkPad keyboard configuration applied"
|
||
}
|
||
|
||
apply_generic_keyboard_config() {
|
||
log "Applying generic laptop keyboard configuration"
|
||
|
||
# Remove any existing autostart keyboard mapping
|
||
rm -f "$HOME/.config/autostart/keyboard-mapping.desktop" 2>/dev/null || true
|
||
|
||
# Reset XKB options to default
|
||
setxkbmap -option 2>/dev/null || log "Warning: Could not reset setxkbmap options"
|
||
|
||
# Configure standard GNOME shortcuts
|
||
gsettings set org.gnome.desktop.wm.keybindings switch-applications "['<Alt>Tab']" 2>/dev/null || log "Warning: Could not set switch-applications"
|
||
gsettings set org.gnome.desktop.wm.keybindings switch-windows "['<Alt>grave']" 2>/dev/null || log "Warning: Could not set switch-windows"
|
||
|
||
log "Generic keyboard configuration applied"
|
||
}
|
||
|
||
apply_keyboard_reset() {
|
||
log "Resetting keyboard configuration to Ubuntu defaults"
|
||
|
||
# Remove autostart keyboard mapping
|
||
rm -f "$HOME/.config/autostart/keyboard-mapping.desktop" 2>/dev/null || true
|
||
|
||
# Reset XKB options to default
|
||
setxkbmap -option 2>/dev/null || log "Warning: Could not reset setxkbmap options"
|
||
|
||
# Reset GNOME shortcuts to defaults
|
||
gsettings reset org.gnome.desktop.wm.keybindings switch-applications 2>/dev/null || log "Warning: Could not reset switch-applications"
|
||
gsettings reset org.gnome.desktop.wm.keybindings switch-windows 2>/dev/null || log "Warning: Could not reset switch-windows"
|
||
gsettings reset org.gnome.shell.keybindings toggle-overview 2>/dev/null || log "Warning: Could not reset toggle-overview"
|
||
|
||
log "Keyboard configuration reset to defaults"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# MAIN PROGRAM
|
||
# ==============================================================================
|
||
|
||
main() {
|
||
# Initialize
|
||
init_logging
|
||
log "Initializing Ultrabunt Ultimate Buntstaller..."
|
||
|
||
ensure_deps
|
||
log "Dependencies check completed"
|
||
|
||
log "╔═══════════════════════════════════════════════════════╗"
|
||
log "║ ULTRABUNT ULTIMATE BUNTSTALLER v4.2.0 STARTED ║"
|
||
log "╚═══════════════════════════════════════════════════════╝"
|
||
|
||
# Update buntage cache
|
||
log "Starting APT cache update..."
|
||
apt_update
|
||
log "APT cache update completed"
|
||
|
||
# Build buntage installation cache for fast lookups
|
||
build_package_cache
|
||
|
||
# Show main menu
|
||
log "Loading main menu..."
|
||
show_category_menu
|
||
log "Main menu completed"
|
||
|
||
# Farewell
|
||
log "Ultrabunt Ultimate Buntstaller exiting."
|
||
ui_msg "Goodbye!" "Thanks for bunting Ultrabunt Ultimate Buntstaller!\n\nLog: $LOGFILE\nBackups: $BACKUP_DIR\n\n💡 Tip: Reboot to apply all changes."
|
||
}
|
||
|
||
# Run main program
|
||
main
|
||
exit 0 |