BUNTSTARTER/ultrabunt.sh

13648 lines
495 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"
# Dialog box size configuration
DIALOG_HEIGHT=24
DIALOG_WIDTH=80
DIALOG_MENU_HEIGHT=14
# Dialog size presets
declare -A DIALOG_PRESETS
DIALOG_PRESETS[compact]="20 70 12"
DIALOG_PRESETS[standard]="24 80 14"
DIALOG_PRESETS[large]="30 100 18"
DIALOG_PRESETS[wide]="24 120 14"
DIALOG_PRESETS[tall]="35 80 20"
DIALOG_PRESETS[huge]="40 140 25"
# Current dialog preset
CURRENT_DIALOG_PRESET="standard"
# Cache for installed buntages - populated once at startup
declare -A INSTALLED_CACHE
# Session variables for database authentication
MARIADB_SESSION_PASSWORD=""
MARIADB_SESSION_ACTIVE=false
# Selective loading configuration - categories to exclude
declare -A EXCLUDED_CATEGORIES
EXCLUDED_CATEGORIES=()
# Show ASCII art splash screen
show_ascii_splash() {
# Clear screen
clear
# Get terminal dimensions
local term_width=$(tput cols 2>/dev/null || echo "80")
local term_height=$(tput lines 2>/dev/null || echo "24")
# ASCII art width is 81 characters
local ascii_width=81
if [[ $ascii_width -gt $term_width ]]; then
log "WARNING: ASCII art width ($ascii_width) exceeds terminal width ($term_width)"
echo -e "${YELLOW}⚠️ Terminal too narrow for optimal display. Recommended width: ${ascii_width} columns${NC}"
echo -e "${BLUE}💡 Try maximizing your terminal or reducing font size${NC}"
echo ""
sleep 2
fi
# Display ASCII art with scrolling effect
echo -e "${GREEN}"
# Embedded ASCII art (cleaner version - 81 characters wide)
local ascii_lines=(
" "
" ... "
" :==-:. - "
" .=**+-. :-.-#%%#*#%%%%%%%= "
" -=*=+-. :*%%#%#************#@+. "
" .::=:-:...=%%#####****************#%=.. "
" ..:::----#########****************####+. .-:. "
" .::::----==++####****************#####*#= .=*+-.. "
" -######**++==-==++*####***#%%#********###***+*+. .-#*+-. "
" .*%#####**++++*==++*##*****%@@@%*******####***+*+--==+*+. "
" :%#####***+++++%++*####***#@@%%@********##%@@%++*---==++ "
" -%######*****+==%#####*#**#@@@@%*+++****#%@%*%*+*=-=++#-.. "
" *%######*****+==%%####****#%%%%#*++++***#%@@@@*+*###########+. "
" +%#######****#++%######**********++++++***#%@@@*+****##########- "
" .#%%########**#*##%######*#***#@@%#*+++++***##****++*#*##########= "
" .#@%####%######%%%#########***#@@@@%**+++****#%%%*+++%**#########*. "
" *%%###%%######%%%########****#@@@@@********##@@@%+++:###%#######= "
" =*+*#%%#######*%#%%##########**#@@%%%********#%@@@@*+=..##########= "
" =*==-*#*#######: =%##########***#%%%%#********#%@%%@*+=.%**#######*: "
":#++=*#*######### =%%%#######********####*****###%@@%++=#***######*: "
"+*===#################%#####***#**#%%*-*+:-+=+*###***+++****######*=:--.. "
"=*--++-*%##########**+%#####*#****%%#*##***+=-=-.=#*++++****#####*+=:-*+=. "
" *=-=*-=#%%#%#*****+++*%######*####%%#*-+*-:+=-=--*%++++****#####=:..:**-. "
":%%=-+*=+#%#*+*######%%%###########**########+-=-*%*++++****##+=*--:.:**=--:. "
"**%%%###*+==+#- *%%%%#%#+=-----------==+*#######**+++*#%**#==-*+=:..+-.:*+=. "
"*--=*#%%%%%%%#- *%%+:.......:.::::::::::::::::=##*+*%%%@%%=.+%#**-.....:+*=. "
".*==++====--== +=....:=###**++++*#%%#*+-::......:**+%*####++#*..:.:+=-.+*=. "
" *=**+++++*#+ .+..+#++***=#==****#+*#%##*#+:....=:*+**#%+###%##*****=-:=#=. "
".+**##*++*+*- .+-+=*%:==::#--++++#++#***#*#+:...--.:=++*=++##*+=*= .:=#=. "
" .+=*##+=-+##=.. .+=+*=%:=+::#::++==#*+##*##*#-::. ...:- :*+==+###**= .=#=. "
"- .*####**=+#+**#+=:*#+#**+=======---=====+*+-:::.. -===: .+==+***++%: -*=. "
"=-. =++##**#=+#*++=....:::::::::::::::-++====-::::.+:+::-=-:-===+*#*=+* "
"#=::-+#%%*+*#*.:-+=..===+=+=::::::::=+=+=+*+++=:::=:*===::*-++=+++**#** "
"+*=:-=+-..+##*=++:=-*=*:::-*-::::::-*+*-:-=-:**:::+-+:--+-===+=+*+*+-#- "
"-+*-.:-....==+::-=--++----=%=::::::=*+=---==+#*:::-=+=*##-==+++=+##*=* "
".=+*-..:=:..-+=++*--++--=++#-:::::::**=--=+++%-:::=#######+++++**++*# "
" :=++--+*+:..::::-=.-*+-==*+::::::::-*+-===+#+:::*##*===+###*++**+=-#. "
" .....-+*=...--:-=..:-**+-:::::::::.:=***+=:....-#*==-::=#**+*+=-=+. "
" .=+*=:=+++-=.....:::::::::::.................-=*+=+++*+--=#= "
" .:--- :++*+......::...:::::.............=*+:..--+*=+*-==+* "
" =+*......:......::..............-+=...++--=*..::== "
" .*=. ...........:................ ..:*++#==--:-+ "
" .=*:..................... .....=#%#==--*===++*. "
" :+:...::..::::.............-=*#+=#= :: "
" .:=*:...::::::::...... ...=%#+----:. "
" .:--::+*:..::::::............+#*-::::.:-. "
" :----:...+*:...:::.............:+*:..:....: "
" -:.... -##-:::::.:......:+#= ...... "
" . -*%%#####%%#=. . .:::. "
" =@@ .#@-+@%@@+*@* @#-@@%*:@@@%.@@%+*@@%@# "
" =@@#.#@@=#@ +@++@@@@%=@# %# @% -@* %# "
" +@#@@%%@.*@ *@*-@%@@% @+ @# @@%-=@@%@- "
" =@+=%=#@.*@#%@+-@ =@#.#=@@: @# %@*:-@#-@+ "
" .====: . .=**= . :=++ .=**= :=++++- "
" -#%%%@@: %@%++@@#.=#%%%@@..%@%++%@#+#@@%#+ "
" +@*@@= -@*:---=%@:#@- =@* #@= "
" +@@@@@# @%: :@*#@%##%@+%%: -@# #@= "
" *@=.*@+ -@%=::=@%-*@=::+@%-@%=::=@%: #@=. "
" =%- +%- .=#@@#=. +%@@@%+ .=#@@#=. +%= "
" @%##* +@#*@#. @###=#@@#+ "
" @@% --%: %# @: *% "
" =@-%+. -@+ #@@% *% "
" ####= .+##*. # +* "
)
# Display each line with scrolling effect
for line in "${ascii_lines[@]}"; do
echo "$line"
sleep 0.05 # Small delay for scrolling effect
done
echo -e "${NC}"
# Add some spacing and info
echo ""
echo -e "${BLUE}╔═══════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ ULTRABUNT ULTIMATE BUNTSTALLER v4.2.0 ║${NC}"
echo -e "${BLUE}║ Professional Ubuntu/Mint Setup & Package Manager ║${NC}"
echo -e "${BLUE}╚═══════════════════════════════════════════════════════╝${NC}"
echo ""
# Brief pause before continuing
sleep 1.5
}
# Parse command line arguments
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
exit 0
;;
-dev|--no-dev)
EXCLUDED_CATEGORIES["dev"]=1
log "Excluding development category"
;;
-wp|--no-wordpress)
EXCLUDED_CATEGORIES["wordpress"]=1
log "Excluding WordPress category"
;;
-gaming|--no-gaming)
EXCLUDED_CATEGORIES["gaming-platforms"]=1
EXCLUDED_CATEGORIES["gaming-emulators"]=1
log "Excluding gaming categories"
;;
-ai|--no-ai)
EXCLUDED_CATEGORIES["ai"]=1
log "Excluding AI category"
;;
-media|--no-media)
EXCLUDED_CATEGORIES["multimedia"]=1
EXCLUDED_CATEGORIES["music"]=1
log "Excluding media categories"
;;
-web|--no-web)
EXCLUDED_CATEGORIES["web-browsers"]=1
log "Excluding web browsers category"
;;
-comm|--no-communication)
EXCLUDED_CATEGORIES["communication"]=1
log "Excluding communication category"
;;
-office|--no-office)
EXCLUDED_CATEGORIES["office"]=1
log "Excluding office category"
;;
-graphics|--no-graphics)
EXCLUDED_CATEGORIES["graphics"]=1
log "Excluding graphics category"
;;
-security|--no-security)
EXCLUDED_CATEGORIES["security"]=1
log "Excluding security category"
;;
-network|--no-network)
EXCLUDED_CATEGORIES["network"]=1
log "Excluding network category"
;;
-system|--no-system)
EXCLUDED_CATEGORIES["system"]=1
log "Excluding system category"
;;
-education|--no-education)
EXCLUDED_CATEGORIES["education"]=1
log "Excluding education category"
;;
-science|--no-science)
EXCLUDED_CATEGORIES["science"]=1
log "Excluding science category"
;;
-finance|--no-finance)
EXCLUDED_CATEGORIES["finance"]=1
log "Excluding finance category"
;;
-virtualization|--no-virtualization)
EXCLUDED_CATEGORIES["virtualization"]=1
log "Excluding virtualization category"
;;
-cloud|--no-cloud)
EXCLUDED_CATEGORIES["cloud"]=1
log "Excluding cloud category"
;;
-minimal|--minimal)
# Exclude most categories, keep only core and system
for cat in dev wordpress gaming-platforms gaming-emulators ai multimedia music web-browsers communication office graphics security network education science finance virtualization cloud; do
EXCLUDED_CATEGORIES["$cat"]=1
done
log "Minimal mode: excluding most categories"
;;
-core|--core-only)
# Exclude everything except core
for cat in dev wordpress gaming-platforms gaming-emulators ai multimedia music web-browsers communication office graphics security network system education science finance virtualization cloud; do
EXCLUDED_CATEGORIES["$cat"]=1
done
log "Core-only mode: excluding all except core category"
;;
*)
echo "Unknown option: $1"
echo "Use -h or --help for usage information"
exit 1
;;
esac
shift
done
}
# Show help information
show_help() {
cat << EOF
ULTRABUNT ULTIMATE BUNTSTALLER v4.2.0
Professional Ubuntu/Mint setup & package manager
USAGE:
./ultrabunt.sh [OPTIONS]
OPTIONS:
-h, --help Show this help message
SELECTIVE LOADING (exclude categories to speed up startup):
-dev, --no-dev Exclude development tools
-wp, --no-wordpress Exclude WordPress tools
-gaming, --no-gaming Exclude gaming platforms and emulators
-ai, --no-ai Exclude AI/ML tools
-media, --no-media Exclude multimedia and music tools
-web, --no-web Exclude web browsers
-comm, --no-communication Exclude communication tools
-office, --no-office Exclude office applications
-graphics, --no-graphics Exclude graphics tools
-security, --no-security Exclude security tools
-network, --no-network Exclude network tools
-system, --no-system Exclude system utilities
-education, --no-education Exclude educational software
-science, --no-science Exclude scientific applications
-finance, --no-finance Exclude financial software
-virtualization, --no-virtualization Exclude virtualization tools
-cloud, --no-cloud Exclude cloud tools
PRESET MODES:
-minimal, --minimal Load only core and system categories
-core, --core-only Load only core category
EXAMPLES:
./ultrabunt.sh # Load all categories (default)
./ultrabunt.sh -dev -gaming # Exclude development and gaming
./ultrabunt.sh --minimal # Minimal installation (fastest)
./ultrabunt.sh --core-only # Core packages only
./ultrabunt.sh -wp -ai -media # Exclude WordPress, AI, and media
NOTES:
- Excluding categories significantly speeds up startup time
- Categories can still be accessed if needed (they just won't be scanned)
- Log file: /var/log/ultrabunt.log
- Backups: /var/backups/ultrabunt
EOF
}
# 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=()
# If selective loading is enabled, only cache packages from non-excluded categories
local selective_mode=false
if [[ ${#EXCLUDED_CATEGORIES[@]} -gt 0 ]]; then
selective_mode=true
log "Selective loading enabled - optimizing cache for included categories only"
fi
# Cache APT buntages
log "Caching APT buntages..."
if [[ "$selective_mode" == "true" ]]; then
# Only cache packages from included categories
for name in "${!PACKAGES[@]}"; do
local pkg_category="${PKG_CATEGORY[$name]:-}"
local method="${PKG_METHOD[$name]:-}"
# Skip if category is excluded
if [[ -n "${EXCLUDED_CATEGORIES[$pkg_category]:-}" ]]; then
continue
fi
# Only check APT packages in this section
if [[ "$method" == "apt" ]]; then
local pkg="${PACKAGES[$name]}"
if dpkg-query -W "$pkg" &>/dev/null; then
INSTALLED_CACHE["apt:$pkg"]=1
fi
fi
done
else
# Cache all APT packages (original behavior)
while IFS= read -r pkg; do
INSTALLED_CACHE["apt:$pkg"]=1
done < <(dpkg-query -W -f='${Package}\n' 2>/dev/null | sort)
fi
# Cache Snap buntages
if command -v snap &>/dev/null; then
log "Caching Snap buntages..."
if [[ "$selective_mode" == "true" ]]; then
# Only cache packages from included categories
for name in "${!PACKAGES[@]}"; do
local pkg_category="${PKG_CATEGORY[$name]:-}"
local method="${PKG_METHOD[$name]:-}"
# Skip if category is excluded
if [[ -n "${EXCLUDED_CATEGORIES[$pkg_category]:-}" ]]; then
continue
fi
# Only check Snap packages in this section
if [[ "$method" == "snap" ]]; then
local pkg="${PACKAGES[$name]}"
if snap list "$pkg" &>/dev/null; then
INSTALLED_CACHE["snap:$pkg"]=1
fi
fi
done
else
# Cache all Snap packages (original behavior)
while IFS= read -r pkg; do
INSTALLED_CACHE["snap:$pkg"]=1
done < <(snap list 2>/dev/null | awk 'NR>1 {print $1}' | sort)
fi
fi
# Cache Flatpak buntages
if command -v flatpak &>/dev/null; then
log "Caching Flatpak buntages..."
if [[ "$selective_mode" == "true" ]]; then
# Only cache packages from included categories
for name in "${!PACKAGES[@]}"; do
local pkg_category="${PKG_CATEGORY[$name]:-}"
local method="${PKG_METHOD[$name]:-}"
# Skip if category is excluded
if [[ -n "${EXCLUDED_CATEGORIES[$pkg_category]:-}" ]]; then
continue
fi
# Only check Flatpak packages in this section
if [[ "$method" == "flatpak" ]]; then
local pkg="${PACKAGES[$name]}"
if flatpak list --app | grep -q "$pkg"; then
INSTALLED_CACHE["flatpak:$pkg"]=1
fi
fi
done
else
# Cache all Flatpak packages (original behavior)
while IFS= read -r pkg; do
INSTALLED_CACHE["flatpak:$pkg"]=1
done < <(flatpak list --app --columns=application 2>/dev/null | sort)
fi
fi
local total_cached=${#INSTALLED_CACHE[@]}
log "Buntage cache built with $total_cached entries (selective mode: $selective_mode)"
}
# Function to update cache for a specific buntage
update_package_cache() {
local name="$1"
local silent="${2:-false}" # Add silent mode parameter
if [[ -z "$name" ]]; then
[[ "$silent" != "true" ]] && log "ERROR: No buntage name provided to update_package_cache"
return 1
fi
if [[ -z "${PACKAGES[$name]:-}" ]]; then
[[ "$silent" != "true" ]] && log "WARNING: Buntage '$name' not found in PACKAGES array"
return 1
fi
local pkg="${PACKAGES[$name]}"
local method="${PKG_METHOD[$name]:-}"
[[ "$silent" != "true" ]] && 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"
[[ "$silent" != "true" ]] && 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"
[[ "$silent" != "true" ]] && 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"
[[ "$silent" != "true" ]] && 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
[[ "$silent" != "true" ]] && log "Cache update skipped for custom buntage: $name"
;;
*)
[[ "$silent" != "true" ]] && 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" >&2
}
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_password() {
whiptail --title "$1" --passwordbox "$2" 10 70 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 in preferred order: APT → Snap → Flatpak → Custom
for variant in "${base_name}-deb" "${base_name}-snap" "${base_name}-flatpak"; 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:" $DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${available_methods[@]}")
# Handle user cancellation gracefully - don't exit script
if [[ -n "$choice" && "$choice" != "back" ]]; then
echo "$choice"
return 0
else
log "User cancelled installation method selection for $base_name"
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"
PACKAGES[rustup]="rustup"
PKG_DESC[rustup]="Rust toolchain installer [CURL]"
PKG_METHOD[rustup]="custom"
PKG_CATEGORY[rustup]="dev"
PACKAGES[poetry]="poetry"
PKG_DESC[poetry]="Modern Python dependency manager [PIP]"
PKG_METHOD[poetry]="custom"
PKG_CATEGORY[poetry]="dev"
PACKAGES[pipx]="pipx"
PKG_DESC[pipx]="Install Python CLIs in isolated environments [APT]"
PKG_METHOD[pipx]="apt"
PKG_CATEGORY[pipx]="dev"
PACKAGES[deno]="deno"
PKG_DESC[deno]="Secure JS/TS runtime (Node's calmer cousin) [DEB]"
PKG_METHOD[deno]="custom"
PKG_CATEGORY[deno]="dev"
PACKAGES[bun]="bun"
PKG_DESC[bun]="Insanely fast JS runtime + package manager + bundler [SH]"
PKG_METHOD[bun]="custom"
PKG_CATEGORY[bun]="dev"
PACKAGES[mkdocs]="mkdocs"
PKG_DESC[mkdocs]="Static site generator for docs (dev favorite) [PIP]"
PKG_METHOD[mkdocs]="custom"
PKG_CATEGORY[mkdocs]="dev"
PACKAGES[insomnia]="insomnia"
PKG_DESC[insomnia]="Gorgeous API testing tool [DEB]"
PKG_METHOD[insomnia]="custom"
PKG_CATEGORY[insomnia]="dev"
PACKAGES[zed]="zed"
PKG_DESC[zed]="Ultra-fast modern code editor (from Atom devs) [DEB]"
PKG_METHOD[zed]="custom"
PKG_CATEGORY[zed]="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"
PACKAGES[podman]="podman"
PKG_DESC[podman]="Daemonless container engine [APT]"
PKG_METHOD[podman]="apt"
PKG_CATEGORY[podman]="containers"
PACKAGES[minikube]="minikube"
PKG_DESC[minikube]="Local Kubernetes cluster [DEB]"
PKG_METHOD[minikube]="custom"
PKG_CATEGORY[minikube]="containers"
PACKAGES[kind]="kind"
PKG_DESC[kind]="Kubernetes in Docker [BIN]"
PKG_METHOD[kind]="custom"
PKG_CATEGORY[kind]="containers"
PACKAGES[ctop]="ctop"
PKG_DESC[ctop]="Top-like interface for containers [BIN]"
PKG_METHOD[ctop]="custom"
PKG_CATEGORY[ctop]="containers"
PACKAGES[lazydocker]="lazydocker"
PKG_DESC[lazydocker]="Terminal UI for Docker and Docker Compose [BIN]"
PKG_METHOD[lazydocker]="custom"
PKG_CATEGORY[lazydocker]="containers"
# 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"
PACKAGES[caddy]="caddy"
PKG_DESC[caddy]="Fast web server with automatic HTTPS [DEB]"
PKG_METHOD[caddy]="custom"
PKG_CATEGORY[caddy]="web"
PACKAGES[pm2]="pm2"
PKG_DESC[pm2]="Production process manager for Node.js [NPM]"
PKG_METHOD[pm2]="npm"
PKG_CATEGORY[pm2]="web"
PACKAGES[ngrok]="ngrok"
PKG_DESC[ngrok]="Secure tunnels to localhost [BIN]"
PKG_METHOD[ngrok]="custom"
PKG_CATEGORY[ngrok]="web"
PACKAGES[mailhog]="mailhog"
PKG_DESC[mailhog]="Email testing tool [BIN]"
PKG_METHOD[mailhog]="custom"
PKG_CATEGORY[mailhog]="web"
PACKAGES[adminmongo]="adminmongo"
PKG_DESC[adminmongo]="MongoDB admin interface [NPM]"
PKG_METHOD[adminmongo]="npm"
PKG_CATEGORY[adminmongo]="web"
PACKAGES[sqlitestudio]="sqlitestudio"
PKG_DESC[sqlitestudio]="SQLite database browser [APT]"
PKG_METHOD[sqlitestudio]="apt"
PKG_CATEGORY[sqlitestudio]="web"
# CONTAINERIZED DEVELOPMENT ENVIRONMENTS
PACKAGES[localwp]="local"
PKG_DESC[localwp]="LocalWP - Local WordPress development environment [DEB]"
PKG_METHOD[localwp]="custom"
PKG_CATEGORY[localwp]="web"
PACKAGES[devkinsta]="devkinsta"
PKG_DESC[devkinsta]="DevKinsta - Kinsta's local WordPress development tool [DEB]"
PKG_METHOD[devkinsta]="custom"
PKG_CATEGORY[devkinsta]="web"
PACKAGES[lando]="lando"
PKG_DESC[lando]="Lando - Containerized local development [DEB]"
PKG_METHOD[lando]="custom"
PKG_CATEGORY[lando]="web"
PACKAGES[ddev]="ddev"
PKG_DESC[ddev]="DDEV - Docker-based local development [BIN]"
PKG_METHOD[ddev]="custom"
PKG_CATEGORY[ddev]="web"
PACKAGES[xampp]="xampp"
PKG_DESC[xampp]="XAMPP - Cross-platform web server solution stack [BIN]"
PKG_METHOD[xampp]="custom"
PKG_CATEGORY[xampp]="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"
PACKAGES[oh-my-zsh]="oh-my-zsh"
PKG_DESC[oh-my-zsh]="Framework for managing Zsh configuration [SCRIPT]"
PKG_METHOD[oh-my-zsh]="custom"
PKG_CATEGORY[oh-my-zsh]="shell"
PACKAGES[starship]="starship"
PKG_DESC[starship]="Cross-shell prompt with customization [BINARY]"
PKG_METHOD[starship]="custom"
PKG_CATEGORY[starship]="shell"
PACKAGES[zoxide]="zoxide"
PKG_DESC[zoxide]="Smarter cd command that learns your habits [BINARY]"
PKG_METHOD[zoxide]="custom"
PKG_CATEGORY[zoxide]="shell"
PACKAGES[zsh-autosuggestions]="zsh-autosuggestions"
PKG_DESC[zsh-autosuggestions]="Fish-like autosuggestions for Zsh [APT]"
PKG_METHOD[zsh-autosuggestions]="apt"
PKG_CATEGORY[zsh-autosuggestions]="shell"
# HACKER PLAYGROUND - Fun Terminal Tools
PACKAGES[cmatrix]="cmatrix"
PKG_DESC[cmatrix]="Matrix digital rain [APT]"
PKG_METHOD[cmatrix]="apt"
PKG_CATEGORY[cmatrix]="fun"
PACKAGES[hollywood]="hollywood"
PKG_DESC[hollywood]="Turn your terminal into a fake hacking montage [APT]"
PKG_METHOD[hollywood]="apt"
PKG_CATEGORY[hollywood]="fun"
PACKAGES[sl]="sl"
PKG_DESC[sl]="Steam locomotive for when you mistype ls [APT]"
PKG_METHOD[sl]="apt"
PKG_CATEGORY[sl]="fun"
PACKAGES[lolcat]="lolcat"
PKG_DESC[lolcat]="Rainbow text output [APT]"
PKG_METHOD[lolcat]="apt"
PKG_CATEGORY[lolcat]="fun"
PACKAGES[toilet]="toilet"
PKG_DESC[toilet]="Big ASCII banners [APT]"
PKG_METHOD[toilet]="apt"
PKG_CATEGORY[toilet]="fun"
PACKAGES[figlet]="figlet"
PKG_DESC[figlet]="Classic ASCII text art generator [APT]"
PKG_METHOD[figlet]="apt"
PKG_CATEGORY[figlet]="fun"
PACKAGES[boxes]="boxes"
PKG_DESC[boxes]="ASCII box drawings around text [APT]"
PKG_METHOD[boxes]="apt"
PKG_CATEGORY[boxes]="fun"
PACKAGES[asciiquarium]="asciiquarium"
PKG_DESC[asciiquarium]="ASCII aquarium in your terminal [APT]"
PKG_METHOD[asciiquarium]="apt"
PKG_CATEGORY[asciiquarium]="fun"
PACKAGES[cowsay]="cowsay"
PKG_DESC[cowsay]="Talking ASCII cows [APT]"
PKG_METHOD[cowsay]="apt"
PKG_CATEGORY[cowsay]="fun"
PACKAGES[fortune]="fortune-mod"
PKG_DESC[fortune]="Random fortune cookies [APT]"
PKG_METHOD[fortune]="apt"
PKG_CATEGORY[fortune]="fun"
# 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]="system-monitoring"
PACKAGES[btop]="btop"
PKG_DESC[btop]="Resource monitor with better graphs [APT]"
PKG_METHOD[btop]="apt"
PKG_CATEGORY[btop]="system-monitoring"
PACKAGES[glances]="glances"
PKG_DESC[glances]="Cross-platform system monitor [APT]"
PKG_METHOD[glances]="apt"
PKG_CATEGORY[glances]="system-monitoring"
PACKAGES[nethogs]="nethogs"
PKG_DESC[nethogs]="Network bandwidth monitor per process [APT]"
PKG_METHOD[nethogs]="apt"
PKG_CATEGORY[nethogs]="network-monitoring"
PACKAGES[iotop]="iotop"
PKG_DESC[iotop]="I/O monitor [APT]"
PKG_METHOD[iotop]="apt"
PKG_CATEGORY[iotop]="system-monitoring"
# Additional Process Monitors
PACKAGES[bpytop]="bpytop"
PKG_DESC[bpytop]="Python-based resource monitor (btop predecessor) [APT]"
PKG_METHOD[bpytop]="apt"
PKG_CATEGORY[bpytop]="system-monitoring"
PACKAGES[bashtop]="bashtop"
PKG_DESC[bashtop]="Bash-based resource monitor (original) [APT]"
PKG_METHOD[bashtop]="apt"
PKG_CATEGORY[bashtop]="system-monitoring"
PACKAGES[bottom]="bottom"
PKG_DESC[bottom]="Cross-platform graphical process monitor (btm) [APT]"
PKG_METHOD[bottom]="apt"
PKG_CATEGORY[bottom]="system-monitoring"
PACKAGES[gotop]="gotop"
PKG_DESC[gotop]="Terminal-based graphical activity monitor [SNAP]"
PKG_METHOD[gotop]="snap"
PKG_CATEGORY[gotop]="system-monitoring"
PACKAGES[vtop]="vtop"
PKG_DESC[vtop]="Visually appealing terminal monitor [NPM]"
PKG_METHOD[vtop]="npm"
PKG_CATEGORY[vtop]="system-monitoring"
PACKAGES[zenith]="zenith"
PKG_DESC[zenith]="Terminal monitor with zoomable charts [CARGO]"
PKG_METHOD[zenith]="cargo"
PKG_CATEGORY[zenith]="system-monitoring"
PACKAGES[nmon]="nmon"
PKG_DESC[nmon]="Nigel's Monitor - modular system statistics [APT]"
PKG_METHOD[nmon]="apt"
PKG_CATEGORY[nmon]="system-monitoring"
PACKAGES[atop]="atop"
PKG_DESC[atop]="Advanced system and process monitor [APT]"
PKG_METHOD[atop]="apt"
PKG_CATEGORY[atop]="system-monitoring"
# I/O, Memory, and Disk Tools
PACKAGES[iostat]="sysstat"
PKG_DESC[iostat]="CPU utilization and disk I/O statistics [APT]"
PKG_METHOD[iostat]="apt"
PKG_CATEGORY[iostat]="system-monitoring"
PACKAGES[vmstat]="procps"
PKG_DESC[vmstat]="Virtual memory, processes, I/O, and CPU activity [APT]"
PKG_METHOD[vmstat]="apt"
PKG_CATEGORY[vmstat]="system-monitoring"
PACKAGES[free]="procps"
PKG_DESC[free]="Display free and used memory [APT]"
PKG_METHOD[free]="apt"
PKG_CATEGORY[free]="system-monitoring"
# Network Monitoring Tools
PACKAGES[iftop]="iftop"
PKG_DESC[iftop]="Real-time bandwidth usage per network interface [APT]"
PKG_METHOD[iftop]="apt"
PKG_CATEGORY[iftop]="network-monitoring"
PACKAGES[nload]="nload"
PKG_DESC[nload]="Network traffic visualizer with graphical bars [APT]"
PKG_METHOD[nload]="apt"
PKG_CATEGORY[nload]="network-monitoring"
PACKAGES[bmon]="bmon"
PKG_DESC[bmon]="Interactive bandwidth monitor [APT]"
PKG_METHOD[bmon]="apt"
PKG_CATEGORY[bmon]="network-monitoring"
PACKAGES[iptraf-ng]="iptraf-ng"
PKG_DESC[iptraf-ng]="Console-based network monitoring utility [APT]"
PKG_METHOD[iptraf-ng]="apt"
PKG_CATEGORY[iptraf-ng]="network-monitoring"
PACKAGES[ss]="iproute2"
PKG_DESC[ss]="Socket investigation utility (netstat replacement) [APT]"
PKG_METHOD[ss]="apt"
PKG_CATEGORY[ss]="network-monitoring"
# Process and System Information Tools
PACKAGES[lsof]="lsof"
PKG_DESC[lsof]="List open files and processes [APT]"
PKG_METHOD[lsof]="apt"
PKG_CATEGORY[lsof]="system-monitoring"
PACKAGES[sar]="sysstat"
PKG_DESC[sar]="System Activity Reporter - historical monitoring [APT]"
PKG_METHOD[sar]="apt"
PKG_CATEGORY[sar]="system-monitoring"
PACKAGES[mpstat]="sysstat"
PKG_DESC[mpstat]="Individual or combined CPU processor statistics [APT]"
PKG_METHOD[mpstat]="apt"
PKG_CATEGORY[mpstat]="system-monitoring"
PACKAGES[pidstat]="sysstat"
PKG_DESC[pidstat]="Per-process CPU, memory, and I/O statistics [APT]"
PKG_METHOD[pidstat]="apt"
PKG_CATEGORY[pidstat]="system-monitoring"
# GPU Monitoring Tools
PACKAGES[nvtop]="nvtop"
PKG_DESC[nvtop]="htop-like utility for monitoring NVIDIA GPUs [APT]"
PKG_METHOD[nvtop]="apt"
PKG_CATEGORY[nvtop]="system-monitoring"
PACKAGES[radeontop]="radeontop"
PKG_DESC[radeontop]="TUI utility for monitoring AMD GPUs [APT]"
PKG_METHOD[radeontop]="apt"
# System Information Tools
PACKAGES[ps]="procps"
PKG_DESC[ps]="Standard process status command - reports a snapshot of current processes (non-interactive)"
PKG_METHOD[ps]="apt"
PKG_CATEGORY[ps]="system-monitoring"
PKG_CATEGORY[radeontop]="system-monitoring"
# Additional GPU Monitoring Tool
PACKAGES[qmasa]="qmasa"
PKG_DESC[qmasa]="Terminal-based tool for displaying general GPU usage stats on Linux [Cargo]"
PKG_METHOD[qmasa]="cargo"
PKG_CATEGORY[qmasa]="system-monitoring"
# Additional Process Monitoring Tool
PACKAGES[gtop]="gtop"
PKG_DESC[gtop]="System monitoring dashboard for the terminal, written in Node.js [NPM]"
PKG_METHOD[gtop]="npm"
PKG_CATEGORY[gtop]="system-monitoring"
# Additional System Utilities
PACKAGES[pv]="pv"
PKG_DESC[pv]="Pipe Viewer - monitor progress of data through a pipeline with progress bar [APT]"
PKG_METHOD[pv]="apt"
PKG_CATEGORY[pv]="utilities"
PACKAGES[duf]="duf"
PKG_DESC[duf]="Disk Usage/Free Utility - better 'df' alternative with colors [APT]"
PKG_METHOD[duf]="apt"
PKG_CATEGORY[duf]="utilities"
PACKAGES[dust]="dust"
PKG_DESC[dust]="More intuitive version of du written in Rust [Cargo]"
PKG_METHOD[dust]="cargo"
PKG_CATEGORY[dust]="utilities"
PACKAGES[fd-find]="fd-find"
PKG_DESC[fd-find]="Simple, fast and user-friendly alternative to 'find' [APT]"
PKG_METHOD[fd-find]="apt"
PKG_CATEGORY[fd-find]="utilities"
PACKAGES[exa]="exa"
PKG_DESC[exa]="Modern replacement for 'ls' with colors and Git status [APT]"
PKG_METHOD[exa]="apt"
PKG_CATEGORY[exa]="utilities"
PACKAGES[bandwhich]="bandwhich"
PKG_DESC[bandwhich]="Terminal bandwidth utilization tool by process [Cargo]"
PKG_METHOD[bandwhich]="cargo"
PKG_CATEGORY[bandwhich]="network-monitoring"
PACKAGES[procs]="procs"
PKG_DESC[procs]="Modern replacement for ps written in Rust [Cargo]"
PKG_METHOD[procs]="cargo"
PKG_CATEGORY[procs]="system-monitoring"
PACKAGES[tokei]="tokei"
PKG_DESC[tokei]="Count lines of code quickly [Cargo]"
PKG_METHOD[tokei]="cargo"
PKG_CATEGORY[tokei]="performance-tools"
PACKAGES[hyperfine]="hyperfine"
PKG_DESC[hyperfine]="Command-line benchmarking tool [APT]"
PKG_METHOD[hyperfine]="apt"
PKG_CATEGORY[hyperfine]="performance-tools"
PACKAGES[batcat]="batcat"
PKG_DESC[batcat]="Enhanced cat command with paging, syntax highlight & Git integration [APT]"
PKG_METHOD[batcat]="apt"
PKG_CATEGORY[batcat]="utilities"
PACKAGES[micro]="micro"
PKG_DESC[micro]="Terminal-based text editor that feels like Sublime [APT]"
PKG_METHOD[micro]="apt"
PKG_CATEGORY[micro]="utilities"
PACKAGES[atool]="atool"
PKG_DESC[atool]="Manage archive files (tar, zip, etc.) from terminal [APT]"
PKG_METHOD[atool]="apt"
PKG_CATEGORY[atool]="utilities"
PACKAGES[plocate]="plocate"
PKG_DESC[plocate]="Superfast locate replacement using modern indexing [APT]"
PKG_METHOD[plocate]="apt"
PKG_CATEGORY[plocate]="utilities"
PACKAGES[silversearcher-ag]="silversearcher-ag"
PKG_DESC[silversearcher-ag]="The silver searcher, crazy-fast grep alternative [APT]"
PKG_METHOD[silversearcher-ag]="apt"
PKG_CATEGORY[silversearcher-ag]="utilities"
PACKAGES[yq]="yq"
PKG_DESC[yq]="Command-line YAML processor (jq wrapper for YAML files) [Snap]"
PKG_METHOD[yq]="snap"
PKG_CATEGORY[yq]="utilities"
PACKAGES[delta]="git-delta"
PKG_DESC[delta]="Syntax-highlighting pager for git and diff output [APT]"
PKG_METHOD[delta]="apt"
PKG_CATEGORY[delta]="utilities"
PACKAGES[mirrorselect]="mirrorselect"
PKG_DESC[mirrorselect]="Tool to select the fastest Ubuntu mirror for optimal download speeds [SNAP]"
PKG_METHOD[mirrorselect]="snap"
PKG_CATEGORY[mirrorselect]="utilities"
PACKAGES[apt-mirror]="apt-mirror"
PKG_DESC[apt-mirror]="Tool to create local Ubuntu repository mirrors for offline installations [APT]"
PKG_METHOD[apt-mirror]="apt"
PKG_CATEGORY[apt-mirror]="utilities"
# DATABASE MANAGEMENT TOOLS
PACKAGES[phpmyadmin]="phpmyadmin"
PKG_DESC[phpmyadmin]="Web-based MySQL/MariaDB administration tool [CUSTOM]"
PKG_METHOD[phpmyadmin]="custom"
PKG_CATEGORY[phpmyadmin]="database"
PACKAGES[adminer]="adminer"
PKG_DESC[adminer]="Full-featured database management tool in a single PHP file [CUSTOM]"
PKG_METHOD[adminer]="custom"
PKG_CATEGORY[adminer]="database"
PACKAGES[dbeaver-ce]="dbeaver-ce"
PKG_DESC[dbeaver-ce]="Universal database tool and SQL client (Community Edition) [Snap]"
PKG_METHOD[dbeaver-ce]="snap"
PKG_CATEGORY[dbeaver-ce]="database"
PACKAGES[dbeaver-ce-flatpak]="io.dbeaver.DBeaverCommunity"
PKG_DESC[dbeaver-ce-flatpak]="Universal database tool and SQL client (Community Edition) [Flatpak]"
PKG_METHOD[dbeaver-ce-flatpak]="flatpak"
PKG_CATEGORY[dbeaver-ce-flatpak]="database"
PACKAGES[mysql-workbench-community]="mysql-workbench-community"
PKG_DESC[mysql-workbench-community]="Visual database design tool for MySQL [APT]"
PKG_METHOD[mysql-workbench-community]="apt"
PKG_CATEGORY[mysql-workbench-community]="database"
PACKAGES[pgadmin4]="pgadmin4"
PKG_DESC[pgadmin4]="Web-based PostgreSQL administration and development platform [APT]"
PKG_METHOD[pgadmin4]="apt"
PKG_CATEGORY[pgadmin4]="database"
PACKAGES[sqlitebrowser]="sqlitebrowser"
PKG_DESC[sqlitebrowser]="High quality, visual, open source tool to create, design, and edit SQLite databases [APT]"
PKG_METHOD[sqlitebrowser]="apt"
PKG_CATEGORY[sqlitebrowser]="database"
PACKAGES[mycli]="mycli"
PKG_DESC[mycli]="Command line interface for MySQL with auto-completion and syntax highlighting [APT]"
PKG_METHOD[mycli]="apt"
PKG_CATEGORY[mycli]="database"
PACKAGES[pgcli]="pgcli"
PKG_DESC[pgcli]="Command line interface for PostgreSQL with auto-completion and syntax highlighting [APT]"
PKG_METHOD[pgcli]="apt"
PKG_CATEGORY[pgcli]="database"
PACKAGES[litecli]="litecli"
PKG_DESC[litecli]="Command-line client for SQLite databases with auto-completion and syntax highlighting [APT]"
PKG_METHOD[litecli]="apt"
PKG_CATEGORY[litecli]="database"
PACKAGES[redis-tools]="redis-tools"
PKG_DESC[redis-tools]="Command-line tools for Redis key-value store [APT]"
PKG_METHOD[redis-tools]="apt"
PKG_CATEGORY[redis-tools]="database"
PACKAGES[mongodb-compass]="mongodb-compass"
PKG_DESC[mongodb-compass]="GUI for MongoDB - explore and manipulate your data [Snap]"
PKG_METHOD[mongodb-compass]="snap"
PKG_CATEGORY[mongodb-compass]="database"
PACKAGES[postbird]="postbird"
PKG_DESC[postbird]="Cross-platform PostgreSQL GUI client [Snap]"
PKG_METHOD[postbird]="snap"
PKG_CATEGORY[postbird]="database"
PACKAGES[beekeeper-studio]="beekeeper-studio"
PKG_DESC[beekeeper-studio]="Modern and easy to use SQL client for MySQL, Postgres, SQLite, SQL Server, and more [Snap]"
PKG_METHOD[beekeeper-studio]="snap"
PKG_CATEGORY[beekeeper-studio]="database"
PACKAGES[tableplus]="tableplus"
PKG_DESC[tableplus]="Modern, native tool with elegant UI for relational databases [Snap]"
PKG_METHOD[tableplus]="snap"
PKG_CATEGORY[tableplus]="database"
# 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"
# FILE SHARING & NETWORK
PACKAGES[samba]="samba"
PKG_DESC[samba]="SMB/CIFS file sharing server [APT]"
PKG_METHOD[samba]="apt"
PKG_CATEGORY[samba]="system"
PACKAGES[nfs-kernel-server]="nfs-kernel-server"
PKG_DESC[nfs-kernel-server]="Network File System server [APT]"
PKG_METHOD[nfs-kernel-server]="apt"
PKG_CATEGORY[nfs-kernel-server]="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]="audio"
PACKAGES[postman]="postman"
PKG_DESC[postman]="API development platform [SNP]"
PKG_METHOD[postman]="snap"
PKG_CATEGORY[postman]="dev"
PACKAGES[bruno]="bruno"
PKG_DESC[bruno]="Open-source Postman alternative [CUSTOM]"
PKG_METHOD[bruno]="custom"
PKG_CATEGORY[bruno]="dev"
PACKAGES[bruno-snap]="bruno"
PKG_DESC[bruno-snap]="Open-source Postman alternative [SNAP]"
PKG_METHOD[bruno-snap]="snap"
PKG_CATEGORY[bruno-snap]="dev"
PACKAGES[bruno-flatpak]="com.usebruno.Bruno"
PKG_DESC[bruno-flatpak]="Open-source Postman alternative [FLATPAK]"
PKG_METHOD[bruno-flatpak]="flatpak"
PKG_CATEGORY[bruno-flatpak]="dev"
PKG_DEPS[bruno-flatpak]="flatpak"
PACKAGES[yaak]="yaak"
PKG_DESC[yaak]="Modern API client [DEB]"
PKG_METHOD[yaak]="custom"
PKG_CATEGORY[yaak]="dev"
# 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]="video"
PACKAGES[vlc]="vlc"
PKG_DESC[vlc]="VLC media player [APT]"
PKG_METHOD[vlc]="apt"
PKG_CATEGORY[vlc]="video"
# CLOUD & SYNC
PACKAGES[rclone]="rclone"
PKG_DESC[rclone]="Cloud storage sync tool [APT]"
PKG_METHOD[rclone]="apt"
PKG_CATEGORY[rclone]="cloud"
PACKAGES[dropbox]="dropbox"
PKG_DESC[dropbox]="File synchronization [DEB]"
PKG_METHOD[dropbox]="custom"
PKG_CATEGORY[dropbox]="cloud"
PACKAGES[nextcloud-desktop]="nextcloud-desktop"
PKG_DESC[nextcloud-desktop]="Self-hosted cloud client [APT]"
PKG_METHOD[nextcloud-desktop]="apt"
PKG_CATEGORY[nextcloud-desktop]="cloud"
PACKAGES[syncthing]="syncthing"
PKG_DESC[syncthing]="Decentralized sync [APT]"
PKG_METHOD[syncthing]="apt"
PKG_CATEGORY[syncthing]="cloud"
PACKAGES[google-drive-ocamlfuse]="google-drive-ocamlfuse"
PKG_DESC[google-drive-ocamlfuse]="Google Drive filesystem [APT]"
PKG_METHOD[google-drive-ocamlfuse]="apt"
PKG_CATEGORY[google-drive-ocamlfuse]="cloud"
PACKAGES[nextcloud-server]="nextcloud-server"
PKG_DESC[nextcloud-server]="Self-hosted cloud storage server [SNAP]"
PKG_METHOD[nextcloud-server]="snap"
PKG_CATEGORY[nextcloud-server]="cloud"
PACKAGES[seafile]="seafile"
PKG_DESC[seafile]="Lightweight cloud storage with sync [CUSTOM]"
PKG_METHOD[seafile]="custom"
PKG_CATEGORY[seafile]="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"
PACKAGES[alacritty]="alacritty"
PKG_DESC[alacritty]="GPU-accelerated terminal emulator [APT]"
PKG_METHOD[alacritty]="apt"
PKG_CATEGORY[alacritty]="terminals"
PACKAGES[terminator]="terminator"
PKG_DESC[terminator]="Multiple terminals in one window [APT]"
PKG_METHOD[terminator]="apt"
PKG_CATEGORY[terminator]="terminals"
PACKAGES[tilix]="tilix"
PKG_DESC[tilix]="Tiling terminal emulator [APT]"
PKG_METHOD[tilix]="apt"
PKG_CATEGORY[tilix]="terminals"
PACKAGES[ghostty]="ghostty"
PKG_DESC[ghostty]="Fast, feature-rich terminal emulator [SNP]"
PKG_METHOD[ghostty]="snap"
PKG_CATEGORY[ghostty]="terminals"
# GAMING
PACKAGES[steam]="steam"
PKG_DESC[steam]="Steam gaming platform [APT]"
PKG_METHOD[steam]="apt"
PKG_CATEGORY[steam]="gaming-platforms"
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-platforms"
PKG_DEPS[heroic-launcher]="snapd"
PACKAGES[lutris]="lutris"
PKG_DESC[lutris]="Gaming on Linux made easy [APT]"
PKG_METHOD[lutris]="apt"
PKG_CATEGORY[lutris]="gaming-platforms"
PACKAGES[gamemode]="gamemode"
PKG_DESC[gamemode]="Optimize gaming performance [APT]"
PKG_METHOD[gamemode]="apt"
PKG_CATEGORY[gamemode]="gaming-platforms"
PACKAGES[gimp]="gimp"
PKG_DESC[gimp]="GIMP image editor [APT]"
PKG_METHOD[gimp]="apt"
PKG_CATEGORY[gimp]="graphics"
# 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"
PACKAGES[calibre]="calibre"
PKG_DESC[calibre]="E-book management [APT]"
PKG_METHOD[calibre]="apt"
PKG_CATEGORY[calibre]="office"
PACKAGES[obsidian]="obsidian"
PKG_DESC[obsidian]="Knowledge management [SNP]"
PKG_METHOD[obsidian]="snap"
PKG_CATEGORY[obsidian]="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]="audio"
PACKAGES[blender]="blender"
PKG_DESC[blender]="Blender 3D creation suite [SNP]"
PKG_METHOD[blender]="snap"
PKG_CATEGORY[blender]="graphics"
PACKAGES[inkscape]="inkscape"
PKG_DESC[inkscape]="Inkscape vector graphics editor [APT]"
PKG_METHOD[inkscape]="apt"
PKG_CATEGORY[inkscape]="graphics"
# 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[lm-studio]="lm-studio"
PKG_DESC[lm-studio]="GUI for local LLMs (Ollama compatible) [APPIMAGE]"
PKG_METHOD[lm-studio]="custom"
PKG_CATEGORY[lm-studio]="ai"
PACKAGES[text-generation-webui]="text-generation-webui"
PKG_DESC[text-generation-webui]="Self-hosted interface for local LLMs [GIT]"
PKG_METHOD[text-generation-webui]="custom"
PKG_CATEGORY[text-generation-webui]="ai"
PACKAGES[whisper-cpp]="whisper-cpp"
PKG_DESC[whisper-cpp]="Offline speech-to-text engine [BUILD]"
PKG_METHOD[whisper-cpp]="custom"
PKG_CATEGORY[whisper-cpp]="ai"
PACKAGES[comfyui]="comfyui"
PKG_DESC[comfyui]="Visual Stable Diffusion node-based interface [GIT]"
PKG_METHOD[comfyui]="custom"
PKG_CATEGORY[comfyui]="ai"
PACKAGES[invokeai]="invokeai"
PKG_DESC[invokeai]="Stable Diffusion image generator [PYTHON]"
PKG_METHOD[invokeai]="custom"
PKG_CATEGORY[invokeai]="ai"
PACKAGES[lmdeploy]="lmdeploy"
PKG_DESC[lmdeploy]="Lightweight framework to serve LLMs efficiently [PIP]"
PKG_METHOD[lmdeploy]="pip"
PKG_CATEGORY[lmdeploy]="ai"
PACKAGES[koboldcpp]="koboldcpp"
PKG_DESC[koboldcpp]="LLM interface optimized for story and RP generation [BIN]"
PKG_METHOD[koboldcpp]="custom"
PKG_CATEGORY[koboldcpp]="ai"
PACKAGES[automatic1111]="automatic1111"
PKG_DESC[automatic1111]="Stable Diffusion WebUI - feature-packed interface [GIT]"
PKG_METHOD[automatic1111]="custom"
PKG_CATEGORY[automatic1111]="ai"
PACKAGES[fooocus]="fooocus"
PKG_DESC[fooocus]="Simplified Stable Diffusion frontend - zero-config [GIT]"
PKG_METHOD[fooocus]="custom"
PKG_CATEGORY[fooocus]="ai"
PACKAGES[sd-next]="sd-next"
PKG_DESC[sd-next]="Modernized fork of A1111 with optimizations [GIT]"
PKG_METHOD[sd-next]="custom"
PKG_CATEGORY[sd-next]="ai"
PACKAGES[kohya-ss-gui]="kohya-ss-gui"
PKG_DESC[kohya-ss-gui]="Fine-tune and train models with GUI [GIT]"
PKG_METHOD[kohya-ss-gui]="custom"
PKG_CATEGORY[kohya-ss-gui]="ai"
PACKAGES[faster-whisper]="faster-whisper"
PKG_DESC[faster-whisper]="Optimized Python Whisper with CTranslate2 [PIP]"
PKG_METHOD[faster-whisper]="pip"
PKG_CATEGORY[faster-whisper]="ai"
PACKAGES[whisperx]="whisperx"
PKG_DESC[whisperx]="Whisper with speaker diarization support [PIP]"
PKG_METHOD[whisperx]="pip"
PKG_CATEGORY[whisperx]="ai"
PACKAGES[coqui-stt]="coqui-stt"
PKG_DESC[coqui-stt]="Open-source speech-to-text engine [PIP]"
PKG_METHOD[coqui-stt]="pip"
PKG_CATEGORY[coqui-stt]="ai"
PACKAGES[piper-tts]="piper-tts"
PKG_DESC[piper-tts]="Modern local text-to-speech by Rhasspy [PIP]"
PKG_METHOD[piper-tts]="pip"
PKG_CATEGORY[piper-tts]="ai"
PACKAGES[mimic3]="mimic3"
PKG_DESC[mimic3]="Neural voice synthesis by Mycroft AI [PIP]"
PKG_METHOD[mimic3]="pip"
PKG_CATEGORY[mimic3]="ai"
PACKAGES[coqui-tts]="coqui-tts"
PKG_DESC[coqui-tts]="Deep-learning TTS with voice cloning [PIP]"
PKG_METHOD[coqui-tts]="pip"
PKG_CATEGORY[coqui-tts]="ai"
PACKAGES[ffmpeg]="ffmpeg"
PKG_DESC[ffmpeg]="Complete multimedia processing toolkit [APT]"
PKG_METHOD[ffmpeg]="apt"
PKG_CATEGORY[ffmpeg]="video"
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]="video"
PACKAGES[freetube]="freetube"
PKG_DESC[freetube]="Privacy-focused YouTube client [FLATPAK]"
PKG_METHOD[freetube]="flatpak"
PKG_CATEGORY[freetube]="video"
PACKAGES[invidious]="invidious"
PKG_DESC[invidious]="Alternative YouTube frontend [CUSTOM]"
PKG_METHOD[invidious]="custom"
PKG_CATEGORY[invidious]="video"
PACKAGES[mpv]="mpv"
PKG_DESC[mpv]="Minimalist media player [APT]"
PKG_METHOD[mpv]="apt"
PKG_CATEGORY[mpv]="video"
PACKAGES[kodi]="kodi"
PKG_DESC[kodi]="Open-source media center [APT]"
PKG_METHOD[kodi]="apt"
PKG_CATEGORY[kodi]="media-servers"
PACKAGES[stremio]="stremio"
PKG_DESC[stremio]="Modern media center with streaming [FLATPAK]"
PKG_METHOD[stremio]="flatpak"
PKG_CATEGORY[stremio]="media-servers"
# MEDIA SERVERS & STREAMING
PACKAGES[plex]="plexmediaserver"
PKG_DESC[plex]="Plex Media Server [CUSTOM]"
PKG_METHOD[plex]="custom"
PKG_CATEGORY[plex]="media-servers"
PACKAGES[jellyfin]="jellyfin"
PKG_DESC[jellyfin]="Free media server [APT]"
PKG_METHOD[jellyfin]="apt"
PKG_CATEGORY[jellyfin]="media-servers"
PACKAGES[ums]="ums"
PKG_DESC[ums]="Universal Media Server [CUSTOM]"
PKG_METHOD[ums]="custom"
PKG_CATEGORY[ums]="media-servers"
# MUSIC PRODUCTION & AUDIO
PACKAGES[ardour]="ardour"
PKG_DESC[ardour]="Professional digital audio workstation [APT]"
PKG_METHOD[ardour]="apt"
PKG_CATEGORY[ardour]="audio"
PACKAGES[lmms]="lmms"
PKG_DESC[lmms]="Free pattern-based music production suite [APT]"
PKG_METHOD[lmms]="apt"
PKG_CATEGORY[lmms]="audio"
PACKAGES[mixxx]="mixxx"
PKG_DESC[mixxx]="Open-source DJ software [APT]"
PKG_METHOD[mixxx]="apt"
PKG_CATEGORY[mixxx]="audio"
# GAMING & EMULATION
PACKAGES[retroarch]="retroarch"
PKG_DESC[retroarch]="Multi-system emulator frontend [APT]"
PKG_METHOD[retroarch]="apt"
PKG_CATEGORY[retroarch]="gaming-emulators"
PACKAGES[retroarch-snap]="retroarch"
PKG_DESC[retroarch-snap]="Multi-system emulator frontend [SNAP]"
PKG_METHOD[retroarch-snap]="snap"
PKG_CATEGORY[retroarch-snap]="gaming-emulators"
PACKAGES[retroarch-flatpak]="org.libretro.RetroArch"
PKG_DESC[retroarch-flatpak]="Multi-system emulator frontend [FLATPAK]"
PKG_METHOD[retroarch-flatpak]="flatpak"
PKG_CATEGORY[retroarch-flatpak]="gaming-emulators"
PACKAGES[mame]="mame"
PKG_DESC[mame]="Multiple Arcade Machine Emulator [APT]"
PKG_METHOD[mame]="apt"
PKG_CATEGORY[mame]="gaming-emulators"
PACKAGES[mame-flatpak]="net.mame.MAME"
PKG_DESC[mame-flatpak]="Multiple Arcade Machine Emulator [FLATPAK]"
PKG_METHOD[mame-flatpak]="flatpak"
PKG_CATEGORY[mame-flatpak]="gaming-emulators"
PACKAGES[dolphin-emu]="dolphin-emu"
PKG_DESC[dolphin-emu]="GameCube and Wii emulator [APT]"
PKG_METHOD[dolphin-emu]="apt"
PKG_CATEGORY[dolphin-emu]="gaming-emulators"
PACKAGES[dolphin-emu-flatpak]="org.DolphinEmu.dolphin-emu"
PKG_DESC[dolphin-emu-flatpak]="GameCube and Wii emulator [FLATPAK]"
PKG_METHOD[dolphin-emu-flatpak]="flatpak"
PKG_CATEGORY[dolphin-emu-flatpak]="gaming-emulators"
PACKAGES[pcsx2]="pcsx2"
PKG_DESC[pcsx2]="PlayStation 2 emulator [APT]"
PKG_METHOD[pcsx2]="apt"
PKG_CATEGORY[pcsx2]="gaming-emulators"
PACKAGES[pcsx2-flatpak]="net.pcsx2.PCSX2"
PKG_DESC[pcsx2-flatpak]="PlayStation 2 emulator [FLATPAK]"
PKG_METHOD[pcsx2-flatpak]="flatpak"
PKG_CATEGORY[pcsx2-flatpak]="gaming-emulators"
PACKAGES[rpcs3-flatpak]="net.rpcs3.RPCS3"
PKG_DESC[rpcs3-flatpak]="PlayStation 3 emulator [FLATPAK]"
PKG_METHOD[rpcs3-flatpak]="flatpak"
PKG_CATEGORY[rpcs3-flatpak]="gaming-emulators"
PACKAGES[yuzu-flatpak]="org.yuzu_emu.yuzu"
PKG_DESC[yuzu-flatpak]="Nintendo Switch emulator [FLATPAK]"
PKG_METHOD[yuzu-flatpak]="flatpak"
PKG_CATEGORY[yuzu-flatpak]="gaming-emulators"
PACKAGES[cemu-flatpak]="info.cemu.Cemu"
PKG_DESC[cemu-flatpak]="Wii U emulator [FLATPAK]"
PKG_METHOD[cemu-flatpak]="flatpak"
PKG_CATEGORY[cemu-flatpak]="gaming-emulators"
PACKAGES[mednafen]="mednafen"
PKG_DESC[mednafen]="Multi-system accurate emulator [APT]"
PKG_METHOD[mednafen]="apt"
PKG_CATEGORY[mednafen]="gaming-emulators"
PACKAGES[duckstation-flatpak]="org.duckstation.DuckStation"
PKG_DESC[duckstation-flatpak]="PlayStation 1 emulator [FLATPAK]"
PKG_METHOD[duckstation-flatpak]="flatpak"
PKG_CATEGORY[duckstation-flatpak]="gaming-emulators"
PACKAGES[bsnes]="bsnes"
PKG_DESC[bsnes]="Super Nintendo emulator [APT]"
PKG_METHOD[bsnes]="apt"
PKG_CATEGORY[bsnes]="gaming-emulators"
PACKAGES[mgba]="mgba-qt"
PKG_DESC[mgba]="Game Boy Advance emulator [APT]"
PKG_METHOD[mgba]="apt"
PKG_CATEGORY[mgba]="gaming-emulators"
PACKAGES[mgba-snap]="mgba"
PKG_DESC[mgba-snap]="Game Boy Advance emulator [SNAP]"
PKG_METHOD[mgba-snap]="snap"
PKG_CATEGORY[mgba-snap]="gaming-emulators"
PACKAGES[mgba-flatpak]="io.mgba.mGBA"
PKG_DESC[mgba-flatpak]="Game Boy Advance emulator [FLATPAK]"
PKG_METHOD[mgba-flatpak]="flatpak"
PKG_CATEGORY[mgba-flatpak]="gaming-emulators"
PACKAGES[desmume]="desmume"
PKG_DESC[desmume]="Nintendo DS emulator [APT]"
PKG_METHOD[desmume]="apt"
PKG_CATEGORY[desmume]="gaming-emulators"
PACKAGES[desmume-flatpak]="org.desmume.DeSmuME"
PKG_DESC[desmume-flatpak]="Nintendo DS emulator [FLATPAK]"
PKG_METHOD[desmume-flatpak]="flatpak"
PKG_CATEGORY[desmume-flatpak]="gaming-emulators"
PACKAGES[citra]="citra"
PKG_DESC[citra]="Nintendo 3DS emulator [APT]"
PKG_METHOD[citra]="apt"
PKG_CATEGORY[citra]="gaming-emulators"
PACKAGES[citra-flatpak]="org.citra_emu.citra"
PKG_DESC[citra-flatpak]="Nintendo 3DS emulator [FLATPAK]"
PKG_METHOD[citra-flatpak]="flatpak"
PKG_CATEGORY[citra-flatpak]="gaming-emulators"
PACKAGES[dosbox]="dosbox"
PKG_DESC[dosbox]="DOS emulator [APT]"
PKG_METHOD[dosbox]="apt"
PKG_CATEGORY[dosbox]="gaming-emulators"
PACKAGES[dosbox-snap]="dosbox"
PKG_DESC[dosbox-snap]="DOS emulator [SNAP]"
PKG_METHOD[dosbox-snap]="snap"
PKG_CATEGORY[dosbox-snap]="gaming-emulators"
PACKAGES[dosbox-flatpak]="com.dosbox.DOSBox"
PKG_DESC[dosbox-flatpak]="DOS emulator [FLATPAK]"
PKG_METHOD[dosbox-flatpak]="flatpak"
PKG_CATEGORY[dosbox-flatpak]="gaming-emulators"
PACKAGES[mupen64plus]="mupen64plus-qt"
PKG_DESC[mupen64plus]="Nintendo 64 emulator [APT]"
PKG_METHOD[mupen64plus]="apt"
PKG_CATEGORY[mupen64plus]="gaming-emulators"
PACKAGES[scummvm]="scummvm"
PKG_DESC[scummvm]="Adventure game engine [APT]"
PKG_METHOD[scummvm]="apt"
PKG_CATEGORY[scummvm]="gaming-emulators"
PACKAGES[scummvm-snap]="scummvm"
PKG_DESC[scummvm-snap]="Adventure game engine [SNAP]"
PKG_METHOD[scummvm-snap]="snap"
PKG_CATEGORY[scummvm-snap]="gaming-emulators"
PACKAGES[scummvm-flatpak]="org.scummvm.ScummVM"
PKG_DESC[scummvm-flatpak]="Adventure game engine [FLATPAK]"
PKG_METHOD[scummvm-flatpak]="flatpak"
PKG_CATEGORY[scummvm-flatpak]="gaming-emulators"
PACKAGES[qemu]="qemu-system"
PKG_DESC[qemu]="System hardware emulator [APT]"
PKG_METHOD[qemu]="apt"
PKG_CATEGORY[qemu]="gaming-emulators"
PACKAGES[wine]="wine"
PKG_DESC[wine]="Windows API compatibility layer [APT]"
PKG_METHOD[wine]="apt"
PKG_CATEGORY[wine]="gaming-platforms"
PACKAGES[wine-snap]="wine-platform-runtime"
PKG_DESC[wine-snap]="Windows API compatibility layer [SNAP]"
PKG_METHOD[wine-snap]="snap"
PKG_CATEGORY[wine-snap]="gaming-platforms"
PACKAGES[wine-flatpak]="org.winehq.Wine"
PKG_DESC[wine-flatpak]="Windows API compatibility layer [FLATPAK]"
PKG_METHOD[wine-flatpak]="flatpak"
PKG_CATEGORY[wine-flatpak]="gaming-platforms"
PACKAGES[stella]="stella"
PKG_DESC[stella]="Atari 2600 emulator [APT]"
PKG_METHOD[stella]="apt"
PKG_CATEGORY[stella]="gaming-emulators"
PACKAGES[dosbox-staging]="dosbox-staging"
PKG_DESC[dosbox-staging]="DOS emulator for retro PC games [APT]"
PKG_METHOD[dosbox-staging]="apt"
PKG_CATEGORY[dosbox-staging]="gaming-emulators"
PACKAGES[n8n]="n8n"
PKG_DESC[n8n]="Workflow automation tool (self-hosted Zapier alternative) [NPM]"
PKG_METHOD[n8n]="custom"
PKG_CATEGORY[n8n]="dev"
# ==============================================================================
# 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]="video"
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]="video"
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]="graphics"
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]="graphics"
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]="video"
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]="audio"
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]="audio"
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]="graphics"
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]="graphics"
PKG_DEPS[inkscape-flatpak]="flatpak"
# Drawing and Creative Tools
PACKAGES[webcamize]="webcamize"
PKG_DESC[webcamize]="Webcam effects and virtual camera tool [APT]"
PKG_METHOD[webcamize]="apt"
PKG_CATEGORY[webcamize]="graphics"
PACKAGES[durdraw]="durdraw"
PKG_DESC[durdraw]="ASCII art drawing and animation tool [PIP]"
PKG_METHOD[durdraw]="pip"
PKG_CATEGORY[durdraw]="graphics"
PACKAGES[pastel]="pastel"
PKG_DESC[pastel]="Command-line tool for color manipulation and palette generation [APT]"
PKG_METHOD[pastel]="apt"
PKG_CATEGORY[pastel]="graphics"
# Disk Management Tools
PACKAGES[dysk]="dysk"
PKG_DESC[dysk]="Modern disk usage analyzer with colorful output [CARGO]"
PKG_METHOD[dysk]="cargo"
PKG_CATEGORY[dysk]="system"
# STORAGE MANAGEMENT
PACKAGES[lvm2]="lvm2"
PKG_DESC[lvm2]="Logical Volume Manager for flexible disk management [APT]"
PKG_METHOD[lvm2]="apt"
PKG_CATEGORY[lvm2]="system"
PACKAGES[snapraid]="snapraid"
PKG_DESC[snapraid]="Parity protection for different sized disks [CUSTOM]"
PKG_METHOD[snapraid]="custom"
PKG_CATEGORY[snapraid]="system"
PACKAGES[greyhole]="greyhole"
PKG_DESC[greyhole]="Samba-based storage pooling with redundancy [CUSTOM]"
PKG_METHOD[greyhole]="custom"
PKG_CATEGORY[greyhole]="system"
PACKAGES[mergerfs]="mergerfs"
PKG_DESC[mergerfs]="Union filesystem for pooling drives [CUSTOM]"
PKG_METHOD[mergerfs]="custom"
PKG_CATEGORY[mergerfs]="system"
# Additional CLI tools from awesome-cli-apps
PACKAGES[nmap]="nmap"
PKG_DESC[nmap]="Network exploration tool and security scanner [APT]"
PKG_METHOD[nmap]="apt"
PKG_CATEGORY[nmap]="network-monitoring"
PACKAGES[httpie]="httpie"
PKG_DESC[httpie]="Modern command-line HTTP client [APT]"
PKG_METHOD[httpie]="apt"
PKG_CATEGORY[httpie]="dev"
PACKAGES[ranger]="ranger"
PKG_DESC[ranger]="Console file manager with VI key bindings [APT]"
PKG_METHOD[ranger]="apt"
PKG_CATEGORY[ranger]="utilities"
PACKAGES[mc]="mc"
PKG_DESC[mc]="Midnight Commander file manager [APT]"
PKG_METHOD[mc]="apt"
PKG_CATEGORY[mc]="utilities"
PACKAGES[ag]="silversearcher-ag"
PKG_DESC[ag]="The Silver Searcher - fast text search [APT]"
PKG_METHOD[ag]="apt"
PKG_CATEGORY[ag]="utilities"
PACKAGES[thefuck]="thefuck"
PKG_DESC[thefuck]="Corrects errors in previous console commands [PIP]"
PKG_METHOD[thefuck]="pip"
PKG_CATEGORY[thefuck]="utilities"
PACKAGES[lazygit]="lazygit"
PKG_DESC[lazygit]="Simple terminal UI for git commands [CUSTOM]"
PKG_METHOD[lazygit]="custom"
PKG_CATEGORY[lazygit]="dev"
PACKAGES[glow]="glow"
PKG_DESC[glow]="Terminal based markdown reader [CUSTOM]"
PKG_METHOD[glow]="custom"
PKG_CATEGORY[glow]="utilities"
PACKAGES[cheat]="cheat"
PKG_DESC[cheat]="Interactive cheatsheets on the command-line [CUSTOM]"
PKG_METHOD[cheat]="custom"
PKG_CATEGORY[cheat]="utilities"
PACKAGES[broot]="broot"
PKG_DESC[broot]="A new way to see and navigate directory trees [CUSTOM]"
PKG_METHOD[broot]="custom"
PKG_CATEGORY[broot]="utilities"
PACKAGES[dog]="dog"
PKG_DESC[dog]="Command-line DNS lookup tool [CUSTOM]"
PKG_METHOD[dog]="custom"
PKG_CATEGORY[dog]="network-monitoring"
# Entertainment
PACKAGES[newsboat]="newsboat"
PKG_DESC[newsboat]="An RSS/Atom feed reader for text terminals [APT]"
PKG_METHOD[newsboat]="apt"
PKG_CATEGORY[newsboat]="fun"
PACKAGES[mal-cli]="mal-cli"
PKG_DESC[mal-cli]="MyAnimeList command line client [CUSTOM]"
PKG_METHOD[mal-cli]="custom"
PKG_CATEGORY[mal-cli]="fun"
# Music
PACKAGES[cmus]="cmus"
PKG_DESC[cmus]="Small, fast and powerful console music player [APT]"
PKG_METHOD[cmus]="apt"
PKG_CATEGORY[cmus]="audio"
PACKAGES[pianobar]="pianobar"
PKG_DESC[pianobar]="Console-based Pandora client [APT]"
PKG_METHOD[pianobar]="apt"
PKG_CATEGORY[pianobar]="audio"
PACKAGES[somafm-cli]="somafm-cli"
PKG_DESC[somafm-cli]="Listen to SomaFM in your terminal [CUSTOM]"
PKG_METHOD[somafm-cli]="custom"
PKG_CATEGORY[somafm-cli]="audio"
PACKAGES[mpd]="mpd"
PKG_DESC[mpd]="Music Player Daemon [APT]"
PKG_METHOD[mpd]="apt"
PKG_CATEGORY[mpd]="audio"
PACKAGES[ncmpcpp]="ncmpcpp"
PKG_DESC[ncmpcpp]="NCurses Music Player Client (Plus Plus) [APT]"
PKG_METHOD[ncmpcpp]="apt"
PKG_CATEGORY[ncmpcpp]="audio"
PACKAGES[moc]="moc"
PKG_DESC[moc]="Console audio player for Linux/UNIX [APT]"
PKG_METHOD[moc]="apt"
PKG_CATEGORY[moc]="audio"
PACKAGES[musikcube]="musikcube"
PKG_DESC[musikcube]="Cross-platform terminal-based music player [CUSTOM]"
PKG_METHOD[musikcube]="custom"
PKG_CATEGORY[musikcube]="audio"
PACKAGES[beets]="beets"
PKG_DESC[beets]="Music library manager and MusicBrainz tagger [APT]"
PKG_METHOD[beets]="apt"
PKG_CATEGORY[beets]="audio"
PACKAGES[spotify-tui]="spotify-tui"
PKG_DESC[spotify-tui]="Spotify for the terminal written in Rust [CUSTOM]"
PKG_METHOD[spotify-tui]="custom"
PKG_CATEGORY[spotify-tui]="audio"
PACKAGES[swaglyrics-for-spotify]="swaglyrics-for-spotify"
PKG_DESC[swaglyrics-for-spotify]="Spotify lyrics in your terminal [CUSTOM]"
PKG_METHOD[swaglyrics-for-spotify]="custom"
PKG_CATEGORY[swaglyrics-for-spotify]="audio"
PACKAGES[dzr]="dzr"
PKG_DESC[dzr]="Command line Deezer player [CUSTOM]"
PKG_METHOD[dzr]="custom"
PKG_CATEGORY[dzr]="audio"
PACKAGES[radio-active]="radio-active"
PKG_DESC[radio-active]="Internet radio player with 40k+ stations [CUSTOM]"
PKG_METHOD[radio-active]="custom"
PKG_CATEGORY[radio-active]="audio"
PACKAGES[mpvc]="mpvc"
PKG_DESC[mpvc]="Music player interfacing mpv [CUSTOM]"
PKG_METHOD[mpvc]="custom"
PKG_CATEGORY[mpvc]="audio"
# Video
PACKAGES[streamlink]="streamlink"
PKG_DESC[streamlink]="Extract streams from various websites [APT]"
PKG_METHOD[streamlink]="apt"
PKG_CATEGORY[streamlink]="video"
PACKAGES[mps-youtube]="mps-youtube"
PKG_DESC[mps-youtube]="Terminal based YouTube player and downloader [CUSTOM]"
PKG_METHOD[mps-youtube]="custom"
PKG_CATEGORY[mps-youtube]="video"
PACKAGES[editly]="editly"
PKG_DESC[editly]="Declarative command line video editing [CUSTOM]"
PKG_METHOD[editly]="custom"
PKG_CATEGORY[editly]="video"
# Movies
PACKAGES[moviemon]="moviemon"
PKG_DESC[moviemon]="Everything about your movies within the command line [CUSTOM]"
PKG_METHOD[moviemon]="custom"
PKG_CATEGORY[moviemon]="fun"
PACKAGES[movie]="movie"
PKG_DESC[movie]="Get movie info or compare movies in terminal [CUSTOM]"
PKG_METHOD[movie]="custom"
PKG_CATEGORY[movie]="fun"
# Games
PACKAGES[pokete]="pokete"
PKG_DESC[pokete]="A terminal based Pokemon like game [CUSTOM]"
PKG_METHOD[pokete]="custom"
PKG_CATEGORY[pokete]="fun"
# Books
PACKAGES[epr]="epr"
PKG_DESC[epr]="CLI Epub reader [CUSTOM]"
PKG_METHOD[epr]="custom"
PKG_CATEGORY[epr]="utilities"
PACKAGES[speedread]="speedread"
PKG_DESC[speedread]="A simple terminal-based speed reading tool [CUSTOM]"
PKG_METHOD[speedread]="custom"
PKG_CATEGORY[speedread]="utilities"
PACKAGES[medium-cli]="medium-cli"
PKG_DESC[medium-cli]="Read medium.com stories within terminal [CUSTOM]"
PKG_METHOD[medium-cli]="custom"
PKG_CATEGORY[medium-cli]="utilities"
PACKAGES[hygg]="hygg"
PKG_DESC[hygg]="Document reader for various formats [CUSTOM]"
PKG_METHOD[hygg]="custom"
PKG_CATEGORY[hygg]="utilities"
# Chat
PACKAGES[weechat]="weechat"
PKG_DESC[weechat]="Fast, light and extensible chat client [APT]"
PKG_METHOD[weechat]="apt"
PKG_CATEGORY[weechat]="communication"
PACKAGES[irssi]="irssi"
PKG_DESC[irssi]="Terminal based IRC client [APT]"
PKG_METHOD[irssi]="apt"
PKG_CATEGORY[irssi]="communication"
PACKAGES[kirc]="kirc"
PKG_DESC[kirc]="A tiny IRC client written in POSIX C99 [CUSTOM]"
PKG_METHOD[kirc]="custom"
PKG_CATEGORY[kirc]="communication"
# Development Tools
PACKAGES[legit]="legit"
PKG_DESC[legit]="Generate Open Source licenses as files or headers [CUSTOM]"
PKG_METHOD[legit]="custom"
PKG_CATEGORY[legit]="dev"
PACKAGES[mklicense]="mklicense"
PKG_DESC[mklicense]="Create a custom LICENSE file painlessly [CUSTOM]"
PKG_METHOD[mklicense]="custom"
PKG_CATEGORY[mklicense]="dev"
PACKAGES[rebound]="rebound"
PKG_DESC[rebound]="Fetch Stack Overflow results on compiler error [CUSTOM]"
PKG_METHOD[rebound]="custom"
PKG_CATEGORY[rebound]="dev"
PACKAGES[foy]="foy"
PKG_DESC[foy]="Lightweight general purpose task runner/build tool [CUSTOM]"
PKG_METHOD[foy]="custom"
PKG_CATEGORY[foy]="dev"
PACKAGES[just]="just"
PKG_DESC[just]="A handy way to save and run project-specific commands [CUSTOM]"
PKG_METHOD[just]="custom"
PKG_CATEGORY[just]="dev"
PACKAGES[bcal]="bcal"
PKG_DESC[bcal]="Byte CALculator for storage conversions and calculations [CUSTOM]"
PKG_METHOD[bcal]="custom"
PKG_CATEGORY[bcal]="dev"
PACKAGES[bitwise]="bitwise"
PKG_DESC[bitwise]="Base conversion and bit manipulation [CUSTOM]"
PKG_METHOD[bitwise]="custom"
PKG_CATEGORY[bitwise]="dev"
PACKAGES[cgasm]="cgasm"
PKG_DESC[cgasm]="x86 assembly documentation [CUSTOM]"
PKG_METHOD[cgasm]="custom"
PKG_CATEGORY[cgasm]="dev"
PACKAGES[grex]="grex"
PKG_DESC[grex]="Generate regular expressions from user-provided test cases [CUSTOM]"
PKG_METHOD[grex]="custom"
PKG_CATEGORY[grex]="dev"
PACKAGES[iola]="iola"
PKG_DESC[iola]="Socket client with REST API [CUSTOM]"
PKG_METHOD[iola]="custom"
PKG_CATEGORY[iola]="dev"
PACKAGES[add-gitignore]="add-gitignore"
PKG_DESC[add-gitignore]="Interactively generate a .gitignore for your project [CUSTOM]"
PKG_METHOD[add-gitignore]="custom"
PKG_CATEGORY[add-gitignore]="dev"
PACKAGES[is-up-cli]="is-up-cli"
PKG_DESC[is-up-cli]="Check if a domain is up [CUSTOM]"
PKG_METHOD[is-up-cli]="custom"
PKG_CATEGORY[is-up-cli]="dev"
PACKAGES[reachable]="reachable"
PKG_DESC[reachable]="Check if a domain is up [CUSTOM]"
PKG_METHOD[reachable]="custom"
PKG_CATEGORY[reachable]="dev"
PACKAGES[diff2html-cli]="diff2html-cli"
PKG_DESC[diff2html-cli]="Create pretty HTML from diffs [CUSTOM]"
PKG_METHOD[diff2html-cli]="custom"
PKG_CATEGORY[diff2html-cli]="dev"
# Text Editors
PACKAGES[vim]="vim"
PKG_DESC[vim]="Vi IMproved - enhanced vi editor [APT]"
PKG_METHOD[vim]="apt"
PKG_CATEGORY[vim]="editors"
PACKAGES[emacs]="emacs"
PKG_DESC[emacs]="GNU Emacs editor [APT]"
PKG_METHOD[emacs]="apt"
PKG_CATEGORY[emacs]="editors"
PACKAGES[kakoune]="kakoune"
PKG_DESC[kakoune]="Modal editor inspired by vim [APT]"
PKG_METHOD[kakoune]="apt"
PKG_CATEGORY[kakoune]="editors"
PACKAGES[o]="o"
PKG_DESC[o]="Configuration-free text editor and IDE [CUSTOM]"
PKG_METHOD[o]="custom"
PKG_CATEGORY[o]="editors"
PACKAGES[helix]="helix"
PKG_DESC[helix]="A post-modern modal text editor [CUSTOM]"
PKG_METHOD[helix]="custom"
PKG_CATEGORY[helix]="editors"
# Frontend Development
PACKAGES[caniuse-cmd]="caniuse-cmd"
PKG_DESC[caniuse-cmd]="Search caniuse.com about browser support [CUSTOM]"
PKG_METHOD[caniuse-cmd]="custom"
PKG_CATEGORY[caniuse-cmd]="dev"
PACKAGES[strip-css-comments-cli]="strip-css-comments-cli"
PKG_DESC[strip-css-comments-cli]="Strip comments from CSS [CUSTOM]"
PKG_METHOD[strip-css-comments-cli]="custom"
PKG_CATEGORY[strip-css-comments-cli]="dev"
PACKAGES[viewport-list-cli]="viewport-list-cli"
PKG_DESC[viewport-list-cli]="Return a list of devices and their viewports [CUSTOM]"
PKG_METHOD[viewport-list-cli]="custom"
PKG_CATEGORY[viewport-list-cli]="dev"
PACKAGES[surge]="surge"
PKG_DESC[surge]="Publish static websites for free [CUSTOM]"
PKG_METHOD[surge]="custom"
PKG_CATEGORY[surge]="dev"
# Public localhost
PACKAGES[localtunnel]="localtunnel"
PKG_DESC[localtunnel]="Expose localhost to the world for easy testing [CUSTOM]"
PKG_METHOD[localtunnel]="custom"
PKG_CATEGORY[localtunnel]="dev"
PACKAGES[tunnelmole]="tunnelmole"
PKG_DESC[tunnelmole]="Connect to localhost from anywhere [CUSTOM]"
PKG_METHOD[tunnelmole]="custom"
PKG_CATEGORY[tunnelmole]="dev"
# Mobile Development
PACKAGES[mobicon-cli]="mobicon-cli"
PKG_DESC[mobicon-cli]="Mobile app icon generator [CUSTOM]"
PKG_METHOD[mobicon-cli]="custom"
PKG_CATEGORY[mobicon-cli]="dev"
PACKAGES[mobisplash-cli]="mobisplash-cli"
PKG_DESC[mobisplash-cli]="Mobile app splash screen generator [CUSTOM]"
PKG_METHOD[mobisplash-cli]="custom"
PKG_CATEGORY[mobisplash-cli]="dev"
PACKAGES[deviceframe]="deviceframe"
PKG_DESC[deviceframe]="Put device frames around your screenshots [CUSTOM]"
PKG_METHOD[deviceframe]="custom"
PKG_CATEGORY[deviceframe]="dev"
# Database
PACKAGES[sqlline]="sqlline"
PKG_DESC[sqlline]="Shell for issuing SQL via JDBC [CUSTOM]"
PKG_METHOD[sqlline]="custom"
PKG_CATEGORY[sqlline]="database"
PACKAGES[iredis]="iredis"
PKG_DESC[iredis]="Redis client with autocompletion and syntax highlighting [CUSTOM]"
PKG_METHOD[iredis]="custom"
PKG_CATEGORY[iredis]="database"
PACKAGES[usql]="usql"
PKG_DESC[usql]="Universal SQL client with autocompletion [CUSTOM]"
PKG_METHOD[usql]="custom"
PKG_CATEGORY[usql]="database"
# DevOps
PACKAGES[htconvert]="htconvert"
PKG_DESC[htconvert]="Convert .htaccess redirects to nginx.conf redirects [CUSTOM]"
PKG_METHOD[htconvert]="custom"
PKG_CATEGORY[htconvert]="dev"
PACKAGES[saws]="saws"
PKG_DESC[saws]="Supercharged AWS CLI [CUSTOM]"
PKG_METHOD[saws]="custom"
PKG_CATEGORY[saws]="dev"
PACKAGES[s3cmd]="s3cmd"
PKG_DESC[s3cmd]="Fully-Featured S3 client [APT]"
PKG_METHOD[s3cmd]="apt"
PKG_CATEGORY[s3cmd]="dev"
PACKAGES[ops]="ops"
PKG_DESC[ops]="Unikernel compilation and orchestration tool [CUSTOM]"
PKG_METHOD[ops]="custom"
PKG_CATEGORY[ops]="dev"
PACKAGES[flog]="flog"
PKG_DESC[flog]="A fake log generator for common log formats [CUSTOM]"
PKG_METHOD[flog]="custom"
PKG_CATEGORY[flog]="dev"
PACKAGES[k9s]="k9s"
PKG_DESC[k9s]="Kubernetes CLI To Manage Your Clusters In Style [CUSTOM]"
PKG_METHOD[k9s]="custom"
PKG_CATEGORY[k9s]="containers"
PACKAGES[pingme]="pingme"
PKG_DESC[pingme]="Send messages/alerts to multiple messaging platforms [CUSTOM]"
PKG_METHOD[pingme]="custom"
PKG_CATEGORY[pingme]="dev"
PACKAGES[ipfs-deploy]="ipfs-deploy"
PKG_DESC[ipfs-deploy]="Deploy static websites to IPFS [CUSTOM]"
PKG_METHOD[ipfs-deploy]="custom"
PKG_CATEGORY[ipfs-deploy]="dev"
PACKAGES[discharge]="discharge"
PKG_DESC[discharge]="Deploy static websites to Amazon S3 [CUSTOM]"
PKG_METHOD[discharge]="custom"
PKG_CATEGORY[discharge]="dev"
PACKAGES[updatecli]="updatecli"
PKG_DESC[updatecli]="A declarative dependency management tool [CUSTOM]"
PKG_METHOD[updatecli]="custom"
PKG_CATEGORY[updatecli]="dev"
PACKAGES[telert]="telert"
PKG_DESC[telert]="Multi-channel alerts for long-running commands [CUSTOM]"
PKG_METHOD[telert]="custom"
PKG_CATEGORY[telert]="dev"
PACKAGES[logdy]="logdy"
PKG_DESC[logdy]="Supercharge terminal logs with web UI [CUSTOM]"
PKG_METHOD[logdy]="custom"
PKG_CATEGORY[logdy]="dev"
PACKAGES[s5cmd]="s5cmd"
PKG_DESC[s5cmd]="Blazing fast S3 and local filesystem execution tool [CUSTOM]"
PKG_METHOD[s5cmd]="custom"
PKG_CATEGORY[s5cmd]="dev"
# Docker
PACKAGES[lstags]="lstags"
PKG_DESC[lstags]="Synchronize images across registries [CUSTOM]"
PKG_METHOD[lstags]="custom"
PKG_CATEGORY[lstags]="containers"
PACKAGES[dockly]="dockly"
PKG_DESC[dockly]="Interactively manage containers [CUSTOM]"
PKG_METHOD[dockly]="custom"
PKG_CATEGORY[dockly]="containers"
PACKAGES[docker-pushrm]="docker-pushrm"
PKG_DESC[docker-pushrm]="Push a readme to container registries [CUSTOM]"
PKG_METHOD[docker-pushrm]="custom"
PKG_CATEGORY[docker-pushrm]="containers"
# Release
PACKAGES[release-it]="release-it"
PKG_DESC[release-it]="Automate releases for Git repositories and npm packages [CUSTOM]"
PKG_METHOD[release-it]="custom"
PKG_CATEGORY[release-it]="dev"
PACKAGES[clog]="clog"
PKG_DESC[clog]="A conventional changelog for the rest of us [CUSTOM]"
PKG_METHOD[clog]="custom"
PKG_CATEGORY[clog]="dev"
PACKAGES[np]="np"
PKG_DESC[np]="A better npm publish [CUSTOM]"
PKG_METHOD[np]="custom"
PKG_CATEGORY[np]="dev"
PACKAGES[release]="release"
PKG_DESC[release]="Generate changelogs with a single command [CUSTOM]"
PKG_METHOD[release]="custom"
PKG_CATEGORY[release]="dev"
PACKAGES[semantic-release]="semantic-release"
PKG_DESC[semantic-release]="Fully automated version management and package publishing [CUSTOM]"
PKG_METHOD[semantic-release]="custom"
PKG_CATEGORY[semantic-release]="dev"
# NPM
PACKAGES[npm-name-cli]="npm-name-cli"
PKG_DESC[npm-name-cli]="Check whether a package name is available on npm [CUSTOM]"
PKG_METHOD[npm-name-cli]="custom"
PKG_CATEGORY[npm-name-cli]="dev"
PACKAGES[npm-user-cli]="npm-user-cli"
PKG_DESC[npm-user-cli]="Get user info of a npm user [CUSTOM]"
PKG_METHOD[npm-user-cli]="custom"
PKG_CATEGORY[npm-user-cli]="dev"
PACKAGES[npm-home]="npm-home"
PKG_DESC[npm-home]="Open the npm page of the package in the current directory [CUSTOM]"
PKG_METHOD[npm-home]="custom"
PKG_CATEGORY[npm-home]="dev"
PACKAGES[pkg-dir-cli]="pkg-dir-cli"
PKG_DESC[pkg-dir-cli]="Find the root directory of a npm package [CUSTOM]"
PKG_METHOD[pkg-dir-cli]="custom"
PKG_CATEGORY[pkg-dir-cli]="dev"
PACKAGES[npm-check-updates]="npm-check-updates"
PKG_DESC[npm-check-updates]="Find newer versions of package dependencies [CUSTOM]"
PKG_METHOD[npm-check-updates]="custom"
PKG_CATEGORY[npm-check-updates]="dev"
PACKAGES[updates]="updates"
PKG_DESC[updates]="Flexible npm dependency update tool [CUSTOM]"
PKG_METHOD[updates]="custom"
PKG_CATEGORY[updates]="dev"
PACKAGES[wipe-modules]="wipe-modules"
PKG_DESC[wipe-modules]="Remove node_modules of inactive projects [CUSTOM]"
PKG_METHOD[wipe-modules]="custom"
PKG_CATEGORY[wipe-modules]="dev"
# Boilerplate Tools
PACKAGES[yo]="yo"
PKG_DESC[yo]="Scaffolding tool for running Yeoman generators [CUSTOM]"
PKG_METHOD[yo]="custom"
PKG_CATEGORY[yo]="dev"
PACKAGES[boilr]="boilr"
PKG_DESC[boilr]="Create projects from boilerplate templates [CUSTOM]"
PKG_METHOD[boilr]="custom"
PKG_CATEGORY[boilr]="dev"
PACKAGES[cookiecutter]="cookiecutter"
PKG_DESC[cookiecutter]="Create projects from templates [APT]"
PKG_METHOD[cookiecutter]="apt"
PKG_CATEGORY[cookiecutter]="dev"
PACKAGES[mevn-cli]="mevn-cli"
PKG_DESC[mevn-cli]="Light speed setup for MEVN (Mongo Express Vue Node) Apps [CUSTOM]"
PKG_METHOD[mevn-cli]="custom"
PKG_CATEGORY[mevn-cli]="dev"
PACKAGES[scaffold-static]="scaffold-static"
PKG_DESC[scaffold-static]="Scaffolding utility for vanilla JS [CUSTOM]"
PKG_METHOD[scaffold-static]="custom"
PKG_CATEGORY[scaffold-static]="dev"
# HTTP Server Tools
PACKAGES[serve]="serve"
PKG_DESC[serve]="Serve static files (https, CORS, GZIP compression, etc) [CUSTOM]"
PKG_METHOD[serve]="custom"
PKG_CATEGORY[serve]="web"
PACKAGES[simplehttp]="simplehttp"
PKG_DESC[simplehttp]="Easily serve a local directory over HTTP [CUSTOM]"
PKG_METHOD[simplehttp]="custom"
PKG_CATEGORY[simplehttp]="web"
PACKAGES[shell2http]="shell2http"
PKG_DESC[shell2http]="Shell script based HTTP server [CUSTOM]"
PKG_METHOD[shell2http]="custom"
PKG_CATEGORY[shell2http]="web"
# HTTP Client Tools
PACKAGES[http-prompt]="http-prompt"
PKG_DESC[http-prompt]="Interactive HTTP client featuring autocomplete and syntax highlighting [CUSTOM]"
PKG_METHOD[http-prompt]="custom"
PKG_CATEGORY[http-prompt]="dev"
PACKAGES[ain]="ain"
PKG_DESC[ain]="HTTP client with a simple format to organize API endpoints [CUSTOM]"
PKG_METHOD[ain]="custom"
PKG_CATEGORY[ain]="dev"
PACKAGES[curlie]="curlie"
PKG_DESC[curlie]="A curl frontend with the ease of use of HTTPie [CUSTOM]"
PKG_METHOD[curlie]="custom"
PKG_CATEGORY[curlie]="dev"
PACKAGES[atac]="atac"
PKG_DESC[atac]="A feature-full TUI API client made in Rust [CUSTOM]"
PKG_METHOD[atac]="custom"
PKG_CATEGORY[atac]="dev"
# Testing Tools
PACKAGES[shellspec]="shellspec"
PKG_DESC[shellspec]="A full-featured BDD unit-testing framework for all POSIX shells [CUSTOM]"
PKG_METHOD[shellspec]="custom"
PKG_CATEGORY[shellspec]="dev"
PACKAGES[gdb-dashboard]="gdb-dashboard"
PKG_DESC[gdb-dashboard]="Modular visual interface for GDB [CUSTOM]"
PKG_METHOD[gdb-dashboard]="custom"
PKG_CATEGORY[gdb-dashboard]="dev"
PACKAGES[loadtest]="loadtest"
PKG_DESC[loadtest]="Run load tests [CUSTOM]"
PKG_METHOD[loadtest]="custom"
PKG_CATEGORY[loadtest]="dev"
PACKAGES[step-ci]="step-ci"
PKG_DESC[step-ci]="API testing and QA framework [CUSTOM]"
PKG_METHOD[step-ci]="custom"
PKG_CATEGORY[step-ci]="dev"
# Productivity Tools
PACKAGES[doing]="doing"
PKG_DESC[doing]="Keep track of what you're doing and track what you've done [CUSTOM]"
PKG_METHOD[doing]="custom"
PKG_CATEGORY[doing]="utilities"
PACKAGES[ffscreencast]="ffscreencast"
PKG_DESC[ffscreencast]="A ffmpeg screencast with video overlay and multi monitor support [CUSTOM]"
PKG_METHOD[ffscreencast]="custom"
PKG_CATEGORY[ffscreencast]="video"
PACKAGES[meetup-cli]="meetup-cli"
PKG_DESC[meetup-cli]="Meetup.com client [CUSTOM]"
PKG_METHOD[meetup-cli]="custom"
PKG_CATEGORY[meetup-cli]="utilities"
PACKAGES[neomutt]="neomutt"
PKG_DESC[neomutt]="Email client [APT]"
PKG_METHOD[neomutt]="apt"
PKG_CATEGORY[neomutt]="communication"
PACKAGES[terjira]="terjira"
PKG_DESC[terjira]="Jira client [CUSTOM]"
PKG_METHOD[terjira]="custom"
PKG_CATEGORY[terjira]="dev"
PACKAGES[ipt]="ipt"
PKG_DESC[ipt]="Pivotal Tracker client [CUSTOM]"
PKG_METHOD[ipt]="custom"
PKG_CATEGORY[ipt]="dev"
PACKAGES[uber-cli]="uber-cli"
PKG_DESC[uber-cli]="Uber client [CUSTOM]"
PKG_METHOD[uber-cli]="custom"
PKG_CATEGORY[uber-cli]="utilities"
PACKAGES[buku]="buku"
PKG_DESC[buku]="Browser-independent bookmark manager [APT]"
PKG_METHOD[buku]="apt"
PKG_CATEGORY[buku]="utilities"
PACKAGES[fjira]="fjira"
PKG_DESC[fjira]="Fuzzy finder and TUI application for Jira [CUSTOM]"
PKG_METHOD[fjira]="custom"
PKG_CATEGORY[fjira]="dev"
PACKAGES[overtime]="overtime"
PKG_DESC[overtime]="Time-overlap tables for remote teams [CUSTOM]"
PKG_METHOD[overtime]="custom"
PKG_CATEGORY[overtime]="utilities"
# Time Tracking Tools
PACKAGES[timetrap]="timetrap"
PKG_DESC[timetrap]="Simple timetracker [CUSTOM]"
PKG_METHOD[timetrap]="custom"
PKG_CATEGORY[timetrap]="utilities"
PACKAGES[moro]="moro"
PKG_DESC[moro]="Simple tool for tracking work hours [CUSTOM]"
PKG_METHOD[moro]="custom"
PKG_CATEGORY[moro]="utilities"
PACKAGES[timewarrior]="timewarrior"
PKG_DESC[timewarrior]="Utility with simple stopwatch, calendar-based backfill and flexible reporting [APT]"
PKG_METHOD[timewarrior]="apt"
PKG_CATEGORY[timewarrior]="utilities"
PACKAGES[watson]="watson"
PKG_DESC[watson]="Generate reports for clients and manage your time [CUSTOM]"
PKG_METHOD[watson]="custom"
PKG_CATEGORY[watson]="utilities"
PACKAGES[utt]="utt"
PKG_DESC[utt]="Simple time tracking tool [CUSTOM]"
PKG_METHOD[utt]="custom"
PKG_CATEGORY[utt]="utilities"
PACKAGES[bartib]="bartib"
PKG_DESC[bartib]="Easy to use time tracking tool [CUSTOM]"
PKG_METHOD[bartib]="custom"
PKG_CATEGORY[bartib]="utilities"
PACKAGES[arttime]="arttime"
PKG_DESC[arttime]="Featureful timer with native desktop notifications and curated ASCII art [CUSTOM]"
PKG_METHOD[arttime]="custom"
PKG_CATEGORY[arttime]="utilities"
# Note Taking and Lists
PACKAGES[idea]="idea"
PKG_DESC[idea]="A lightweight tool for keeping ideas in a safe place quick and easy [CUSTOM]"
PKG_METHOD[idea]="custom"
PKG_CATEGORY[idea]="utilities"
PACKAGES[geeknote]="geeknote"
PKG_DESC[geeknote]="Evernote client [CUSTOM]"
PKG_METHOD[geeknote]="custom"
PKG_CATEGORY[geeknote]="utilities"
PACKAGES[taskwarrior]="taskwarrior"
PKG_DESC[taskwarrior]="Manage your TODO list [APT]"
PKG_METHOD[taskwarrior]="apt"
PKG_CATEGORY[taskwarrior]="utilities"
PACKAGES[terminal-velocity]="terminal-velocity"
PKG_DESC[terminal-velocity]="A fast note-taking app [CUSTOM]"
PKG_METHOD[terminal-velocity]="custom"
PKG_CATEGORY[terminal-velocity]="utilities"
PACKAGES[eureka]="eureka"
PKG_DESC[eureka]="Input and store your ideas [CUSTOM]"
PKG_METHOD[eureka]="custom"
PKG_CATEGORY[eureka]="utilities"
PACKAGES[sncli]="sncli"
PKG_DESC[sncli]="Simplenote client [CUSTOM]"
PKG_METHOD[sncli]="custom"
PKG_CATEGORY[sncli]="utilities"
PACKAGES[td-cli]="td-cli"
PKG_DESC[td-cli]="A TODO manager to organize and manage your TODO's across multiple projects [CUSTOM]"
PKG_METHOD[td-cli]="custom"
PKG_CATEGORY[td-cli]="utilities"
PACKAGES[taskbook]="taskbook"
PKG_DESC[taskbook]="Tasks, boards & notes for the command-line habitat [CUSTOM]"
PKG_METHOD[taskbook]="custom"
PKG_CATEGORY[taskbook]="utilities"
PACKAGES[dnote]="dnote"
PKG_DESC[dnote]="A interactive, multi-device notebook [CUSTOM]"
PKG_METHOD[dnote]="custom"
PKG_CATEGORY[dnote]="utilities"
PACKAGES[nb]="nb"
PKG_DESC[nb]="A notetaking, bookmarking, archiving, and knowledge base application [CUSTOM]"
PKG_METHOD[nb]="custom"
PKG_CATEGORY[nb]="utilities"
PACKAGES[obs-cli]="obs-cli"
PKG_DESC[obs-cli]="Interact with your Obsidian vault [CUSTOM]"
PKG_METHOD[obs-cli]="custom"
PKG_CATEGORY[obs-cli]="utilities"
PACKAGES[journalot]="journalot"
PKG_DESC[journalot]="Journaling tool with git sync [CUSTOM]"
PKG_METHOD[journalot]="custom"
PKG_CATEGORY[journalot]="utilities"
# Finance Tools
PACKAGES[ledger]="ledger"
PKG_DESC[ledger]="Powerful, double-entry accounting system [APT]"
PKG_METHOD[ledger]="apt"
PKG_CATEGORY[ledger]="utilities"
PACKAGES[hledger]="hledger"
PKG_DESC[hledger]="Robust, fast, intuitive plain text accounting tool with CLI, TUI and web interfaces [APT]"
PKG_METHOD[hledger]="apt"
PKG_CATEGORY[hledger]="utilities"
PACKAGES[moeda]="moeda"
PKG_DESC[moeda]="Foreign exchange rates and currency conversion [CUSTOM]"
PKG_METHOD[moeda]="custom"
PKG_CATEGORY[moeda]="utilities"
PACKAGES[cash-cli]="cash-cli"
PKG_DESC[cash-cli]="Convert Currency Rates [CUSTOM]"
PKG_METHOD[cash-cli]="custom"
PKG_CATEGORY[cash-cli]="utilities"
PACKAGES[cointop]="cointop"
PKG_DESC[cointop]="Track cryptocurrencies [CUSTOM]"
PKG_METHOD[cointop]="custom"
PKG_CATEGORY[cointop]="utilities"
PACKAGES[ticker]="ticker"
PKG_DESC[ticker]="Stock ticker [CUSTOM]"
PKG_METHOD[ticker]="custom"
PKG_CATEGORY[ticker]="utilities"
# Presentation Tools
PACKAGES[wopr]="wopr"
PKG_DESC[wopr]="A simple markup language for creating rich terminal reports, presentations and infographics [CUSTOM]"
PKG_METHOD[wopr]="custom"
PKG_CATEGORY[wopr]="utilities"
PACKAGES[decktape]="decktape"
PKG_DESC[decktape]="PDF exporter for HTML presentations [CUSTOM]"
PKG_METHOD[decktape]="custom"
PKG_CATEGORY[decktape]="utilities"
PACKAGES[mdp]="mdp"
PKG_DESC[mdp]="A markdown presentation tool [APT]"
PKG_METHOD[mdp]="apt"
PKG_CATEGORY[mdp]="utilities"
PACKAGES[sent]="sent"
PKG_DESC[sent]="Simple plaintext presentation tool [CUSTOM]"
PKG_METHOD[sent]="custom"
PKG_CATEGORY[sent]="utilities"
PACKAGES[slides]="slides"
PKG_DESC[slides]="A markdown presentation tool [CUSTOM]"
PKG_METHOD[slides]="custom"
PKG_CATEGORY[slides]="utilities"
PACKAGES[marp]="marp"
PKG_DESC[marp]="Export Markdown to HTML/PDF/Powerpoint presentations [CUSTOM]"
PKG_METHOD[marp]="custom"
PKG_CATEGORY[marp]="utilities"
# Calendar Tools
PACKAGES[calcurse]="calcurse"
PKG_DESC[calcurse]="Calendar and scheduling [APT]"
PKG_METHOD[calcurse]="apt"
PKG_CATEGORY[calcurse]="utilities"
PACKAGES[gcalcli]="gcalcli"
PKG_DESC[gcalcli]="Google calendar client [CUSTOM]"
PKG_METHOD[gcalcli]="custom"
PKG_CATEGORY[gcalcli]="utilities"
PACKAGES[khal]="khal"
PKG_DESC[khal]="CalDAV ready CLI and TUI calendar [APT]"
PKG_METHOD[khal]="apt"
PKG_CATEGORY[khal]="utilities"
PACKAGES[vdirsyncer]="vdirsyncer"
PKG_DESC[vdirsyncer]="CalDAV sync [APT]"
PKG_METHOD[vdirsyncer]="apt"
PKG_CATEGORY[vdirsyncer]="utilities"
PACKAGES[remind]="remind"
PKG_DESC[remind]="A sophisticated calendar and alarm program [APT]"
PKG_METHOD[remind]="apt"
PKG_CATEGORY[remind]="utilities"
PACKAGES[birthday]="birthday"
PKG_DESC[birthday]="Know when a friend's birthday is coming [CUSTOM]"
PKG_METHOD[birthday]="custom"
PKG_CATEGORY[birthday]="utilities"
# Additional Utilities
PACKAGES[aria2]="aria2"
PKG_DESC[aria2]="HTTP, FTP, SFTP, BitTorrent and Metalink download utility [APT]"
PKG_METHOD[aria2]="apt"
PKG_CATEGORY[aria2]="utilities"
PACKAGES[bitly-client]="bitly-client"
PKG_DESC[bitly-client]="Bitly client [CUSTOM]"
PKG_METHOD[bitly-client]="custom"
PKG_CATEGORY[bitly-client]="utilities"
PACKAGES[deadlink]="deadlink"
PKG_DESC[deadlink]="Find dead links in files [CUSTOM]"
PKG_METHOD[deadlink]="custom"
PKG_CATEGORY[deadlink]="utilities"
PACKAGES[crawley]="crawley"
PKG_DESC[crawley]="Unix-way web crawler [CUSTOM]"
PKG_METHOD[crawley]="custom"
PKG_CATEGORY[crawley]="utilities"
PACKAGES[kill-tabs]="kill-tabs"
PKG_DESC[kill-tabs]="Kill all Chrome tabs [CUSTOM]"
PKG_METHOD[kill-tabs]="custom"
PKG_CATEGORY[kill-tabs]="utilities"
PACKAGES[alex]="alex"
PKG_DESC[alex]="Catch insensitive, inconsiderate writing [CUSTOM]"
PKG_METHOD[alex]="custom"
PKG_CATEGORY[alex]="utilities"
PACKAGES[clevercli]="clevercli"
PKG_DESC[clevercli]="Collection of ChatGPT powered utilities [CUSTOM]"
PKG_METHOD[clevercli]="custom"
PKG_CATEGORY[clevercli]="ai"
# Terminal Sharing Utilities
PACKAGES[gotty]="gotty"
PKG_DESC[gotty]="Share your terminal as a web application [CUSTOM]"
PKG_METHOD[gotty]="custom"
PKG_CATEGORY[gotty]="utilities"
PACKAGES[tmate]="tmate"
PKG_DESC[tmate]="Instant terminal (tmux) sharing [APT]"
PKG_METHOD[tmate]="apt"
PKG_CATEGORY[tmate]="utilities"
PACKAGES[warp-sharing]="warp-sharing"
PKG_DESC[warp-sharing]="Secure and simple terminal sharing [CUSTOM]"
PKG_METHOD[warp-sharing]="custom"
PKG_CATEGORY[warp-sharing]="utilities"
# SSH Tools
PACKAGES[mosh]="mosh"
PKG_DESC[mosh]="Remote SSH client that allows roaming with intermittent connectivity [APT]"
PKG_METHOD[mosh]="apt"
PKG_CATEGORY[mosh]="network-monitoring"
PACKAGES[xxh]="xxh"
PKG_DESC[xxh]="Bring your favorite shell wherever you go through SSH [CUSTOM]"
PKG_METHOD[xxh]="custom"
PKG_CATEGORY[xxh]="network-monitoring"
# Network Utilities
PACKAGES[get-port-cli]="get-port-cli"
PKG_DESC[get-port-cli]="Get an available port [CUSTOM]"
PKG_METHOD[get-port-cli]="custom"
PKG_CATEGORY[get-port-cli]="network-monitoring"
PACKAGES[is-reachable-cli]="is-reachable-cli"
PKG_DESC[is-reachable-cli]="Check if hostnames are reachable or not [CUSTOM]"
PKG_METHOD[is-reachable-cli]="custom"
PKG_CATEGORY[is-reachable-cli]="network-monitoring"
PACKAGES[acmetool]="acmetool"
PKG_DESC[acmetool]="Automatic certificate acquisition for ACME (Let's Encrypt) [CUSTOM]"
PKG_METHOD[acmetool]="custom"
PKG_CATEGORY[acmetool]="security"
PACKAGES[certificate-ripper]="certificate-ripper"
PKG_DESC[certificate-ripper]="Extract server certificates [CUSTOM]"
PKG_METHOD[certificate-ripper]="custom"
PKG_CATEGORY[certificate-ripper]="security"
PACKAGES[neoss]="neoss"
PKG_DESC[neoss]="User-friendly and detailed socket statistics [CUSTOM]"
PKG_METHOD[neoss]="custom"
PKG_CATEGORY[neoss]="network-monitoring"
PACKAGES[gg]="gg"
PKG_DESC[gg]="One-click proxy without installing v2ray or anything else [CUSTOM]"
PKG_METHOD[gg]="custom"
PKG_CATEGORY[gg]="network-monitoring"
PACKAGES[rustnet]="rustnet"
PKG_DESC[rustnet]="Network monitoring with process identification and deep packet inspection [CUSTOM]"
PKG_METHOD[rustnet]="custom"
PKG_CATEGORY[rustnet]="network-monitoring"
PACKAGES[sshuttle]="sshuttle"
PKG_DESC[sshuttle]="Transparent proxy server that works as a poor man's VPN [APT]"
PKG_METHOD[sshuttle]="apt"
PKG_CATEGORY[sshuttle]="network-monitoring"
# Theming and Customization
PACKAGES[splash-cli]="splash-cli"
PKG_DESC[splash-cli]="Beautiful wallpapers from Unsplash [CUSTOM]"
PKG_METHOD[splash-cli]="custom"
PKG_CATEGORY[splash-cli]="shell"
PACKAGES[wallpaper-cli]="wallpaper-cli"
PKG_DESC[wallpaper-cli]="Get or set the desktop wallpaper [CUSTOM]"
PKG_METHOD[wallpaper-cli]="custom"
PKG_CATEGORY[wallpaper-cli]="shell"
PACKAGES[themer]="themer"
PKG_DESC[themer]="Generate personalized themes for your editor, terminal, wallpaper, Slack, and more [CUSTOM]"
PKG_METHOD[themer]="custom"
PKG_CATEGORY[themer]="shell"
PACKAGES[jackpaper]="jackpaper"
PKG_DESC[jackpaper]="Set images from Unsplash as wallpaper [CUSTOM]"
PKG_METHOD[jackpaper]="custom"
PKG_CATEGORY[jackpaper]="shell"
PACKAGES[quickwall]="quickwall"
PKG_DESC[quickwall]="Directly set wallpapers from Unsplash [CUSTOM]"
PKG_METHOD[quickwall]="custom"
PKG_CATEGORY[quickwall]="shell"
PACKAGES[oh-my-posh]="oh-my-posh"
PKG_DESC[oh-my-posh]="Prompt theme engine [CUSTOM]"
PKG_METHOD[oh-my-posh]="custom"
PKG_CATEGORY[oh-my-posh]="shell"
# Shell Utilities
PACKAGES[has]="has"
PKG_DESC[has]="Checks for the presence of various commands and their versions on the path [CUSTOM]"
PKG_METHOD[has]="custom"
PKG_CATEGORY[has]="shell"
PACKAGES[ultimate-plumber]="ultimate-plumber"
PKG_DESC[ultimate-plumber]="Write Linux pipes with live previews [CUSTOM]"
PKG_METHOD[ultimate-plumber]="custom"
PKG_CATEGORY[ultimate-plumber]="shell"
PACKAGES[fkill-cli]="fkill-cli"
PKG_DESC[fkill-cli]="Simple cross-platform process killer [CUSTOM]"
PKG_METHOD[fkill-cli]="custom"
PKG_CATEGORY[fkill-cli]="utilities"
PACKAGES[task-spooler]="task-spooler"
PKG_DESC[task-spooler]="Queue jobs for linear execution [APT]"
PKG_METHOD[task-spooler]="apt"
PKG_CATEGORY[task-spooler]="utilities"
PACKAGES[undollar]="undollar"
PKG_DESC[undollar]="Strip the '$' preceding copy-pasted terminal commands [CUSTOM]"
PKG_METHOD[undollar]="custom"
PKG_CATEGORY[undollar]="shell"
PACKAGES[pipe-exec]="pipe-exec"
PKG_DESC[pipe-exec]="Run exes from stdin, pipes and ttys without creating a temp [CUSTOM]"
PKG_METHOD[pipe-exec]="custom"
PKG_CATEGORY[pipe-exec]="shell"
# System Interaction Utilities
PACKAGES[fastfetch]="fastfetch"
PKG_DESC[fastfetch]="System information tool [APT]"
PKG_METHOD[fastfetch]="apt"
PKG_CATEGORY[fastfetch]="utilities"
PACKAGES[battery-level-cli]="battery-level-cli"
PKG_DESC[battery-level-cli]="Get current battery level [CUSTOM]"
PKG_METHOD[battery-level-cli]="custom"
PKG_CATEGORY[battery-level-cli]="utilities"
PACKAGES[brightness-cli]="brightness-cli"
PKG_DESC[brightness-cli]="Change screen brightness [CUSTOM]"
PKG_METHOD[brightness-cli]="custom"
PKG_CATEGORY[brightness-cli]="utilities"
PACKAGES[clipboard]="clipboard"
PKG_DESC[clipboard]="Cut, copy, and paste anything, anywhere [CUSTOM]"
PKG_METHOD[clipboard]="custom"
PKG_CATEGORY[clipboard]="utilities"
PACKAGES[yank]="yank"
PKG_DESC[yank]="Yank terminal output to clipboard [CUSTOM]"
PKG_METHOD[yank]="custom"
PKG_CATEGORY[yank]="utilities"
PACKAGES[screensaver]="screensaver"
PKG_DESC[screensaver]="Start the screensaver [CUSTOM]"
PKG_METHOD[screensaver]="custom"
PKG_CATEGORY[screensaver]="utilities"
PACKAGES[google-font-installer]="google-font-installer"
PKG_DESC[google-font-installer]="Download and install Google Web Fonts [CUSTOM]"
PKG_METHOD[google-font-installer]="custom"
PKG_CATEGORY[google-font-installer]="utilities"
PACKAGES[tiptop]="tiptop"
PKG_DESC[tiptop]="System monitor [CUSTOM]"
PKG_METHOD[tiptop]="custom"
PKG_CATEGORY[tiptop]="system-monitoring"
PACKAGES[gzip-size-cli]="gzip-size-cli"
PKG_DESC[gzip-size-cli]="Get the gzipped size of a file [CUSTOM]"
PKG_METHOD[gzip-size-cli]="custom"
PKG_CATEGORY[gzip-size-cli]="utilities"
# Markdown Tools
PACKAGES[doctoc]="doctoc"
PKG_DESC[doctoc]="Generates table of contents for markdown files [CUSTOM]"
PKG_METHOD[doctoc]="custom"
PKG_CATEGORY[doctoc]="dev"
PACKAGES[grip]="grip"
PKG_DESC[grip]="Preview markdown files as GitHub would render them [CUSTOM]"
PKG_METHOD[grip]="custom"
PKG_CATEGORY[grip]="dev"
PACKAGES[mdv]="mdv"
PKG_DESC[mdv]="Styled terminal markdown viewer [CUSTOM]"
PKG_METHOD[mdv]="custom"
PKG_CATEGORY[mdv]="utilities"
PACKAGES[gtree]="gtree"
PKG_DESC[gtree]="Use markdown to generate directory trees [CUSTOM]"
PKG_METHOD[gtree]="custom"
PKG_CATEGORY[gtree]="utilities"
# Security Tools
PACKAGES[pass]="pass"
PKG_DESC[pass]="Password manager [APT]"
PKG_METHOD[pass]="apt"
PKG_CATEGORY[pass]="security"
PACKAGES[gopass]="gopass"
PKG_DESC[gopass]="Fully-featured password manager [CUSTOM]"
PKG_METHOD[gopass]="custom"
PKG_CATEGORY[gopass]="security"
PACKAGES[xiringuito]="xiringuito"
PKG_DESC[xiringuito]="SSH-based VPN [CUSTOM]"
PKG_METHOD[xiringuito]="custom"
PKG_CATEGORY[xiringuito]="security"
PACKAGES[hasha-cli]="hasha-cli"
PKG_DESC[hasha-cli]="Get the hash of text or stdin [CUSTOM]"
PKG_METHOD[hasha-cli]="custom"
PKG_CATEGORY[hasha-cli]="security"
PACKAGES[ots]="ots"
PKG_DESC[ots]="Share secrets with others via a one-time URL [CUSTOM]"
PKG_METHOD[ots]="custom"
PKG_CATEGORY[ots]="security"
# Math Tools
PACKAGES[mdlt]="mdlt"
PKG_DESC[mdlt]="Do quick math right from the command line [CUSTOM]"
PKG_METHOD[mdlt]="custom"
PKG_CATEGORY[mdlt]="utilities"
PACKAGES[qalculate]="qalculate"
PKG_DESC[qalculate]="Calculate non-trivial math expressions [APT]"
PKG_METHOD[qalculate]="apt"
PKG_CATEGORY[qalculate]="utilities"
# Academia Tools
PACKAGES[papis]="papis"
PKG_DESC[papis]="Extensible document and bibliography manager [CUSTOM]"
PKG_METHOD[papis]="custom"
PKG_CATEGORY[papis]="office"
PACKAGES[pubs]="pubs"
PKG_DESC[pubs]="Scientific bibliography manager [CUSTOM]"
PKG_METHOD[pubs]="custom"
PKG_CATEGORY[pubs]="office"
# Weather Tools
PACKAGES[wttr-in]="wttr-in"
PKG_DESC[wttr-in]="Weather information from wttr.in [CUSTOM]"
PKG_METHOD[wttr-in]="custom"
PKG_CATEGORY[wttr-in]="utilities"
PACKAGES[wego]="wego"
PKG_DESC[wego]="Weather app for the terminal [CUSTOM]"
PKG_METHOD[wego]="custom"
PKG_CATEGORY[wego]="utilities"
PACKAGES[weather-cli]="weather-cli"
PKG_DESC[weather-cli]="Get weather information from command line [CUSTOM]"
PKG_METHOD[weather-cli]="custom"
PKG_CATEGORY[weather-cli]="utilities"
# Browser Replacement Tools
PACKAGES[s]="s"
PKG_DESC[s]="Open a web search in your terminal [CUSTOM]"
PKG_METHOD[s]="custom"
PKG_CATEGORY[s]="utilities"
PACKAGES[hget]="hget"
PKG_DESC[hget]="Render websites in plain text from your terminal [CUSTOM]"
PKG_METHOD[hget]="custom"
PKG_CATEGORY[hget]="utilities"
PACKAGES[mapscii]="mapscii"
PKG_DESC[mapscii]="Terminal Map Viewer [CUSTOM]"
PKG_METHOD[mapscii]="custom"
PKG_CATEGORY[mapscii]="utilities"
PACKAGES[nasa-cli]="nasa-cli"
PKG_DESC[nasa-cli]="Download NASA Picture of the Day [CUSTOM]"
PKG_METHOD[nasa-cli]="custom"
PKG_CATEGORY[nasa-cli]="utilities"
PACKAGES[getnews-tech]="getnews-tech"
PKG_DESC[getnews-tech]="Fetch news headlines from various news outlets [CUSTOM]"
PKG_METHOD[getnews-tech]="custom"
PKG_CATEGORY[getnews-tech]="utilities"
PACKAGES[trino]="trino"
PKG_DESC[trino]="Translation of words and phrases [CUSTOM]"
PKG_METHOD[trino]="custom"
PKG_CATEGORY[trino]="utilities"
PACKAGES[translate-shell]="translate-shell"
PKG_DESC[translate-shell]="Google Translate interface [APT]"
PKG_METHOD[translate-shell]="apt"
PKG_CATEGORY[translate-shell]="utilities"
# Internet Speedtest Tools
PACKAGES[speedtest-net]="speedtest-net"
PKG_DESC[speedtest-net]="Test internet connection speed using speedtest.net [CUSTOM]"
PKG_METHOD[speedtest-net]="custom"
PKG_CATEGORY[speedtest-net]="network-monitoring"
PACKAGES[speed-test]="speed-test"
PKG_DESC[speed-test]="speedtest-net wrapper with different UI [CUSTOM]"
PKG_METHOD[speed-test]="custom"
PKG_CATEGORY[speed-test]="network-monitoring"
PACKAGES[speedtest-cli]="speedtest-cli"
PKG_DESC[speedtest-cli]="Test internet bandwidth using speedtest.net [APT]"
PKG_METHOD[speedtest-cli]="apt"
PKG_CATEGORY[speedtest-cli]="network-monitoring"
# Command Line Learning Tools
PACKAGES[cmdchallenge]="cmdchallenge"
PKG_DESC[cmdchallenge]="Presents small shell challenge with user submitted solutions [CUSTOM]"
PKG_METHOD[cmdchallenge]="custom"
PKG_CATEGORY[cmdchallenge]="dev"
PACKAGES[explainshell]="explainshell"
PKG_DESC[explainshell]="Type a snippet to see the help text for each argument [CUSTOM]"
PKG_METHOD[explainshell]="custom"
PKG_CATEGORY[explainshell]="dev"
PACKAGES[howdoi]="howdoi"
PKG_DESC[howdoi]="Instant coding answers [CUSTOM]"
PKG_METHOD[howdoi]="custom"
PKG_CATEGORY[howdoi]="dev"
PACKAGES[how2]="how2"
PKG_DESC[how2]="Node.js implementation of howdoi [CUSTOM]"
PKG_METHOD[how2]="custom"
PKG_CATEGORY[how2]="dev"
PACKAGES[wat]="wat"
PKG_DESC[wat]="Instant, central, community-built docs [CUSTOM]"
PKG_METHOD[wat]="custom"
PKG_CATEGORY[wat]="dev"
PACKAGES[teachcode]="teachcode"
PKG_DESC[teachcode]="Guide for the earliest lessons of coding [CUSTOM]"
PKG_METHOD[teachcode]="custom"
PKG_CATEGORY[teachcode]="dev"
PACKAGES[navi]="navi"
PKG_DESC[navi]="Interactive cheatsheet tool [CUSTOM]"
PKG_METHOD[navi]="custom"
PKG_CATEGORY[navi]="dev"
PACKAGES[yai]="yai"
PKG_DESC[yai]="AI powered terminal assistant [CUSTOM]"
PKG_METHOD[yai]="custom"
PKG_CATEGORY[yai]="ai"
# Data Manipulation Tools
PACKAGES[visidata]="visidata"
PKG_DESC[visidata]="Spreadsheet multitool for data discovery and arrangement [CUSTOM]"
PKG_METHOD[visidata]="custom"
PKG_CATEGORY[visidata]="utilities"
PACKAGES[jp]="jp"
PKG_DESC[jp]="JSON parser [CUSTOM]"
PKG_METHOD[jp]="custom"
PKG_CATEGORY[jp]="dev"
PACKAGES[fx]="fx"
PKG_DESC[fx]="Command-line JSON viewer [CUSTOM]"
PKG_METHOD[fx]="custom"
PKG_CATEGORY[fx]="dev"
PACKAGES[vj]="vj"
PKG_DESC[vj]="Makes JSON human readable [CUSTOM]"
PKG_METHOD[vj]="custom"
PKG_CATEGORY[vj]="dev"
PACKAGES[underscore-cli]="underscore-cli"
PKG_DESC[underscore-cli]="Utility-belt for hacking JSON and Javascript [CUSTOM]"
PKG_METHOD[underscore-cli]="custom"
PKG_CATEGORY[underscore-cli]="dev"
PACKAGES[strip-json-comments-cli]="strip-json-comments-cli"
PKG_DESC[strip-json-comments-cli]="Strip comments from JSON [CUSTOM]"
PKG_METHOD[strip-json-comments-cli]="custom"
PKG_CATEGORY[strip-json-comments-cli]="dev"
PACKAGES[groq]="groq"
PKG_DESC[groq]="JSON processor with queries and projections [CUSTOM]"
PKG_METHOD[groq]="custom"
PKG_CATEGORY[groq]="dev"
PACKAGES[gron]="gron"
PKG_DESC[gron]="Make JSON greppable [CUSTOM]"
PKG_METHOD[gron]="custom"
PKG_CATEGORY[gron]="dev"
PACKAGES[config-file-validator]="config-file-validator"
PKG_DESC[config-file-validator]="Validate configuration files [CUSTOM]"
PKG_METHOD[config-file-validator]="custom"
PKG_CATEGORY[config-file-validator]="dev"
PACKAGES[dyff]="dyff"
PKG_DESC[dyff]="YAML diff tool [CUSTOM]"
PKG_METHOD[dyff]="custom"
PKG_CATEGORY[dyff]="dev"
PACKAGES[parse-columns-cli]="parse-columns-cli"
PKG_DESC[parse-columns-cli]="Parse text columns to JSON [CUSTOM]"
PKG_METHOD[parse-columns-cli]="custom"
PKG_CATEGORY[parse-columns-cli]="dev"
PACKAGES[q]="q"
PKG_DESC[q]="Execution of SQL-like queries on CSV/TSV/tabular text file [CUSTOM]"
PKG_METHOD[q]="custom"
PKG_CATEGORY[q]="dev"
PACKAGES[stegcloak]="stegcloak"
PKG_DESC[stegcloak]="Hide secrets with invisible characters in plain text [CUSTOM]"
PKG_METHOD[stegcloak]="custom"
PKG_CATEGORY[stegcloak]="security"
# File Managers
PACKAGES[vifm]="vifm"
PKG_DESC[vifm]="VI influenced file manager [APT]"
PKG_METHOD[vifm]="apt"
PKG_CATEGORY[vifm]="utilities"
PACKAGES[nnn]="nnn"
PKG_DESC[nnn]="File browser and disk usage analyzer [APT]"
PKG_METHOD[nnn]="apt"
PKG_CATEGORY[nnn]="utilities"
PACKAGES[lf]="lf"
PKG_DESC[lf]="Fast, extensively customizable file manager [CUSTOM]"
PKG_METHOD[lf]="custom"
PKG_CATEGORY[lf]="utilities"
PACKAGES[clifm]="clifm"
PKG_DESC[clifm]="The command line file manager [CUSTOM]"
PKG_METHOD[clifm]="custom"
PKG_CATEGORY[clifm]="utilities"
PACKAGES[far2l]="far2l"
PKG_DESC[far2l]="Orthodox file manager [CUSTOM]"
PKG_METHOD[far2l]="custom"
PKG_CATEGORY[far2l]="utilities"
PACKAGES[yazi]="yazi"
PKG_DESC[yazi]="Blazing fast file manager [CUSTOM]"
PKG_METHOD[yazi]="custom"
PKG_CATEGORY[yazi]="utilities"
PACKAGES[xplr]="xplr"
PKG_DESC[xplr]="A hackable, minimal, fast TUI file explorer [CUSTOM]"
PKG_METHOD[xplr]="custom"
PKG_CATEGORY[xplr]="utilities"
# File Operations
PACKAGES[trash-cli]="trash-cli"
PKG_DESC[trash-cli]="Move files and directories to the trash [APT]"
PKG_METHOD[trash-cli]="apt"
PKG_CATEGORY[trash-cli]="utilities"
PACKAGES[empty-trash-cli]="empty-trash-cli"
PKG_DESC[empty-trash-cli]="Empty the trash [CUSTOM]"
PKG_METHOD[empty-trash-cli]="custom"
PKG_CATEGORY[empty-trash-cli]="utilities"
PACKAGES[del-cli]="del-cli"
PKG_DESC[del-cli]="Delete files and folders [CUSTOM]"
PKG_METHOD[del-cli]="custom"
PKG_CATEGORY[del-cli]="utilities"
PACKAGES[cpy-cli]="cpy-cli"
PKG_DESC[cpy-cli]="Copies files [CUSTOM]"
PKG_METHOD[cpy-cli]="custom"
PKG_CATEGORY[cpy-cli]="utilities"
PACKAGES[rename-cli]="rename-cli"
PKG_DESC[rename-cli]="Rename files quickly [CUSTOM]"
PKG_METHOD[rename-cli]="custom"
PKG_CATEGORY[rename-cli]="utilities"
PACKAGES[renameutils]="renameutils"
PKG_DESC[renameutils]="Mass renaming in your editor [APT]"
PKG_METHOD[renameutils]="apt"
PKG_CATEGORY[renameutils]="utilities"
PACKAGES[diskonaut]="diskonaut"
PKG_DESC[diskonaut]="Disk space navigator [CUSTOM]"
PKG_METHOD[diskonaut]="custom"
PKG_CATEGORY[diskonaut]="utilities"
PACKAGES[dua-cli]="dua-cli"
PKG_DESC[dua-cli]="Disk usage analyzer [CUSTOM]"
PKG_METHOD[dua-cli]="custom"
PKG_CATEGORY[dua-cli]="utilities"
PACKAGES[dutree]="dutree"
PKG_DESC[dutree]="A tool to analyze file system usage written in Rust [CUSTOM]"
PKG_METHOD[dutree]="custom"
PKG_CATEGORY[dutree]="utilities"
PACKAGES[chokidar-cli]="chokidar-cli"
PKG_DESC[chokidar-cli]="CLI to watch file system changes [CUSTOM]"
PKG_METHOD[chokidar-cli]="custom"
PKG_CATEGORY[chokidar-cli]="utilities"
PACKAGES[file-type-cli]="file-type-cli"
PKG_DESC[file-type-cli]="Detect the file type of a file or stdin [CUSTOM]"
PKG_METHOD[file-type-cli]="custom"
PKG_CATEGORY[file-type-cli]="utilities"
PACKAGES[unix-permissions]="unix-permissions"
PKG_DESC[unix-permissions]="Swiss Army knife for Unix permissions [CUSTOM]"
PKG_METHOD[unix-permissions]="custom"
PKG_CATEGORY[unix-permissions]="utilities"
PACKAGES[transmission-cli]="transmission-cli"
PKG_DESC[transmission-cli]="Torrent client for your command line [APT]"
PKG_METHOD[transmission-cli]="apt"
PKG_CATEGORY[transmission-cli]="utilities"
PACKAGES[webtorrent-cli]="webtorrent-cli"
PKG_DESC[webtorrent-cli]="Streaming torrent client [CUSTOM]"
PKG_METHOD[webtorrent-cli]="custom"
PKG_CATEGORY[webtorrent-cli]="utilities"
PACKAGES[entr]="entr"
PKG_DESC[entr]="Run an arbitrary command when files change [APT]"
PKG_METHOD[entr]="apt"
PKG_CATEGORY[entr]="utilities"
PACKAGES[organize-cli]="organize-cli"
PKG_DESC[organize-cli]="Organize your files automatically [CUSTOM]"
PKG_METHOD[organize-cli]="custom"
PKG_CATEGORY[organize-cli]="utilities"
PACKAGES[organize-rt]="organize-rt"
PKG_DESC[organize-rt]="organize-cli in Rust with more customization [CUSTOM]"
PKG_METHOD[organize-rt]="custom"
PKG_CATEGORY[organize-rt]="utilities"
PACKAGES[recoverpy]="recoverpy"
PKG_DESC[recoverpy]="Recover overwritten or deleted files [CUSTOM]"
PKG_METHOD[recoverpy]="custom"
PKG_CATEGORY[recoverpy]="utilities"
PACKAGES[f2]="f2"
PKG_DESC[f2]="A cross-platform tool for fast, safe, and flexible batch renaming [CUSTOM]"
PKG_METHOD[f2]="custom"
PKG_CATEGORY[f2]="utilities"
PACKAGES[scc]="scc"
PKG_DESC[scc]="Count lines of code, blank lines, comment lines, and physical lines [CUSTOM]"
PKG_METHOD[scc]="custom"
PKG_CATEGORY[scc]="dev"
# File Sync/Sharing
PACKAGES[ffsend]="ffsend"
PKG_DESC[ffsend]="Quick file share [CUSTOM]"
PKG_METHOD[ffsend]="custom"
PKG_CATEGORY[ffsend]="utilities"
PACKAGES[share-cli]="share-cli"
PKG_DESC[share-cli]="Share files with your local network [CUSTOM]"
PKG_METHOD[share-cli]="custom"
PKG_CATEGORY[share-cli]="utilities"
PACKAGES[google-drive-upload]="google-drive-upload"
PKG_DESC[google-drive-upload]="Upload/sync with Google Drive [CUSTOM]"
PKG_METHOD[google-drive-upload]="custom"
PKG_CATEGORY[google-drive-upload]="utilities"
PACKAGES[gdrive-downloader]="gdrive-downloader"
PKG_DESC[gdrive-downloader]="Download files/folders from Google Drive [CUSTOM]"
PKG_METHOD[gdrive-downloader]="custom"
PKG_CATEGORY[gdrive-downloader]="utilities"
PACKAGES[portal]="portal"
PKG_DESC[portal]="Send files between computers [CUSTOM]"
PKG_METHOD[portal]="custom"
PKG_CATEGORY[portal]="utilities"
PACKAGES[shbin]="shbin"
PKG_DESC[shbin]="Turn a Github repo into a pastebin [CUSTOM]"
PKG_METHOD[shbin]="custom"
PKG_CATEGORY[shbin]="utilities"
PACKAGES[sharing]="sharing"
PKG_DESC[sharing]="Send and receive files on your mobile device [CUSTOM]"
PKG_METHOD[sharing]="custom"
PKG_CATEGORY[sharing]="utilities"
PACKAGES[ncp]="ncp"
PKG_DESC[ncp]="Transfer files and folders, to and from NFS servers [CUSTOM]"
PKG_METHOD[ncp]="custom"
PKG_CATEGORY[ncp]="utilities"
# Directory Listing
PACKAGES[alder]="alder"
PKG_DESC[alder]="Minimal tree with colors [CUSTOM]"
PKG_METHOD[alder]="custom"
PKG_CATEGORY[alder]="utilities"
PACKAGES[tre]="tre"
PKG_DESC[tre]="tree with git awareness, editor aliasing, and more [CUSTOM]"
PKG_METHOD[tre]="custom"
PKG_CATEGORY[tre]="utilities"
# ==============================================================================
# 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"
"system-monitoring:System Monitoring"
"network-monitoring:Network Monitoring"
"performance-tools:Performance Tools"
"utilities:Utilities"
"database:Database Management"
"security:Security Tools"
"system:System Tools"
"office:Office & Productivity"
"communication:Communication"
"graphics:Graphics & Design"
"audio:Audio & Music"
"video:Video & Media"
"media-servers:Media Servers & Streaming"
"cloud:Cloud & Sync"
"terminals:Terminals"
"gaming-platforms:Gaming Platforms"
"gaming-emulators:Gaming Emulators"
)
# Category hotkey mappings (A-Z)
declare -A CATEGORY_HOTKEYS=(
["core"]="A"
["dev"]="B"
["ai"]="C"
["containers"]="D"
["web"]="E"
["shell"]="F"
["editors"]="G"
["browsers"]="H"
["system-monitoring"]="I"
["network-monitoring"]="J"
["performance-tools"]="K"
["utilities"]="L"
["database"]="M"
["security"]="N"
["system"]="O"
["office"]="P"
["communication"]="Q"
["graphics"]="R"
["audio"]="S"
["video"]="T"
["media-servers"]="U"
["cloud"]="V"
["terminals"]="W"
["gaming-platforms"]="X"
["gaming-emulators"]="Y"
# Note: Z is reserved for back navigation in submenus
)
# ==============================================================================
# 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..."
# Check if snapd is running
if ! systemctl is-active --quiet snapd; then
log "Starting snapd service..."
sudo systemctl start snapd
sleep 2
fi
# Wait for snap to be ready
log "Waiting for snap to be ready..."
timeout 30 sudo snap wait system seed.loaded || {
log "Warning: Snap system not fully ready, proceeding anyway..."
}
# Packages that require classic confinement
case "$pkg" in
ghostty)
log "Installing $pkg with classic confinement..."
timeout 300 sudo snap install "$pkg" --classic 2>&1 | tee -a "$LOGFILE"
local exit_code=${PIPESTATUS[0]}
;;
*)
log "Installing $pkg..."
timeout 300 sudo snap install "$pkg" 2>&1 | tee -a "$LOGFILE"
local exit_code=${PIPESTATUS[0]}
;;
esac
# Check installation result
if [ $exit_code -eq 124 ]; then
log "ERROR: Snap installation timed out after 5 minutes"
return 1
elif [ $exit_code -ne 0 ]; then
log "ERROR: Snap installation failed with exit code $exit_code"
return 1
fi
log "Successfully installed $pkg via Snap"
return 0
}
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"
}
install_npm_package() {
local pkg="$1"
log "Installing $pkg via NPM..."
# Ensure Node.js and npm are installed
if ! command -v npm >/dev/null 2>&1; then
ui_msg "Node.js Required" "Installing Node.js and npm first...\n\n⏳ This process is automatic - please wait..."
install_nodejs
if [[ $? -ne 0 ]]; then
log_error "Failed to install Node.js/npm prerequisite"
return 1
fi
fi
sudo npm install -g "$pkg" 2>&1 | tee -a "$LOGFILE"
}
remove_npm_package() {
local pkg="$1"
log "Removing $pkg via NPM..."
sudo npm uninstall -g "$pkg" 2>&1 | tee -a "$LOGFILE"
}
install_pip_package() {
local pkg="$1"
log "Installing $pkg via pip..."
# Ensure Python3 and pip are installed
if ! command -v pip3 >/dev/null 2>&1; then
ui_msg "Python3 Required" "Installing Python3 and pip first...\n\n⏳ This process is automatic - please wait..."
apt_update
install_apt_package "python3-pip"
if [[ $? -ne 0 ]]; then
log_error "Failed to install Python3/pip prerequisite"
return 1
fi
fi
pip3 install --user "$pkg" 2>&1 | tee -a "$LOGFILE"
}
remove_pip_package() {
local pkg="$1"
log "Removing $pkg via pip..."
pip3 uninstall -y "$pkg" 2>/dev/null || true
}
install_cargo_package() {
local pkg="$1"
log "Installing $pkg via Cargo..."
# Ensure Rust and Cargo are installed
if ! command -v cargo >/dev/null 2>&1; then
ui_msg "Rust Required" "Installing Rust and Cargo first...\n\n⏳ This process is automatic - please wait..."
# Install Rust via rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 2>&1 | tee -a "$LOGFILE"
# Source the cargo environment
source "$HOME/.cargo/env" 2>/dev/null || true
if ! command -v cargo >/dev/null 2>&1; then
log_error "Failed to install Rust/Cargo prerequisite"
return 1
fi
fi
cargo install "$pkg" 2>&1 | tee -a "$LOGFILE"
}
remove_cargo_package() {
local pkg="$1"
log "Removing $pkg via Cargo..."
cargo uninstall "$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_minikube() {
log "Installing Minikube..."
local arch
arch=$(dpkg --print-architecture)
case "$arch" in
amd64) arch="amd64" ;;
arm64) arch="arm64" ;;
*) log_error "Unsupported architecture: $arch"; return 1 ;;
esac
curl -LO "https://storage.googleapis.com/minikube/releases/latest/minikube-linux-${arch}"
sudo install minikube-linux-${arch} /usr/local/bin/minikube
rm -f minikube-linux-${arch}
ui_msg "Minikube Installed" "Minikube installed successfully."
}
remove_minikube() {
sudo rm -f /usr/local/bin/minikube
rm -rf ~/.minikube 2>/dev/null || true
}
install_kind() {
log "Installing Kind..."
local arch
arch=$(dpkg --print-architecture)
case "$arch" in
amd64) arch="amd64" ;;
arm64) arch="arm64" ;;
*) log_error "Unsupported architecture: $arch"; return 1 ;;
esac
curl -Lo ./kind "https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-${arch}"
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
ui_msg "Kind Installed" "Kind installed successfully."
}
remove_kind() {
sudo rm -f /usr/local/bin/kind
}
install_ctop() {
log "Installing ctop..."
local arch
arch=$(dpkg --print-architecture)
case "$arch" in
amd64) arch="amd64" ;;
arm64) arch="arm64" ;;
*) log_error "Unsupported architecture: $arch"; return 1 ;;
esac
curl -Lo ctop "https://github.com/bcicen/ctop/releases/latest/download/ctop-0.7.7-linux-${arch}"
chmod +x ctop
sudo mv ctop /usr/local/bin/ctop
ui_msg "ctop Installed" "ctop installed successfully."
}
remove_ctop() {
sudo rm -f /usr/local/bin/ctop
}
install_lazydocker() {
log "Installing LazyDocker..."
local arch
arch=$(dpkg --print-architecture)
case "$arch" in
amd64) arch="x86_64" ;;
arm64) arch="armv6" ;;
*) log_error "Unsupported architecture: $arch"; return 1 ;;
esac
local version
version=$(curl -s https://api.github.com/repos/jesseduffield/lazydocker/releases/latest | grep -Po '"tag_name": "v\K[^"]*')
curl -Lo lazydocker.tar.gz "https://github.com/jesseduffield/lazydocker/releases/latest/download/lazydocker_${version}_Linux_${arch}.tar.gz"
tar xf lazydocker.tar.gz lazydocker
sudo mv lazydocker /usr/local/bin/
rm -f lazydocker.tar.gz
ui_msg "LazyDocker Installed" "LazyDocker installed successfully."
}
remove_lazydocker() {
sudo rm -f /usr/local/bin/lazydocker
}
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_invidious() {
log "Installing Invidious..."
# Check if Docker is installed
if ! command -v docker >/dev/null 2>&1; then
log_error "Docker is required for Invidious. Please install Docker first."
return 1
fi
# Create invidious directory
mkdir -p "$HOME/invidious"
cd "$HOME/invidious"
# Download docker-compose file
if curl -L -o docker-compose.yml https://raw.githubusercontent.com/iv-org/invidious/master/docker-compose.yml; then
ui_msg "Invidious Installed" "Invidious Docker setup downloaded to $HOME/invidious. Run 'docker-compose up -d' to start."
else
log_error "Failed to download Invidious docker-compose file"
return 1
fi
}
remove_invidious() {
log "Removing Invidious..."
if [ -d "$HOME/invidious" ]; then
cd "$HOME/invidious"
docker-compose down 2>/dev/null || true
cd "$HOME"
rm -rf "$HOME/invidious"
fi
}
install_seafile() {
log "Installing Seafile..."
# Check if Docker is installed
if ! command -v docker >/dev/null 2>&1; then
log_error "Docker is required for Seafile. Please install Docker first."
return 1
fi
# Create seafile directory
mkdir -p "$HOME/seafile"
cd "$HOME/seafile"
# Download docker-compose file for Seafile
if curl -L -o docker-compose.yml https://raw.githubusercontent.com/haiwen/seafile-docker/master/docker-compose.yml; then
ui_msg "Seafile Installed" "Seafile Docker setup downloaded to $HOME/seafile. Run 'docker-compose up -d' to start."
else
log_error "Failed to download Seafile docker-compose file"
return 1
fi
}
remove_seafile() {
log "Removing Seafile..."
if [ -d "$HOME/seafile" ]; then
cd "$HOME/seafile"
docker-compose down 2>/dev/null || true
cd "$HOME"
rm -rf "$HOME/seafile"
fi
}
install_snapraid() {
log "Installing SnapRAID..."
# Install dependencies
sudo apt update
sudo apt install -y build-essential
# Download and compile SnapRAID
cd /tmp
if wget https://github.com/amadvance/snapraid/releases/download/v12.2/snapraid-12.2.tar.gz; then
tar -xzf snapraid-12.2.tar.gz
cd snapraid-12.2
./configure
make
sudo make install
ui_msg "SnapRAID Installed" "SnapRAID parity protection tool installed successfully."
else
log_error "Failed to download SnapRAID"
return 1
fi
}
remove_snapraid() {
log "Removing SnapRAID..."
sudo rm -f /usr/local/bin/snapraid
sudo rm -f /usr/local/share/man/man1/snapraid.1
}
install_greyhole() {
log "Installing Greyhole..."
# Check if Samba is installed
if ! command -v smbd >/dev/null 2>&1; then
log_error "Samba is required for Greyhole. Please install Samba first."
return 1
fi
# Add Greyhole PPA and install
sudo add-apt-repository -y ppa:greyhole/greyhole
sudo apt update
if sudo apt install -y greyhole; then
ui_msg "Greyhole Installed" "Greyhole storage pooling installed. Configure /etc/greyhole.conf before use."
else
log_error "Failed to install Greyhole"
return 1
fi
}
remove_greyhole() {
log "Removing Greyhole..."
sudo apt remove -y greyhole 2>/dev/null || true
sudo add-apt-repository -r ppa:greyhole/greyhole 2>/dev/null || true
}
install_mergerfs() {
log "Installing MergerFS..."
# Download and install MergerFS .deb package
cd /tmp
if wget https://github.com/trapexit/mergerfs/releases/download/2.35.1/mergerfs_2.35.1.ubuntu-jammy_amd64.deb; then
if sudo dpkg -i mergerfs_2.35.1.ubuntu-jammy_amd64.deb; then
ui_msg "MergerFS Installed" "MergerFS union filesystem installed successfully."
else
sudo apt install -f -y
ui_msg "MergerFS Installed" "MergerFS union filesystem installed successfully."
fi
else
log_error "Failed to download MergerFS"
return 1
fi
}
remove_mergerfs() {
log "Removing MergerFS..."
sudo apt remove -y mergerfs 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_plex() {
log "Installing Plex Media Server..."
# Add Plex repository key
curl https://downloads.plex.tv/plex-keys/PlexSign.key | sudo apt-key add -
# Add Plex repository
echo "deb https://downloads.plex.tv/repo/deb public main" | sudo tee /etc/apt/sources.list.d/plexmediaserver.list
# Update package list and install Plex
if sudo apt update && sudo apt install -y plexmediaserver; then
# Enable and start Plex service
sudo systemctl enable plexmediaserver
sudo systemctl start plexmediaserver
ui_msg "Plex Installed" "Plex Media Server installed successfully. Access at http://localhost:32400/web"
else
log_error "Failed to install Plex Media Server"
return 1
fi
}
remove_plex() {
log "Removing Plex Media Server..."
sudo systemctl stop plexmediaserver 2>/dev/null || true
sudo systemctl disable plexmediaserver 2>/dev/null || true
remove_apt_package "plexmediaserver"
sudo rm -f /etc/apt/sources.list.d/plexmediaserver.list
sudo apt-key del 7BB9C367 2>/dev/null || true
}
install_ums() {
log "Installing Universal Media Server (UMS)..."
# Check if Java is installed
if ! command -v java >/dev/null 2>&1; then
log_error "Java is required for UMS. Installing OpenJDK..."
sudo apt update && sudo apt install -y openjdk-11-jre
fi
# Download UMS DEB package
local ums_url="https://github.com/UniversalMediaServer/UniversalMediaServer/releases/latest/download/ums.deb"
local temp_file="/tmp/ums.deb"
if wget -O "$temp_file" "$ums_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 "UMS Installed" "Universal Media Server installed successfully. Access at http://localhost:5001"
else
log_error "Failed to install UMS DEB package"
rm -f "$temp_file"
return 1
fi
else
log_error "Failed to download UMS DEB package"
return 1
fi
}
remove_ums() {
log "Removing Universal Media Server..."
remove_apt_package "ums"
}
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
}
install_lm-studio() {
log "Installing LM Studio..."
# Download LM Studio AppImage
local lm_studio_url="https://releases.lmstudio.ai/linux/x86/0.3.5/LM_Studio-0.3.5.AppImage"
local temp_file="/tmp/LM_Studio.AppImage"
if wget -O "$temp_file" "$lm_studio_url" 2>/dev/null; then
chmod +x "$temp_file"
sudo mv "$temp_file" /usr/local/bin/lm-studio
ui_msg "LM Studio Installed" "LM Studio GUI for local LLMs installed successfully.\n\nRun with: lm-studio"
else
log_error "Failed to download LM Studio"
return 1
fi
}
remove_lm-studio() {
log "Removing LM Studio..."
sudo rm -f /usr/local/bin/lm-studio
}
install_text-generation-webui() {
log "Installing Text Generation WebUI..."
# Clone the repository
local install_dir="/opt/text-generation-webui"
if git clone https://github.com/oobabooga/text-generation-webui.git "$install_dir" 2>/dev/null; then
cd "$install_dir"
# Install dependencies
pip3 install -r requirements.txt 2>/dev/null || true
# Create launcher script
sudo tee /usr/local/bin/text-generation-webui > /dev/null << 'EOF'
#!/bin/bash
cd /opt/text-generation-webui
python3 server.py "$@"
EOF
sudo chmod +x /usr/local/bin/text-generation-webui
ui_msg "Text Generation WebUI Installed" "Text Generation WebUI installed successfully.\n\nRun with: text-generation-webui"
else
log_error "Failed to clone Text Generation WebUI"
return 1
fi
}
remove_text-generation-webui() {
log "Removing Text Generation WebUI..."
sudo rm -rf /opt/text-generation-webui
sudo rm -f /usr/local/bin/text-generation-webui
}
install_whisper-cpp() {
log "Installing Whisper.cpp..."
# Install build dependencies
install_apt_package "build-essential"
install_apt_package "cmake"
# Clone and build whisper.cpp
local build_dir="/tmp/whisper.cpp"
if git clone https://github.com/ggerganov/whisper.cpp.git "$build_dir" 2>/dev/null; then
cd "$build_dir"
make -j$(nproc) 2>/dev/null
# Install binaries
sudo cp main /usr/local/bin/whisper-main
sudo cp quantize /usr/local/bin/whisper-quantize
sudo cp server /usr/local/bin/whisper-server
# Create convenience script
sudo tee /usr/local/bin/whisper-cpp > /dev/null << 'EOF'
#!/bin/bash
whisper-main "$@"
EOF
sudo chmod +x /usr/local/bin/whisper-cpp
# Clean up
rm -rf "$build_dir"
ui_msg "Whisper.cpp Installed" "Whisper.cpp offline speech-to-text installed successfully.\n\nRun with: whisper-cpp"
else
log_error "Failed to clone Whisper.cpp"
return 1
fi
}
remove_whisper-cpp() {
log "Removing Whisper.cpp..."
sudo rm -f /usr/local/bin/whisper-main
sudo rm -f /usr/local/bin/whisper-quantize
sudo rm -f /usr/local/bin/whisper-server
sudo rm -f /usr/local/bin/whisper-cpp
}
install_comfyui() {
log "Installing ComfyUI..."
# Clone ComfyUI repository
local install_dir="/opt/ComfyUI"
if git clone https://github.com/comfyanonymous/ComfyUI.git "$install_dir" 2>/dev/null; then
cd "$install_dir"
# Install Python dependencies
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu 2>/dev/null || true
pip3 install -r requirements.txt 2>/dev/null || true
# Create launcher script
sudo tee /usr/local/bin/comfyui > /dev/null << 'EOF'
#!/bin/bash
cd /opt/ComfyUI
python3 main.py "$@"
EOF
sudo chmod +x /usr/local/bin/comfyui
ui_msg "ComfyUI Installed" "ComfyUI visual Stable Diffusion interface installed successfully.\n\nRun with: comfyui"
else
log_error "Failed to clone ComfyUI"
return 1
fi
}
remove_comfyui() {
log "Removing ComfyUI..."
sudo rm -rf /opt/ComfyUI
sudo rm -f /usr/local/bin/comfyui
}
install_invokeai() {
log "Installing InvokeAI..."
# Install InvokeAI via pip
if pip3 install invokeai[xformers] --upgrade 2>/dev/null; then
ui_msg "InvokeAI Installed" "InvokeAI Stable Diffusion generator installed successfully.\n\nRun with: invokeai-web"
else
log_error "Failed to install InvokeAI"
return 1
fi
}
remove_invokeai() {
log "Removing InvokeAI..."
pip3 uninstall -y invokeai 2>/dev/null || true
}
install_koboldcpp() {
log "Installing KoboldCpp..."
# Download KoboldCpp binary
local kobold_url="https://github.com/LostRuins/koboldcpp/releases/latest/download/koboldcpp-linux-x64"
local temp_file="/tmp/koboldcpp"
if wget -O "$temp_file" "$kobold_url" 2>/dev/null; then
chmod +x "$temp_file"
sudo mv "$temp_file" /usr/local/bin/koboldcpp
ui_msg "KoboldCpp Installed" "KoboldCpp LLM interface installed successfully.\n\nRun with: koboldcpp"
else
log_error "Failed to download KoboldCpp"
return 1
fi
}
remove_koboldcpp() {
log "Removing KoboldCpp..."
sudo rm -f /usr/local/bin/koboldcpp
}
install_phpmyadmin() {
log "Installing phpMyAdmin with non-interactive configuration..."
# Ensure MariaDB/MySQL is running and properly configured
if ! systemctl is-active --quiet mariadb && ! systemctl is-active --quiet mysql; then
ui_msg "Database Required" "MariaDB/MySQL must be installed and running first.\n\nInstall MariaDB from the Web Stack category, then try phpMyAdmin again."
return 1
fi
# FIRST: Read the MySQL root password from file if it exists
local mysql_root_pass=""
if sudo test -f "/root/.mysql_root_password"; then
mysql_root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null | tr -d '\n\r')
if [[ -n "$mysql_root_pass" ]]; then
log "Read MySQL root password from /root/.mysql_root_password"
else
log "Password file exists but is empty"
fi
else
log "No MySQL root password file found at /root/.mysql_root_password"
fi
# If no password file exists, check if we can connect without password
if [[ -z "$mysql_root_pass" ]]; then
log "No MySQL root password file found, checking if MySQL allows passwordless root access..."
if mysql -u root -e "SELECT 1;" >/dev/null 2>&1; then
log "MySQL allows passwordless root access, will set a password"
else
ui_msg "MySQL Setup Required" "MySQL root password needs to be configured.\n\nPlease run the MariaDB installation from Web Stack first, or manually configure MySQL root access."
return 1
fi
fi
# Now handle MySQL authentication setup
if [[ -n "$mysql_root_pass" ]]; then
# We have a password, test if it works
if mysql -u root -p"$mysql_root_pass" -e "SELECT 1;" >/dev/null 2>&1; then
log "MySQL root password verified successfully"
else
log "Existing MySQL root password failed verification"
ui_msg "MySQL Authentication Error" "The stored MySQL root password is not working.\n\nPlease reset the MariaDB root password from the Database Management section first."
return 1
fi
else
# No password exists, set one up (fresh install scenario)
log "Setting up MySQL root authentication for fresh install..."
mysql_root_pass=$(openssl rand -base64 32)
mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '$mysql_root_pass';" 2>/dev/null || \
mysql -u root -e "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('$mysql_root_pass');" 2>/dev/null || \
mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('$mysql_root_pass') WHERE User='root'; FLUSH PRIVILEGES;" 2>/dev/null
# Save the password
echo "$mysql_root_pass" | sudo tee /root/.mysql_root_password > /dev/null
sudo chmod 600 /root/.mysql_root_password
log "New MySQL root password set and saved"
fi
# Pre-configure phpMyAdmin with minimal debconf configuration
# Clear any existing debconf selections first
sudo debconf-communicate <<< "PURGE phpmyadmin" 2>/dev/null || true
# Use only the essential debconf questions that actually exist
echo "phpmyadmin phpmyadmin/dbconfig-install boolean true" | sudo debconf-set-selections
echo "phpmyadmin phpmyadmin/reconfigure-webserver multiselect " | sudo debconf-set-selections
# Set MySQL root password via debconf database directly
echo "phpmyadmin phpmyadmin/mysql/admin-pass password $mysql_root_pass" | sudo debconf-set-selections 2>/dev/null || true
# Alternative approach: Set MySQL root password via environment and config file
export MYSQL_ROOT_PASSWORD="$mysql_root_pass"
# Install phpMyAdmin with simplified approach
apt_update
# Set environment variables for non-interactive installation
export DEBIAN_FRONTEND=noninteractive
# Create MySQL defaults file for phpMyAdmin configuration
local mysql_defaults_file="/tmp/mysql_defaults_phpmyadmin.cnf"
sudo tee "$mysql_defaults_file" > /dev/null <<EOF
[mysql]
user=root
password=$mysql_root_pass
host=localhost
[mysqldump]
user=root
password=$mysql_root_pass
host=localhost
EOF
sudo chmod 600 "$mysql_defaults_file"
# Pre-configure MySQL for phpMyAdmin setup
mysql --defaults-file="$mysql_defaults_file" -e "
CREATE DATABASE IF NOT EXISTS phpmyadmin;
GRANT ALL PRIVILEGES ON phpmyadmin.* TO 'root'@'localhost';
FLUSH PRIVILEGES;
" 2>/dev/null || true
# Install phpMyAdmin with minimal interaction
if sudo DEBIAN_FRONTEND=noninteractive apt-get install -y phpmyadmin php-mbstring php-zip php-gd php-json php-curl 2>&1 | tee -a "$LOGFILE"; then
# Clean up temporary config file
sudo rm -f "$mysql_defaults_file"
# Detect web server and configure accordingly
local web_server=""
if systemctl is-active --quiet nginx; then
web_server="nginx"
elif systemctl is-active --quiet apache2; then
web_server="apache2"
fi
case "$web_server" in
nginx)
# Configure phpMyAdmin for Nginx (fix variable expansion)
local nginx_config="/etc/nginx/sites-available/phpmyadmin"
sudo tee "$nginx_config" > /dev/null << EOF
server {
listen 80;
server_name phpmyadmin.localhost;
root /usr/share/phpmyadmin;
index index.php index.html index.htm;
location / {
try_files \$uri \$uri/ =404;
}
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;
}
location ~ /\.ht {
deny all;
}
}
EOF
# Enable the site
sudo ln -sf "$nginx_config" /etc/nginx/sites-enabled/
if sudo nginx -t 2>/dev/null; then
sudo systemctl reload nginx
ui_msg "phpMyAdmin Installed" "✅ phpMyAdmin installed successfully!\n\n🌐 Access via: http://phpmyadmin.localhost\n📁 Files located at: /usr/share/phpmyadmin\n🔑 MySQL root password saved to: /root/.mysql_root_password\n\n⚠ Note: Configure your hosts file or DNS to point phpmyadmin.localhost to this server."
else
ui_msg "phpMyAdmin Installed" "✅ phpMyAdmin installed successfully!\n\n📁 Files located at: /usr/share/phpmyadmin\n🔑 MySQL root password saved to: /root/.mysql_root_password\n\n⚠ Nginx configuration has errors. Please check manually."
fi
;;
apache2)
# Configure phpMyAdmin for Apache
sudo ln -sf /etc/phpmyadmin/apache.conf /etc/apache2/conf-available/phpmyadmin.conf
sudo a2enconf phpmyadmin
sudo systemctl reload apache2
ui_msg "phpMyAdmin Installed" "✅ phpMyAdmin installed successfully!\n\n🌐 Access via: http://localhost/phpmyadmin\n📁 Files located at: /usr/share/phpmyadmin\n🔑 MySQL root password saved to: /root/.mysql_root_password\n\n🔧 Apache configuration enabled automatically."
;;
*)
ui_msg "phpMyAdmin Installed" "✅ phpMyAdmin installed successfully!\n\n📁 Files located at: /usr/share/phpmyadmin\n🔑 MySQL root password saved to: /root/.mysql_root_password\n\n⚠ Manual web server configuration required:\n• For Nginx: Configure server block\n• For Apache: Enable phpmyadmin.conf\n• Access via your web server setup"
;;
esac
log "phpMyAdmin installed successfully"
return 0
else
# Clean up temporary config file on failure
sudo rm -f "$mysql_defaults_file"
log_error "Failed to install phpMyAdmin"
ui_msg "Installation Failed" "❌ phpMyAdmin installation failed.\n\nCheck the log file for details:\n$LOGFILE\n\nCommon issues:\n• MySQL/MariaDB not properly configured\n• Network connectivity problems\n• Package repository issues"
return 1
fi
}
install_adminer() {
log "Installing Adminer with database credential integration..."
# Ensure web server is available
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 "Web Server Required" "A web server (Nginx or Apache) must be installed and running first.\n\nInstall a web server from the Web Stack category, then try Adminer again."
return 1
fi
# Install PHP if not already installed
if ! command -v php >/dev/null 2>&1; then
ui_msg "PHP Required" "PHP must be installed first.\n\nInstall PHP from the Web Stack category, then try Adminer again."
return 1
fi
# Create adminer directory
local adminer_dir="/var/www/adminer"
sudo mkdir -p "$adminer_dir"
# Download latest Adminer
log "Downloading latest Adminer..."
if sudo wget -O "$adminer_dir/index.php" "https://www.adminer.org/latest.php" 2>&1 | tee -a "$LOGFILE"; then
sudo chown -R www-data:www-data "$adminer_dir"
sudo chmod 644 "$adminer_dir/index.php"
# Configure web server
case "$web_server" in
nginx)
# Configure Adminer for Nginx
local nginx_config="/etc/nginx/sites-available/adminer"
sudo tee "$nginx_config" > /dev/null << EOF
server {
listen 80;
server_name adminer.localhost;
root $adminer_dir;
index index.php;
location / {
try_files \$uri \$uri/ =404;
}
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;
}
location ~ /\.ht {
deny all;
}
}
EOF
# Enable the site
sudo ln -sf "$nginx_config" /etc/nginx/sites-enabled/
if sudo nginx -t 2>/dev/null; then
sudo systemctl reload nginx
show_adminer_success_message "nginx"
else
ui_msg "Adminer Installed" "✅ Adminer installed successfully!\n\n📁 Files located at: $adminer_dir\n\n⚠ Nginx configuration has errors. Please check manually."
fi
;;
apache2)
# Configure Adminer for Apache
local apache_config="/etc/apache2/sites-available/adminer.conf"
sudo tee "$apache_config" > /dev/null << EOF
<VirtualHost *:80>
ServerName adminer.localhost
DocumentRoot $adminer_dir
<Directory $adminer_dir>
AllowOverride All
Require all granted
</Directory>
ErrorLog \${APACHE_LOG_DIR}/adminer_error.log
CustomLog \${APACHE_LOG_DIR}/adminer_access.log combined
</VirtualHost>
EOF
sudo a2ensite adminer.conf
sudo systemctl reload apache2
show_adminer_success_message "apache2"
;;
esac
log "Adminer installed successfully"
return 0
else
log_error "Failed to download Adminer"
return 1
fi
}
show_adminer_success_message() {
local web_server="$1"
local access_url=""
local config_note=""
case "$web_server" in
nginx)
access_url="http://adminer.localhost"
config_note="⚠️ Note: Configure your hosts file or DNS to point adminer.localhost to this server."
;;
apache2)
access_url="http://adminer.localhost"
config_note="🔧 Apache virtual host configured automatically."
;;
esac
# Get database credentials if available
local creds_info=""
if [[ -f "/root/.mysql_root_password" ]]; then
local root_pass
root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null)
if [[ -n "$root_pass" ]]; then
creds_info="\n🔑 Available Database Credentials:\n"
creds_info+="• Server: localhost\n"
creds_info+="• Username: root\n"
creds_info+="• Password: $root_pass\n"
creds_info+="• Database: (select from dropdown)\n\n"
creds_info+="💡 These credentials are also available in Database Management > Show Credentials"
fi
elif sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then
creds_info="\n🔑 Database Access Available:\n"
creds_info+="• Server: localhost\n"
creds_info+="• Username: root\n"
creds_info+="• Password: (leave empty - using system auth)\n"
creds_info+="• Database: (select from dropdown)\n\n"
creds_info+="💡 Or set a password in Database Management > Reset Root Password"
fi
ui_msg "Adminer Installed" "✅ Adminer installed successfully!\n\n🌐 Access via: $access_url\n📁 Files located at: /var/www/adminer$creds_info\n\n$config_note"
}
remove_adminer() {
log "Removing Adminer..."
# Remove Apache configuration if exists
if [[ -f /etc/apache2/sites-enabled/adminer.conf ]]; then
sudo a2dissite adminer.conf
sudo systemctl reload apache2 2>/dev/null || true
fi
# Remove Nginx configuration if exists
if [[ -f /etc/nginx/sites-enabled/adminer ]]; then
sudo rm -f /etc/nginx/sites-enabled/adminer
sudo nginx -t && sudo systemctl reload nginx 2>/dev/null || true
fi
# Remove the files
if sudo rm -rf /var/www/adminer 2>&1 | tee -a "$LOGFILE"; then
log "Successfully removed Adminer"
return 0
else
log "ERROR: Failed to remove Adminer"
return 1
fi
}
remove_phpmyadmin() {
log "Removing phpMyAdmin..."
# Set debconf to non-interactive mode
export DEBIAN_FRONTEND=noninteractive
# Preseed debconf to handle removal prompts automatically
log "Preseeding debconf for non-interactive removal..."
sudo debconf-set-selections << EOF
phpmyadmin phpmyadmin/dbconfig-remove boolean true
phpmyadmin phpmyadmin/purge boolean true
phpmyadmin phpmyadmin/dbconfig-upgrade boolean true
phpmyadmin phpmyadmin/remove-error select ignore
phpmyadmin phpmyadmin/database-removal-error select ignore
phpmyadmin phpmyadmin/mysql/admin-user string root
phpmyadmin phpmyadmin/mysql/admin-pass password
phpmyadmin phpmyadmin/internal/skip-preseed boolean true
EOF
# Remove Apache configuration if exists
if [[ -f /etc/apache2/conf-enabled/phpmyadmin.conf ]]; then
log "Removing Apache phpMyAdmin configuration..."
sudo a2disconf phpmyadmin 2>/dev/null || true
sudo systemctl reload apache2 2>/dev/null || true
fi
# Remove Nginx configuration if exists
if [[ -f /etc/nginx/sites-enabled/phpmyadmin ]]; then
log "Removing Nginx phpMyAdmin configuration..."
sudo rm -f /etc/nginx/sites-enabled/phpmyadmin
sudo rm -f /etc/nginx/sites-available/phpmyadmin 2>/dev/null || true
sudo nginx -t && sudo systemctl reload nginx 2>/dev/null || true
fi
# Stop any phpMyAdmin related services
sudo systemctl stop apache2 2>/dev/null || true
sudo systemctl stop nginx 2>/dev/null || true
# Try to stop MySQL/MariaDB to avoid database connection issues
log "Temporarily stopping database services to avoid connection errors..."
sudo systemctl stop mysql 2>/dev/null || true
sudo systemctl stop mariadb 2>/dev/null || true
# Force remove the package with all configurations
log "Removing phpMyAdmin package and configurations..."
# First attempt: Standard removal with ignore database errors
if sudo apt-get remove --purge -y phpmyadmin phpmyadmin-* 2>&1 | tee -a "$LOGFILE"; then
log "Package removal completed successfully"
else
log "Standard removal failed, attempting force cleanup..."
# Second attempt: Force removal bypassing debconf
sudo DEBIAN_FRONTEND=noninteractive apt-get remove --purge -y --force-yes phpmyadmin phpmyadmin-* 2>/dev/null || true
# Third attempt: Direct dpkg force removal
sudo dpkg --remove --force-remove-reinstreq phpmyadmin 2>/dev/null || true
sudo dpkg --purge --force-remove-reinstreq phpmyadmin 2>/dev/null || true
# Fourth attempt: Manual cleanup of all phpMyAdmin files
log "Performing manual cleanup of phpMyAdmin files..."
sudo rm -rf /etc/phpmyadmin 2>/dev/null || true
sudo rm -rf /usr/share/phpmyadmin 2>/dev/null || true
sudo rm -rf /var/lib/phpmyadmin 2>/dev/null || true
sudo rm -rf /etc/apache2/conf-available/phpmyadmin.conf 2>/dev/null || true
sudo rm -rf /etc/apache2/conf-enabled/phpmyadmin.conf 2>/dev/null || true
# Remove from dpkg status if still listed
sudo dpkg --force-all --purge phpmyadmin 2>/dev/null || true
fi
# Clean up debconf database entries
log "Cleaning up debconf entries..."
sudo debconf-communicate << EOF || true
PURGE phpmyadmin
EOF
# Additional debconf cleanup
sudo debconf-show phpmyadmin 2>/dev/null | while read line; do
key=$(echo "$line" | cut -d: -f1 | tr -d ' ')
sudo debconf-set-selections <<< "$key PURGE" 2>/dev/null || true
done
# Remove any remaining configuration files
sudo rm -rf /etc/phpmyadmin 2>/dev/null || true
sudo rm -rf /usr/share/phpmyadmin 2>/dev/null || true
sudo rm -rf /var/lib/phpmyadmin 2>/dev/null || true
# Clean up package cache
sudo apt-get autoremove -y 2>/dev/null || true
sudo apt-get autoclean 2>/dev/null || true
# Restart database services
log "Restarting database services..."
if systemctl is-enabled mysql 2>/dev/null; then
sudo systemctl start mysql 2>/dev/null || true
fi
if systemctl is-enabled mariadb 2>/dev/null; then
sudo systemctl start mariadb 2>/dev/null || true
fi
# Restart web servers
if systemctl is-enabled apache2 2>/dev/null; then
sudo systemctl start apache2 2>/dev/null || true
fi
if systemctl is-enabled nginx 2>/dev/null; then
sudo systemctl start nginx 2>/dev/null || true
fi
# Reset DEBIAN_FRONTEND
unset DEBIAN_FRONTEND
log "phpMyAdmin removal completed successfully"
return 0
}
install_bruno() {
log "Installing Bruno API client..."
# Create keyrings directory
sudo mkdir -p /etc/apt/keyrings
# Update and install GPG and curl
sudo apt update && sudo apt install -y gpg curl
# Add the Bruno repository key
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" | gpg --dearmor | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
# Add the Bruno repository
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
# Update and install Bruno
if sudo apt update && sudo apt install -y bruno; then
ui_msg "Bruno Installed" "Bruno API client installed successfully via APT repository."
return 0
else
log_error "Failed to install Bruno via APT"
return 1
fi
}
remove_bruno() {
log "Removing Bruno..."
# Remove the package
sudo apt remove --purge -y bruno
# Remove repository and key
sudo rm -f /etc/apt/sources.list.d/bruno.list
sudo rm -f /etc/apt/keyrings/bruno.gpg
# Update package list
sudo apt update
log "Bruno removed successfully"
return 0
}
install_yaak() {
log "Installing Yaak API client..."
local yaak_url="https://github.com/mountain-loop/yaak/releases/download/v2025.7.3/yaak_2025.7.3_amd64.deb"
local temp_file="/tmp/yaak.deb"
# Download the .deb file
if wget -O "$temp_file" "$yaak_url" 2>/dev/null; then
# Install the .deb package
if sudo dpkg -i "$temp_file" 2>/dev/null || sudo apt-get install -f -y; then
rm -f "$temp_file"
ui_msg "Yaak Installed" "Yaak API client installed successfully from GitHub release."
return 0
else
rm -f "$temp_file"
log_error "Failed to install Yaak .deb package"
return 1
fi
else
log_error "Failed to download Yaak from $yaak_url"
return 1
fi
}
remove_yaak() {
log "Removing Yaak..."
# Remove the package
sudo apt remove --purge -y yaak
log "Yaak removed successfully"
return 0
}
install_caddy() {
log "Installing Caddy web server..."
# Install dependencies
sudo apt update
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
# Add Caddy repository key
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
# Add Caddy repository
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
# Update and install Caddy
sudo apt update
install_apt_package "caddy"
ui_msg "Caddy Installed" "Caddy web server installed successfully."
}
remove_caddy() {
remove_apt_package "caddy"
sudo rm -f /etc/apt/sources.list.d/caddy-stable.list
sudo rm -f /usr/share/keyrings/caddy-stable-archive-keyring.gpg
}
install_ngrok() {
log "Installing ngrok..."
local arch
arch=$(dpkg --print-architecture)
case "$arch" in
amd64) arch="amd64" ;;
arm64) arch="arm64" ;;
*) log_error "Unsupported architecture: $arch"; return 1 ;;
esac
# Download and install ngrok
curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list
sudo apt update
install_apt_package "ngrok"
ui_msg "ngrok Installed" "ngrok installed successfully. Run 'ngrok config add-authtoken <token>' to authenticate."
}
remove_ngrok() {
remove_apt_package "ngrok"
sudo rm -f /etc/apt/sources.list.d/ngrok.list
sudo rm -f /etc/apt/trusted.gpg.d/ngrok.asc
}
install_mailhog() {
log "Installing MailHog..."
local arch
arch=$(dpkg --print-architecture)
case "$arch" in
amd64) arch="amd64" ;;
arm64) arch="arm64" ;;
*) log_error "Unsupported architecture: $arch"; return 1 ;;
esac
# Download MailHog binary
curl -Lo mailhog "https://github.com/mailhog/MailHog/releases/latest/download/MailHog_linux_${arch}"
chmod +x mailhog
sudo mv mailhog /usr/local/bin/mailhog
# Create systemd service
sudo tee /etc/systemd/system/mailhog.service > /dev/null << EOF
[Unit]
Description=MailHog Email Testing Tool
After=network.target
[Service]
Type=simple
User=mailhog
Group=mailhog
ExecStart=/usr/local/bin/mailhog
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# Create mailhog user
sudo useradd -r -s /bin/false mailhog 2>/dev/null || true
# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable mailhog
sudo systemctl start mailhog
ui_msg "MailHog Installed" "MailHog installed successfully. Web UI available at http://localhost:8025"
}
remove_mailhog() {
sudo systemctl stop mailhog 2>/dev/null || true
sudo systemctl disable mailhog 2>/dev/null || true
sudo rm -f /etc/systemd/system/mailhog.service
sudo rm -f /usr/local/bin/mailhog
sudo userdel mailhog 2>/dev/null || true
sudo systemctl daemon-reload
}
remove_rustup() {
log "Removing Rustup..."
# Remove rustup and all Rust toolchains
if command -v rustup >/dev/null 2>&1; then
rustup self uninstall -y 2>/dev/null || true
fi
# Remove any remaining Rust directories
rm -rf "$HOME/.rustup" "$HOME/.cargo" 2>/dev/null || true
log "Rustup removed successfully"
return 0
}
# AI Image Generation Tools
install_automatic1111() {
log "Installing Automatic1111 Stable Diffusion WebUI..."
local install_dir="$HOME/automatic1111"
# Install dependencies
sudo apt update && sudo apt install -y python3 python3-pip python3-venv git wget
# Clone repository
git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git "$install_dir"
cd "$install_dir"
# Make webui script executable
chmod +x webui.sh
# Create desktop entry
cat > ~/.local/share/applications/automatic1111.desktop << EOF
[Desktop Entry]
Name=Automatic1111 WebUI
Comment=Stable Diffusion Web Interface
Exec=bash -c 'cd $install_dir && ./webui.sh'
Icon=applications-graphics
Terminal=true
Type=Application
Categories=Graphics;Photography;
EOF
ui_msg "Automatic1111 Installed" "Automatic1111 WebUI installed to $install_dir. Run ./webui.sh to start."
}
remove_automatic1111() {
rm -rf "$HOME/automatic1111"
rm -f ~/.local/share/applications/automatic1111.desktop
}
install_fooocus() {
log "Installing Fooocus Stable Diffusion frontend..."
local install_dir="$HOME/fooocus"
# Install dependencies
sudo apt update && sudo apt install -y python3 python3-pip python3-venv git
# Clone repository
git clone https://github.com/lllyasviel/Fooocus.git "$install_dir"
cd "$install_dir"
# Create virtual environment and install requirements
python3 -m venv venv
source venv/bin/activate
pip install -r requirements_versions.txt
# Create desktop entry
cat > ~/.local/share/applications/fooocus.desktop << EOF
[Desktop Entry]
Name=Fooocus
Comment=Simplified Stable Diffusion Frontend
Exec=bash -c 'cd $install_dir && source venv/bin/activate && python entry_with_update.py'
Icon=applications-graphics
Terminal=true
Type=Application
Categories=Graphics;Photography;
EOF
ui_msg "Fooocus Installed" "Fooocus installed to $install_dir. Run entry_with_update.py to start."
}
remove_fooocus() {
rm -rf "$HOME/fooocus"
rm -f ~/.local/share/applications/fooocus.desktop
}
install_sd-next() {
log "Installing SD.Next (Stable Diffusion Next)..."
local install_dir="$HOME/sd-next"
# Install dependencies
sudo apt update && sudo apt install -y python3 python3-pip python3-venv git
# Clone repository
git clone https://github.com/vladmandic/automatic.git "$install_dir"
cd "$install_dir"
# Make launch script executable
chmod +x launch.py
# Create desktop entry
cat > ~/.local/share/applications/sd-next.desktop << EOF
[Desktop Entry]
Name=SD.Next
Comment=Modernized Stable Diffusion WebUI
Exec=bash -c 'cd $install_dir && python3 launch.py'
Icon=applications-graphics
Terminal=true
Type=Application
Categories=Graphics;Photography;
EOF
ui_msg "SD.Next Installed" "SD.Next installed to $install_dir. Run launch.py to start."
}
remove_sd-next() {
rm -rf "$HOME/sd-next"
rm -f ~/.local/share/applications/sd-next.desktop
}
install_kohya-ss-gui() {
log "Installing Kohya-ss GUI for model training..."
local install_dir="$HOME/kohya-ss"
# Install dependencies
sudo apt update && sudo apt install -y python3 python3-pip python3-venv git build-essential
# Clone repository
git clone https://github.com/bmaltais/kohya_ss.git "$install_dir"
cd "$install_dir"
# Make setup script executable
chmod +x setup.sh
# Create desktop entry
cat > ~/.local/share/applications/kohya-ss.desktop << EOF
[Desktop Entry]
Name=Kohya-ss GUI
Comment=Model Training GUI
Exec=bash -c 'cd $install_dir && ./gui.sh'
Icon=applications-science
Terminal=true
Type=Application
Categories=Science;Education;
EOF
ui_msg "Kohya-ss GUI Installed" "Kohya-ss GUI installed to $install_dir. Run setup.sh first, then gui.sh."
}
remove_kohya-ss-gui() {
rm -rf "$HOME/kohya-ss"
rm -f ~/.local/share/applications/kohya-ss.desktop
}
# Shell Enhancement Tools
install_oh-my-zsh() {
log "Installing Oh My Zsh..."
# Check if zsh is installed
if ! command -v zsh >/dev/null 2>&1; then
log_error "Zsh is required for Oh My Zsh. Install zsh first."
return 1
fi
# Check if Oh My Zsh is already installed
if [[ -d "$HOME/.oh-my-zsh" ]]; then
ui_msg "Already Installed" "Oh My Zsh is already installed."
return 0
fi
# Install Oh My Zsh
sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
ui_msg "Oh My Zsh Installed" "Oh My Zsh installed successfully. Restart your terminal or run 'zsh' to use it."
}
remove_oh-my-zsh() {
log "Removing Oh My Zsh..."
if [[ -d "$HOME/.oh-my-zsh" ]]; then
# Run the uninstall script if it exists
if [[ -f "$HOME/.oh-my-zsh/tools/uninstall.sh" ]]; then
sh "$HOME/.oh-my-zsh/tools/uninstall.sh" --unattended 2>/dev/null || true
fi
# Remove directory if still exists
rm -rf "$HOME/.oh-my-zsh"
# Restore original shell config if backup exists
[[ -f "$HOME/.zshrc.pre-oh-my-zsh" ]] && mv "$HOME/.zshrc.pre-oh-my-zsh" "$HOME/.zshrc"
fi
log "Oh My Zsh removed successfully"
}
install_starship() {
log "Installing Starship prompt..."
# Download and install Starship
curl -sS https://starship.rs/install.sh | sh -s -- -y
# Add to shell configs if not already present
local starship_init='eval "$(starship init zsh)"'
# Add to .zshrc if it exists and doesn't already contain starship
if [[ -f "$HOME/.zshrc" ]] && ! grep -q "starship init" "$HOME/.zshrc"; then
echo "" >> "$HOME/.zshrc"
echo "# Starship prompt" >> "$HOME/.zshrc"
echo "$starship_init" >> "$HOME/.zshrc"
fi
# Add to .bashrc if it exists and doesn't already contain starship
if [[ -f "$HOME/.bashrc" ]] && ! grep -q "starship init" "$HOME/.bashrc"; then
echo "" >> "$HOME/.bashrc"
echo "# Starship prompt" >> "$HOME/.bashrc"
echo 'eval "$(starship init bash)"' >> "$HOME/.bashrc"
fi
ui_msg "Starship Installed" "Starship prompt installed successfully. Restart your terminal to see changes."
}
remove_starship() {
log "Removing Starship..."
# Remove binary
sudo rm -f /usr/local/bin/starship
rm -f "$HOME/.cargo/bin/starship"
# Remove from shell configs
if [[ -f "$HOME/.zshrc" ]]; then
sed -i '/starship init/d' "$HOME/.zshrc" 2>/dev/null || true
sed -i '/# Starship prompt/d' "$HOME/.zshrc" 2>/dev/null || true
fi
if [[ -f "$HOME/.bashrc" ]]; then
sed -i '/starship init/d' "$HOME/.bashrc" 2>/dev/null || true
sed -i '/# Starship prompt/d' "$HOME/.bashrc" 2>/dev/null || true
fi
# Remove config directory
rm -rf "$HOME/.config/starship.toml"
log "Starship removed successfully"
}
install_zoxide() {
log "Installing Zoxide..."
# Download and install Zoxide
curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash
# Add to shell configs if not already present
local zoxide_init_zsh='eval "$(zoxide init zsh)"'
local zoxide_init_bash='eval "$(zoxide init bash)"'
# Add to .zshrc if it exists and doesn't already contain zoxide
if [[ -f "$HOME/.zshrc" ]] && ! grep -q "zoxide init" "$HOME/.zshrc"; then
echo "" >> "$HOME/.zshrc"
echo "# Zoxide - smarter cd" >> "$HOME/.zshrc"
echo "$zoxide_init_zsh" >> "$HOME/.zshrc"
fi
# Add to .bashrc if it exists and doesn't already contain zoxide
if [[ -f "$HOME/.bashrc" ]] && ! grep -q "zoxide init" "$HOME/.bashrc"; then
echo "" >> "$HOME/.bashrc"
echo "# Zoxide - smarter cd" >> "$HOME/.bashrc"
echo "$zoxide_init_bash" >> "$HOME/.bashrc"
fi
ui_msg "Zoxide Installed" "Zoxide installed successfully. Use 'z' instead of 'cd'. Restart terminal to activate."
}
remove_zoxide() {
log "Removing Zoxide..."
# Remove binary
rm -f "$HOME/.local/bin/zoxide"
# Remove from shell configs
if [[ -f "$HOME/.zshrc" ]]; then
sed -i '/zoxide init/d' "$HOME/.zshrc" 2>/dev/null || true
sed -i '/# Zoxide - smarter cd/d' "$HOME/.zshrc" 2>/dev/null || true
fi
if [[ -f "$HOME/.bashrc" ]]; then
sed -i '/zoxide init/d' "$HOME/.bashrc" 2>/dev/null || true
sed -i '/# Zoxide - smarter cd/d' "$HOME/.bashrc" 2>/dev/null || true
fi
# Remove data directory
rm -rf "$HOME/.local/share/zoxide"
log "Zoxide removed successfully"
}
install_poetry() {
log "Installing Poetry Python dependency manager..."
# Install via pip3
if pip3 install --user poetry; then
# Add to PATH if not already there
if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc 2>/dev/null || true
fi
ui_msg "Poetry Installed" "Poetry Python dependency manager installed successfully via pip3."
return 0
else
log_error "Failed to install Poetry"
return 1
fi
}
remove_poetry() {
log "Removing Poetry..."
pip3 uninstall -y poetry 2>/dev/null || true
log "Poetry removed successfully"
return 0
}
install_deno() {
log "Installing Deno JS/TS runtime..."
local deno_url="https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip"
local temp_dir="/tmp/deno_install"
local temp_file="$temp_dir/deno.zip"
mkdir -p "$temp_dir"
# Download and install Deno
if wget -O "$temp_file" "$deno_url" 2>/dev/null; then
cd "$temp_dir"
unzip -q deno.zip
sudo mv deno /usr/local/bin/
sudo chmod +x /usr/local/bin/deno
rm -rf "$temp_dir"
ui_msg "Deno Installed" "Deno JS/TS runtime installed successfully."
return 0
else
rm -rf "$temp_dir"
log_error "Failed to download Deno"
return 1
fi
}
remove_deno() {
log "Removing Deno..."
sudo rm -f /usr/local/bin/deno
log "Deno removed successfully"
return 0
}
install_bun() {
log "Installing Bun JS runtime..."
# Install via official installer
if curl -fsSL https://bun.sh/install | bash; then
# Add to PATH if not already there
if ! echo "$PATH" | grep -q "$HOME/.bun/bin"; then
echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.bashrc
echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.zshrc 2>/dev/null || true
fi
ui_msg "Bun Installed" "Bun JS runtime installed successfully."
return 0
else
log_error "Failed to install Bun"
return 1
fi
}
remove_bun() {
log "Removing Bun..."
rm -rf "$HOME/.bun" 2>/dev/null || true
log "Bun removed successfully"
return 0
}
install_mkdocs() {
log "Installing MkDocs static site generator..."
# Install via pip3
if pip3 install --user mkdocs mkdocs-material; then
ui_msg "MkDocs Installed" "MkDocs static site generator installed successfully via pip3."
return 0
else
log_error "Failed to install MkDocs"
return 1
fi
}
remove_mkdocs() {
log "Removing MkDocs..."
pip3 uninstall -y mkdocs mkdocs-material 2>/dev/null || true
log "MkDocs removed successfully"
return 0
}
install_insomnia() {
log "Installing Insomnia API client..."
local insomnia_url="https://github.com/Kong/insomnia/releases/latest/download/Insomnia.Core-8.6.1.deb"
local temp_file="/tmp/insomnia.deb"
# Download the .deb file
if wget -O "$temp_file" "$insomnia_url" 2>/dev/null; then
# Install the .deb package
if sudo dpkg -i "$temp_file" 2>/dev/null || sudo apt-get install -f -y; then
rm -f "$temp_file"
ui_msg "Insomnia Installed" "Insomnia API client installed successfully."
return 0
else
rm -f "$temp_file"
log_error "Failed to install Insomnia .deb package"
return 1
fi
else
log_error "Failed to download Insomnia"
return 1
fi
}
remove_insomnia() {
log "Removing Insomnia..."
sudo apt remove --purge -y insomnia 2>/dev/null || true
log "Insomnia removed successfully"
return 0
}
install_zed() {
log "Installing Zed code editor..."
local zed_url="https://zed.dev/api/releases/stable/latest/zed-linux-x86_64.tar.gz"
local temp_dir="/tmp/zed_install"
local temp_file="$temp_dir/zed.tar.gz"
mkdir -p "$temp_dir"
# Download and install Zed
if wget -O "$temp_file" "$zed_url" 2>/dev/null; then
cd "$temp_dir"
tar -xzf zed.tar.gz
sudo mv zed-linux-x86_64/zed /usr/local/bin/
sudo chmod +x /usr/local/bin/zed
rm -rf "$temp_dir"
ui_msg "Zed Installed" "Zed code editor installed successfully."
return 0
else
rm -rf "$temp_dir"
log_error "Failed to download Zed"
return 1
fi
}
remove_zed() {
log "Removing Zed..."
sudo rm -f /usr/local/bin/zed
log "Zed removed successfully"
return 0
}
install_localwp() {
log "Installing LocalWP..."
local localwp_url="https://cdn.localwp.com/releases-stable/6.7.0+6387/local-6.7.0-linux.deb"
local temp_file="/tmp/localwp.deb"
# Download the .deb file
if wget -O "$temp_file" "$localwp_url" 2>/dev/null; then
# Install the .deb package
if sudo dpkg -i "$temp_file" 2>/dev/null || sudo apt-get install -f -y; then
rm -f "$temp_file"
ui_msg "LocalWP Installed" "LocalWP WordPress development environment installed successfully."
return 0
else
rm -f "$temp_file"
log_error "Failed to install LocalWP .deb package"
return 1
fi
else
log_error "Failed to download LocalWP"
return 1
fi
}
remove_localwp() {
log "Removing LocalWP..."
sudo apt remove --purge -y local 2>/dev/null || true
log "LocalWP removed successfully"
return 0
}
install_devkinsta() {
log "Installing DevKinsta..."
local devkinsta_url="https://kinsta.com/devkinsta/download/devkinsta.deb"
local temp_file="/tmp/devkinsta.deb"
# Download the .deb file
if wget -O "$temp_file" "$devkinsta_url" 2>/dev/null; then
# Install the .deb package
if sudo dpkg -i "$temp_file" 2>/dev/null || sudo apt-get install -f -y; then
rm -f "$temp_file"
ui_msg "DevKinsta Installed" "DevKinsta WordPress development environment installed successfully."
return 0
else
rm -f "$temp_file"
log_error "Failed to install DevKinsta .deb package"
return 1
fi
else
log_error "Failed to download DevKinsta"
return 1
fi
}
remove_devkinsta() {
log "Removing DevKinsta..."
sudo apt remove --purge -y devkinsta 2>/dev/null || true
log "DevKinsta removed successfully"
return 0
}
install_lando() {
log "Installing Lando..."
local arch
arch=$(dpkg --print-architecture)
case "$arch" in
amd64) arch="x64" ;;
arm64) arch="arm64" ;;
*) log_error "Unsupported architecture: $arch"; return 1 ;;
esac
local lando_url="https://github.com/lando/lando/releases/latest/download/lando-linux-${arch}-stable.deb"
local temp_file="/tmp/lando.deb"
# Download the .deb file
if wget -O "$temp_file" "$lando_url" 2>/dev/null; then
# Install the .deb package
if sudo dpkg -i "$temp_file" 2>/dev/null || sudo apt-get install -f -y; then
rm -f "$temp_file"
ui_msg "Lando Installed" "Lando containerized development environment installed successfully."
return 0
else
rm -f "$temp_file"
log_error "Failed to install Lando .deb package"
return 1
fi
else
log_error "Failed to download Lando"
return 1
fi
}
remove_lando() {
log "Removing Lando..."
sudo apt remove --purge -y lando 2>/dev/null || true
log "Lando removed successfully"
return 0
}
install_ddev() {
log "Installing DDEV..."
# Install via official installer script
if curl -fsSL https://raw.githubusercontent.com/drud/ddev/master/scripts/install_ddev.sh | bash; then
ui_msg "DDEV Installed" "DDEV containerized development environment installed successfully."
return 0
else
log_error "Failed to install DDEV"
return 1
fi
}
remove_ddev() {
log "Removing DDEV..."
sudo rm -f /usr/local/bin/ddev
rm -rf ~/.ddev 2>/dev/null || true
log "DDEV removed successfully"
return 0
}
install_xampp() {
log "Installing XAMPP..."
local xampp_url="https://www.apachefriends.org/xampp-files/8.2.12/xampp-linux-x64-8.2.12-0-installer.run"
local temp_file="/tmp/xampp-installer.run"
# Download the installer
if wget -O "$temp_file" "$xampp_url" 2>/dev/null; then
chmod +x "$temp_file"
# Run installer in unattended mode
if sudo "$temp_file" --mode unattended; then
rm -f "$temp_file"
ui_msg "XAMPP Installed" "XAMPP development stack installed successfully to /opt/lampp."
return 0
else
rm -f "$temp_file"
log_error "Failed to install XAMPP"
return 1
fi
else
log_error "Failed to download XAMPP"
return 1
fi
}
remove_xampp() {
log "Removing XAMPP..."
sudo rm -rf /opt/lampp 2>/dev/null || true
log "XAMPP removed successfully"
return 0
}
# ==============================================================================
# ADDITIONAL CLI TOOLS INSTALLATION FUNCTIONS
# ==============================================================================
install_lazygit() {
log "Installing lazygit..."
local arch
case "$(uname -m)" in
x86_64) arch="x86_64" ;;
aarch64|arm64) arch="arm64" ;;
*) log_error "Unsupported architecture"; return 1 ;;
esac
local latest_url=$(curl -s https://api.github.com/repos/jesseduffield/lazygit/releases/latest | grep "browser_download_url.*Linux_${arch}.tar.gz" | cut -d '"' -f 4)
if [[ -z "$latest_url" ]]; then
log_error "Failed to get lazygit download URL"
return 1
fi
local temp_file="/tmp/lazygit.tar.gz"
if wget -O "$temp_file" "$latest_url" 2>/dev/null; then
tar -xzf "$temp_file" -C /tmp/
sudo mv /tmp/lazygit /usr/local/bin/
sudo chmod +x /usr/local/bin/lazygit
rm -f "$temp_file"
log "lazygit installed successfully"
return 0
else
log_error "Failed to download lazygit"
return 1
fi
}
install_glow() {
log "Installing glow..."
local arch
case "$(uname -m)" in
x86_64) arch="x86_64" ;;
aarch64|arm64) arch="arm64" ;;
*) log_error "Unsupported architecture"; return 1 ;;
esac
local latest_url=$(curl -s https://api.github.com/repos/charmbracelet/glow/releases/latest | grep "browser_download_url.*linux_${arch}.tar.gz" | cut -d '"' -f 4)
if [[ -z "$latest_url" ]]; then
log_error "Failed to get glow download URL"
return 1
fi
local temp_file="/tmp/glow.tar.gz"
if wget -O "$temp_file" "$latest_url" 2>/dev/null; then
tar -xzf "$temp_file" -C /tmp/
sudo mv /tmp/glow /usr/local/bin/
sudo chmod +x /usr/local/bin/glow
rm -f "$temp_file"
log "glow installed successfully"
return 0
else
log_error "Failed to download glow"
return 1
fi
}
install_cheat() {
log "Installing cheat..."
local arch
case "$(uname -m)" in
x86_64) arch="amd64" ;;
aarch64|arm64) arch="arm64" ;;
*) log_error "Unsupported architecture"; return 1 ;;
esac
local latest_url=$(curl -s https://api.github.com/repos/cheat/cheat/releases/latest | grep "browser_download_url.*linux-${arch}.gz" | cut -d '"' -f 4)
if [[ -z "$latest_url" ]]; then
log_error "Failed to get cheat download URL"
return 1
fi
local temp_file="/tmp/cheat.gz"
if wget -O "$temp_file" "$latest_url" 2>/dev/null; then
gunzip "$temp_file"
sudo mv /tmp/cheat /usr/local/bin/
sudo chmod +x /usr/local/bin/cheat
log "cheat installed successfully"
return 0
else
log_error "Failed to download cheat"
return 1
fi
}
install_broot() {
log "Installing broot..."
local arch
case "$(uname -m)" in
x86_64) arch="x86_64-linux" ;;
aarch64|arm64) arch="aarch64-linux" ;;
*) log_error "Unsupported architecture"; return 1 ;;
esac
local latest_url=$(curl -s https://api.github.com/repos/Canop/broot/releases/latest | grep "browser_download_url.*${arch}" | cut -d '"' -f 4)
if [[ -z "$latest_url" ]]; then
log_error "Failed to get broot download URL"
return 1
fi
local temp_file="/tmp/broot"
if wget -O "$temp_file" "$latest_url" 2>/dev/null; then
sudo mv "$temp_file" /usr/local/bin/broot
sudo chmod +x /usr/local/bin/broot
log "broot installed successfully"
return 0
else
log_error "Failed to download broot"
return 1
fi
}
install_dog() {
log "Installing dog..."
local arch
case "$(uname -m)" in
x86_64) arch="x86_64" ;;
aarch64|arm64) arch="aarch64" ;;
*) log_error "Unsupported architecture"; return 1 ;;
esac
local latest_url=$(curl -s https://api.github.com/repos/ogham/dog/releases/latest | grep "browser_download_url.*${arch}-unknown-linux-gnu.zip" | cut -d '"' -f 4)
if [[ -z "$latest_url" ]]; then
log_error "Failed to get dog download URL"
return 1
fi
local temp_file="/tmp/dog.zip"
if wget -O "$temp_file" "$latest_url" 2>/dev/null; then
unzip -q "$temp_file" -d /tmp/
sudo mv /tmp/bin/dog /usr/local/bin/
sudo chmod +x /usr/local/bin/dog
rm -f "$temp_file"
rm -rf /tmp/bin
log "dog installed successfully"
return 0
else
log_error "Failed to download dog"
return 1
fi
}
# ==============================================================================
# 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=$?
;;
npm)
install_npm_package "$pkg"
install_result=$?
;;
pip)
install_pip_package "$pkg"
install_result=$?
;;
cargo)
install_cargo_package "$pkg"
install_result=$?
;;
flatpak)
if ! is_apt_installed "flatpak"; then
ui_msg "Flatpak Required" "Installing Flatpak first...\n\n⏳ This process is automatic - please wait..."
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=$? ;;
invidious) install_invidious; install_result=$? ;;
n8n) install_n8n; install_result=$? ;;
gollama) install_gollama; install_result=$? ;;
discord-deb) install_discord-deb; install_result=$? ;;
phpmyadmin) install_phpmyadmin; install_result=$? ;;
adminer) install_adminer; install_result=$? ;;
bruno) install_bruno; install_result=$? ;;
yaak) install_yaak; install_result=$? ;;
rustup) install_rustup; install_result=$? ;;
poetry) install_poetry; install_result=$? ;;
deno) install_deno; install_result=$? ;;
bun) install_bun; install_result=$? ;;
mkdocs) install_mkdocs; install_result=$? ;;
insomnia) install_insomnia; install_result=$? ;;
zed) install_zed; install_result=$? ;;
localwp) install_localwp; install_result=$? ;;
devkinsta) install_devkinsta; install_result=$? ;;
lando) install_lando; install_result=$? ;;
ddev) install_ddev; install_result=$? ;;
xampp) install_xampp; install_result=$? ;;
lm-studio) install_lm-studio; install_result=$? ;;
text-generation-webui) install_text-generation-webui; install_result=$? ;;
whisper-cpp) install_whisper-cpp; install_result=$? ;;
comfyui) install_comfyui; install_result=$? ;;
invokeai) install_invokeai; install_result=$? ;;
koboldcpp) install_koboldcpp; install_result=$? ;;
minikube) install_minikube; install_result=$? ;;
kind) install_kind; install_result=$? ;;
ctop) install_ctop; install_result=$? ;;
lazydocker) install_lazydocker; install_result=$? ;;
caddy) install_caddy; install_result=$? ;;
ngrok) install_ngrok; install_result=$? ;;
mailhog) install_mailhog; install_result=$? ;;
automatic1111) install_automatic1111; install_result=$? ;;
fooocus) install_fooocus; install_result=$? ;;
sd-next) install_sd-next; install_result=$? ;;
kohya-ss-gui) install_kohya-ss-gui; install_result=$? ;;
oh-my-zsh) install_oh-my-zsh; install_result=$? ;;
starship) install_starship; install_result=$? ;;
zoxide) install_zoxide; install_result=$? ;;
seafile) install_seafile; install_result=$? ;;
snapraid) install_snapraid; install_result=$? ;;
greyhole) install_greyhole; install_result=$? ;;
mergerfs) install_mergerfs; install_result=$? ;;
plex) install_plex; install_result=$? ;;
ums) install_ums; install_result=$? ;;
lazygit) install_lazygit; install_result=$? ;;
glow) install_glow; install_result=$? ;;
cheat) install_cheat; install_result=$? ;;
broot) install_broot; install_result=$? ;;
dog) install_dog; 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=$?
;;
npm)
remove_npm_package "$pkg"
remove_result=$?
;;
cargo)
remove_cargo_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=$? ;;
invidious) remove_invidious; remove_result=$? ;;
n8n) remove_n8n; remove_result=$? ;;
gollama) remove_gollama; remove_result=$? ;;
discord-deb) remove_discord-deb; remove_result=$? ;;
phpmyadmin) remove_phpmyadmin; remove_result=$? ;;
adminer) remove_adminer; remove_result=$? ;;
bruno) remove_bruno; remove_result=$? ;;
yaak) remove_yaak; remove_result=$? ;;
rustup) remove_rustup; remove_result=$? ;;
poetry) remove_poetry; remove_result=$? ;;
deno) remove_deno; remove_result=$? ;;
bun) remove_bun; remove_result=$? ;;
mkdocs) remove_mkdocs; remove_result=$? ;;
insomnia) remove_insomnia; remove_result=$? ;;
zed) remove_zed; remove_result=$? ;;
localwp) remove_localwp; remove_result=$? ;;
devkinsta) remove_devkinsta; remove_result=$? ;;
lando) remove_lando; remove_result=$? ;;
ddev) remove_ddev; remove_result=$? ;;
xampp) remove_xampp; remove_result=$? ;;
lm-studio) remove_lm-studio; remove_result=$? ;;
text-generation-webui) remove_text-generation-webui; remove_result=$? ;;
whisper-cpp) remove_whisper-cpp; remove_result=$? ;;
comfyui) remove_comfyui; remove_result=$? ;;
invokeai) remove_invokeai; remove_result=$? ;;
koboldcpp) remove_koboldcpp; remove_result=$? ;;
minikube) remove_minikube; remove_result=$? ;;
kind) remove_kind; remove_result=$? ;;
ctop) remove_ctop; remove_result=$? ;;
lazydocker) remove_lazydocker; remove_result=$? ;;
caddy) remove_caddy; remove_result=$? ;;
ngrok) remove_ngrok; remove_result=$? ;;
mailhog) remove_mailhog; remove_result=$? ;;
automatic1111) remove_automatic1111; remove_result=$? ;;
fooocus) remove_fooocus; remove_result=$? ;;
sd-next) remove_sd-next; remove_result=$? ;;
kohya-ss-gui) remove_kohya-ss-gui; remove_result=$? ;;
oh-my-zsh) remove_oh-my-zsh; remove_result=$? ;;
starship) remove_starship; remove_result=$? ;;
zoxide) remove_zoxide; remove_result=$? ;;
seafile) remove_seafile; remove_result=$? ;;
snapraid) remove_snapraid; remove_result=$? ;;
greyhole) remove_greyhole; remove_result=$? ;;
mergerfs) remove_mergerfs; remove_result=$? ;;
plex) remove_plex; remove_result=$? ;;
ums) remove_ums; 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"
local silent="${2:-false}" # Add silent mode parameter
# Add error handling for empty or invalid buntage names
if [[ -z "$name" ]]; then
[[ "$silent" != "true" ]] && log "ERROR: Empty buntage name provided to is_package_installed"
return 1
fi
if [[ -z "${PACKAGES[$name]:-}" ]]; then
[[ "$silent" != "true" ]] && 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
[[ "$silent" != "true" ]] && log "WARNING: No method defined for buntage '$name'"
return 1
fi
[[ "$silent" != "true" ]] && 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
;;
phpmyadmin)
if is_apt_installed "phpmyadmin"; then
result=0
else
result=1
fi
;;
adminer)
if [[ -f "/var/www/adminer/index.php" ]]; 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
# ==============================================================================
# Function to refresh package cache in background without logging
refresh_cache_silent() {
local refresh_pid
{
# Update cache for all packages silently
for name in "${!PACKAGES[@]}"; do
update_package_cache "$name" "true" &>/dev/null
done
} &
refresh_pid=$!
# Store the PID for potential cleanup
echo "$refresh_pid" > "/tmp/ultrabunt_cache_refresh.pid" 2>/dev/null || true
}
# Function to check if background cache refresh is complete
is_cache_refresh_complete() {
local pid_file="/tmp/ultrabunt_cache_refresh.pid"
if [[ -f "$pid_file" ]]; then
local pid
pid=$(cat "$pid_file" 2>/dev/null)
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
return 1 # Still running
else
rm -f "$pid_file" 2>/dev/null || true
return 0 # Complete
fi
fi
return 0 # No refresh running
}
show_category_menu() {
log "Entering show_category_menu function"
# Hotkey cycling state tracking
local -A last_hotkey_selection
local -A hotkey_categories
# Build reverse mapping of hotkeys to categories for cycling
for cat_id in "${!CATEGORY_HOTKEYS[@]}"; do
local hotkey="${CATEGORY_HOTKEYS[$cat_id]}"
if [[ -n "${hotkey_categories[$hotkey]:-}" ]]; then
hotkey_categories[$hotkey]="${hotkey_categories[$hotkey]} $cat_id"
else
hotkey_categories[$hotkey]="$cat_id"
fi
done
# Add special Q hotkey handling for communication and quit
hotkey_categories["Q"]="communication quit"
# Start background cache refresh on first load
refresh_cache_silent
while true; do
log "Building category menu items..."
local menu_items=()
# System management tools at the top
log "Adding system management menu items..."
menu_items+=("system-info" "(.Y.) System Information")
menu_items+=("mirror-management" "(.Y.) Mirror Management")
menu_items+=("database-management" "(.Y.) Database Management")
menu_items+=("wordpress-setup" "(.Y.) WordPress Management")
menu_items+=("keyboard-layout" "(.Y.) Keyboard Layout Configuration")
menu_items+=("php-settings" "(.Y.) PHP Configuration")
menu_items+=("log-viewer" "(.Y.) Log Viewer")
menu_items+=("bulk-ops" "(.Y.) Bulk Operations")
menu_items+=("terminal-size" "(.Y.) Dialog Box Size Configuration")
menu_items+=("" "(_*_)")
log "Processing categories array with ${#CATEGORIES[@]} entries"
for entry in "${CATEGORIES[@]}"; do
local cat_id="${entry%%:*}"
local cat_name="${entry#*:}"
# Skip excluded categories
if [[ -n "${EXCLUDED_CATEGORIES[$cat_id]:-}" ]]; then
log "Skipping excluded category: $cat_id"
continue
fi
# 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 with silent mode
if is_package_installed "$name" "true"; then
installed=$((installed + 1))
fi
fi
done
# Add refresh indicator if cache is still updating
local refresh_indicator=""
if ! is_cache_refresh_complete; then
refresh_indicator=" 🔄"
fi
# Get hotkey for this category
local hotkey="${CATEGORY_HOTKEYS[$cat_id]:-}"
local hotkey_display=""
if [[ -n "$hotkey" ]]; then
hotkey_display="($hotkey) "
fi
menu_items+=("$cat_id" "${hotkey_display}$cat_name [$installed/$total installed]$refresh_indicator")
done
log "Adding additional menu items..."
# System management tools at the top
menu_items+=("" "(_*_)")
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:" \
$DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${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)
# Simple confirmation for quit
if [[ "$choice" == "quit" || "$choice" == "q" ]]; then
if ui_yesno "Confirm Exit" "Are you sure you want to exit Ultrabunt?"; then
break
else
continue
fi
else
break
fi
;;
terminal-size)
show_dialog_size_menu || {
log "ERROR: show_dialog_size_menu failed"
ui_msg "Error" "Failed to display dialog size menu. Please check the logs."
}
;;
system-info)
show_system_info || {
log "ERROR: show_system_info failed"
ui_msg "Error" "Failed to display system information. Please check the logs."
}
;;
mirror-management)
show_mirror_management_menu || {
log "ERROR: show_mirror_management_menu failed"
ui_msg "Error" "Failed to display mirror management menu. 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."
}
;;
php-settings)
show_php_settings_menu || {
log "ERROR: show_php_settings_menu failed"
ui_msg "Error" "Failed to display PHP settings menu. Please check the logs."
}
;;
database-management)
show_database_management_menu || {
log "ERROR: show_database_management_menu failed"
ui_msg "Error" "Failed to display database management menu. Please check the logs."
}
;;
log-viewer)
show_log_viewer_menu || {
log "ERROR: show_log_viewer_menu failed"
ui_msg "Error" "Failed to display log viewer menu. Please check the logs."
}
;;
bulk-ops)
show_bulk_operations
;;
"")
continue
;;
*)
# Handle A-Z hotkey cycling
local upper_choice="${choice^^}" # Convert to uppercase
# Check if it's a valid hotkey (A-Y, excluding Z which is for back navigation)
if [[ "$upper_choice" =~ ^[A-Y]$ ]]; then
# Get categories for this hotkey
local categories="${hotkey_categories[$upper_choice]:-}"
if [[ -n "$categories" ]]; then
# Convert categories string to array
local cat_array=($categories)
local selected_category=""
if [[ ${#cat_array[@]} -eq 1 ]]; then
# Only one category for this hotkey
selected_category="${cat_array[0]}"
else
# Multiple categories - cycle through them
local last_selected="${last_hotkey_selection[$upper_choice]:-}"
local current_index=0
# Find current index if we have a previous selection
if [[ -n "$last_selected" ]]; then
for i in "${!cat_array[@]}"; do
if [[ "${cat_array[$i]}" == "$last_selected" ]]; then
current_index=$(( (i + 1) % ${#cat_array[@]} ))
break
fi
done
fi
selected_category="${cat_array[$current_index]}"
fi
# Update last selection for this hotkey
last_hotkey_selection[$upper_choice]="$selected_category"
# Handle special cases
if [[ "$selected_category" == "quit" ]]; then
if ui_yesno "Confirm Exit" "Are you sure you want to exit Ultrabunt?"; then
break
else
continue
fi
else
show_buntage_list "$selected_category"
fi
else
# Not a valid hotkey, treat as category ID
show_buntage_list "$choice"
fi
else
# Not a hotkey, treat as category ID
show_buntage_list "$choice"
fi
;;
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:" \
$DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${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" $DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${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"
}
# ==============================================================================
# DIALOG BOX SIZE MANAGEMENT FUNCTIONS
# ==============================================================================
# Function to apply dialog preset
apply_dialog_preset() {
local preset="$1"
if [[ -n "${DIALOG_PRESETS[$preset]:-}" ]]; then
local preset_values=(${DIALOG_PRESETS[$preset]})
DIALOG_HEIGHT="${preset_values[0]}"
DIALOG_WIDTH="${preset_values[1]}"
DIALOG_MENU_HEIGHT="${preset_values[2]}"
CURRENT_DIALOG_PRESET="$preset"
log "Applied dialog preset: $preset (${DIALOG_HEIGHT}x${DIALOG_WIDTH}, menu: ${DIALOG_MENU_HEIGHT})"
fi
}
# Function to get current dialog dimensions
get_dialog_dimensions() {
echo "$DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT"
}
show_dialog_size_menu() {
log "Entering show_dialog_size_menu function"
while true; do
# Get current dialog size info
local current_info="Current: ${CURRENT_DIALOG_PRESET} (${DIALOG_HEIGHT}×${DIALOG_WIDTH})"
local menu_items=()
menu_items+=("compact" "(.Y.) Compact (20×70) - Minimal space")
menu_items+=("standard" "(.Y.) Standard (24×80) - Default size")
menu_items+=("large" "(.Y.) Large (30×100) - More content")
menu_items+=("wide" "(.Y.) Wide (24×120) - Wider menus")
menu_items+=("tall" "(.Y.) Tall (35×80) - More menu items")
menu_items+=("huge" "(.Y.) Huge (40×140) - Maximum size")
menu_items+=("" "(_*_)")
menu_items+=("custom" "(.Y.) Custom Size - Set your own")
menu_items+=("preview" "(.Y.) Preview Current Size")
menu_items+=("back" "(Z) ← Back to Main Menu")
local choice
choice=$(ui_menu "Dialog Box Size Configuration" \
"$current_info\n\nSelect a dialog box size preset:\n(This controls the grey menu boxes, not the terminal window)" \
$DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${menu_items[@]}") || break
case "$choice" in
compact|standard|large|wide|tall|huge)
apply_dialog_preset "$choice"
ui_msg "Success" "Dialog size changed to $choice preset!\n\nNew size: ${DIALOG_HEIGHT}×${DIALOG_WIDTH}\nMenu height: ${DIALOG_MENU_HEIGHT}\n\nYou'll see the change in the next menu."
;;
custom)
set_custom_dialog_size
;;
preview)
show_dialog_preview
;;
back|z)
break
;;
esac
done
log "Exiting show_dialog_size_menu function"
}
set_custom_dialog_size() {
local height
local width
local menu_height
# Get current size as defaults
height=$(ui_input "Custom Dialog Height" "Enter dialog box height (lines):" "$DIALOG_HEIGHT")
[[ -z "$height" ]] && return
width=$(ui_input "Custom Dialog Width" "Enter dialog box width (columns):" "$DIALOG_WIDTH")
[[ -z "$width" ]] && return
menu_height=$(ui_input "Custom Menu Height" "Enter menu area height (lines):" "$DIALOG_MENU_HEIGHT")
[[ -z "$menu_height" ]] && return
# Validate inputs
if [[ "$height" =~ ^[0-9]+$ ]] && [[ "$width" =~ ^[0-9]+$ ]] && [[ "$menu_height" =~ ^[0-9]+$ ]]; then
if [[ $height -ge 15 && $height -le 50 && $width -ge 60 && $width -le 200 && $menu_height -ge 5 && $menu_height -le 30 ]]; then
DIALOG_HEIGHT="$height"
DIALOG_WIDTH="$width"
DIALOG_MENU_HEIGHT="$menu_height"
CURRENT_DIALOG_PRESET="custom"
ui_msg "Success" "Custom dialog size set!\n\nSize: ${DIALOG_HEIGHT}×${DIALOG_WIDTH}\nMenu height: ${DIALOG_MENU_HEIGHT}\n\nYou'll see the change in the next menu."
log "Custom dialog size set: ${DIALOG_HEIGHT}x${DIALOG_WIDTH}, menu: ${DIALOG_MENU_HEIGHT}"
else
ui_msg "Error" "Invalid dimensions!\n\nHeight: 15-50 lines\nWidth: 60-200 columns\nMenu height: 5-30 lines"
fi
else
ui_msg "Error" "Please enter valid numbers only."
fi
}
show_dialog_preview() {
local preview_items=()
preview_items+=("item1" "(.Y.) Sample menu item 1")
preview_items+=("item2" "(.Y.) Sample menu item 2")
preview_items+=("item3" "(.Y.) Sample menu item 3")
preview_items+=("item4" "(.Y.) Sample menu item 4")
preview_items+=("item5" "(.Y.) Sample menu item 5")
ui_menu "Dialog Size Preview" \
"This is how your dialog boxes look with current settings:\n\nSize: ${DIALOG_HEIGHT}×${DIALOG_WIDTH}\nMenu height: ${DIALOG_MENU_HEIGHT}\nPreset: ${CURRENT_DIALOG_PRESET}" \
$DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${preview_items[@]}" >/dev/null || true
}
# ==============================================================================
# TERMINAL SIZE MANAGEMENT FUNCTIONS (Legacy - kept for compatibility)
# ==============================================================================
show_terminal_size_menu() {
log "Entering show_terminal_size_menu function"
while true; do
# Get current terminal size
local current_cols=$(tput cols 2>/dev/null || echo "Unknown")
local current_lines=$(tput lines 2>/dev/null || echo "Unknown")
local current_size="${current_cols}x${current_lines}"
local menu_items=()
menu_items+=("small" "(.Y.) Small Terminal (80x24) - Classic size")
menu_items+=("medium" "(.Y.) Medium Terminal (120x30) - Balanced")
menu_items+=("large" "(.Y.) Large Terminal (160x40) - Development")
menu_items+=("xlarge" "(.Y.) Extra Large (200x50) - Multi-pane")
menu_items+=("custom" "(.Y.) Custom Size - Set your own")
menu_items+=("" "(_*_)")
menu_items+=("detect" "(.Y.) Show Current Size")
menu_items+=("back" "(Z) ← Back to Main Menu")
local choice
choice=$(ui_menu "Terminal Size Configuration" \
"Current Terminal Size: $current_size\n\nSelect a terminal size preset or configure custom size:" \
20 80 12 "${menu_items[@]}") || break
case "$choice" in
small)
resize_terminal 80 24
;;
medium)
resize_terminal 120 30
;;
large)
resize_terminal 160 40
;;
xlarge)
resize_terminal 200 50
;;
custom)
set_custom_terminal_size
;;
detect)
show_terminal_info
;;
back|z|"")
break
;;
esac
done
log "Exiting show_terminal_size_menu function"
}
resize_terminal() {
local cols="$1"
local lines="$2"
log "Attempting to resize terminal to ${cols}x${lines}"
# Try different methods to resize terminal
local success=false
# Method 1: Using printf escape sequences (works in most terminals)
if command -v printf >/dev/null 2>&1; then
printf '\e[8;%d;%dt' "$lines" "$cols" 2>/dev/null && success=true
fi
# Method 2: Using resize command if available
if ! $success && command -v resize >/dev/null 2>&1; then
resize -s "$lines" "$cols" 2>/dev/null && success=true
fi
# Method 3: Using stty if available
if ! $success && command -v stty >/dev/null 2>&1; then
stty rows "$lines" cols "$cols" 2>/dev/null && success=true
fi
# Give terminal time to resize
sleep 0.5
# Verify the resize worked
local new_cols=$(tput cols 2>/dev/null || echo "0")
local new_lines=$(tput lines 2>/dev/null || echo "0")
if [[ "$new_cols" -eq "$cols" && "$new_lines" -eq "$lines" ]]; then
ui_msg "Success" "Terminal resized to ${cols}x${lines} successfully!"
log "Terminal resize successful: ${cols}x${lines}"
elif $success; then
ui_msg "Partial Success" "Resize command sent, but verification shows ${new_cols}x${new_lines}.\n\nSome terminals may need manual adjustment or don't support programmatic resizing."
log "Terminal resize partially successful: requested ${cols}x${lines}, got ${new_cols}x${new_lines}"
else
ui_msg "Notice" "Terminal resizing may not be supported by your terminal emulator.\n\nCurrent size: ${new_cols}x${new_lines}\nRequested: ${cols}x${lines}\n\nTry manually resizing your terminal window."
log "Terminal resize failed: terminal may not support programmatic resizing"
fi
}
set_custom_terminal_size() {
local cols
local lines
# Get current size as defaults
local current_cols=$(tput cols 2>/dev/null || echo "80")
local current_lines=$(tput lines 2>/dev/null || echo "24")
cols=$(ui_input "Custom Terminal Width" "Enter terminal width (columns):" "$current_cols")
[[ -z "$cols" ]] && return
lines=$(ui_input "Custom Terminal Height" "Enter terminal height (lines):" "$current_lines")
[[ -z "$lines" ]] && return
# Validate input
if ! [[ "$cols" =~ ^[0-9]+$ ]] || ! [[ "$lines" =~ ^[0-9]+$ ]]; then
ui_msg "Error" "Please enter valid numbers for width and height."
return
fi
# Reasonable limits
if [[ "$cols" -lt 20 || "$cols" -gt 500 || "$lines" -lt 5 || "$lines" -gt 200 ]]; then
ui_msg "Error" "Please enter reasonable values:\nWidth: 20-500 columns\nHeight: 5-200 lines"
return
fi
resize_terminal "$cols" "$lines"
}
show_terminal_info() {
local cols=$(tput cols 2>/dev/null || echo "Unknown")
local lines=$(tput lines 2>/dev/null || echo "Unknown")
local term_type="${TERM:-Unknown}"
local term_program="${TERM_PROGRAM:-Unknown}"
local info="TERMINAL INFORMATION\n"
info+="═══════════════════════════════════════\n\n"
info+="Current Size: ${cols} columns × ${lines} lines\n"
info+="Terminal Type: $term_type\n"
info+="Terminal Program: $term_program\n\n"
info+="COMMON SIZES FOR REFERENCE\n"
info+="─────────────────────────────────────\n"
info+="Small (80×24): Classic terminal size\n"
info+="Medium (120×30): Good for most tasks\n"
info+="Large (160×40): Development work\n"
info+="Extra Large (200×50): Multi-pane setups\n\n"
info+="NOTE: Some terminal emulators may not\n"
info+="support programmatic resizing."
ui_info "Terminal Information" "$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" "true"; 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" "true"; 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=(
"status" "(.Y.) 📊 Manage WordPress Sites"
"" "(_*_)"
"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"
"custom-db-nginx" "(.Y.) 🔧 Custom Database + Nginx Setup"
"custom-db-apache" "(.Y.) 🔧 Custom Database + Apache Setup"
"" "(_*_)"
"wp-cli" "(.Y.) 🔧 Install/Manage WP-CLI"
"wp-cleanup" "(.Y.) 🧹 WordPress Cleanup & Customization"
"sql-import-optimize" "(.Y.) 📊 Optimize for Large SQL Imports"
"ssl-setup" "(.Y.) 🔒 Add SSL Certificate (Let's Encrypt)"
"wp-security" "(.Y.) 🛡️ WordPress Security Hardening"
"" "(_*_)"
"zback" "(Z) ← Back to Main Menu"
)
local choice
choice=$(ui_menu "WordPress Management" \
"Manage your WordPress installations:\n\n📊 Manage Sites: View, configure, and troubleshoot existing WordPress sites\n🚀 Quick Setup: Auto-generated database credentials\n⚙ Custom Setup: Choose site directory\n🔧 Custom Database: Choose database name, user, and password\n🧹 Cleanup: Remove default content and optimize existing WordPress sites" \
22 90 15 "${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"
;;
custom-db-nginx)
wordpress_custom_database_setup "nginx"
;;
custom-db-apache)
wordpress_custom_database_setup "apache"
;;
wp-cli)
wordpress_wpcli_management
;;
wp-cleanup)
wordpress_cleanup_menu
;;
sql-import-optimize)
optimize_for_large_sql_imports
;;
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
ui_msg "Step 1/6" "Installing WordPress prerequisites...\n\nThis includes:\n• Web server ($web_server)\n• PHP and extensions\n• MariaDB database\n• Required system packages\n\n⏳ This process is automatic - please wait..."
install_wordpress_prerequisites "$web_server" || {
ui_msg "Installation Failed" "Failed to install prerequisites. Check the logs for details."
return 1
}
log "Step 1 Complete: Prerequisites installed successfully"
# Get domain name with validation
ui_msg "Step 2/6" "Configuring domain settings...\n\nPlease enter your domain name or use 'localhost' for local development.\n\nFor local development, we recommend using '.localhost' suffix (e.g., mysite.localhost)\nwhich works automatically without DNS configuration.\n\n👆 User input required below..."
local domain
while true; do
domain=$(ui_input "Domain Name" "Enter your domain name (or 'localhost' for local development):" "localhost") || return
domain=${domain:-localhost}
# Auto-suggest .localhost for simple names
if [[ "$domain" != "localhost" ]] && [[ ! "$domain" =~ \. ]] && [[ ${#domain} -le 20 ]]; then
if ui_yesno "Domain Suggestion" "For local development, would you like to use '$domain.localhost' instead of '$domain'?\n\nThis will work automatically without DNS configuration."; then
domain="$domain.localhost"
fi
fi
# Validate domain format
if [[ "$domain" == "localhost" ]] || [[ "$domain" =~ ^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$ ]]; then
break
else
ui_msg "Invalid Domain" "Please enter a valid domain name or 'localhost'.\n\nExamples:\n• localhost\n• mysite.localhost\n• mysite.local\n• example.com\n• subdomain.example.com"
fi
done
ui_msg "Step 2 Complete" "✅ Domain configured: $domain\n\n• Site URL: http://$domain\n• Directory: /var/www/$domain\n• Admin URL: http://$domain/wp-admin/"
# SSL Configuration for local domains
local use_ssl=false
if [[ "$domain" =~ \.(localhost|local)$ ]] || [[ "$domain" == "localhost" ]]; then
if ui_yesno "SSL Configuration" "Would you like to enable HTTPS for your local WordPress site?\n\n✅ Pros:\n• Secure local development\n• Test SSL-dependent features\n• Modern browser compatibility\n\n⚠ Note:\n• Uses self-signed certificate\n• Browser will show security warning (normal for local dev)\n• You can add certificate to system trust store"; then
use_ssl=true
fi
fi
# Confirm installation details
local confirm_msg="WordPress Installation Summary\n\n"
confirm_msg+="Web Server: $web_server\n"
confirm_msg+="Domain: $domain\n"
confirm_msg+="Site Directory: /var/www/$domain\n"
confirm_msg+="Database: Auto-generated secure credentials\n"
if [[ "$use_ssl" == true ]]; then
confirm_msg+="SSL: Self-signed certificate (HTTPS enabled)\n"
else
confirm_msg+="SSL: HTTP only\n"
fi
confirm_msg+="\nProceed with installation?"
if ! ui_yesno "Confirm Installation" "$confirm_msg"; then
return
fi
# Create the WordPress database (this now handles MariaDB root access internally)
ui_msg "Step 3/6" "Creating WordPress database...\n\n• Verifying MariaDB root access\n• Creating WordPress database\n• Setting up database user\n• Configuring permissions\n\n⏳ This process is automatic - please wait..."
local db_info
db_info=$(setup_wordpress_database "$domain") || {
ui_msg "Database Creation Failed" "❌ Failed to create WordPress database.\n\nPossible causes:\n• MariaDB authentication issues\n• Insufficient privileges\n• Database service problems\n\nCheck the logs for details."
return 1
}
# Parse and display database info
IFS=':' read -r db_name db_user db_pass <<< "$db_info"
# Debug logging for database info
log "Database info received: '$db_info'"
log "Parsed - DB Name: '$db_name', DB User: '$db_user', DB Pass: '${db_pass:0:4}...'"
# Validate that we have all required database info
if [[ -z "$db_name" || -z "$db_user" || -z "$db_pass" ]]; then
log "ERROR: Incomplete database information - Name: '$db_name', User: '$db_user', Pass: '${db_pass:+[SET]}'"
ui_msg "Database Error" "❌ Database creation returned incomplete information.\n\nReceived: '$db_info'\n\nPlease check the logs for details."
return 1
fi
ui_msg "Step 3 Complete" "✅ Database created successfully!\n\n• Database Name: $db_name\n• Database User: $db_user\n• Database Password: $db_pass\n• Host: localhost\n\n🔒 Credentials saved for your reference."
# Download and configure WordPress
ui_msg "Step 4/6" "Downloading and configuring WordPress...\n\n• Downloading latest WordPress\n• Extracting files to /var/www/$domain\n• Configuring wp-config.php\n• Setting file permissions\n\n⏳ This process is automatic - please wait..."
setup_wordpress_files "$domain" "$db_info" || {
ui_msg "WordPress Setup Error" "Failed to download or configure WordPress files."
return 1
}
ui_msg "Step 4 Complete" "✅ WordPress files configured!\n\n• Latest WordPress: Downloaded\n• Configuration: Complete\n• File permissions: Set\n• Upload directory: Created"
# Configure web server
ui_msg "Step 5/6" "Configuring $web_server web server...\n\n• Creating server configuration\n• Setting up PHP processing\n• Configuring security headers\n• Enabling site\n\n⏳ This process is automatic - please wait..."
if [[ "$web_server" == "nginx" ]]; then
configure_nginx_wordpress "$domain" || {
ui_msg "Nginx Configuration Error" "Failed to configure Nginx. Check configuration syntax."
return 1
}
else
configure_apache_wordpress "$domain" || {
ui_msg "Apache Configuration Error" "Failed to configure Apache. Check configuration syntax."
return 1
}
fi
# Set up SSL if requested
if [[ "$use_ssl" == true ]]; then
ui_msg "Step 5b/6" "Setting up SSL certificate...\n\n• Generating self-signed certificate\n• Configuring HTTPS\n• Updating server configuration\n\n⏳ This process is automatic - please wait..."
if setup_local_ssl "$domain" "$web_server"; then
ui_msg "SSL Setup Complete" "✅ SSL certificate configured successfully!\n\n• Self-signed certificate: Generated\n• HTTPS: Enabled\n• Security headers: Added\n\nYour site is accessible via HTTPS."
else
log "SSL setup failed, continuing with HTTP-only installation"
ui_msg "SSL Setup Warning" "⚠️ SSL setup failed, but site will work with HTTP.\n\nYour WordPress site is fully functional at: http://$domain\n\nYou can set up SSL later using:\n• WordPress management menu\n• Manual SSL configuration\n• Let's Encrypt (for public domains)"
fi
fi
ui_msg "Step 5 Complete" "$web_server configured successfully!\n\n• Server block: Created\n• PHP processing: Enabled\n• Security headers: Set\n• Site: Active and running$(if [[ "$use_ssl" == true ]]; then echo "\n• SSL: HTTPS enabled"; fi)"
# Show completion message
ui_msg "Step 6/6" "Finalizing WordPress installation...\n\nPreparing completion summary with all details.\n\n⏳ This process is automatic - please wait..."
show_wordpress_completion "$domain" "$db_info"
ui_msg "Installation Complete!" "🎉 WordPress installation finished successfully!\n\nYour site is ready at: http://$domain\n\nNext steps:\n1. Visit your site to complete WordPress setup\n2. Create your admin account\n3. Choose your theme and plugins\n\n💡 Tip: Use the WordPress Cleanup option in the WordPress menu to remove default content and optimize your site."
}
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"
}
wordpress_custom_database_setup() {
local web_server="$1"
log "Starting WordPress custom database setup with $web_server"
local info="WordPress Custom Database Setup ($web_server)\n\n"
info+="🔧 This setup allows you to:\n"
info+="• Choose your own database name\n"
info+="• Set custom database username\n"
info+="• Define your own password\n"
info+="• Understand user management\n\n"
info+="🔐 You'll learn about:\n"
info+="• ROOT user (administrative access)\n"
info+="• WordPress user (application access)\n"
info+="• Security best practices\n\n"
info+="Continue with custom database setup?"
if ! ui_yesno "Custom Database 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
# Domain configuration
local domain
domain=$(ui_input "Domain Name" "Enter your domain name:" "localhost") || return
domain=${domain:-localhost}
# Custom database setup with detailed explanations
local db_info
db_info=$(setup_wordpress_database_custom "$domain") || {
ui_msg "Database Setup Failed" "Database setup was cancelled or failed. Please try again."
return 1
}
# Parse database info for confirmation
IFS=':' read -r db_name db_user db_pass <<< "$db_info"
# Confirm setup before proceeding
local confirm_msg="🔐 Database Configuration Summary:\n\n"
confirm_msg+="• Database Name: $db_name\n"
confirm_msg+="• Database User: $db_user\n"
confirm_msg+="• Database Password: $db_pass\n"
confirm_msg+="• Host: localhost\n\n"
confirm_msg+="📁 WordPress will be installed to: /var/www/$domain\n\n"
confirm_msg+="Continue with WordPress installation?"
if ! ui_yesno "Confirm Installation" "$confirm_msg"; then
return
fi
# WordPress file setup
ui_msg "Step 2/4" "Setting up WordPress files...\n\nDownloading and configuring WordPress with your custom database settings.\n\n⏳ This process is automatic - please wait..."
setup_wordpress_files "$domain" "$db_info" || {
ui_msg "WordPress Setup Error" "Failed to download or configure WordPress files."
return 1
}
# Web server configuration
ui_msg "Step 3/4" "Configuring web server...\n\nSetting up $web_server virtual host for $domain.\n\n⏳ This process is automatic - please wait..."
if [[ "$web_server" == "nginx" ]]; then
configure_nginx_wordpress "$domain" || {
ui_msg "Nginx Configuration Error" "Failed to configure Nginx. Please check the logs."
return 1
}
else
configure_apache_wordpress "$domain" || {
ui_msg "Apache Configuration Error" "Failed to configure Apache. Please check the logs."
return 1
}
fi
# Final setup and permissions
ui_msg "Step 4/4" "Finalizing installation...\n\nSetting file permissions and restarting services.\n\n⏳ This process is automatic - please wait..."
finalize_wordpress_setup "$domain" "$web_server" || {
ui_msg "Finalization Error" "WordPress setup completed but some final steps failed."
}
# Show completion with custom database details
show_wordpress_completion_custom "$domain" "$db_info" "$web_server"
}
show_wordpress_completion_custom() {
local domain="$1"
local db_info="$2"
local web_server="$3"
# Parse database info
IFS=':' read -r db_name db_user db_pass <<< "$db_info"
local completion_msg="🎉 WordPress Installation Complete!\n\n"
completion_msg+="🌐 Website Details:\n"
completion_msg+="• URL: http://$domain\n"
completion_msg+="• Directory: /var/www/$domain\n"
completion_msg+="• Web Server: $web_server\n\n"
completion_msg+="🔐 Custom Database Configuration:\n"
completion_msg+="• Database Name: $db_name\n"
completion_msg+="• Database User: $db_user (WordPress-specific)\n"
completion_msg+="• Database Password: $db_pass\n"
completion_msg+="• Host: localhost\n\n"
completion_msg+="👤 User Management Summary:\n"
completion_msg+="• ROOT USER: Administrative MariaDB access\n"
completion_msg+="• WORDPRESS USER: '$db_user' (limited to '$db_name' database)\n\n"
completion_msg+="🔧 Next Steps:\n"
completion_msg+="1. Visit http://$domain to complete WordPress setup\n"
completion_msg+="2. Use the database credentials above during WordPress installation\n"
completion_msg+="3. Consider adding SSL certificate for security\n\n"
completion_msg+="📝 Database credentials are saved in wp-config.php"
ui_msg "Installation Complete" "$completion_msg"
}
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.\n\n⏳ This process is automatic - please wait..."
# 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"
# Verify PHP-FPM is running
if ! systemctl is-active --quiet php${PHP_VER}-fpm; then
log "ERROR: PHP-FPM failed to start"
ui_msg "Service Error" "PHP-FPM failed to start. This will cause 500 errors.\n\nTrying to restart..."
sudo systemctl restart php${PHP_VER}-fpm
sleep 2
if ! systemctl is-active --quiet php${PHP_VER}-fpm; then
ui_msg "Critical Error" "PHP-FPM still not running. Check: sudo systemctl status php${PHP_VER}-fpm"
return 1
fi
fi
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"
# Check if MariaDB is already configured
local existing_pass=""
if [[ -f "/root/.mysql_root_password" ]]; then
existing_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null)
log "Found existing MariaDB password file"
# Test if existing password works
if [[ -n "$existing_pass" ]] && mysql -u root -p"$existing_pass" -e "SELECT 1;" >/dev/null 2>&1; then
log "Existing MariaDB password is valid, skipping setup"
return 0
else
log "Existing password doesn't work, reconfiguring MariaDB"
fi
fi
# Generate random root password
local root_pass
root_pass=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 20)
# First, check if MariaDB is using unix_socket authentication
local auth_plugin
auth_plugin=$(sudo mysql -e "SELECT plugin FROM mysql.user WHERE User='root' AND Host='localhost';" 2>/dev/null | tail -n +2 | head -n 1)
# Configure root user for password authentication
if [[ "$auth_plugin" == "unix_socket" ]] || [[ -z "$auth_plugin" ]]; then
# Change from unix_socket to mysql_native_password
sudo mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${root_pass}';" 2>/dev/null || {
# If that fails, try creating the user
sudo mysql -e "CREATE USER IF NOT EXISTS 'root'@'localhost' IDENTIFIED BY '${root_pass}';" 2>/dev/null || true
sudo mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;" 2>/dev/null || true
}
else
# For existing installations, we might need to reset the password
log "Attempting to reset existing MariaDB root password"
# Try multiple methods to set the password
if sudo mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '$root_pass';" 2>/dev/null; then
log "Password set using ALTER USER"
elif sudo mysql -e "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('$root_pass');" 2>/dev/null; then
log "Password set using SET PASSWORD"
elif sudo mysql -e "UPDATE mysql.user SET Password=PASSWORD('$root_pass') WHERE User='root' AND Host='localhost';" 2>/dev/null; then
log "Password set using UPDATE"
else
log "All password setting methods failed, trying to reset authentication plugin"
sudo mysql -e "UPDATE mysql.user SET plugin='mysql_native_password', Password=PASSWORD('$root_pass') WHERE User='root' AND Host='localhost';" 2>/dev/null
fi
fi
# Secure installation - clean up
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
# Verify the password works
if mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then
# Save root password and display to user
echo "$root_pass" | sudo tee /root/.mysql_root_password > /dev/null
sudo chmod 600 /root/.mysql_root_password
# Display password to user immediately
ui_msg "MariaDB Root Password Generated" "🔐 MariaDB Root Password Created!\n\n📋 ROOT PASSWORD: $root_pass\n\n📝 Important:\n• This password has been saved to /root/.mysql_root_password\n• Copy this password now for your records\n• You can view it later in Database Management > Show Credentials\n\n🔧 Access Methods:\n• Command line: mysql -u root -p'$root_pass'\n• Sudo access: sudo mysql"
# Log credentials (non-blocking)
log "MariaDB Root Password Set: $root_pass"
log "Password saved to: /root/.mysql_root_password"
log "Password displayed to user during setup"
else
log "MariaDB Setup Warning: Password authentication setup may have failed"
fi
log "MariaDB security setup completed"
}
setup_wordpress_database() {
local domain="$1"
# Prompt user for database configuration
ui_msg "WordPress Database Configuration" "🔐 WordPress Database Setup\n\nYou can customize your database settings or use auto-generated values.\n\nCustom settings allow you to:\n• Choose meaningful database names\n• Set your own passwords\n• Maintain consistent naming\n\nAuto-generated settings provide:\n• Unique, secure credentials\n• No naming conflicts\n• Quick setup" >/dev/tty
local use_custom=false
if ui_yesno "Database Configuration" "Do you want to customize database settings?\n\n• YES: Choose database name, user, and password\n• NO: Use auto-generated secure credentials" >/dev/tty; then
use_custom=true
fi
local db_name db_user db_pass
if [[ "$use_custom" == "true" ]]; then
# Custom database configuration
ui_msg "Custom Database Setup" "🔧 Custom Database Configuration\n\nPlease provide your preferred database settings.\n\nDatabase names should:\n• Be descriptive (e.g., mysite_wp, blog_db)\n• Use only letters, numbers, and underscores\n• Be unique on this server" >/dev/tty
# Get database name
while true; do
db_name=$(ui_input "Database Name" "Enter database name for WordPress:" "wp_${domain//[^a-zA-Z0-9]/_}") || return
db_name=${db_name:-"wp_${domain//[^a-zA-Z0-9]/_}"}
# Validate database name
if [[ "$db_name" =~ ^[a-zA-Z0-9_]+$ ]] && [[ ${#db_name} -le 64 ]]; then
break
else
ui_msg "Invalid Database Name" "Database name must:\n• Contain only letters, numbers, and underscores\n• Be 64 characters or less\n• Not be empty\n\nExample: wp_mysite, blog_database" >/dev/tty
fi
done
# Get database user
while true; do
db_user=$(ui_input "Database User" "Enter database username:" "wp_user_${domain//[^a-zA-Z0-9]/_}") || return
db_user=${db_user:-"wp_user_${domain//[^a-zA-Z0-9]/_}"}
# Validate username
if [[ "$db_user" =~ ^[a-zA-Z0-9_]+$ ]] && [[ ${#db_user} -le 32 ]]; then
break
else
ui_msg "Invalid Username" "Username must:\n• Contain only letters, numbers, and underscores\n• Be 32 characters or less\n• Not be empty\n\nExample: wp_user, mysite_user" >/dev/tty
fi
done
# Get database password
db_pass=$(ui_input "Database Password" "Enter 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)
ui_msg "Auto-Generated Password" "🔐 Password auto-generated for security:\n\nPassword: $db_pass\n\n📝 Please save this password - you'll need it for WordPress setup!" >/dev/tty
fi
else
# Auto-generated configuration
db_name="wp_${domain//[^a-zA-Z0-9]/_}_$(date +%s)"
db_user="wpuser_$(date +%s)"
db_pass=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 20)
ui_msg "Auto-Generated Credentials" "🔐 Secure database credentials generated:\n\n• Database: $db_name\n• User: $db_user\n• Password: $db_pass\n\n📝 These will be automatically configured in WordPress!" >/dev/tty
fi
log "Creating WordPress database: $db_name for domain: $domain"
log "Database user: $db_user"
# Check MariaDB service first
if ! systemctl is-active --quiet mariadb; then
log "ERROR: MariaDB service is not running"
return 1
fi
# Ensure MariaDB root access for database operations
local root_pass
local auth_method="sudo"
ui_msg "Database Access" "🔐 Checking MariaDB root access...\n\nThis is required to create the WordPress database and user." >/dev/tty
# First, try to get saved root password
if [[ -f "/root/.mysql_root_password" ]]; then
root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null)
if [[ -n "$root_pass" ]]; then
# Test if password authentication works
if mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then
auth_method="password"
log "Using saved root password for database operations"
ui_msg "Root Access" "✅ MariaDB root access verified using saved credentials." >/dev/tty
else
log "Saved password authentication failed"
root_pass=""
fi
fi
fi
# If password auth failed, try sudo
if [[ "$auth_method" != "password" ]]; then
if sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then
auth_method="sudo"
log "Using sudo authentication for database operations"
ui_msg "Root Access" "✅ MariaDB root access verified using sudo." >/dev/tty
else
log "Sudo authentication also failed"
# Need to prompt for root password
ui_msg "Root Password Required" "🔑 MariaDB root access is required to create the WordPress database.\n\nPlease enter the MariaDB root password, or we can attempt to reset it." >/dev/tty
local max_attempts=3
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
root_pass=$(ui_input "MariaDB Root Password" "Enter MariaDB root password (attempt $attempt/$max_attempts):" "" "password") || {
if ui_yesno "Reset Root Password" "Would you like to attempt resetting the MariaDB root password instead?\n\n⚠ This will stop MariaDB temporarily." >/dev/tty; then
if reset_mariadb_root_password; then
# Try to get the new password
if [[ -f "/root/.mysql_root_password" ]]; then
root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null)
if mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then
auth_method="password"
ui_msg "Root Access" "✅ MariaDB root password reset successfully." >/dev/tty
break
fi
fi
fi
ui_msg "Reset Failed" "❌ Failed to reset MariaDB root password. Please try entering the password manually." >/dev/tty
continue
else
return 1
fi
}
if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then
auth_method="password"
# Save the working password
echo "$root_pass" | sudo tee /root/.mysql_root_password >/dev/null
sudo chmod 600 /root/.mysql_root_password
ui_msg "Root Access" "✅ MariaDB root access verified with provided password." >/dev/tty
break
else
ui_msg "Authentication Failed" "❌ Invalid password. Please try again." >/dev/tty
((attempt++))
fi
done
if [[ $attempt -gt $max_attempts ]]; then
ui_msg "Access Failed" "❌ Failed to establish MariaDB root access after $max_attempts attempts.\n\nWordPress installation cannot continue without database access." >/dev/tty
return 1
fi
fi
fi
log "Authentication method: $auth_method"
# Function to execute MySQL commands with proper error handling
execute_mysql_cmd() {
local cmd="$1"
local operation="$2"
local result
if [[ "$auth_method" == "password" ]]; then
if result=$(mysql -u root -p"$root_pass" -e "$cmd" 2>&1); then
log "SUCCESS: $operation completed with password auth"
return 0
else
log "FAILED: $operation with password auth: $result"
# Try sudo fallback
if result=$(sudo mysql -e "$cmd" 2>&1); then
log "SUCCESS: $operation completed with sudo fallback"
return 0
else
log "FAILED: $operation with sudo fallback: $result"
return 1
fi
fi
else
if result=$(sudo mysql -e "$cmd" 2>&1); then
log "SUCCESS: $operation completed with sudo"
return 0
else
log "FAILED: $operation with sudo: $result"
return 1
fi
fi
}
# Create database
log "Creating database: $db_name"
execute_mysql_cmd "CREATE DATABASE IF NOT EXISTS \`${db_name}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" "Database creation" || {
log "ERROR: Failed to create database $db_name"
return 1
}
# Create user
log "Creating database user: $db_user"
execute_mysql_cmd "CREATE USER IF NOT EXISTS '${db_user}'@'localhost' IDENTIFIED BY '${db_pass}';" "User creation" || {
log "ERROR: Failed to create user $db_user"
return 1
}
# Grant privileges
log "Granting privileges to user: $db_user on database: $db_name"
execute_mysql_cmd "GRANT ALL PRIVILEGES ON \`${db_name}\`.* TO '${db_user}'@'localhost';" "Grant privileges" || {
log "ERROR: Failed to grant privileges"
return 1
}
# Flush privileges
log "Flushing privileges"
execute_mysql_cmd "FLUSH PRIVILEGES;" "Flush privileges" || {
log "ERROR: Failed to flush privileges"
return 1
}
# Verify database creation
log "Verifying database creation"
local db_exists
if [[ "$auth_method" == "password" ]]; then
db_exists=$(mysql -u root -p"$root_pass" -e "SHOW DATABASES LIKE '${db_name}';" 2>/dev/null | tail -n +2)
else
db_exists=$(sudo mysql -e "SHOW DATABASES LIKE '${db_name}';" 2>/dev/null | tail -n +2)
fi
if [[ -n "$db_exists" ]]; then
log "SUCCESS: Database $db_name verified to exist"
# Test WordPress user connection
log "Testing WordPress user connection"
if mysql -u "$db_user" -p"$db_pass" -e "USE \`${db_name}\`; SELECT 1;" >/dev/null 2>&1; then
log "SUCCESS: WordPress user can connect to database"
echo "${db_name}:${db_user}:${db_pass}"
return 0
else
log "ERROR: WordPress user cannot connect to database"
return 1
fi
else
log "ERROR: Database $db_name was not created or cannot be verified"
return 1
fi
}
setup_wordpress_database_custom() {
local domain="$1"
# Display user management explanation
ui_msg "Database User Management" "🔐 WordPress Database Setup\n\n📋 User Management Explanation:\n\n• ROOT USER: Administrative access to MariaDB\n - Username: root\n - Used for: Creating databases, managing users\n - Authentication: Password or sudo access\n\n• WORDPRESS USER: Dedicated user for each WordPress site\n - Username: Custom (e.g., wp_user_sitename)\n - Used for: WordPress application database access\n - Authentication: Password only\n - Privileges: Limited to specific database only\n\n🎯 This setup follows security best practices:\n- Separation of administrative and application access\n- Minimal privileges for WordPress users\n- Unique credentials per site"
local db_name
db_name=$(ui_input "Database Name" "Enter WordPress database name:\n\n💡 Suggestions:\n• wp_${domain//[^a-zA-Z0-9]/_}\n• ${domain//[^a-zA-Z0-9]/_}_wp\n• custom_name_wp" "wp_${domain//[^a-zA-Z0-9]/_}") || return
# Validate database name
if [[ ! "$db_name" =~ ^[a-zA-Z0-9_]+$ ]]; then
ui_msg "Invalid Name" "Database name can only contain letters, numbers, and underscores."
return 1
fi
local db_user
db_user=$(ui_input "Database User" "Enter WordPress database username:\n\n💡 Suggestions:\n• wp_user_${domain//[^a-zA-Z0-9]/_}\n• ${domain//[^a-zA-Z0-9]/_}_user\n• wpuser_$(date +%s)" "wp_user_${domain//[^a-zA-Z0-9]/_}") || return
# Validate username
if [[ ! "$db_user" =~ ^[a-zA-Z0-9_]+$ ]]; then
ui_msg "Invalid Username" "Username can only contain letters, numbers, and underscores."
return 1
fi
local db_pass
db_pass=$(ui_input "Database Password" "Enter database password:\n\n🔒 Password Requirements:\n• Minimum 8 characters\n• Mix of letters, numbers, symbols\n• Avoid spaces and quotes\n\n💡 Leave empty for auto-generated secure password" "") || return
if [[ -z "$db_pass" ]]; then
db_pass=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9!@#$%^&*' | head -c 16)
ui_msg "Generated Password" "🔐 Auto-generated secure password:\n\n$db_pass\n\n📝 Please save this password securely!"
else
# Validate password strength
if [[ ${#db_pass} -lt 8 ]]; then
ui_msg "Weak Password" "Password must be at least 8 characters long."
return 1
fi
fi
log "Creating custom WordPress database: $db_name with user: $db_user"
# Check MariaDB service first
if ! systemctl is-active --quiet mariadb; then
ui_msg "Service Error" "MariaDB service is not running. Please start MariaDB first."
return 1
fi
# Get root authentication method
local root_pass
local auth_method="sudo"
if [[ -f "/root/.mysql_root_password" ]]; then
root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null)
if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then
auth_method="password"
fi
fi
# Function to execute MySQL commands with proper error handling
execute_mysql_cmd() {
local cmd="$1"
local operation="$2"
local result
if [[ "$auth_method" == "password" ]]; then
if result=$(mysql -u root -p"$root_pass" -e "$cmd" 2>&1); then
log "SUCCESS: $operation completed with password auth"
return 0
else
log "FAILED: $operation with password auth: $result"
ui_msg "Database Error" "Failed: $operation\n\nError: $result\n\nTrying alternative authentication..."
# Try sudo fallback
if result=$(sudo mysql -e "$cmd" 2>&1); then
log "SUCCESS: $operation completed with sudo fallback"
return 0
else
log "FAILED: $operation with sudo fallback: $result"
ui_msg "Database Error" "Failed: $operation\n\nError: $result"
return 1
fi
fi
else
if result=$(sudo mysql -e "$cmd" 2>&1); then
log "SUCCESS: $operation completed with sudo"
return 0
else
log "FAILED: $operation with sudo: $result"
ui_msg "Database Error" "Failed: $operation\n\nError: $result"
return 1
fi
fi
}
# Check if database already exists
local existing_db
if [[ "$auth_method" == "password" ]]; then
existing_db=$(mysql -u root -p"$root_pass" -e "SHOW DATABASES LIKE '${db_name}';" 2>/dev/null | tail -n +2)
else
existing_db=$(sudo mysql -e "SHOW DATABASES LIKE '${db_name}';" 2>/dev/null | tail -n +2)
fi
if [[ -n "$existing_db" ]]; then
if ! ui_yesno "Database Exists" "Database '$db_name' already exists.\n\nDo you want to use the existing database?\n\n⚠ WARNING: This will create a new user with access to the existing database."; then
return 1
fi
else
# Create database
execute_mysql_cmd "CREATE DATABASE \`${db_name}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" "Database creation" || return 1
fi
# Check if user already exists
local existing_user
if [[ "$auth_method" == "password" ]]; then
existing_user=$(mysql -u root -p"$root_pass" -e "SELECT User FROM mysql.user WHERE User='${db_user}' AND Host='localhost';" 2>/dev/null | tail -n +2)
else
existing_user=$(sudo mysql -e "SELECT User FROM mysql.user WHERE User='${db_user}' AND Host='localhost';" 2>/dev/null | tail -n +2)
fi
if [[ -n "$existing_user" ]]; then
if ui_yesno "User Exists" "User '$db_user' already exists.\n\nDo you want to update the password and grant access to '$db_name'?"; then
# Update existing user password
execute_mysql_cmd "ALTER USER '${db_user}'@'localhost' IDENTIFIED BY '${db_pass}';" "User password update" || return 1
else
return 1
fi
else
# Create new user
execute_mysql_cmd "CREATE USER '${db_user}'@'localhost' IDENTIFIED BY '${db_pass}';" "User creation" || return 1
fi
# Grant privileges
execute_mysql_cmd "GRANT ALL PRIVILEGES ON \`${db_name}\`.* TO '${db_user}'@'localhost';" "Grant privileges" || return 1
# Flush privileges
execute_mysql_cmd "FLUSH PRIVILEGES;" "Flush privileges" || return 1
# Test WordPress user connection
if mysql -u "$db_user" -p"$db_pass" -e "USE \`${db_name}\`; SELECT 1;" >/dev/null 2>&1; then
ui_msg "Database Setup Complete" "✅ WordPress Database Setup Successful!\n\n🔐 Database Credentials:\n• Database Name: $db_name\n• Username: $db_user\n• Password: $db_pass\n• Host: localhost\n\n👤 User Management Summary:\n• ROOT USER: Administrative access (existing)\n• WORDPRESS USER: '$db_user' (dedicated for this site)\n• Database: '$db_name' (exclusive access)\n\n🔒 Security: WordPress user has minimal privileges"
echo "${db_name}:${db_user}:${db_pass}"
else
ui_msg "Connection Test Failed" "Database and user created but connection test failed.\n\nPlease check the credentials manually."
return 1
fi
}
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 with proper initial permissions
sudo mkdir -p "$site_dir"
# Download WordPress
ui_msg "Downloading WordPress" "Downloading latest WordPress...\n\n⏳ This process is automatic - please wait..."
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
sudo rsync -a /tmp/wordpress/ "$site_dir/"
rm -rf /tmp/wordpress /tmp/wordpress.tar.gz
# Configure WordPress with sudo
sudo cp "$site_dir/wp-config-sample.php" "$site_dir/wp-config.php"
# Database configuration - escape special characters in passwords
local escaped_db_name="${db_name//\'/\\\'}"
local escaped_db_user="${db_user//\'/\\\'}"
local escaped_db_pass="${db_pass//\'/\\\'}"
# Use more robust sed replacements with different delimiters (with sudo)
sudo sed -i "s|database_name_here|${escaped_db_name}|g" "$site_dir/wp-config.php"
sudo sed -i "s|username_here|${escaped_db_user}|g" "$site_dir/wp-config.php"
sudo sed -i "s|password_here|${escaped_db_pass}|g" "$site_dir/wp-config.php"
sudo sed -i "s|localhost|localhost|g" "$site_dir/wp-config.php"
# Verify database configuration was applied (using fixed-string grep to avoid regex issues)
if grep -F "$escaped_db_name" "$site_dir/wp-config.php" >/dev/null 2>&1 && \
grep -F "$escaped_db_user" "$site_dir/wp-config.php" >/dev/null 2>&1 && \
grep -F "$escaped_db_pass" "$site_dir/wp-config.php" >/dev/null 2>&1; then
log "Database configuration applied successfully to wp-config.php"
# Test database connection
if mysql -u"$db_user" -p"$db_pass" -h"localhost" -e "USE \`$db_name\`; SELECT 1;" >/dev/null 2>&1; then
log "Database connection test successful"
else
log "WARNING: Database connection test failed - WordPress may have connection issues"
ui_msg "Database Warning" "⚠️ Database configuration applied but connection test failed.\n\nThis may cause WordPress installation issues.\n\nPlease verify:\n• Database '$db_name' exists\n• User '$db_user' has access\n• Password is correct\n\nContinuing with installation..."
fi
else
log "WARNING: Database configuration may not have been applied correctly"
# Try alternative method with PHP-style replacement (with sudo)
sudo tee /tmp/wp-db-config.php > /dev/null << EOF
<?php
\$config_content = file_get_contents('$site_dir/wp-config.php');
\$config_content = str_replace('database_name_here', '$escaped_db_name', \$config_content);
\$config_content = str_replace('username_here', '$escaped_db_user', \$config_content);
\$config_content = str_replace('password_here', '$escaped_db_pass', \$config_content);
file_put_contents('$site_dir/wp-config.php', \$config_content);
echo "Database configuration updated via PHP";
?>
EOF
sudo php /tmp/wp-db-config.php
sudo rm -f /tmp/wp-db-config.php
# Test database connection after PHP method
if mysql -u"$db_user" -p"$db_pass" -h"localhost" -e "USE \`$db_name\`; SELECT 1;" >/dev/null 2>&1; then
log "Database connection test successful after PHP method"
else
log "ERROR: Database connection test failed after both methods"
ui_msg "Database Error" "❌ Database configuration failed!\n\nWordPress will not be able to connect to the database.\n\nPlease check:\n• Database '$db_name' exists\n• User '$db_user' has proper permissions\n• Password is correct\n• MariaDB is running\n\nYou may need to recreate the database and user."
fi
fi
# 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 using a temporary file to avoid sed escaping issues
if [[ -n "$salts" ]]; then
# Create temporary file with salts
echo "$salts" > /tmp/wp-salts.txt
# Remove existing placeholder keys
sudo sed -i '/AUTH_KEY/,/NONCE_SALT/d' "$site_dir/wp-config.php"
# Insert new keys before MySQL settings using a more robust method
sudo awk '
/\/\*\* MySQL settings/ {
while ((getline line < "/tmp/wp-salts.txt") > 0) {
print line
}
close("/tmp/wp-salts.txt")
print ""
}
{ print }
' "$site_dir/wp-config.php" > /tmp/wp-config-new.php
sudo mv /tmp/wp-config-new.php "$site_dir/wp-config.php"
rm -f /tmp/wp-salts.txt
else
log "Skipping security keys insertion due to fetch failure"
fi
# 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 {} \;
# Ensure wp-config.php has correct permissions
sudo chmod 600 "$site_dir/wp-config.php"
# Create uploads directory with proper permissions
sudo mkdir -p "$site_dir/wp-content/uploads"
sudo chown -R www-data:www-data "$site_dir/wp-content/uploads"
sudo chmod -R 755 "$site_dir/wp-content/uploads"
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"
# Extract base domain name for config file naming (remove .localhost/.local suffix if present)
local base_domain="$domain"
if [[ "$domain" == *.localhost ]]; then
base_domain="${domain%.localhost}"
elif [[ "$domain" == *.local ]]; then
base_domain="${domain%.local}"
fi
# Use full domain for directory path to match where WordPress files are installed
local actual_site_dir="/var/www/$domain"
local nginx_conf="/etc/nginx/sites-available/$base_domain"
sudo tee "$nginx_conf" > /dev/null <<EOF
server {
listen 80;
server_name $domain www.$domain;
root $actual_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;
# Additional PHP settings for WordPress
fastcgi_read_timeout 300;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
}
# 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/$base_domain"
# Verify the symlink was created correctly
if [[ ! -L "/etc/nginx/sites-enabled/$base_domain" ]]; then
log "ERROR: Failed to create symlink for $base_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/$base_domain\n🔗 Enabled at: /etc/nginx/sites-enabled/$base_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/$base_domain"
return 1
fi
}
configure_nginx_wordpress_custom() {
local domain="$1"
local site_dir="$2"
configure_nginx_wordpress "$domain"
}
cleanup_wordpress_installation() {
local domain="$1"
local site_dir="/var/www/$domain"
log "Starting WordPress cleanup and customization for: $domain"
# Check if wp-cli is available
if ! command -v wp >/dev/null 2>&1; then
log "ERROR: wp-cli not found, skipping WordPress cleanup"
ui_error "WP-CLI Missing" "WordPress cleanup requires wp-cli to be installed.\n\nPlease install wp-cli first or run cleanup manually."
return 1
fi
# Check if WordPress is properly installed
if [[ ! -f "$site_dir/wp-config.php" ]]; then
log "ERROR: wp-config.php not found, skipping cleanup"
ui_error "WordPress Not Found" "WordPress installation not found at: $site_dir\n\nPlease complete WordPress installation first."
return 1
fi
local cleanup_info="🧹 WordPress Cleanup & Customization\n\n"
cleanup_info+="This will customize your fresh WordPress installation:\n\n"
cleanup_info+="🗑️ Remove Default Content:\n"
cleanup_info+="• Delete default plugins (Akismet, Hello Dolly)\n"
cleanup_info+="• Remove default themes (Twenty Twenty series)\n"
cleanup_info+="• Delete sample posts, pages, and comments\n\n"
cleanup_info+="✨ Install Essentials:\n"
cleanup_info+="• LiteSpeed Cache plugin\n"
cleanup_info+="• Hello Elementor theme (clean, fast)\n\n"
cleanup_info+="⚙️ Configure Settings:\n"
cleanup_info+="• Set homepage as static page\n"
cleanup_info+="• Disable comments globally\n"
cleanup_info+="• Match system timezone and locale\n"
cleanup_info+="• Clean up dashboard widgets\n\n"
cleanup_info+="📁 Site: $site_dir\n\n"
cleanup_info+="Continue with cleanup?"
if ! ui_yesno "WordPress Cleanup" "$cleanup_info"; then
return
fi
# Change to WordPress directory
cd "$site_dir" || {
ui_error "Directory Error" "Could not access WordPress directory: $site_dir"
return 1
}
local cleanup_results=""
local cleanup_success=true
# Detect system timezone and locale
local system_tz
local system_locale
system_tz=$(timedatectl show -p Timezone --value 2>/dev/null || echo "UTC")
system_locale=$(locale | grep LANG= | cut -d= -f2 | cut -d. -f1 2>/dev/null || echo "en_US")
cleanup_results+="🌍 System Detection:\n"
cleanup_results+="• Timezone: $system_tz\n"
cleanup_results+="• Locale: $system_locale\n\n"
# Remove default plugins
cleanup_results+="🗑️ Removing default plugins...\n"
if wp plugin delete akismet hello --quiet 2>/dev/null; then
cleanup_results+="• Default plugins removed ✅\n"
else
cleanup_results+="• Default plugins removal failed ❌\n"
cleanup_success=false
fi
# Install and activate essential plugins
cleanup_results+="📦 Installing LiteSpeed Cache...\n"
if wp plugin install litespeed-cache --activate --quiet 2>/dev/null; then
cleanup_results+="• LiteSpeed Cache installed ✅\n"
else
cleanup_results+="• LiteSpeed Cache installation failed ❌\n"
cleanup_success=false
fi
# Install and activate Hello Elementor theme
cleanup_results+="🎨 Installing Hello Elementor theme...\n"
if wp theme install hello-elementor --activate --quiet 2>/dev/null; then
cleanup_results+="• Hello Elementor theme installed ✅\n"
else
cleanup_results+="• Hello Elementor theme installation failed ❌\n"
cleanup_success=false
fi
# Remove default themes
cleanup_results+="🗑️ Removing default themes...\n"
local themes_removed=0
for theme in twentytwentythree twentytwentyfour twentytwentyfive; do
if wp theme delete "$theme" --quiet 2>/dev/null; then
((themes_removed++))
fi
done
cleanup_results+="$themes_removed default themes removed ✅\n"
# Delete default posts, pages, and comments
cleanup_results+="🗑️ Cleaning default content...\n"
local posts_deleted=0
local comments_deleted=0
# Count and delete posts
local post_ids
post_ids=$(wp post list --format=ids 2>/dev/null || echo "")
if [[ -n "$post_ids" ]]; then
if wp post delete $post_ids --force --quiet 2>/dev/null; then
posts_deleted=$(echo "$post_ids" | wc -w)
fi
fi
# Count and delete comments
local comment_ids
comment_ids=$(wp comment list --format=ids 2>/dev/null || echo "")
if [[ -n "$comment_ids" ]]; then
if wp comment delete $comment_ids --force --quiet 2>/dev/null; then
comments_deleted=$(echo "$comment_ids" | wc -w)
fi
fi
cleanup_results+="$posts_deleted posts deleted ✅\n"
cleanup_results+="$comments_deleted comments deleted ✅\n"
# Create a clean homepage
cleanup_results+="🏠 Creating homepage...\n"
if wp post create --post_type=page --post_title='Home' --post_status=publish --quiet 2>/dev/null; then
local home_id
home_id=$(wp post list --post_type=page --name=home --format=ids 2>/dev/null)
if [[ -n "$home_id" ]] && wp option update show_on_front 'page' --quiet 2>/dev/null && wp option update page_on_front "$home_id" --quiet 2>/dev/null; then
cleanup_results+="• Homepage created and set ✅\n"
else
cleanup_results+="• Homepage configuration failed ❌\n"
cleanup_success=false
fi
else
cleanup_results+="• Homepage creation failed ❌\n"
cleanup_success=false
fi
# Disable comments globally
cleanup_results+="💬 Disabling comments...\n"
local comment_settings=(
"default_comment_status:closed"
"default_ping_status:closed"
"comment_registration:0"
"close_comments_for_old_posts:1"
"comments_notify:0"
"moderation_notify:0"
)
local comment_success=true
for setting in "${comment_settings[@]}"; do
local key="${setting%:*}"
local value="${setting#*:}"
if ! wp option update "$key" "$value" --quiet 2>/dev/null; then
comment_success=false
fi
done
if [[ "$comment_success" == "true" ]]; then
cleanup_results+="• Comments disabled globally ✅\n"
else
cleanup_results+="• Comment settings failed ❌\n"
cleanup_success=false
fi
# Create mu-plugin to disable comment support
if mkdir -p wp-content/mu-plugins 2>/dev/null; then
cat > wp-content/mu-plugins/disable-comments.php << 'PHP'
<?php
// Disable comment support on all post types
add_action('init', function() {
$post_types = get_post_types(array('public' => true), 'names');
foreach ($post_types as $type) {
remove_post_type_support($type, 'comments');
remove_post_type_support($type, 'trackbacks');
}
});
add_action('admin_init', function() {
$types = get_post_types(array('public' => true), 'names');
foreach ($types as $type) {
remove_post_type_support($type, 'comments');
remove_post_type_support($type, 'trackbacks');
}
});
PHP
cleanup_results+="• Comment support disabled ✅\n"
else
cleanup_results+="• Comment support disable failed ❌\n"
cleanup_success=false
fi
# Set timezone and language
cleanup_results+="🌍 Setting timezone and locale...\n"
if wp option update timezone_string "$system_tz" --quiet 2>/dev/null; then
cleanup_results+="• Timezone set to $system_tz ✅\n"
else
cleanup_results+="• Timezone setting failed ❌\n"
cleanup_success=false
fi
# Try to install matching language if available
if wp language core is-installed "$system_locale" >/dev/null 2>&1; then
if wp site switch-language "$system_locale" --quiet 2>/dev/null; then
cleanup_results+="• Language set to $system_locale ✅\n"
else
cleanup_results+="• Language setting failed ❌\n"
fi
else
cleanup_results+="• System locale $system_locale not available, keeping English \n"
fi
# Hide dashboard widgets
cleanup_results+="📊 Cleaning dashboard...\n"
cat > wp-content/mu-plugins/clean-dashboard.php << 'PHP'
<?php
add_action('wp_dashboard_setup', function() {
remove_meta_box('dashboard_quick_press', 'dashboard', 'side');
remove_meta_box('dashboard_activity', 'dashboard', 'normal');
remove_meta_box('dashboard_primary', 'dashboard', 'side'); // WP Events & News
});
PHP
cleanup_results+="• Dashboard widgets cleaned ✅\n"
# Show final results
if [[ "$cleanup_success" == "true" ]]; then
# Get current theme and plugin info
local current_theme
local active_plugins
current_theme=$(wp theme list --status=active --field=name 2>/dev/null || echo "Unknown")
active_plugins=$(wp plugin list --status=active --field=name 2>/dev/null | tr '\n' ', ' | sed 's/,$//' || echo "None")
cleanup_results+="🎉 Cleanup completed successfully!\n\n"
cleanup_results+="📊 Final Status:\n"
cleanup_results+="• Active Theme: $current_theme\n"
cleanup_results+="• Active Plugins: $active_plugins\n"
ui_info "WordPress Cleanup Complete" "$cleanup_results"
else
cleanup_results+="⚠️ Cleanup completed with some errors.\n\nSome operations may have failed. Check the details above."
ui_info "WordPress Cleanup Finished" "$cleanup_results"
fi
log "WordPress cleanup completed for: $domain"
}
wordpress_cleanup_menu() {
log "Entering WordPress cleanup menu"
# Detect WordPress installations
local wp_sites=()
if [[ -d "/var/www" ]]; then
while IFS= read -r -d '' site_dir; do
local domain
domain=$(basename "$site_dir")
if [[ -f "$site_dir/wp-config.php" ]]; then
wp_sites+=("$domain")
fi
done < <(find /var/www -maxdepth 1 -type d -name "*.localhost" -o -name "*.local" -print0 2>/dev/null)
fi
if [[ ${#wp_sites[@]} -eq 0 ]]; then
ui_error "No WordPress Sites Found" "No WordPress installations found in /var/www/\n\nPlease install WordPress first using the setup options."
return
fi
local cleanup_info="🧹 WordPress Cleanup & Customization\n\n"
cleanup_info+="Select a WordPress site to clean up and customize:\n\n"
cleanup_info+="Found ${#wp_sites[@]} WordPress installation(s):\n"
for site in "${wp_sites[@]}"; do
cleanup_info+="$site\n"
done
cleanup_info+="\nThis will:\n"
cleanup_info+="• Remove default content and plugins\n"
cleanup_info+="• Install essential plugins (LiteSpeed Cache)\n"
cleanup_info+="• Set up a clean theme (Hello Elementor)\n"
cleanup_info+="• Configure optimal settings\n"
cleanup_info+="• Disable comments globally\n"
cleanup_info+="• Match system timezone and locale\n\n"
cleanup_info+="⚠️ Requires wp-cli to be installed"
while true; do
local menu_items=()
# Add WordPress sites to menu
for site in "${wp_sites[@]}"; do
menu_items+=("$site" "(.Y.) 🧹 Clean up $site")
done
menu_items+=("" "(_*_)")
menu_items+=("zback" "(Z) ← Back to WordPress Menu")
local choice
choice=$(ui_menu "WordPress Cleanup" "$cleanup_info" $DIALOG_HEIGHT $DIALOG_WIDTH $((${#wp_sites[@]} + 2)) "${menu_items[@]}") || break
case "$choice" in
back|zback|z|"")
break
;;
*)
# Check if choice is a valid WordPress site
local found=false
for site in "${wp_sites[@]}"; do
if [[ "$choice" == "$site" ]]; then
found=true
break
fi
done
if [[ "$found" == "true" ]]; then
cleanup_wordpress_installation "$choice"
fi
;;
esac
done
log "Exiting WordPress cleanup menu"
}
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 with comprehensive details
local creds_file="$BACKUP_DIR/wordpress-${domain}-$(date +%Y%m%d-%H%M%S).txt"
cat > "$creds_file" <<EOF
WordPress Installation Complete - Full Details
═══════════════════════════════════════════════════════════════
Installation Date: $(date)
Domain: $domain
Site Directory: /var/www/$domain
DATABASE CREDENTIALS:
═══════════════════════════════════════════════════════════════
Database Name: $db_name
Database User: $db_user
Database Password: $db_pass
Database Host: localhost
Database Port: 3306
SITE ACCESS:
═══════════════════════════════════════════════════════════════
Site URL: http://$domain
Admin URL: http://$domain/wp-admin/
WordPress Setup: http://$domain/wp-admin/install.php
SYSTEM INFORMATION:
═══════════════════════════════════════════════════════════════
Web Server: $(if systemctl is-active --quiet nginx; then echo "Nginx"; elif systemctl is-active --quiet apache2; then echo "Apache"; else echo "Unknown"; fi)
PHP Version: ${PHP_VER}
Database: MariaDB
Site Directory: /var/www/$domain
Config File: /var/www/$domain/wp-config.php
NEXT STEPS:
═══════════════════════════════════════════════════════════════
1. Visit: http://$domain
2. Complete the WordPress installation wizard
3. Create your admin account with strong password
4. Configure site title and description
5. Choose your theme and install plugins
SECURITY RECOMMENDATIONS:
═══════════════════════════════════════════════════════════════
- Set up SSL certificate with Let's Encrypt
- Install security plugins (Wordfence, etc.)
- Configure regular backups
- Update WordPress core and plugins regularly
- Use strong passwords for all accounts
TROUBLESHOOTING:
═══════════════════════════════════════════════════════════════
If you encounter 500 errors:
• Check PHP-FPM: sudo systemctl status php${PHP_VER}-fpm
• Check web server: sudo systemctl status nginx (or apache2)
• Check error logs: sudo tail -f /var/log/nginx/error.log
• Verify file permissions: ls -la /var/www/$domain
Support: Keep this file safe - it contains all your installation details!
EOF
# Enhanced completion message with all details
local completion_msg="🎉 WORDPRESS INSTALLATION COMPLETE! 🎉\n\n"
completion_msg+="═══════════════════════════════════════════════════════════════\n"
completion_msg+="🌐 SITE ACCESS:\n"
completion_msg+=" Site URL: http://$domain\n"
completion_msg+=" Admin URL: http://$domain/wp-admin/\n"
completion_msg+=" Setup URL: http://$domain/wp-admin/install.php\n\n"
completion_msg+="🗄️ DATABASE CREDENTIALS:\n"
completion_msg+=" Database Name: $db_name\n"
completion_msg+=" Database User: $db_user\n"
completion_msg+=" Database Password: $db_pass\n"
completion_msg+=" Database Host: localhost\n"
completion_msg+=" Database Port: 3306\n\n"
completion_msg+="📁 SYSTEM DETAILS:\n"
completion_msg+=" Site Directory: /var/www/$domain\n"
completion_msg+=" Config File: /var/www/$domain/wp-config.php\n"
completion_msg+=" PHP Version: ${PHP_VER}\n"
completion_msg+=" Web Server: $(if systemctl is-active --quiet nginx; then echo "Nginx"; elif systemctl is-active --quiet apache2; then echo "Apache"; else echo "Unknown"; fi)\n\n"
completion_msg+="📝 CREDENTIALS SAVED TO:\n"
completion_msg+=" $creds_file\n\n"
completion_msg+="🚀 NEXT STEPS:\n"
completion_msg+=" 1. Visit http://$domain to start WordPress setup\n"
completion_msg+=" 2. Complete the 5-minute installation wizard\n"
completion_msg+=" 3. Create your admin account (use strong password!)\n"
completion_msg+=" 4. Configure site title and description\n"
completion_msg+=" 5. Start customizing your site!\n\n"
completion_msg+="🔧 TROUBLESHOOTING (if needed):\n"
completion_msg+=" • Check PHP-FPM: sudo systemctl status php${PHP_VER}-fpm\n"
completion_msg+=" • Check Nginx: sudo systemctl status nginx\n"
completion_msg+=" • View error logs: sudo tail -f /var/log/nginx/error.log\n"
completion_msg+=" • Check permissions: ls -la /var/www/$domain\n\n"
completion_msg+="🔒 SECURITY TIP: Set up SSL with Let's Encrypt after setup!\n"
completion_msg+="═══════════════════════════════════════════════════════════════"
ui_info "WordPress Installation Complete!" "$completion_msg"
log "WordPress installation completed for $domain with full details displayed"
}
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
}
setup_local_ssl() {
local domain="$1"
local web_server="$2"
log "Setting up local SSL certificate for $domain"
# Create SSL directory
local ssl_dir="/etc/ssl/local"
sudo mkdir -p "$ssl_dir"
# Generate self-signed certificate
local cert_file="$ssl_dir/$domain.crt"
local key_file="$ssl_dir/$domain.key"
# Create certificate with proper extensions for modern browsers
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout "$key_file" \
-out "$cert_file" \
-subj "/C=US/ST=Local/L=Local/O=Local Development/CN=$domain" \
-extensions v3_req \
-config <(cat /etc/ssl/openssl.cnf <(printf "\n[v3_req]\nsubjectAltName=DNS:$domain,DNS:*.$domain,IP:127.0.0.1")) 2>/dev/null || {
log_error "Failed to generate SSL certificate"
return 1
}
# Set proper permissions
sudo chmod 600 "$key_file"
sudo chmod 644 "$cert_file"
# Update web server configuration for SSL
if [[ "$web_server" == "nginx" ]]; then
setup_nginx_ssl "$domain" "$cert_file" "$key_file"
else
setup_apache_ssl "$domain" "$cert_file" "$key_file"
fi
log "Local SSL certificate setup completed for $domain"
return 0
}
setup_nginx_ssl() {
local domain="$1"
local cert_file="$2"
local key_file="$3"
local nginx_conf="/etc/nginx/sites-available/$domain"
# Add SSL server block to existing configuration
sudo tee -a "$nginx_conf" > /dev/null <<EOF
# HTTPS server block
server {
listen 443 ssl http2;
server_name $domain www.$domain;
root /var/www/$domain;
index index.php index.html index.htm;
# SSL Configuration
ssl_certificate $cert_file;
ssl_certificate_key $key_file;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# 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;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" 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;
# Additional PHP settings for WordPress
fastcgi_read_timeout 300;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
}
# 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;
}
}
EOF
# Test and reload nginx
if sudo nginx -t; then
sudo systemctl reload nginx
return 0
else
log_error "Nginx SSL configuration test failed"
return 1
fi
}
setup_apache_ssl() {
local domain="$1"
local cert_file="$2"
local key_file="$3"
# Enable SSL module
sudo a2enmod ssl
# Create SSL virtual host
local apache_ssl_conf="/etc/apache2/sites-available/${domain}-ssl.conf"
sudo tee "$apache_ssl_conf" > /dev/null <<EOF
<VirtualHost *:443>
ServerName $domain
ServerAlias www.$domain
DocumentRoot /var/www/$domain
# SSL Configuration
SSLEngine on
SSLCertificateFile $cert_file
SSLCertificateKeyFile $key_file
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
# 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 Strict-Transport-Security "max-age=31536000; includeSubDomains"
# WordPress configuration
<Directory /var/www/$domain>
AllowOverride All
Require all granted
</Directory>
# PHP processing
<FilesMatch \.php$>
SetHandler "proxy:unix:/var/run/php/php${PHP_VER}-fpm.sock|fcgi://localhost"
</FilesMatch>
ErrorLog \${APACHE_LOG_DIR}/${domain}_ssl_error.log
CustomLog \${APACHE_LOG_DIR}/${domain}_ssl_access.log combined
</VirtualHost>
EOF
# Enable the SSL site
sudo a2ensite "${domain}-ssl.conf"
# Test and reload Apache
if sudo apache2ctl configtest; then
sudo systemctl reload apache2
return 0
else
log_error "Apache SSL configuration test failed"
return 1
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" $DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${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" $DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${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 and detailed diagnostics
local db_status="❌"
local db_error_msg=""
local db_tables_count="0"
local wp_tables_exist="❌"
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
# Test database connection
local db_test_result
if db_test_result=$(mysql -u"$db_user" -p"$db_pass" -h"${db_host:-localhost}" -e "USE \`$db_name\`; SELECT 'Connection OK' as status;" 2>&1); then
if echo "$db_test_result" | grep -q "Connection OK"; then
db_status="✅ Connected"
# Count tables in database
db_tables_count=$(mysql -u"$db_user" -p"$db_pass" -h"${db_host:-localhost}" -e "USE \`$db_name\`; SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$db_name';" 2>/dev/null | tail -n1)
# Check for WordPress core tables
local wp_core_tables=$(mysql -u"$db_user" -p"$db_pass" -h"${db_host:-localhost}" -e "USE \`$db_name\`; SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$db_name' AND table_name LIKE '%wp_posts' OR table_name LIKE '%wp_users' OR table_name LIKE '%wp_options';" 2>/dev/null | tail -n1)
if [[ "$wp_core_tables" -ge 3 ]]; then
wp_tables_exist="✅ WordPress tables found"
else
wp_tables_exist="⚠️ WordPress tables missing ($wp_core_tables/3)"
fi
else
db_status="❌ Connection failed"
db_error_msg="Database exists but connection failed"
fi
else
db_status="❌ Connection failed"
# Parse error message for common issues
if echo "$db_test_result" | grep -q "Access denied"; then
db_error_msg="Access denied - check credentials"
elif echo "$db_test_result" | grep -q "Unknown database"; then
db_error_msg="Database '$db_name' does not exist"
elif echo "$db_test_result" | grep -q "Can't connect"; then
db_error_msg="Cannot connect to MySQL server"
else
db_error_msg="Connection error: $(echo "$db_test_result" | head -n1)"
fi
fi
else
db_status="❌ Missing credentials"
db_error_msg="Database credentials not found in wp-config.php"
fi
else
db_status="❌ No wp-config.php"
db_error_msg="WordPress configuration file not found"
fi
status_info+="🗄️ Database Connection: $db_status\n"
if [[ -n "$db_error_msg" ]]; then
status_info+="⚠️ Error: $db_error_msg\n"
fi
# Database credentials and diagnostics (if available)
if [[ -f "$site_dir/wp-config.php" ]]; then
status_info+="\nDatabase Information:\n"
status_info+="───────────────────────────────────────────────────────────\n"
status_info+="📊 Database: ${db_name:-❌ Not found}\n"
status_info+="👤 User: ${db_user:-❌ Not found}\n"
status_info+="🔑 Password: ${db_pass:+✅ Set}${db_pass:-❌ Not found}\n"
status_info+="🏠 Host: ${db_host:-localhost}\n"
status_info+="📋 Tables Count: $db_tables_count\n"
status_info+="🔧 WordPress Tables: $wp_tables_exist\n"
# Additional database diagnostics
if [[ "$db_status" == "✅ Connected" ]]; then
# Check database charset
local db_charset=$(mysql -u"$db_user" -p"$db_pass" -h"${db_host:-localhost}" -e "SELECT DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$db_name';" 2>/dev/null | tail -n1)
status_info+="🔤 Charset: ${db_charset:-unknown}\n"
# Check if WordPress is installed (has admin user)
local wp_installed=$(mysql -u"$db_user" -p"$db_pass" -h"${db_host:-localhost}" -e "USE \`$db_name\`; SELECT COUNT(*) FROM wp_users WHERE user_login != '';" 2>/dev/null | tail -n1)
if [[ "$wp_installed" -gt 0 ]]; then
status_info+="👥 WordPress Users: $wp_installed found\n"
else
status_info+="👥 WordPress Users: ❌ No users found (not installed)\n"
fi
fi
fi
# 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"
# WordPress admin URL and setup status
status_info+="\nWordPress Setup:\n"
status_info+="───────────────────────────────────────────────────────────\n"
status_info+="🔗 Admin URL: http://$site/wp-admin/\n"
status_info+="🏠 Site URL: http://$site/\n"
# Check if WordPress is configured
if [[ -f "$site_dir/wp-config.php" ]]; then
local wp_configured="❌"
if mysql -u"$db_user" -p"$db_pass" -h"${db_host:-localhost}" -e "SELECT COUNT(*) FROM ${db_name}.wp_options WHERE option_name='siteurl';" 2>/dev/null | grep -q "1"; then
wp_configured="✅"
fi
status_info+="⚙️ WordPress Configured: $wp_configured\n"
fi
local menu_items=(
"" "(_*_)"
"view-config" "📄 View Full Web Server Config"
"edit-config" "✏️ Edit Web Server Config"
"wp-config" "⚙️ View/Edit wp-config.php"
"test-db" "🗄️ Test Database Connection"
"repair-db" "🔧 Repair Database Connection"
"" "(_*_)"
"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" $DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${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
;;
wp-config)
if [[ -f "$site_dir/wp-config.php" ]]; then
local wp_config_content
wp_config_content=$(cat "$site_dir/wp-config.php")
ui_info "WordPress Configuration: $site_dir/wp-config.php" "$wp_config_content"
if ui_yesno "Edit wp-config.php" "Do you want to edit the WordPress configuration file?"; then
sudo nano "$site_dir/wp-config.php"
fi
else
ui_error "wp-config.php not found at $site_dir/wp-config.php"
fi
;;
test-db)
test_wordpress_database_connection "$site"
;;
repair-db)
repair_wordpress_database_connection "$site"
;;
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
# For .localhost domains, test both the full domain and localhost fallback
if [[ "$site" =~ \.localhost$ ]]; then
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"
# Also test with Host header for local resolution
test_info+="\nLocal Resolution Test:\n"
local local_result
local_result=$(curl -s -o /dev/null -w "HTTP Code: %{http_code}\nTime: %{time_total}s\nSize: %{size_download} bytes" "http://127.0.0.1" -H "Host: $site" --connect-timeout 10 --max-time 30 2>&1)
test_info+="$local_result\n\n"
else
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"
fi
# HTTPS test if SSL is configured
if [[ -f "/etc/letsencrypt/live/$site/fullchain.pem" ]] || [[ -f "/etc/ssl/certs/$site.crt" ]]; then
test_info+="HTTPS Test:\n"
local https_result
if [[ "$site" =~ \.localhost$ ]]; then
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 -k 2>&1)
else
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)
fi
test_info+="$https_result\n\n"
fi
else
test_info+="curl not available for testing\n"
fi
# DNS resolution test - skip for .localhost domains as they resolve automatically
if command -v nslookup >/dev/null 2>&1; then
if [[ "$site" =~ \.localhost$ ]]; then
test_info+="DNS Resolution:\n"
test_info+="✅ .localhost domain - automatically resolves to 127.0.0.1\n"
test_info+="No external DNS lookup required for .localhost domains\n"
else
test_info+="DNS Resolution:\n"
local dns_result
dns_result=$(nslookup "$site" 2>&1 | head -10)
test_info+="$dns_result\n"
fi
fi
ui_info "Site Test Results" "$test_info"
}
delete_wordpress_site() {
local site="$1"
local site_dir="/var/www/$site"
# Extract database credentials from wp-config.php before deletion
local db_name db_user db_pass db_host
if [[ -f "$site_dir/wp-config.php" ]]; then
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)
db_host=${db_host:-localhost}
fi
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+="• Database: ${db_name:-'N/A'}\n"
warning_info+="• Database user: ${db_user:-'N/A'}\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
# Get MySQL root password for database cleanup
local mysql_root_pass
if [[ -f "/root/.mysql_root_password" ]]; then
mysql_root_pass=$(cat /root/.mysql_root_password)
fi
# Remove database and user
if [[ -n "$db_name" && -n "$db_user" && -n "$mysql_root_pass" ]]; then
# Test if database exists and is accessible
if mysql -uroot -p"$mysql_root_pass" -e "USE \`$db_name\`;" >/dev/null 2>&1; then
# Drop database
if mysql -uroot -p"$mysql_root_pass" -e "DROP DATABASE IF EXISTS \`$db_name\`;" >/dev/null 2>&1; then
ui_success "Removed database: $db_name"
else
ui_warning "Failed to remove database: $db_name"
fi
# Drop database user
if mysql -uroot -p"$mysql_root_pass" -e "DROP USER IF EXISTS '$db_user'@'$db_host';" >/dev/null 2>&1; then
ui_success "Removed database user: $db_user"
else
ui_warning "Failed to remove database user: $db_user"
fi
# Flush privileges
mysql -uroot -p"$mysql_root_pass" -e "FLUSH PRIVILEGES;" >/dev/null 2>&1
else
ui_warning "Database '$db_name' not found or not accessible"
fi
elif [[ -n "$db_name" || -n "$db_user" ]]; then
ui_warning "Could not clean up database - missing credentials or root password"
fi
# Remove web server configuration (check both domain and base_domain patterns)
local nginx_removed=false
for config_name in "$site" "${site%%.*}"; do
if [[ -f "/etc/nginx/sites-available/$config_name" ]]; then
sudo rm -f "/etc/nginx/sites-available/$config_name"
sudo rm -f "/etc/nginx/sites-enabled/$config_name"
ui_success "Removed Nginx configuration: $config_name"
nginx_removed=true
fi
done
if [[ "$nginx_removed" == true ]]; then
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 (check multiple possible certificate names)
local ssl_removed=false
for cert_name in "$site" "${site%%.*}" "www.$site" "www.${site%%.*}"; do
if [[ -d "/etc/letsencrypt/live/$cert_name" ]]; then
sudo certbot delete --cert-name "$cert_name" --non-interactive >/dev/null 2>&1
ui_success "Removed SSL certificate: $cert_name"
ssl_removed=true
fi
done
# Remove site directory completely
if [[ -d "$site_dir" ]]; then
# Double-check we're not deleting something critical
if [[ "$site_dir" == "/var/www/$site" && "$site" != "" && "$site" != "." && "$site" != ".." ]]; then
sudo rm -rf "$site_dir"
ui_success "Removed site directory: $site_dir"
else
ui_warning "Skipped removing directory for safety: $site_dir"
fi
fi
# Clean up any remaining broken symlinks in sites-enabled
sudo find /etc/nginx/sites-enabled/ -type l ! -exec test -e {} \; -delete 2>/dev/null
ui_success "Site $site has been completely removed including database and all files"
else
ui_info "Deletion Cancelled" "Site deletion cancelled - nothing was removed"
fi
}
# Legacy function for backward compatibility
setup_wordpress() {
show_wordpress_setup_menu
}
# PHP Configuration Management Functions
show_php_settings_menu() {
log "Entering PHP settings management menu"
while true; do
local current_settings=""
local php_ini_path=""
# Find PHP ini file - more comprehensive search
local php_ini_candidates=(
"/etc/php/8.3/apache2/php.ini"
"/etc/php/8.2/apache2/php.ini"
"/etc/php/8.1/apache2/php.ini"
"/etc/php/8.0/apache2/php.ini"
"/etc/php/7.4/apache2/php.ini"
"/etc/php/8.3/fpm/php.ini"
"/etc/php/8.2/fpm/php.ini"
"/etc/php/8.1/fpm/php.ini"
"/etc/php/8.0/fpm/php.ini"
"/etc/php/7.4/fpm/php.ini"
"/etc/php/8.3/cli/php.ini"
"/etc/php/8.2/cli/php.ini"
"/etc/php/8.1/cli/php.ini"
"/etc/php/8.0/cli/php.ini"
"/etc/php/7.4/cli/php.ini"
)
# Check predefined candidates first
for candidate in "${php_ini_candidates[@]}"; do
if [[ -f "$candidate" ]]; then
php_ini_path="$candidate"
break
fi
done
# If not found, try to find any PHP ini file
if [[ -z "$php_ini_path" ]]; then
php_ini_path=$(find /etc/php -name "php.ini" 2>/dev/null | head -n1)
fi
# Last resort - check common locations
if [[ -z "$php_ini_path" ]]; then
local fallback_paths=(
"/etc/php.ini"
"/usr/local/etc/php.ini"
"/opt/php/etc/php.ini"
)
for fallback in "${fallback_paths[@]}"; do
if [[ -f "$fallback" ]]; then
php_ini_path="$fallback"
break
fi
done
fi
if [[ -z "$php_ini_path" || ! -f "$php_ini_path" ]]; then
local error_msg="🚫 PHP Configuration Not Found\n\n"
error_msg+="Could not locate php.ini file in common locations:\n\n"
error_msg+="• /etc/php/*/apache2/php.ini\n"
error_msg+="• /etc/php/*/fpm/php.ini\n"
error_msg+="• /etc/php/*/cli/php.ini\n"
error_msg+="• /etc/php.ini\n\n"
error_msg+="Please ensure PHP is installed:\n"
error_msg+="sudo apt update && sudo apt install php"
ui_error "PHP Configuration Not Found" "$error_msg"
return 1
fi
# Get current PHP settings with better error handling
local upload_max=$(grep "^[[:space:]]*upload_max_filesize" "$php_ini_path" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' | tail -n1)
local post_max=$(grep "^[[:space:]]*post_max_size" "$php_ini_path" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' | tail -n1)
local memory_limit=$(grep "^[[:space:]]*memory_limit" "$php_ini_path" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' | tail -n1)
local max_exec=$(grep "^[[:space:]]*max_execution_time" "$php_ini_path" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' | tail -n1)
local max_input=$(grep "^[[:space:]]*max_input_time" "$php_ini_path" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' | tail -n1)
# Set defaults if not found
[[ -z "$upload_max" ]] && upload_max="Not set"
[[ -z "$post_max" ]] && post_max="Not set"
[[ -z "$memory_limit" ]] && memory_limit="Not set"
[[ -z "$max_exec" ]] && max_exec="Not set"
[[ -z "$max_input" ]] && max_input="Not set"
current_settings="🐘 PHP Configuration Management\n\n"
current_settings+="📁 Configuration File: $php_ini_path\n\n"
current_settings+="📊 Current Settings:\n"
current_settings+="• Upload Max Filesize: $upload_max\n"
current_settings+="• Post Max Size: $post_max\n"
current_settings+="• Memory Limit: $memory_limit\n"
current_settings+="• Max Execution Time: $max_exec\n"
current_settings+="• Max Input Time: $max_input\n\n"
current_settings+="💡 Recommended for large file uploads:\n"
current_settings+="• Upload Max Filesize: 8G\n"
current_settings+="• Post Max Size: 8G\n"
current_settings+="• Memory Limit: 8G\n"
current_settings+="• Max Execution Time: 300\n"
current_settings+="• Max Input Time: 300"
local menu_items=(
"optimize-uploads" "🚀 Optimize for Large File Uploads (8G)"
"custom-settings" "⚙️ Custom PHP Settings"
"view-config" "📄 View Full php.ini File"
"restart-services" "🔄 Restart Web Services"
"back" "(Z) ← Back to Main Menu"
)
local choice
choice=$(ui_menu "PHP Settings Management" "$current_settings" $DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${menu_items[@]}") || break
case "$choice" in
optimize-uploads)
optimize_php_for_uploads "$php_ini_path"
;;
custom-settings)
configure_custom_php_settings "$php_ini_path"
;;
view-config)
view_php_config "$php_ini_path"
;;
restart-services)
restart_web_services
;;
back|z|"")
break
;;
esac
done
}
optimize_php_for_uploads() {
local php_ini_path="$1"
log "Optimizing PHP settings for large file uploads"
local optimization_info="🚀 PHP Upload Optimization\n\n"
optimization_info+="This will configure PHP for large file uploads:\n\n"
optimization_info+="📤 Upload Settings:\n"
optimization_info+="• upload_max_filesize = 8G\n"
optimization_info+="• post_max_size = 8G\n\n"
optimization_info+="💾 Memory & Performance:\n"
optimization_info+="• memory_limit = 8G\n"
optimization_info+="• max_execution_time = 300\n"
optimization_info+="• max_input_time = 300\n\n"
optimization_info+="⚠️ This will modify: $php_ini_path\n\n"
optimization_info+="Continue with optimization?"
if ! ui_yesno "PHP Upload Optimization" "$optimization_info"; then
return
fi
# Backup current php.ini
local backup_file="$BACKUP_DIR/php.ini.backup.$(date +%Y%m%d_%H%M%S)"
if sudo cp "$php_ini_path" "$backup_file" 2>/dev/null; then
log "PHP configuration backed up to: $backup_file"
else
ui_error "Backup Failed" "Could not create backup of php.ini file.\n\nOperation cancelled for safety."
return 1
fi
# Apply optimizations
local temp_file="/tmp/php_optimization.tmp"
# Create sed script for all modifications
cat > "$temp_file" << 'EOF'
# Update or add upload_max_filesize
/^[[:space:]]*upload_max_filesize[[:space:]]*=/ c\
upload_max_filesize = 8G
# Update or add post_max_size
/^[[:space:]]*post_max_size[[:space:]]*=/ c\
post_max_size = 8G
# Update or add memory_limit
/^[[:space:]]*memory_limit[[:space:]]*=/ c\
memory_limit = 8G
# Update or add max_execution_time
/^[[:space:]]*max_execution_time[[:space:]]*=/ c\
max_execution_time = 300
# Update or add max_input_time
/^[[:space:]]*max_input_time[[:space:]]*=/ c\
max_input_time = 300
EOF
# Apply changes
if sudo sed -i -f "$temp_file" "$php_ini_path" 2>/dev/null; then
# Verify settings were applied, if not, append them
local settings_to_check=(
"upload_max_filesize = 8G"
"post_max_size = 8G"
"memory_limit = 8G"
"max_execution_time = 300"
"max_input_time = 300"
)
for setting in "${settings_to_check[@]}"; do
local key="${setting%% =*}"
if ! grep -q "^[[:space:]]*$key[[:space:]]*=" "$php_ini_path" 2>/dev/null; then
echo "$setting" | sudo tee -a "$php_ini_path" >/dev/null
fi
done
rm -f "$temp_file"
ui_info "Optimization Complete" "✅ PHP has been optimized for large file uploads!\n\n📁 Configuration: $php_ini_path\n💾 Backup: $backup_file\n\n🔄 Web services need to be restarted for changes to take effect.\n\nRestart services now?"
if ui_yesno "Restart Services" "Restart Apache/Nginx and PHP-FPM to apply changes?"; then
restart_web_services
fi
else
rm -f "$temp_file"
ui_error "Optimization Failed" "Could not modify PHP configuration.\n\nPlease check file permissions and try again."
return 1
fi
}
configure_custom_php_settings() {
local php_ini_path="$1"
log "Configuring custom PHP settings"
local custom_info="⚙️ Custom PHP Settings\n\n"
custom_info+="Configure individual PHP parameters:\n\n"
custom_info+="📁 File: $php_ini_path\n\n"
custom_info+="Select a setting to modify:"
local menu_items=(
"upload_max" "📤 Upload Max Filesize"
"post_max" "📮 Post Max Size"
"memory_limit" "💾 Memory Limit"
"max_execution" "⏱️ Max Execution Time"
"max_input" "⏲️ Max Input Time"
"back" "← Back"
)
while true; do
local choice
choice=$(ui_menu "Custom PHP Settings" "$custom_info" $DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${menu_items[@]}") || break
case "$choice" in
upload_max)
modify_php_setting "$php_ini_path" "upload_max_filesize" "Upload Max Filesize" "8G" "Maximum size for uploaded files (e.g., 8G, 512M, 100M)"
;;
post_max)
modify_php_setting "$php_ini_path" "post_max_size" "Post Max Size" "8G" "Maximum size for POST data (e.g., 8G, 512M, 100M)"
;;
memory_limit)
modify_php_setting "$php_ini_path" "memory_limit" "Memory Limit" "8G" "Maximum memory per script (e.g., 8G, 512M, 256M)"
;;
max_execution)
modify_php_setting "$php_ini_path" "max_execution_time" "Max Execution Time" "300" "Maximum execution time in seconds (e.g., 300, 600, 0 for unlimited)"
;;
max_input)
modify_php_setting "$php_ini_path" "max_input_time" "Max Input Time" "300" "Maximum input parsing time in seconds (e.g., 300, 600)"
;;
back)
break
;;
esac
done
}
modify_php_setting() {
local php_ini_path="$1"
local setting_key="$2"
local setting_name="$3"
local default_value="$4"
local description="$5"
local current_value=$(grep "^[[:space:]]*$setting_key[[:space:]]*=" "$php_ini_path" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' || echo "Not set")
local input_info="⚙️ Configure $setting_name\n\n"
input_info+="📁 File: $php_ini_path\n"
input_info+="🔧 Setting: $setting_key\n"
input_info+="📊 Current Value: $current_value\n\n"
input_info+="💡 Description: $description\n\n"
input_info+="Enter new value (or press Cancel to abort):"
local new_value
new_value=$(ui_inputbox "$setting_name Configuration" "$input_info" "$default_value") || return
if [[ -z "$new_value" ]]; then
ui_error "Invalid Input" "Value cannot be empty."
return 1
fi
# Backup and modify
local backup_file="$BACKUP_DIR/php.ini.backup.$(date +%Y%m%d_%H%M%S)"
if sudo cp "$php_ini_path" "$backup_file" 2>/dev/null; then
if grep -q "^[[:space:]]*$setting_key[[:space:]]*=" "$php_ini_path" 2>/dev/null; then
# Update existing setting
sudo sed -i "s/^[[:space:]]*$setting_key[[:space:]]*=.*/$setting_key = $new_value/" "$php_ini_path"
else
# Add new setting
echo "$setting_key = $new_value" | sudo tee -a "$php_ini_path" >/dev/null
fi
ui_info "Setting Updated" "$setting_name updated successfully!\n\n🔧 Setting: $setting_key\n📊 New Value: $new_value\n💾 Backup: $backup_file\n\n🔄 Restart web services to apply changes."
else
ui_error "Update Failed" "Could not modify PHP configuration.\n\nPlease check file permissions."
return 1
fi
}
view_php_config() {
local php_ini_path="$1"
log "Viewing PHP configuration file"
if [[ -f "$php_ini_path" ]]; then
local config_content
config_content=$(sudo cat "$php_ini_path" 2>/dev/null | head -n 100)
ui_info "PHP Configuration: $php_ini_path" "$config_content\n\n(Showing first 100 lines)\n\nFull file path: $php_ini_path"
if ui_yesno "Edit php.ini" "Do you want to edit the PHP configuration file with nano?"; then
sudo nano "$php_ini_path"
fi
else
ui_error "File Not Found" "PHP configuration file not found at: $php_ini_path"
fi
}
restart_web_services() {
log "Restarting web services to apply PHP configuration changes"
local restart_info="🔄 Restart Web Services\n\n"
restart_info+="This will restart the following services to apply PHP configuration changes:\n\n"
restart_info+="🌐 Web Server:\n"
# Detect web server
local web_services=()
if systemctl is-active --quiet apache2 2>/dev/null; then
restart_info+="• Apache2 (active)\n"
web_services+=("apache2")
fi
if systemctl is-active --quiet nginx 2>/dev/null; then
restart_info+="• Nginx (active)\n"
web_services+=("nginx")
fi
restart_info+="🐘 PHP Services:\n"
# Detect PHP-FPM services
local php_services=()
for version in 8.1 8.0 7.4; do
if systemctl is-active --quiet "php$version-fpm" 2>/dev/null; then
restart_info+="• PHP $version FPM (active)\n"
php_services+=("php$version-fpm")
fi
done
if [[ ${#web_services[@]} -eq 0 && ${#php_services[@]} -eq 0 ]]; then
ui_error "No Services Found" "No active web or PHP services found to restart."
return 1
fi
restart_info+="⚠️ Services will be briefly unavailable during restart.\n\nContinue?"
if ! ui_yesno "Restart Services" "$restart_info"; then
return
fi
local restart_results=""
local restart_success=true
# Restart web services
for service in "${web_services[@]}"; do
restart_results+="Restarting $service... "
if sudo systemctl restart "$service" 2>/dev/null; then
restart_results+="✅ Success\n"
else
restart_results+="❌ Failed\n"
restart_success=false
fi
done
# Restart PHP services
for service in "${php_services[@]}"; do
restart_results+="Restarting $service... "
if sudo systemctl restart "$service" 2>/dev/null; then
restart_results+="✅ Success\n"
else
restart_results+="❌ Failed\n"
restart_success=false
fi
done
if [[ "$restart_success" == "true" ]]; then
ui_info "Services Restarted" "✅ All services restarted successfully!\n\n$restart_results\n🎉 PHP configuration changes are now active."
else
ui_info "Restart Issues" "⚠️ Some services failed to restart:\n\n$restart_results\n🔧 Please check service status manually if needed."
fi
}
# WordPress database testing and repair functions
test_wordpress_database_connection() {
local site="$1"
local site_dir="/var/www/$site"
log "Testing database connection for WordPress site: $site"
if [[ ! -f "$site_dir/wp-config.php" ]]; then
ui_error "wp-config.php not found" "Cannot test database connection.\n\nFile not found: $site_dir/wp-config.php"
return 1
fi
# Extract database credentials
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)
db_host=${db_host:-localhost}
local test_info="🗄️ Database Connection Test\n\n"
test_info+="Site: $site\n"
test_info+="Database: ${db_name:-❌ Not found}\n"
test_info+="User: ${db_user:-❌ Not found}\n"
test_info+="Host: $db_host\n\n"
if [[ -z "$db_name" || -z "$db_user" || -z "$db_pass" ]]; then
test_info+="❌ FAILED: Missing database credentials in wp-config.php\n\n"
test_info+="Required credentials not found:\n"
[[ -z "$db_name" ]] && test_info+="• DB_NAME is missing\n"
[[ -z "$db_user" ]] && test_info+="• DB_USER is missing\n"
[[ -z "$db_pass" ]] && test_info+="• DB_PASSWORD is missing\n"
ui_info "Database Test Failed" "$test_info"
return 1
fi
# Test connection
local db_test_result
if db_test_result=$(mysql -u"$db_user" -p"$db_pass" -h"$db_host" -e "USE \`$db_name\`; SELECT 'Connection successful' as status, NOW() as timestamp;" 2>&1); then
if echo "$db_test_result" | grep -q "Connection successful"; then
test_info+="✅ SUCCESS: Database connection established\n\n"
# Additional diagnostics
local table_count=$(mysql -u"$db_user" -p"$db_pass" -h"$db_host" -e "USE \`$db_name\`; SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$db_name';" 2>/dev/null | tail -n1)
local wp_tables=$(mysql -u"$db_user" -p"$db_pass" -h"$db_host" -e "USE \`$db_name\`; SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$db_name' AND (table_name LIKE '%wp_posts' OR table_name LIKE '%wp_users' OR table_name LIKE '%wp_options');" 2>/dev/null | tail -n1)
local user_count=$(mysql -u"$db_user" -p"$db_pass" -h"$db_host" -e "USE \`$db_name\`; SELECT COUNT(*) FROM wp_users;" 2>/dev/null | tail -n1)
test_info+="Database Statistics:\n"
test_info+="• Total tables: $table_count\n"
test_info+="• WordPress core tables: $wp_tables/3\n"
test_info+="• WordPress users: $user_count\n\n"
if [[ "$wp_tables" -ge 3 && "$user_count" -gt 0 ]]; then
test_info+="✅ WordPress appears to be fully installed\n"
elif [[ "$wp_tables" -ge 3 ]]; then
test_info+="⚠️ WordPress tables exist but no users found\n"
else
test_info+="⚠️ WordPress tables missing - installation incomplete\n"
fi
ui_info "Database Test Successful" "$test_info"
else
test_info+="❌ FAILED: Connection established but database query failed\n\n"
test_info+="Error details:\n$db_test_result"
ui_info "Database Test Failed" "$test_info"
fi
else
test_info+="❌ FAILED: Cannot connect to database\n\n"
# Parse error for common issues
if echo "$db_test_result" | grep -q "Access denied"; then
test_info+="Error: Access denied\n"
test_info+="• Check username and password\n"
test_info+="• Verify user has database permissions\n"
elif echo "$db_test_result" | grep -q "Unknown database"; then
test_info+="Error: Database does not exist\n"
test_info+="• Database '$db_name' was not found\n"
test_info+="• Check database name spelling\n"
test_info+="• Create database if needed\n"
elif echo "$db_test_result" | grep -q "Can't connect"; then
test_info+="Error: Cannot connect to MySQL server\n"
test_info+="• Check if MariaDB/MySQL is running\n"
test_info+="• Verify host address: $db_host\n"
test_info+="• Check firewall settings\n"
else
test_info+="Error details:\n$(echo "$db_test_result" | head -n3)\n"
fi
ui_info "Database Test Failed" "$test_info"
fi
}
repair_wordpress_database_connection() {
local site="$1"
local site_dir="/var/www/$site"
log "Starting database repair for WordPress site: $site"
if [[ ! -f "$site_dir/wp-config.php" ]]; then
ui_error "wp-config.php not found" "Cannot repair database connection.\n\nFile not found: $site_dir/wp-config.php"
return 1
fi
local repair_info="🔧 Database Connection Repair\n\n"
repair_info+="This will attempt to:\n"
repair_info+="• Verify database credentials\n"
repair_info+="• Test database connectivity\n"
repair_info+="• Create missing database if needed\n"
repair_info+="• Recreate database user if needed\n"
repair_info+="• Fix common connection issues\n\n"
repair_info+="Continue with repair?"
if ! ui_yesno "Database Repair" "$repair_info"; then
return
fi
# Extract current credentials
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)
db_host=${db_host:-localhost}
local repair_steps=""
local repair_success=true
# Step 1: Validate credentials
repair_steps+="Step 1: Validating credentials...\n"
if [[ -z "$db_name" || -z "$db_user" || -z "$db_pass" ]]; then
repair_steps+="❌ Missing credentials in wp-config.php\n"
repair_success=false
else
repair_steps+="✅ Credentials found\n"
fi
# Step 2: Test MariaDB service
repair_steps+="Step 2: Checking MariaDB service...\n"
if systemctl is-active --quiet mariadb; then
repair_steps+="✅ MariaDB is running\n"
else
repair_steps+="⚠️ MariaDB is not running - attempting to start...\n"
if sudo systemctl start mariadb 2>/dev/null; then
repair_steps+="✅ MariaDB started successfully\n"
else
repair_steps+="❌ Failed to start MariaDB\n"
repair_success=false
fi
fi
if [[ "$repair_success" == "true" && -n "$db_name" && -n "$db_user" && -n "$db_pass" ]]; then
# Step 3: Test connection with current credentials
repair_steps+="Step 3: Testing database connection...\n"
if mysql -u"$db_user" -p"$db_pass" -h"$db_host" -e "USE \`$db_name\`;" 2>/dev/null; then
repair_steps+="✅ Connection successful - no repair needed\n"
else
repair_steps+="⚠️ Connection failed - attempting repair...\n"
# Step 4: Try to recreate database and user with root access
repair_steps+="Step 4: Recreating database and user...\n"
# Get root access
local root_pass=""
if [[ -f "/root/.mysql_root_password" ]]; then
root_pass=$(sudo cat "/root/.mysql_root_password" 2>/dev/null | tr -d '\n\r')
fi
local mysql_cmd=""
if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" 2>/dev/null; then
mysql_cmd="mysql -u root -p\"$root_pass\""
elif sudo mysql -e "SELECT 1;" 2>/dev/null; then
mysql_cmd="sudo mysql"
else
repair_steps+="❌ Cannot access MariaDB as root\n"
repair_success=false
fi
if [[ -n "$mysql_cmd" ]]; then
# Create database
if eval "$mysql_cmd -e \"CREATE DATABASE IF NOT EXISTS \\\`$db_name\\\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\"" 2>/dev/null; then
repair_steps+="✅ Database '$db_name' created/verified\n"
else
repair_steps+="❌ Failed to create database\n"
repair_success=false
fi
# Create user and grant privileges
if eval "$mysql_cmd -e \"CREATE USER IF NOT EXISTS '$db_user'@'localhost' IDENTIFIED BY '$db_pass';\"" 2>/dev/null; then
repair_steps+="✅ User '$db_user' created/verified\n"
else
repair_steps+="❌ Failed to create user\n"
repair_success=false
fi
if eval "$mysql_cmd -e \"GRANT ALL PRIVILEGES ON \\\`$db_name\\\`.* TO '$db_user'@'localhost'; FLUSH PRIVILEGES;\"" 2>/dev/null; then
repair_steps+="✅ Privileges granted\n"
else
repair_steps+="❌ Failed to grant privileges\n"
repair_success=false
fi
# Final test
repair_steps+="Step 5: Final connection test...\n"
if mysql -u"$db_user" -p"$db_pass" -h"$db_host" -e "USE \`$db_name\`;" 2>/dev/null; then
repair_steps+="✅ Database connection repaired successfully!\n"
else
repair_steps+="❌ Connection still failing after repair\n"
repair_success=false
fi
fi
fi
fi
# Show repair results
if [[ "$repair_success" == "true" ]]; then
ui_info "Repair Successful" "✅ Database connection repair completed!\n\n$repair_steps\nYour WordPress site should now be able to connect to the database."
else
ui_info "Repair Failed" "❌ Database connection repair failed\n\n$repair_steps\nManual intervention may be required. Check the database credentials and MariaDB configuration."
fi
}
# ==============================================================================
# LARGE SQL IMPORT OPTIMIZATION
# ==============================================================================
optimize_for_large_sql_imports() {
log "Starting large SQL import optimization"
local info="🗄️ Large SQL Import Optimization\n\n"
info+="This will optimize your server configuration for importing large SQL files:\n\n"
info+="📊 PHP Configuration:\n"
info+="• Memory limit: 1024M (1GB)\n"
info+="• Execution time: 1800s (30 minutes)\n"
info+="• Upload limits: 256M\n\n"
info+="🌐 Web Server (Nginx/Apache):\n"
info+="• Client body size: 256M\n"
info+="• Timeout settings: 30 minutes\n"
info+="• Buffer optimizations\n\n"
info+="🗄️ MySQL/MariaDB:\n"
info+="• Connection timeouts: 30 minutes\n"
info+="• Packet size: 256M\n"
info+="• Buffer optimizations\n\n"
info+="⚠️ Services will be restarted to apply changes.\n\n"
info+="Continue with optimization?"
if ! ui_yesno "SQL Import Optimization" "$info"; then
return
fi
ui_msg "Step 1/4" "🔧 Optimizing PHP configuration...\n\nConfiguring memory limits, execution times, and upload settings for large SQL operations."
# Optimize PHP settings
optimize_php_for_sql_imports || {
ui_error "PHP Optimization Failed" "Failed to optimize PHP configuration. Check the logs for details."
return 1
}
ui_msg "Step 2/4" "🌐 Optimizing web server configuration...\n\nConfiguring Nginx/Apache timeouts and buffer sizes for large requests."
# Optimize web server settings
optimize_webserver_for_sql_imports || {
ui_error "Web Server Optimization Failed" "Failed to optimize web server configuration. Check the logs for details."
return 1
}
ui_msg "Step 3/4" "🗄️ Optimizing MySQL/MariaDB configuration...\n\nConfiguring database timeouts, packet sizes, and buffer settings."
# Optimize MySQL/MariaDB settings
optimize_mysql_for_sql_imports || {
ui_error "MySQL Optimization Failed" "Failed to optimize MySQL configuration. Check the logs for details."
return 1
}
ui_msg "Step 4/4" "🔄 Restarting services...\n\nRestarting web server, PHP-FPM, and MySQL to apply all optimizations."
# Restart all services
restart_services_for_sql_imports || {
ui_error "Service Restart Failed" "Failed to restart services. Some optimizations may not be active."
return 1
}
ui_msg "Optimization Complete!" "✅ Your server is now optimized for large SQL imports!\n\n🎯 Optimized Components:\n• PHP: Memory, execution time, upload limits\n• Web Server: Timeouts, buffer sizes\n• MySQL: Connection limits, packet size\n• Services: All restarted and active\n\n💡 Tips for importing large SQL files:\n• Use phpMyAdmin's import feature\n• Consider using command line: mysql -u user -p database < file.sql\n• Monitor the process in /var/log/mysql/error.log\n• Large imports may still take 10-30 minutes"
}
optimize_php_for_sql_imports() {
log "Optimizing PHP for large SQL imports"
# Detect PHP version
local php_version
php_version=$(php -v 2>/dev/null | head -n1 | cut -d' ' -f2 | cut -d'.' -f1,2) || {
log_error "Could not detect PHP version"
return 1
}
log "Detected PHP version: $php_version"
# Update PHP-FPM configuration (this is what handles web requests!)
local php_fpm_ini="/etc/php/$php_version/fpm/php.ini"
if [[ -f "$php_fpm_ini" ]]; then
log "Updating PHP-FPM configuration: $php_fpm_ini"
# Backup original configuration
local backup_file="${php_fpm_ini}.backup-sql-import-$(date +%Y%m%d-%H%M%S)"
sudo cp "$php_fpm_ini" "$backup_file" || {
log_error "Failed to backup PHP-FPM configuration"
return 1
}
# Apply optimizations using sed (more reliable than append method)
sudo sed -i 's/^upload_max_filesize = .*/upload_max_filesize = 256M/' "$php_fpm_ini"
sudo sed -i 's/^post_max_size = .*/post_max_size = 256M/' "$php_fpm_ini"
sudo sed -i 's/^memory_limit = .*/memory_limit = 1024M/' "$php_fpm_ini"
sudo sed -i 's/^max_execution_time = .*/max_execution_time = 1800/' "$php_fpm_ini"
sudo sed -i 's/^max_input_time = .*/max_input_time = 1800/' "$php_fpm_ini"
sudo sed -i 's/^max_file_uploads = .*/max_file_uploads = 50/' "$php_fpm_ini"
# Add settings that might not exist
if ! grep -q "mysql.connect_timeout" "$php_fpm_ini"; then
echo "mysql.connect_timeout = 300" | sudo tee -a "$php_fpm_ini" >/dev/null
fi
if ! grep -q "default_socket_timeout" "$php_fpm_ini"; then
echo "default_socket_timeout = 300" | sudo tee -a "$php_fpm_ini" >/dev/null
fi
log "PHP-FPM configuration updated successfully"
else
log_error "PHP-FPM configuration file not found: $php_fpm_ini"
return 1
fi
# Update PHP-FPM pool configuration for better handling of large requests
local php_fpm_pool="/etc/php/$php_version/fpm/pool.d/www.conf"
if [[ -f "$php_fpm_pool" ]]; then
log "Updating PHP-FPM pool configuration: $php_fpm_pool"
# Backup the pool configuration
local pool_backup="${php_fpm_pool}.backup-sql-import-$(date +%Y%m%d-%H%M%S)"
sudo cp "$php_fpm_pool" "$pool_backup"
# Update pool settings for better performance with large uploads
sudo sed -i 's/^pm.max_children = .*/pm.max_children = 50/' "$php_fpm_pool"
sudo sed -i 's/^pm.start_servers = .*/pm.start_servers = 10/' "$php_fpm_pool"
sudo sed -i 's/^pm.min_spare_servers = .*/pm.min_spare_servers = 5/' "$php_fpm_pool"
sudo sed -i 's/^pm.max_spare_servers = .*/pm.max_spare_servers = 15/' "$php_fpm_pool"
# Add request_terminate_timeout if it doesn't exist or is commented
if ! grep -q "^request_terminate_timeout" "$php_fpm_pool"; then
if grep -q "^;request_terminate_timeout" "$php_fpm_pool"; then
sudo sed -i 's/^;request_terminate_timeout = .*/request_terminate_timeout = 1800/' "$php_fpm_pool"
else
echo "request_terminate_timeout = 1800" | sudo tee -a "$php_fpm_pool" > /dev/null
fi
else
sudo sed -i 's/^request_terminate_timeout = .*/request_terminate_timeout = 1800/' "$php_fpm_pool"
fi
log "PHP-FPM pool configuration updated"
else
log_error "PHP-FPM pool configuration file not found: $php_fpm_pool"
fi
# Also update Apache PHP config if it exists
local php_apache_ini="/etc/php/$php_version/apache2/php.ini"
if [[ -f "$php_apache_ini" ]]; then
log "Also updating Apache PHP configuration: $php_apache_ini"
# Backup and update Apache PHP config
local backup_file="${php_apache_ini}.backup-sql-import-$(date +%Y%m%d-%H%M%S)"
sudo cp "$php_apache_ini" "$backup_file"
sudo sed -i 's/^upload_max_filesize = .*/upload_max_filesize = 256M/' "$php_apache_ini"
sudo sed -i 's/^post_max_size = .*/post_max_size = 256M/' "$php_apache_ini"
sudo sed -i 's/^memory_limit = .*/memory_limit = 1024M/' "$php_apache_ini"
sudo sed -i 's/^max_execution_time = .*/max_execution_time = 1800/' "$php_apache_ini"
sudo sed -i 's/^max_input_time = .*/max_input_time = 1800/' "$php_apache_ini"
sudo sed -i 's/^max_file_uploads = .*/max_file_uploads = 50/' "$php_apache_ini"
log "Apache PHP configuration updated successfully"
fi
log "PHP configuration optimized for SQL imports"
return 0
}
optimize_webserver_for_sql_imports() {
log "Optimizing web server for large SQL imports"
# Check which web server is running
local webserver=""
if systemctl is-active --quiet nginx; then
webserver="nginx"
elif systemctl is-active --quiet apache2; then
webserver="apache2"
else
log_error "No active web server found (nginx or apache2)"
return 1
fi
if [[ "$webserver" == "nginx" ]]; then
optimize_nginx_for_sql_imports
else
optimize_apache_for_sql_imports
fi
}
optimize_nginx_for_sql_imports() {
log "Optimizing Nginx for large SQL imports"
# First, update the global optimization file
local sql_import_conf="/etc/nginx/conf.d/sql-import-optimization.conf"
sudo tee "$sql_import_conf" > /dev/null << 'EOF'
# Large SQL Import Optimizations
client_max_body_size 256M;
client_body_timeout 300s;
client_header_timeout 300s;
fastcgi_read_timeout 1800s;
fastcgi_send_timeout 1800s;
fastcgi_connect_timeout 300s;
fastcgi_buffer_size 256k;
fastcgi_buffers 8 256k;
fastcgi_busy_buffers_size 512k;
keepalive_timeout 300s;
EOF
log "Global Nginx optimization configuration created"
# Now fix site-specific configurations that might override global settings
local sites_dir="/etc/nginx/sites-available"
if [[ -d "$sites_dir" ]]; then
log "Checking site-specific configurations for client_max_body_size overrides"
# Find all site configs with client_max_body_size less than 256M
local site_files
site_files=$(find "$sites_dir" -name "*.conf" -o -name "*" ! -name "*.backup*" ! -name "*.bak" ! -name "*.orig" 2>/dev/null)
for site_file in $site_files; do
if [[ -f "$site_file" ]]; then
# Check if this site has a client_max_body_size setting
if grep -q "client_max_body_size" "$site_file"; then
log "Found client_max_body_size in $site_file, updating to 256M"
# Backup the site file
local backup_file="${site_file}.backup-sql-import-$(date +%Y%m%d-%H%M%S)"
sudo cp "$site_file" "$backup_file"
# Update client_max_body_size to 256M
sudo sed -i 's/client_max_body_size [^;]*;/client_max_body_size 256M;/g' "$site_file"
log "Updated client_max_body_size in $(basename "$site_file")"
fi
fi
done
fi
# Test Nginx configuration
if sudo nginx -t 2>/dev/null; then
log "Nginx configuration optimized for SQL imports"
return 0
else
log_error "Nginx configuration test failed, checking what went wrong..."
# Show the actual error
local nginx_error
nginx_error=$(sudo nginx -t 2>&1)
log_error "Nginx test output: $nginx_error"
# Try to fix common issues
if echo "$nginx_error" | grep -q "duplicate"; then
log "Attempting to fix duplicate directive issues..."
# Remove our optimization file and try again
sudo rm -f "$sql_import_conf"
# Create a simpler version
sudo tee "$sql_import_conf" > /dev/null << 'EOF'
# Large SQL Import Optimizations - Simplified
client_max_body_size 256M;
fastcgi_read_timeout 1800s;
EOF
if sudo nginx -t 2>/dev/null; then
log "Nginx configuration fixed with simplified settings"
return 0
fi
fi
# If still failing, remove our config file
sudo rm -f "$sql_import_conf"
log_error "Could not fix Nginx configuration, removed optimization file"
return 1
fi
}
optimize_apache_for_sql_imports() {
log "Optimizing Apache for large SQL imports"
# Create optimization configuration
local sql_import_conf="/etc/apache2/conf-available/sql-import-optimization.conf"
sudo tee "$sql_import_conf" > /dev/null << 'EOF'
# Large SQL Import Optimizations
LimitRequestBody 268435456
Timeout 1800
KeepAliveTimeout 300
<IfModule mod_php7.c>
php_value max_execution_time 1800
php_value max_input_time 1800
php_value memory_limit 1024M
php_value upload_max_filesize 256M
php_value post_max_size 256M
</IfModule>
<IfModule mod_php8.c>
php_value max_execution_time 1800
php_value max_input_time 1800
php_value memory_limit 1024M
php_value upload_max_filesize 256M
php_value post_max_size 256M
</IfModule>
EOF
# Enable the configuration
sudo a2enconf sql-import-optimization 2>/dev/null || {
log_error "Failed to enable Apache SQL import configuration"
return 1
}
# Test Apache configuration
if sudo apache2ctl configtest 2>/dev/null; then
log "Apache configuration optimized for SQL imports"
return 0
else
log_error "Apache configuration test failed"
sudo a2disconf sql-import-optimization 2>/dev/null
sudo rm -f "$sql_import_conf"
return 1
fi
}
optimize_mysql_for_sql_imports() {
log "Optimizing MySQL/MariaDB for large SQL imports"
local mysql_conf="/etc/mysql/conf.d/sql-import-optimization.cnf"
local backup_dir="/etc/mysql/conf.d/backups"
# Create backup directory
sudo mkdir -p "$backup_dir" 2>/dev/null
# Create optimization configuration
sudo tee "$mysql_conf" > /dev/null << 'EOF'
[mysqld]
# Large SQL Import Optimizations
max_connections = 200
connect_timeout = 300
wait_timeout = 1800
interactive_timeout = 1800
net_read_timeout = 300
net_write_timeout = 300
innodb_buffer_pool_size = 512M
max_allowed_packet = 256M
bulk_insert_buffer_size = 64M
myisam_sort_buffer_size = 128M
tmp_table_size = 256M
max_heap_table_size = 256M
innodb_log_file_size = 256M
innodb_log_buffer_size = 64M
innodb_flush_log_at_trx_commit = 2
EOF
log "MySQL/MariaDB configuration optimized for SQL imports"
return 0
}
restart_services_for_sql_imports() {
log "Restarting services for SQL import optimizations"
local services_restarted=0
local services_failed=()
# Restart PHP-FPM
local php_version
php_version=$(php -v 2>/dev/null | head -n1 | cut -d' ' -f2 | cut -d'.' -f1,2)
if [[ -n "$php_version" ]]; then
if sudo systemctl restart "php${php_version}-fpm" 2>/dev/null; then
log "PHP-FPM restarted successfully"
((services_restarted++))
else
log_error "Failed to restart PHP-FPM"
services_failed+=("PHP-FPM")
fi
fi
# Restart web server
if systemctl is-active --quiet nginx; then
if sudo systemctl restart nginx 2>/dev/null; then
log "Nginx restarted successfully"
((services_restarted++))
else
log_error "Failed to restart Nginx"
services_failed+=("Nginx")
fi
elif systemctl is-active --quiet apache2; then
if sudo systemctl restart apache2 2>/dev/null; then
log "Apache restarted successfully"
((services_restarted++))
else
log_error "Failed to restart Apache"
services_failed+=("Apache")
fi
fi
# Restart MySQL/MariaDB
if sudo systemctl restart mariadb 2>/dev/null; then
log "MariaDB restarted successfully"
((services_restarted++))
else
log_error "Failed to restart MariaDB"
services_failed+=("MariaDB")
fi
if [[ ${#services_failed[@]} -eq 0 ]]; then
log "All services restarted successfully ($services_restarted services)"
return 0
else
log_error "Some services failed to restart: ${services_failed[*]}"
return 1
fi
}
# WordPress WP-CLI Management
wordpress_wpcli_management() {
while true; do
local wpcli_status="WP-CLI Management\n\n"
# Check if WP-CLI is installed
if command -v wp >/dev/null 2>&1; then
local wp_version=$(wp --version 2>/dev/null | cut -d' ' -f2 || echo "Unknown")
wpcli_status+="📦 WP-CLI Status: ✅ Installed (Version: $wp_version)\n"
wpcli_status+="📍 Location: $(which wp)\n"
else
wpcli_status+="📦 WP-CLI Status: ❌ Not installed\n"
fi
# Check for WordPress sites
local wp_sites=()
if [[ -d "/var/www" ]]; then
while IFS= read -r -d '' site_dir; do
if [[ -f "$site_dir/wp-config.php" ]]; then
wp_sites+=("$(basename "$site_dir")")
fi
done < <(find /var/www -maxdepth 1 -type d -print0 2>/dev/null)
fi
if [[ ${#wp_sites[@]} -gt 0 ]]; then
wpcli_status+="\n🌐 WordPress Sites Found: ${#wp_sites[@]}\n"
for site in "${wp_sites[@]}"; do
wpcli_status+="$site\n"
done
else
wpcli_status+="\n⚠ No WordPress sites found in /var/www\n"
fi
wpcli_status+="\n"
local menu_items=(
"install" "(.Y.) 📥 Install WP-CLI"
"update" "(.Y.) 🔄 Update WP-CLI"
"uninstall" "(.Y.) 🗑️ Uninstall WP-CLI"
"" "(_*_)"
"test" "(.Y.) 🧪 Test WP-CLI on Sites"
"info" "(.Y.) Show WP-CLI Info & Commands"
"" "(_*_)"
"zback" "(Z) ← Back to WordPress Menu"
)
local choice=$(show_menu "WP-CLI Management" "$wpcli_status" "${menu_items[@]}")
case $choice in
install)
install_wpcli
;;
update)
update_wpcli
;;
uninstall)
uninstall_wpcli
;;
test)
test_wpcli_sites
;;
info)
show_wpcli_info
;;
zback)
break
;;
esac
done
}
# Install WP-CLI
install_wpcli() {
log "Installing WP-CLI..."
# Check if already installed
if command -v wp >/dev/null 2>&1; then
local wp_version=$(wp --version 2>/dev/null | cut -d' ' -f2 || echo "Unknown")
log_warning "WP-CLI is already installed (Version: $wp_version)"
read -p "Do you want to reinstall? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
return 0
fi
fi
# Download and install WP-CLI
log "Downloading WP-CLI..."
if curl -O https://raw.githubusercontent.com/wp-cli/wp-cli/v2.8.1/utils/wp-cli.phar; then
log "Making WP-CLI executable..."
chmod +x wp-cli.phar
log "Moving WP-CLI to /usr/local/bin/wp..."
if sudo mv wp-cli.phar /usr/local/bin/wp; then
log "Testing WP-CLI installation..."
if wp --info >/dev/null 2>&1; then
log "✅ WP-CLI installed successfully!"
wp --version
else
log_error "WP-CLI installation failed - command not working"
return 1
fi
else
log_error "Failed to move WP-CLI to /usr/local/bin/"
return 1
fi
else
log_error "Failed to download WP-CLI"
return 1
fi
# Install bash completion (optional)
log "Installing bash completion for WP-CLI..."
if curl -o /tmp/wp-completion.bash https://raw.githubusercontent.com/wp-cli/wp-cli/v2.8.1/utils/wp-completion.bash 2>/dev/null; then
sudo mv /tmp/wp-completion.bash /etc/bash_completion.d/wp-cli
log "Bash completion installed"
else
log_warning "Failed to install bash completion (optional)"
fi
read -p "Press Enter to continue..."
}
# Update WP-CLI
update_wpcli() {
log "Updating WP-CLI..."
if ! command -v wp >/dev/null 2>&1; then
log_error "WP-CLI is not installed. Please install it first."
read -p "Press Enter to continue..."
return 1
fi
# Update using WP-CLI's self-update command
if wp cli update; then
log "✅ WP-CLI updated successfully!"
wp --version
else
log_warning "WP-CLI self-update failed. Trying manual update..."
install_wpcli
fi
read -p "Press Enter to continue..."
}
# Uninstall WP-CLI
uninstall_wpcli() {
log "Uninstalling WP-CLI..."
if ! command -v wp >/dev/null 2>&1; then
log_warning "WP-CLI is not installed"
read -p "Press Enter to continue..."
return 0
fi
read -p "Are you sure you want to uninstall WP-CLI? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
return 0
fi
# Remove WP-CLI binary
if sudo rm -f /usr/local/bin/wp; then
log "WP-CLI binary removed"
else
log_error "Failed to remove WP-CLI binary"
fi
# Remove bash completion
if sudo rm -f /etc/bash_completion.d/wp-cli; then
log "Bash completion removed"
fi
log "✅ WP-CLI uninstalled successfully!"
read -p "Press Enter to continue..."
}
# Test WP-CLI on WordPress sites
test_wpcli_sites() {
log "Testing WP-CLI on WordPress sites..."
if ! command -v wp >/dev/null 2>&1; then
log_error "WP-CLI is not installed. Please install it first."
read -p "Press Enter to continue..."
return 1
fi
# Find WordPress sites
local wp_sites=()
if [[ -d "/var/www" ]]; then
while IFS= read -r -d '' site_dir; do
if [[ -f "$site_dir/wp-config.php" ]]; then
wp_sites+=("$site_dir")
fi
done < <(find /var/www -maxdepth 1 -type d -print0 2>/dev/null)
fi
if [[ ${#wp_sites[@]} -eq 0 ]]; then
log_warning "No WordPress sites found in /var/www"
read -p "Press Enter to continue..."
return 0
fi
echo "Testing WP-CLI on found WordPress sites:"
echo "========================================"
for site_dir in "${wp_sites[@]}"; do
local site_name=$(basename "$site_dir")
echo
echo "🌐 Testing site: $site_name"
echo " Path: $site_dir"
cd "$site_dir" || continue
# Test basic WP-CLI functionality
if wp core version 2>/dev/null; then
echo " ✅ WP-CLI working - WordPress version: $(wp core version 2>/dev/null)"
# Show additional info
echo " 📊 Database status: $(wp db check 2>/dev/null && echo "✅ OK" || echo "❌ Error")"
echo " 🔌 Active plugins: $(wp plugin list --status=active --format=count 2>/dev/null || echo "Unknown")"
echo " 🎨 Active theme: $(wp theme list --status=active --field=name 2>/dev/null || echo "Unknown")"
else
echo " ❌ WP-CLI not working on this site"
echo " 🔍 Checking permissions..."
ls -la wp-config.php 2>/dev/null || echo " ⚠️ wp-config.php not readable"
fi
done
echo
read -p "Press Enter to continue..."
}
# Show WP-CLI information and common commands
show_wpcli_info() {
clear
echo "WP-CLI Information & Common Commands"
echo "===================================="
echo
if command -v wp >/dev/null 2>&1; then
echo "📦 WP-CLI Version:"
wp --version
echo
echo "📍 WP-CLI Location:"
which wp
echo
echo " WP-CLI Info:"
wp --info
echo
else
echo "❌ WP-CLI is not installed"
echo
fi
echo "🔧 Common WP-CLI Commands:"
echo "========================="
echo
echo "Core Management:"
echo " wp core download # Download WordPress core"
echo " wp core install # Install WordPress"
echo " wp core update # Update WordPress core"
echo " wp core version # Show WordPress version"
echo
echo "Plugin Management:"
echo " wp plugin list # List all plugins"
echo " wp plugin install <name> # Install a plugin"
echo " wp plugin activate <name> # Activate a plugin"
echo " wp plugin deactivate <name> # Deactivate a plugin"
echo " wp plugin delete <name> # Delete a plugin"
echo
echo "Theme Management:"
echo " wp theme list # List all themes"
echo " wp theme install <name> # Install a theme"
echo " wp theme activate <name> # Activate a theme"
echo " wp theme delete <name> # Delete a theme"
echo
echo "Database Management:"
echo " wp db check # Check database connection"
echo " wp db export # Export database"
echo " wp db import <file> # Import database"
echo " wp db search <text> # Search database"
echo
echo "User Management:"
echo " wp user list # List all users"
echo " wp user create <username> # Create a new user"
echo " wp user delete <username> # Delete a user"
echo " wp user update <username> # Update user info"
echo
echo "Content Management:"
echo " wp post list # List posts"
echo " wp post create # Create a new post"
echo " wp post delete <ID> # Delete a post"
echo " wp media import <file> # Import media file"
echo
echo "Maintenance:"
echo " wp cache flush # Clear all caches"
echo " wp rewrite flush # Flush rewrite rules"
echo " wp cron event list # List cron events"
echo " wp search-replace <old> <new> # Search and replace in database"
echo
echo "📚 For more commands, visit: https://wp-cli.org/commands/"
echo
read -p "Press Enter to continue..."
}
# ==============================================================================
# MIRROR MANAGEMENT FUNCTIONS
# ==============================================================================
# Function to get current mirror configuration
get_current_mirror() {
local sources_file="/etc/apt/sources.list"
local sources_dir="/etc/apt/sources.list.d"
# First try to get from main sources.list
if [[ -f "$sources_file" ]]; then
local mirror=$(grep -E "^deb\s+https?://" "$sources_file" | grep -v "security\|backports" | head -1 | awk '{print $2}' | sed 's|https\?://||' | sed 's|/.*||')
if [[ -n "$mirror" && "$mirror" != "Unknown" ]]; then
echo "$mirror"
return
fi
fi
# Try sources.list.d directory for Ubuntu sources
if [[ -d "$sources_dir" ]]; then
local mirror=$(find "$sources_dir" -name "*.list" -exec grep -l "ubuntu" {} \; | head -1 | xargs grep -E "^deb\s+https?://" | grep -v "security\|backports" | head -1 | awk '{print $2}' | sed 's|https\?://||' | sed 's|/.*||')
if [[ -n "$mirror" && "$mirror" != "Unknown" ]]; then
echo "$mirror"
return
fi
fi
# Fallback: try to detect from apt-cache policy
local mirror=$(apt-cache policy | grep -E "^\s+\d+\s+https?://" | head -1 | awk '{print $2}' | sed 's|https\?://||' | sed 's|/.*||')
if [[ -n "$mirror" ]]; then
echo "$mirror"
else
echo "archive.ubuntu.com"
fi
}
# Function to check if mirrorselect is installed
is_mirrorselect_installed() {
# Ubuntu doesn't use mirrorselect - we'll use apt-mirror and netselect-apt instead
command -v netselect-apt &>/dev/null
}
# Function to install mirror selection tools for Ubuntu
install_mirrorselect() {
log "Installing Ubuntu mirror selection tools..."
if ! is_mirrorselect_installed; then
if sudo apt update && sudo apt install -y netselect-apt; then
log "netselect-apt installed successfully"
return 0
else
log "ERROR: Failed to install netselect-apt"
return 1
fi
else
log "netselect-apt is already installed"
return 0
fi
}
# Function to scan and set fastest mirror
scan_and_set_fastest_mirror() {
log "Starting mirror scan and configuration"
# Ensure mirrorselect is installed
if ! install_mirrorselect; then
ui_msg "Error" "Failed to install MirrorSelect. Cannot proceed with mirror optimization."
return 1
fi
# Show progress dialog
ui_info "Mirror Optimization" "Scanning Ubuntu mirrors to find the fastest one...\n\nThis may take a few minutes. Please wait."
# Create a temporary file for the new sources.list
local temp_sources="/tmp/sources.list.new"
# Run mirrorselect to find fastest mirror
if sudo mirrorselect -i > "$temp_sources" 2>/dev/null; then
# Backup current sources.list
sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup.$(date +%Y%m%d_%H%M%S)
# Apply new mirror configuration
sudo cp "$temp_sources" /etc/apt/sources.list
# Update package cache
if sudo apt update; then
local new_mirror
new_mirror=$(get_current_mirror)
ui_msg "Success" "Mirror optimization completed successfully!\n\nNew fastest mirror: $new_mirror\n\nPackage cache has been updated."
log "Mirror optimization completed. New mirror: $new_mirror"
else
# Restore backup if update fails
sudo cp /etc/apt/sources.list.backup.$(date +%Y%m%d_%H%M%S) /etc/apt/sources.list
ui_msg "Error" "Failed to update package cache with new mirror. Configuration has been restored."
log "ERROR: Failed to update package cache, restored backup"
return 1
fi
else
ui_msg "Error" "Failed to scan mirrors. Please check your internet connection and try again."
log "ERROR: Mirror scan failed"
return 1
fi
# Clean up
rm -f "$temp_sources"
}
# Local mirror management functions
get_local_mirror_path() {
local config_file="/etc/apt/mirror.list"
if [[ -f "$config_file" ]]; then
grep "^set base_path" "$config_file" 2>/dev/null | awk '{print $3}' || echo "/var/spool/apt-mirror"
else
echo "/var/spool/apt-mirror"
fi
}
is_apt_mirror_installed() {
command -v apt-mirror >/dev/null 2>&1
}
install_apt_mirror() {
log "Installing apt-mirror"
ui_info "Installing apt-mirror" "Installing apt-mirror package...\n\nThis may take a few moments."
if sudo apt-get update && sudo apt-get install -y apt-mirror; then
ui_msg "Installation Complete" "apt-mirror has been successfully installed!"
log "apt-mirror installation completed successfully"
return 0
else
ui_msg "Installation Failed" "Failed to install apt-mirror. Please check your internet connection and try again."
log "ERROR: apt-mirror installation failed"
return 1
fi
}
setup_local_mirror() {
log "Setting up local mirror"
# Get mirror path from user
local mirror_path
mirror_path=$(ui_input "Local Mirror Setup" "Enter the path where you want to store the local mirror:\n\n(Default: /var/spool/apt-mirror)\n\nNote: This will require significant disk space (50GB+)" "/var/spool/apt-mirror")
if [[ -z "$mirror_path" ]]; then
mirror_path="/var/spool/apt-mirror"
fi
# Create directory if it doesn't exist
if ! mkdir -p "$mirror_path" 2>/dev/null; then
ui_msg "Permission Error" "Cannot create directory: $mirror_path\n\nPlease run as root or choose a different path."
return 1
fi
# Create apt-mirror configuration
local config_file="/etc/apt/mirror.list"
local ubuntu_version
ubuntu_version=$(lsb_release -cs 2>/dev/null || echo "jammy")
cat > "$config_file" << EOF
############# config ##################
set base_path $mirror_path
set mirror_path \$base_path/mirror
set skel_path \$base_path/skel
set var_path \$base_path/var
set cleanscript \$var_path/clean.sh
set defaultarch amd64
set postmirror_script \$var_path/postmirror.sh
set run_postmirror 0
set nthreads 20
set _tilde 0
#######################################
deb http://archive.ubuntu.com/ubuntu $ubuntu_version main restricted universe multiverse
deb http://archive.ubuntu.com/ubuntu $ubuntu_version-updates main restricted universe multiverse
deb http://archive.ubuntu.com/ubuntu $ubuntu_version-backports main restricted universe multiverse
deb http://security.ubuntu.com/ubuntu $ubuntu_version-security main restricted universe multiverse
clean http://archive.ubuntu.com/ubuntu
clean http://security.ubuntu.com/ubuntu
EOF
ui_msg "Configuration Created" "Local mirror configuration created at:\n$config_file\n\nMirror path: $mirror_path\n\nUse 'Update Local Mirror' to download packages."
log "Local mirror configuration created: $config_file"
}
update_local_mirror() {
log "Updating local mirror"
if ! is_apt_mirror_installed; then
ui_msg "Not Installed" "apt-mirror is not installed. Please install it first."
return 1
fi
local mirror_path
mirror_path=$(get_local_mirror_path)
if ui_yesno "Update Local Mirror" "Start downloading/updating local mirror?\n\nPath: $mirror_path\n\nWarning: This will download many GB of data and may take hours.\nEnsure you have sufficient disk space and bandwidth.\n\nContinue?"; then
ui_info "Mirror Update" "Starting local mirror update...\n\nThis will run in a new terminal window.\nYou can monitor progress there."
# Run apt-mirror in a new terminal
gnome-terminal -- bash -c "
echo 'Starting local mirror update...'
echo 'This may take several hours depending on your connection.'
echo 'Press Ctrl+C to cancel at any time.'
echo ''
sudo apt-mirror
echo ''
echo 'Mirror update completed!'
read -p 'Press Enter to close...'
"
fi
}
use_local_mirror() {
log "Switching to local mirror"
local mirror_path
mirror_path=$(get_local_mirror_path)
local mirror_url="file://$mirror_path/mirror/archive.ubuntu.com/ubuntu"
if [[ ! -d "$mirror_path/mirror/archive.ubuntu.com/ubuntu" ]]; then
ui_msg "Mirror Not Ready" "Local mirror not found at:\n$mirror_path/mirror/archive.ubuntu.com/ubuntu\n\nPlease update the local mirror first."
return 1
fi
if ui_yesno "Use Local Mirror" "Switch to local mirror for package installations?\n\nThis will modify /etc/apt/sources.list to use:\n$mirror_url\n\nYour current configuration will be backed up."; then
# Backup current sources.list
local backup_file="/etc/apt/sources.list.backup.$(date +%Y%m%d_%H%M%S)"
cp /etc/apt/sources.list "$backup_file"
# Create new sources.list with local mirror
local ubuntu_version
ubuntu_version=$(lsb_release -cs 2>/dev/null || echo "jammy")
cat > /etc/apt/sources.list << EOF
# Local mirror configuration - created by ultrabunt
# Backup saved as: $backup_file
deb $mirror_url $ubuntu_version main restricted universe multiverse
deb $mirror_url $ubuntu_version-updates main restricted universe multiverse
deb $mirror_url $ubuntu_version-backports main restricted universe multiverse
deb file://$mirror_path/mirror/security.ubuntu.com/ubuntu $ubuntu_version-security main restricted universe multiverse
EOF
# Update package cache
if apt-get update; then
ui_msg "Success" "Successfully switched to local mirror!\n\nBackup saved as:\n$backup_file\n\nPackage cache updated."
log "Switched to local mirror successfully"
else
ui_msg "Update Failed" "Mirror switched but package cache update failed.\nYou may need to run 'apt update' manually."
log "WARNING: Package cache update failed after switching to local mirror"
fi
fi
}
# Main mirror management menu
show_mirror_management_menu() {
log "Entering show_mirror_management_menu function"
while true; do
local current_mirror
current_mirror=$(get_current_mirror)
local mirrorselect_status="🔴 Not Installed"
if is_mirrorselect_installed; then
mirrorselect_status="🟢 Installed"
fi
local apt_mirror_status="🔴 Not Installed"
if is_apt_mirror_installed; then
apt_mirror_status="🟢 Installed"
fi
local local_mirror_path
local_mirror_path=$(get_local_mirror_path)
local local_mirror_status="🔴 Not Setup"
if [[ -d "$local_mirror_path/mirror/archive.ubuntu.com/ubuntu" ]]; then
local_mirror_status="🟢 Available at $local_mirror_path"
fi
local menu_items=()
menu_items+=("current-mirror" "(.Y.) Current Mirror: $current_mirror")
menu_items+=("" "(_*_)")
menu_items+=("install-mirrorselect" "(.Y.) Install MirrorSelect Tool")
menu_items+=("mirrorselect-options" "(.Y.) MirrorSelect Options & Modes")
menu_items+=("scan-fastest" "(.Y.) Scan & Set Fastest Mirror (Quick)")
menu_items+=("manual-mirrorselect" "(.Y.) Open MirrorSelect Terminal (Advanced)")
menu_items+=("" "(_*_)")
menu_items+=("install-apt-mirror" "(.Y.) Install apt-mirror Tool")
menu_items+=("setup-local-mirror" "(.Y.) Setup Local Mirror Storage")
menu_items+=("update-local-mirror" "(.Y.) Update Local Mirror (Download)")
menu_items+=("use-local-mirror" "(.Y.) Switch to Local Mirror")
menu_items+=("" "(_*_)")
menu_items+=("back" "(Z) Back to Main Menu")
local choice
choice=$(ui_menu "Mirror Management" \
"MirrorSelect Status: $mirrorselect_status\napt-mirror Status: $apt_mirror_status\nLocal Mirror: $local_mirror_status\nCurrent Mirror: $current_mirror\n\nOptimize your Ubuntu package downloads:" \
$DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${menu_items[@]}") || break
case "$choice" in
current-mirror)
local mirror_info="Current Ubuntu Mirror Configuration:\n\n"
mirror_info+="Mirror Server: $current_mirror\n\n"
mirror_info+="This is the server your system uses to download packages.\n"
mirror_info+="A faster mirror can significantly improve download speeds.\n\n"
mirror_info+="Use 'Scan & Set Fastest Mirror' to automatically find\n"
mirror_info+="and configure the optimal mirror for your location.\n\n"
mirror_info+="For offline installations, use the local mirror options\n"
mirror_info+="to create a complete Ubuntu repository on your system."
ui_info "Current Mirror Information" "$mirror_info"
;;
install-mirrorselect)
if is_mirrorselect_installed; then
ui_msg "Already Installed" "MirrorSelect is already installed on your system."
else
if ui_yesno "Install MirrorSelect" "Install MirrorSelect tool for mirror optimization?\n\nThis will download and install the mirrorselect package."; then
install_mirrorselect
fi
fi
;;
mirrorselect-options)
show_mirrorselect_options_menu
;;
scan-fastest)
if ui_yesno "Scan for Fastest Mirror" "Scan Ubuntu mirrors and automatically set the fastest one?\n\nThis will:\n• Test multiple Ubuntu mirrors\n• Select the fastest one\n• Update your system configuration\n• Backup your current settings\n\nThis process may take a few minutes."; then
scan_and_set_fastest_mirror
fi
;;
manual-mirrorselect)
if is_mirrorselect_installed; then
ui_info "Manual MirrorSelect" "Opening terminal for manual mirror selection...\n\nCommon commands:\n• mirrorselect -i (interactive mode)\n• mirrorselect -s3 (select 3 fastest)\n• mirrorselect -h (help)\n\nPress any key to continue..."
# Open terminal with mirrorselect
gnome-terminal -- bash -c "echo 'MirrorSelect Manual Mode - Type \"mirrorselect -h\" for help'; mirrorselect -i; read -p 'Press Enter to close...'"
else
ui_msg "Not Installed" "MirrorSelect is not installed. Please install it first using the 'Install MirrorSelect Tool' option."
fi
;;
install-apt-mirror)
if is_apt_mirror_installed; then
ui_msg "Already Installed" "apt-mirror is already installed on your system."
else
if ui_yesno "Install apt-mirror" "Install apt-mirror tool for local repository creation?\n\nThis will download and install the apt-mirror package."; then
install_apt_mirror
fi
fi
;;
setup-local-mirror)
if ! is_apt_mirror_installed; then
ui_msg "Not Installed" "apt-mirror is not installed. Please install it first."
else
setup_local_mirror
fi
;;
update-local-mirror)
update_local_mirror
;;
use-local-mirror)
use_local_mirror
;;
back|z|"")
break
;;
esac
done
log "Exiting show_mirror_management_menu function"
}
# Function to show mirrorselect options submenu
show_mirrorselect_options_menu() {
log "Entering show_mirrorselect_options_menu function"
if ! is_mirrorselect_installed; then
ui_msg "MirrorSelect Not Installed" "MirrorSelect is not installed on your system.\n\nPlease install it first from the Mirror Management menu."
return
fi
while true; do
local menu_items=()
menu_items+=("interactive" "(.Y.) Interactive Mode - Select from list")
menu_items+=("deep" "(.Y.) Deep Mode - Accurate speed test (100k file)")
menu_items+=("auto" "(.Y.) Automatic Mode - Select fastest mirrors")
menu_items+=("" "(_*_)")
menu_items+=("country" "(.Y.) Filter by Country")
menu_items+=("region" "(.Y.) Filter by Region")
menu_items+=("" "(_*_)")
menu_items+=("http-only" "(.Y.) HTTP Only Mode")
menu_items+=("ftp-only" "(.Y.) FTP Only Mode")
menu_items+=("ipv4-only" "(.Y.) IPv4 Only Mode")
menu_items+=("ipv6-only" "(.Y.) IPv6 Only Mode")
menu_items+=("" "(_*_)")
menu_items+=("custom" "(.Y.) Custom Command Builder")
menu_items+=("" "(_*_)")
menu_items+=("zback" "(Z) ← Back to Mirror Management")
local choice
choice=$(ui_menu "MirrorSelect Options" \
"Choose how to run MirrorSelect:\n\n• Interactive: Select mirrors from a list\n• Deep: More accurate but slower testing\n• Automatic: Let mirrorselect choose the fastest\n• Filters: Limit by country, region, or protocol" \
$DIALOG_HEIGHT $DIALOG_WIDTH $DIALOG_MENU_HEIGHT "${menu_items[@]}") || break
case "$choice" in
interactive)
run_mirrorselect_interactive
;;
deep)
run_mirrorselect_deep
;;
auto)
run_mirrorselect_auto
;;
country)
run_mirrorselect_country
;;
region)
run_mirrorselect_region
;;
http-only)
run_mirrorselect_protocol "http"
;;
ftp-only)
run_mirrorselect_protocol "ftp"
;;
ipv4-only)
run_mirrorselect_protocol "ipv4"
;;
ipv6-only)
run_mirrorselect_protocol "ipv6"
;;
custom)
run_mirrorselect_custom
;;
zback|back|z|"")
break
;;
esac
done
log "Exiting show_mirrorselect_options_menu function"
}
# Function to run mirrorselect in interactive mode
run_mirrorselect_interactive() {
log "Running mirrorselect in interactive mode"
if ui_yesno "Interactive Mirror Selection" "Run MirrorSelect in interactive mode?\n\nThis will present a list of mirrors for you to select from.\nThe selected mirrors will be applied to your system."; then
ui_msg "Running MirrorSelect" "Starting interactive mirror selection...\n\nPlease wait while the mirror list is loaded."
if sudo mirrorselect -i; then
ui_msg "Success" "Mirror selection completed successfully!\n\nYour system has been updated with the selected mirrors."
log "MirrorSelect interactive mode completed successfully"
else
ui_msg "Error" "MirrorSelect failed to complete.\n\nPlease check your internet connection and try again."
log "MirrorSelect interactive mode failed"
fi
fi
}
# Function to run mirrorselect in deep mode
run_mirrorselect_deep() {
log "Running mirrorselect in deep mode"
local warning_msg="Deep Mode Warning:\n\n"
warning_msg+="• Downloads a 100KB file from each server\n"
warning_msg+="• More accurate but significantly slower\n"
warning_msg+="• Recommended only for good connections\n"
warning_msg+="• May take several minutes to complete\n\n"
warning_msg+="Continue with deep mode testing?"
if ui_yesno "Deep Mode Mirror Testing" "$warning_msg"; then
ui_msg "Running Deep Mode" "Starting deep mode mirror testing...\n\nThis will take several minutes.\nPlease be patient while testing completes."
if sudo mirrorselect -D -i; then
ui_msg "Success" "Deep mode mirror selection completed!\n\nYour system has been updated with the fastest mirrors."
log "MirrorSelect deep mode completed successfully"
else
ui_msg "Error" "Deep mode testing failed.\n\nPlease check your internet connection and try again."
log "MirrorSelect deep mode failed"
fi
fi
}
# Function to run mirrorselect in automatic mode
run_mirrorselect_auto() {
log "Running automatic mirror selection for Ubuntu"
local servers
servers=$(ui_input "Number of Servers" "How many servers should be tested for speed?\n\nEnter a number between 1 and 10:" "3")
if [[ -z "$servers" ]] || ! [[ "$servers" =~ ^[0-9]+$ ]] || [[ "$servers" -lt 1 ]] || [[ "$servers" -gt 10 ]]; then
ui_msg "Invalid Input" "Please enter a valid number between 1 and 10."
return
fi
if ui_yesno "Automatic Mirror Selection" "Test and select the fastest Ubuntu mirror?\n\nThis will test multiple mirrors and configure the fastest one."; then
ui_msg "Testing Mirrors" "Testing Ubuntu mirrors for speed...\n\nThis may take a few minutes. Please wait."
# Create temporary directory for netselect-apt
local temp_dir=$(mktemp -d)
cd "$temp_dir" || return 1
# Run netselect-apt to find fastest mirror
if sudo netselect-apt -n -o sources.list; then
# Extract the mirror URL from the generated sources.list
local new_mirror=$(grep -E "^deb\s+https?://" sources.list | head -1 | awk '{print $2}' | sed 's|https\?://||' | sed 's|/.*||')
if [[ -n "$new_mirror" ]]; then
# Backup current sources.list
sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup.$(date +%Y%m%d_%H%M%S)
# Replace the mirror in sources.list
sudo sed -i.bak "s|archive\.ubuntu\.com|$new_mirror|g; s|[a-z][a-z]\.archive\.ubuntu\.com|$new_mirror|g" /etc/apt/sources.list
# Update package lists
if sudo apt update; then
ui_msg "Success" "Fastest mirror configured successfully!\n\nNew mirror: $new_mirror\n\nPackage lists have been updated."
log "Mirror selection completed: $new_mirror"
else
ui_msg "Warning" "Mirror configured but package update failed.\n\nNew mirror: $new_mirror\n\nYou may need to run 'sudo apt update' manually."
fi
else
ui_msg "Error" "Could not determine fastest mirror.\n\nPlease try again or select a mirror manually."
log "Mirror selection failed - no mirror found"
fi
else
ui_msg "Error" "Mirror testing failed.\n\nPlease check your internet connection and try again."
log "netselect-apt failed"
fi
# Cleanup
cd - >/dev/null
rm -rf "$temp_dir"
fi
}
# Function to run mirrorselect with country filter
run_mirrorselect_country() {
log "Running mirrorselect with country filter"
local country
country=$(ui_input "Country Filter" "Enter the country name to filter mirrors:\n\nExamples:\n• United States\n• Germany\n• Japan\n• 'South Korea' (use quotes for spaces)\n\nCountry name:" "")
if [[ -z "$country" ]]; then
ui_msg "No Country" "No country specified. Operation cancelled."
return
fi
if ui_yesno "Country Filter" "Filter mirrors by country: $country\n\nThis will show only mirrors from the specified country."; then
ui_msg "Filtering by Country" "Loading mirrors from $country...\n\nPlease wait while the list is prepared."
if sudo mirrorselect -c "$country" -i; then
ui_msg "Success" "Country-filtered mirror selection completed!"
log "MirrorSelect country filter completed for: $country"
else
ui_msg "Error" "Country filtering failed.\n\nPlease check the country name and try again."
log "MirrorSelect country filter failed for: $country"
fi
fi
}
# Function to run mirrorselect with region filter
run_mirrorselect_region() {
log "Running mirrorselect with region filter"
local region
region=$(ui_input "Region Filter" "Enter the region name to filter mirrors:\n\nExamples:\n• North America\n• Europe\n• Asia\n• 'South America' (use quotes for spaces)\n\nRegion name:" "")
if [[ -z "$region" ]]; then
ui_msg "No Region" "No region specified. Operation cancelled."
return
fi
if ui_yesno "Region Filter" "Filter mirrors by region: $region\n\nThis will show only mirrors from the specified region."; then
ui_msg "Filtering by Region" "Loading mirrors from $region...\n\nPlease wait while the list is prepared."
if sudo mirrorselect -R "$region" -i; then
ui_msg "Success" "Region-filtered mirror selection completed!"
log "MirrorSelect region filter completed for: $region"
else
ui_msg "Error" "Region filtering failed.\n\nPlease check the region name and try again."
log "MirrorSelect region filter failed for: $region"
fi
fi
}
# Function to run mirrorselect with protocol filter
run_mirrorselect_protocol() {
local protocol="$1"
log "Running mirrorselect with $protocol protocol filter"
local protocol_flag=""
local protocol_name=""
case "$protocol" in
http)
protocol_flag="-H"
protocol_name="HTTP"
;;
ftp)
protocol_flag="-F"
protocol_name="FTP"
;;
ipv4)
protocol_flag="-4"
protocol_name="IPv4"
;;
ipv6)
protocol_flag="-6"
protocol_name="IPv6"
;;
esac
if ui_yesno "$protocol_name Only Mode" "Filter mirrors to $protocol_name only?\n\nThis will show only mirrors that support $protocol_name."; then
ui_msg "Filtering by $protocol_name" "Loading $protocol_name mirrors...\n\nPlease wait while the list is prepared."
if sudo mirrorselect "$protocol_flag" -i; then
ui_msg "Success" "$protocol_name-filtered mirror selection completed!"
log "MirrorSelect $protocol filter completed"
else
ui_msg "Error" "$protocol_name filtering failed.\n\nPlease try again."
log "MirrorSelect $protocol filter failed"
fi
fi
}
# Function to build custom mirrorselect command
run_mirrorselect_custom() {
log "Building custom mirrorselect command"
local custom_info="Custom MirrorSelect Command Builder\n\n"
custom_info+="Build your own mirrorselect command with specific options.\n"
custom_info+="You can combine multiple flags for advanced usage.\n\n"
custom_info+="Available options:\n"
custom_info+="• -i: Interactive mode\n"
custom_info+="• -D: Deep mode (100k test)\n"
custom_info+="• -s N: Select N servers\n"
custom_info+="• -b N: Block size for testing\n"
custom_info+="• -c 'Country': Filter by country\n"
custom_info+="• -R 'Region': Filter by region\n"
custom_info+="• -H: HTTP only\n"
custom_info+="• -F: FTP only\n"
custom_info+="• -4: IPv4 only\n"
custom_info+="• -6: IPv6 only\n"
custom_info+="• -t N: Timeout in seconds\n"
custom_info+="• -q: Quiet mode\n"
ui_info "Custom Command Builder" "$custom_info"
local custom_flags
custom_flags=$(ui_input "Custom Flags" "Enter mirrorselect flags (without 'sudo mirrorselect'):\n\nExample: -D -s 3 -c 'United States'\nExample: -i -H -4\n\nFlags:" "-i")
if [[ -z "$custom_flags" ]]; then
ui_msg "No Flags" "No flags specified. Operation cancelled."
return
fi
local full_command="sudo mirrorselect $custom_flags"
if ui_yesno "Execute Custom Command" "Execute this command?\n\n$full_command\n\nThis will run mirrorselect with your custom options."; then
ui_msg "Running Custom Command" "Executing: $full_command\n\nPlease wait..."
if eval "$full_command"; then
ui_msg "Success" "Custom mirrorselect command completed successfully!"
log "Custom mirrorselect command completed: $full_command"
else
ui_msg "Error" "Custom command failed.\n\nPlease check your flags and try again."
log "Custom mirrorselect command failed: $full_command"
fi
fi
}
# ==============================================================================
# KEYBOARD LAYOUT CONFIGURATION
# ==============================================================================
# Function to detect current keyboard layout
get_current_keyboard_layout() {
if [[ -f "$HOME/.config/ultrabunt-keyboard-layout" ]]; then
cat "$HOME/.config/ultrabunt-keyboard-layout"
else
echo "none"
fi
}
show_keyboard_layout_menu() {
log "Entering show_keyboard_layout_menu function"
while true; do
local current_layout
current_layout=$(get_current_keyboard_layout)
local macbook_status="(_*_)"
local thinkpad_status="(_*_)"
local generic_status="(_*_)"
case "$current_layout" in
"macbook")
macbook_status="(.Y.)"
;;
"thinkpad")
thinkpad_status="(.Y.)"
;;
"generic")
generic_status="(.Y.)"
;;
esac
local menu_items=(
"macbook" "$macbook_status MacBook Layout (Cmd as Super, Cmd+Tab switching)"
"thinkpad" "$thinkpad_status ThinkPad Layout (Standard PC layout optimized)"
"generic" "$generic_status 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 MacBook keyboard mapping
cat > "$HOME/.config/autostart/keyboard-mapping.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=Keyboard Mapping
Exec=/bin/bash -c "setxkbmap -option altwin:swap_lalt_lwin"
X-GNOME-Autostart-enabled=true
EOF
# Apply the mapping immediately - swap Alt and Super keys so Command acts like Super
setxkbmap -option altwin:swap_lalt_lwin 2>/dev/null || log "Warning: Could not apply setxkbmap immediately"
# Configure GNOME shortcuts for macOS-like behavior using Super (Command) key
gsettings set org.gnome.desktop.wm.keybindings switch-applications "['<Super>Tab']" 2>/dev/null || log "Warning: Could not set switch-applications"
gsettings set org.gnome.desktop.wm.keybindings switch-windows "['<Super>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"
# Set macOS-style copy/paste shortcuts using Super (Command) key
gsettings set org.gnome.desktop.wm.keybindings close "['<Super>w']" 2>/dev/null || log "Warning: Could not set close window"
gsettings set org.gnome.settings-daemon.plugins.media-keys terminal "['<Super>t']" 2>/dev/null || log "Warning: Could not set terminal shortcut"
# Create a flag file to indicate MacBook layout is active
echo "macbook" > "$HOME/.config/ultrabunt-keyboard-layout"
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"
# Create a flag file to indicate ThinkPad layout is active
echo "thinkpad" > "$HOME/.config/ultrabunt-keyboard-layout"
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"
# Create a flag file to indicate generic layout is active
echo "generic" > "$HOME/.config/ultrabunt-keyboard-layout"
log "Generic laptop 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"
# Remove the layout flag file
rm -f "$HOME/.config/ultrabunt-keyboard-layout" 2>/dev/null || true
log "Keyboard configuration reset to defaults"
}
# ==============================================================================
# DATABASE MANAGEMENT FUNCTIONS
# ==============================================================================
show_database_management_menu() {
log "Entering show_database_management_menu function"
while true; do
local menu_items=()
# Check MariaDB status
local mariadb_status="🔴 Not Running"
if systemctl is-active --quiet mariadb; then
mariadb_status="🟢 Running"
fi
menu_items+=("show-credentials" "(.Y.) Show MariaDB Root Credentials")
menu_items+=("reset-root-password" "(.Y.) Reset MariaDB Root Password")
menu_items+=("list-databases" "(.Y.) List All Databases")
menu_items+=("list-users" "(.Y.) List Database Users")
menu_items+=("create-database" "(.Y.) Create New Database")
menu_items+=("create-user" "(.Y.) Create Database User")
menu_items+=("backup-database" "(.Y.) Backup Database")
menu_items+=("service-control" "(.Y.) MariaDB Service Control")
# Show logout option if session is active
if [[ "$MARIADB_SESSION_ACTIVE" == "true" ]]; then
menu_items+=("logout-session" "(.Y.) Logout (Clear Saved Password)")
fi
menu_items+=("" "(_*_)")
menu_items+=("zback" "(Z) Back to Main Menu")
local choice
choice=$(ui_menu "Database Management" \
"MariaDB Status: $mariadb_status\n\nSelect database management option:" \
35 120 15 "${menu_items[@]}") || break
case "$choice" in
show-credentials)
show_mariadb_credentials
;;
reset-root-password)
reset_mariadb_root_password
;;
list-databases)
list_databases
;;
list-users)
list_database_users
;;
create-database)
create_database_interactive
;;
create-user)
create_database_user_interactive
;;
backup-database)
backup_database_interactive
;;
service-control)
mariadb_service_control
;;
logout-session)
MARIADB_SESSION_ACTIVE=false
MARIADB_SESSION_PASSWORD=""
ui_msg "Session Cleared" "MariaDB session cleared successfully.\n\nYou will need to re-enter the password for future operations."
log "MariaDB session cleared by user"
;;
zback|back|"")
break
;;
esac
done
log "Exiting show_database_management_menu function"
}
# LOG VIEWER FUNCTIONS
show_log_viewer_menu() {
log "Entering show_log_viewer_menu function"
while true; do
local menu_items=()
# Always show ultrabunt log
menu_items+=("ultrabunt" "(.Y.) Ultrabunt Log (/var/log/ultrabunt.log)")
# Check for common service logs and add them if they exist
if [[ -f "/var/log/nginx/error.log" ]]; then
menu_items+=("nginx-error" "(.Y.) Nginx Error Log")
fi
if [[ -f "/var/log/nginx/access.log" ]]; then
menu_items+=("nginx-access" "(.Y.) Nginx Access Log")
fi
if [[ -f "/var/log/apache2/error.log" ]]; then
menu_items+=("apache-error" "(.Y.) Apache Error Log")
fi
if [[ -f "/var/log/apache2/access.log" ]]; then
menu_items+=("apache-access" "(.Y.) Apache Access Log")
fi
if [[ -f "/var/log/mysql/error.log" ]]; then
menu_items+=("mysql-error" "(.Y.) MySQL Error Log")
fi
if [[ -f "/var/log/php8.1-fpm.log" ]] || [[ -f "/var/log/php8.2-fpm.log" ]] || [[ -f "/var/log/php8.3-fpm.log" ]]; then
menu_items+=("php-fpm" "(.Y.) PHP-FPM Log")
fi
if [[ -f "/var/log/syslog" ]]; then
menu_items+=("syslog" "(.Y.) System Log (syslog)")
fi
if [[ -f "/var/log/auth.log" ]]; then
menu_items+=("auth" "(.Y.) Authentication Log")
fi
menu_items+=("" "(_*_)")
menu_items+=("zback" "(Z) ← Back to Main Menu")
local choice
choice=$(ui_menu "Log Viewer" \
"Select a log file to view:" \
20 80 15 "${menu_items[@]}") || break
case "$choice" in
ultrabunt)
view_log_file "/var/log/ultrabunt.log" "Ultrabunt Log"
;;
nginx-error)
view_log_file "/var/log/nginx/error.log" "Nginx Error Log"
;;
nginx-access)
view_log_file "/var/log/nginx/access.log" "Nginx Access Log"
;;
apache-error)
view_log_file "/var/log/apache2/error.log" "Apache Error Log"
;;
apache-access)
view_log_file "/var/log/apache2/access.log" "Apache Access Log"
;;
mysql-error)
view_log_file "/var/log/mysql/error.log" "MySQL Error Log"
;;
php-fpm)
# Find the correct PHP-FPM log file
local php_fpm_log=""
for version in 8.3 8.2 8.1; do
if [[ -f "/var/log/php${version}-fpm.log" ]]; then
php_fpm_log="/var/log/php${version}-fpm.log"
break
fi
done
if [[ -n "$php_fpm_log" ]]; then
view_log_file "$php_fpm_log" "PHP-FPM Log"
fi
;;
syslog)
view_log_file "/var/log/syslog" "System Log"
;;
auth)
view_log_file "/var/log/auth.log" "Authentication Log"
;;
zback|back|"")
break
;;
esac
done
log "Exiting show_log_viewer_menu function"
}
view_log_file() {
local log_file="$1"
local log_name="$2"
log "Viewing log file: $log_file"
if [[ ! -f "$log_file" ]]; then
ui_msg "Log Not Found" "The log file $log_file does not exist or is not accessible."
return 1
fi
# Check if file is readable
if [[ ! -r "$log_file" ]]; then
ui_msg "Permission Denied" "Cannot read $log_file. You may need administrator privileges."
return 1
fi
# Get file size for display
local file_size
file_size=$(du -h "$log_file" 2>/dev/null | cut -f1)
# Show options for viewing the log
local choice
choice=$(ui_menu "$log_name" \
"File: $log_file (Size: $file_size)\nChoose how to view the log:" \
15 70 4 \
"last100" "View Last 100 Lines (Scrollable)" \
"last500" "View Last 500 Lines (Scrollable)" \
"full" "View Full Log File (Scrollable)" \
"tail-follow" "Follow Live Updates (tail -f)")
case "$choice" in
last100)
view_log_with_pager "$log_file" "$log_name" 100
;;
last500)
view_log_with_pager "$log_file" "$log_name" 500
;;
full)
view_log_with_pager "$log_file" "$log_name" "full"
;;
tail-follow)
clear
echo "=== Following $log_name ==="
echo "File: $log_file"
echo "Press Ctrl+C to exit"
echo "=========================="
echo
sudo tail -f "$log_file" 2>/dev/null || tail -f "$log_file" 2>/dev/null
;;
*)
return 0
;;
esac
}
view_log_with_pager() {
local log_file="$1"
local log_name="$2"
local lines="$3"
local temp_file="/tmp/ultrabunt_log_view_$$"
# Create header with file info
{
echo "=== $log_name ==="
echo "File: $log_file"
echo "Size: $(du -h "$log_file" 2>/dev/null | cut -f1)"
if [[ "$lines" == "full" ]]; then
echo "Showing: Full file"
else
echo "Showing: Last $lines lines"
fi
echo "Navigation: Use arrow keys, Page Up/Down, 'q' to quit"
echo "=============================================="
echo
} > "$temp_file"
# Add log content
if [[ "$lines" == "full" ]]; then
sudo cat "$log_file" 2>/dev/null >> "$temp_file" || cat "$log_file" 2>/dev/null >> "$temp_file"
else
sudo tail -n "$lines" "$log_file" 2>/dev/null >> "$temp_file" || tail -n "$lines" "$log_file" 2>/dev/null >> "$temp_file"
fi
# Use less with better options for log viewing
if command -v less >/dev/null 2>&1; then
# less with:
# -R: handle ANSI colors
# -S: don't wrap long lines
# -X: don't clear screen on exit
# -F: quit if content fits on one screen
# +G: start at end of file
PAGER=cat less -RSX +G "$temp_file"
else
# Fallback to more if less is not available
more "$temp_file"
fi
rm -f "$temp_file" 2>/dev/null
}
# Function to prompt for MariaDB root password and test connection
prompt_mariadb_password() {
local password
local attempt=1
local max_attempts=3
# Check if we have a valid session password first
if [[ "$MARIADB_SESSION_ACTIVE" == "true" && -n "$MARIADB_SESSION_PASSWORD" ]]; then
if mysql -u root -p"$MARIADB_SESSION_PASSWORD" -e "SELECT 1;" >/dev/null 2>&1; then
echo "$MARIADB_SESSION_PASSWORD"
return 0
else
# Session password is invalid, reset session
MARIADB_SESSION_ACTIVE=false
MARIADB_SESSION_PASSWORD=""
fi
fi
while [[ $attempt -le $max_attempts ]]; do
password=$(ui_password "MariaDB Root Password" "Enter MariaDB root password (attempt $attempt/$max_attempts):") || return 1
if [[ -n "$password" ]] && mysql -u root -p"$password" -e "SELECT 1;" >/dev/null 2>&1; then
# Store password in session for future use
MARIADB_SESSION_PASSWORD="$password"
MARIADB_SESSION_ACTIVE=true
echo "$password"
return 0
else
if [[ $attempt -eq $max_attempts ]]; then
ui_msg "Authentication Failed" "Failed to authenticate with MariaDB after $max_attempts attempts.\n\nPlease check:\n• Password is correct\n• MariaDB is running\n• Root user exists\n\nTry resetting the root password if needed."
return 1
fi
ui_msg "Invalid Password" "Password incorrect. Please try again.\n\nAttempt $attempt of $max_attempts"
fi
((attempt++))
done
return 1
}
show_mariadb_credentials() {
log "Displaying MariaDB credentials"
# Check if password file exists and try to read it
if sudo test -f "/root/.mysql_root_password"; then
local root_pass
root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null | tr -d '\n\r')
if [[ -n "$root_pass" ]]; then
local creds_msg="🔐 MariaDB Root Credentials\n\n"
creds_msg+="• Username: root\n"
creds_msg+="• Password: $root_pass\n"
creds_msg+="• Host: localhost\n"
creds_msg+="• Port: 3306\n\n"
creds_msg+="🔧 Access Methods:\n"
creds_msg+="• Command line: sudo mysql -u root -p\n"
creds_msg+="• With password: mysql -u root -p'$root_pass'\n"
creds_msg+="• Direct access: sudo mysql (as root user)\n\n"
creds_msg+="📝 Password file: /root/.mysql_root_password\n"
creds_msg+="📋 Copy password: $root_pass\n\n"
creds_msg+="💡 Tips:\n"
creds_msg+="• Use 'sudo mysql' for direct access without password\n"
creds_msg+="• Create separate users for applications\n"
creds_msg+="• Keep root password secure and backed up"
ui_info "MariaDB Credentials" "$creds_msg"
log "MariaDB root password displayed successfully"
else
ui_msg "No Password Found" "Root password file exists but is empty or unreadable.\n\nFile permissions: $(sudo ls -la /root/.mysql_root_password 2>/dev/null || echo 'File not accessible')\n\nYou may need to reset the root password."
log "ERROR: Root password file is empty or unreadable"
fi
else
# Check if we can access MariaDB without password (sudo method)
if sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then
local creds_msg="🔐 MariaDB Root Access Available\n\n"
creds_msg+="• Username: root\n"
creds_msg+="• Password: Not set (using sudo authentication)\n"
creds_msg+="• Host: localhost\n"
creds_msg+="• Port: 3306\n\n"
creds_msg+="🔧 Access Method:\n"
creds_msg+="• Command line: sudo mysql\n\n"
creds_msg+="⚠️ Note: No password file found at /root/.mysql_root_password\n"
creds_msg+="This means MariaDB is using system authentication.\n\n"
creds_msg+="💡 To set a password, use the 'Reset Root Password' option."
ui_info "MariaDB Access" "$creds_msg"
log "MariaDB accessible via sudo, no password file found"
else
ui_msg "No Credentials Found" "MariaDB root password file not found and sudo access failed.\n\nThis could mean:\n• MariaDB was not installed through this script\n• Password file was deleted\n• MariaDB security setup was not completed\n• MariaDB service is not running\n\nYou can:\n• Check MariaDB status\n• Reset the root password\n• Reinstall MariaDB"
log "ERROR: No MariaDB access method available"
fi
fi
}
reset_mariadb_root_password() {
log "Resetting MariaDB root password"
if ! systemctl is-active --quiet mariadb; then
ui_msg "MariaDB Not Running" "MariaDB service is not running.\n\nStart MariaDB first using the service control option."
return
fi
if ui_yesno "Reset Root Password" "This will reset the MariaDB root password.\n\nContinue?"; then
# Generate new password
local new_pass
new_pass=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 20)
# Reset password
if sudo mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '${new_pass}';" 2>/dev/null; then
# Save new password
echo "$new_pass" | sudo tee /root/.mysql_root_password > /dev/null
sudo chmod 600 /root/.mysql_root_password
ui_info "Password Reset Complete" "🔐 New MariaDB Root Password:\n\n• Username: root\n• Password: $new_pass\n• Host: localhost\n\n📝 Password saved to: /root/.mysql_root_password\n\n✅ Password reset successful!"
else
ui_msg "Reset Failed" "Failed to reset MariaDB root password.\n\nThis could be due to:\n• Current password is different\n• MariaDB access issues\n• Permission problems\n\nTry using: sudo mysql_secure_installation"
fi
fi
}
list_databases() {
log "Listing databases"
# Check if MariaDB service is running
if ! systemctl is-active --quiet mariadb; then
ui_msg "MariaDB Not Running" "MariaDB service is not running.\n\nStart MariaDB first using the service control option."
return 1
fi
# Try to connect and list databases
local db_list
local root_pass=""
# Get root password if available
if [[ -f "/root/.mysql_root_password" ]]; then
root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null)
fi
# Try different authentication methods
if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then
db_list=$(mysql -u root -p"$root_pass" -e "SHOW DATABASES;" 2>/dev/null | tail -n +2)
elif sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then
db_list=$(sudo mysql -e "SHOW DATABASES;" 2>/dev/null | tail -n +2)
else
# Prompt for password if other methods fail
local manual_pass
if manual_pass=$(prompt_mariadb_password); then
db_list=$(mysql -u root -p"$manual_pass" -e "SHOW DATABASES;" 2>/dev/null | tail -n +2)
else
ui_msg "Database Access Error" "Cannot connect to MariaDB.\n\nThis could be due to:\n• Authentication issues\n• MariaDB not properly configured\n• Permission problems\n\nTry resetting the root password or check MariaDB logs."
return 1
fi
fi
if [[ -n "$db_list" ]]; then
local db_msg="📊 MariaDB Databases\n\n"
db_msg+="Available databases:\n\n"
while IFS= read -r db; do
if [[ "$db" == "information_schema" || "$db" == "performance_schema" || "$db" == "mysql" || "$db" == "sys" ]]; then
db_msg+="$db (system)\n"
else
db_msg+="$db (user)\n"
fi
done <<< "$db_list"
db_msg+="\n💡 System databases are used by MariaDB internally.\n"
db_msg+="User databases contain your application data."
ui_info "Database List" "$db_msg"
else
ui_msg "No Access" "Cannot access MariaDB databases.\n\nThis could be due to:\n• MariaDB not running\n• Authentication issues\n• Permission problems"
fi
}
list_database_users() {
log "Listing database users"
# Check if MariaDB service is running
if ! systemctl is-active --quiet mariadb; then
ui_msg "MariaDB Not Running" "MariaDB service is not running.\n\nStart MariaDB first using the service control option."
return 1
fi
# Try to connect and list users
local user_list
local root_pass=""
# Get root password if available
if [[ -f "/root/.mysql_root_password" ]]; then
root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null)
fi
# Try different authentication methods
if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then
user_list=$(mysql -u root -p"$root_pass" -e "SELECT User, Host FROM mysql.user;" 2>/dev/null | tail -n +2)
elif sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then
user_list=$(sudo mysql -e "SELECT User, Host FROM mysql.user;" 2>/dev/null | tail -n +2)
else
# Prompt for password if other methods fail
local manual_pass
if manual_pass=$(prompt_mariadb_password); then
user_list=$(mysql -u root -p"$manual_pass" -e "SELECT User, Host FROM mysql.user;" 2>/dev/null | tail -n +2)
else
ui_msg "Database Access Error" "Cannot connect to MariaDB.\n\nThis could be due to:\n• Authentication issues\n• MariaDB not properly configured\n• Permission problems\n\nTry resetting the root password or check MariaDB logs."
return 1
fi
fi
if [[ -n "$user_list" ]]; then
local user_msg="👥 MariaDB Users\n\n"
user_msg+="Current database users:\n\n"
while IFS=$'\t' read -r user host; do
if [[ "$user" == "root" ]]; then
user_msg+="$user@$host (administrator)\n"
elif [[ "$user" == "mysql.sys" || "$user" == "mysql.session" || "$user" == "mysql.infoschema" ]]; then
user_msg+="$user@$host (system)\n"
else
user_msg+="$user@$host (application)\n"
fi
done <<< "$user_list"
user_msg+="\n💡 Root users have full administrative access.\n"
user_msg+="Application users should have limited privileges."
ui_info "Database Users" "$user_msg"
else
ui_msg "No Access" "Cannot access MariaDB user list.\n\nThis could be due to:\n• MariaDB not running\n• Authentication issues\n• Permission problems"
fi
}
create_database_interactive() {
log "Creating database interactively"
if ! systemctl is-active --quiet mariadb; then
ui_msg "MariaDB Not Running" "MariaDB service is not running.\n\nStart MariaDB first using the service control option."
return
fi
local db_name
db_name=$(ui_input "Database Name" "Enter database name:" "") || return
if [[ -z "$db_name" ]]; then
ui_msg "Invalid Input" "Database name cannot be empty."
return
fi
# Validate database name
if [[ ! "$db_name" =~ ^[a-zA-Z][a-zA-Z0-9_]*$ ]]; then
ui_msg "Invalid Name" "Database name must:\n• Start with a letter\n• Contain only letters, numbers, and underscores\n• Not contain spaces or special characters"
return
fi
# Try to create database with different authentication methods
local root_pass=""
local success=false
# Get root password if available
if [[ -f "/root/.mysql_root_password" ]]; then
root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null)
fi
# Try different authentication methods
if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then
mysql -u root -p"$root_pass" -e "CREATE DATABASE ${db_name} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null && success=true
elif sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then
sudo mysql -e "CREATE DATABASE ${db_name} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null && success=true
else
# Prompt for password if other methods fail
local manual_pass
if manual_pass=$(prompt_mariadb_password); then
mysql -u root -p"$manual_pass" -e "CREATE DATABASE ${db_name} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null && success=true
fi
fi
if [[ "$success" == "true" ]]; then
ui_info "Database Created" "✅ Database '$db_name' created successfully!\n\n• Name: $db_name\n• Character Set: utf8mb4\n• Collation: utf8mb4_unicode_ci\n\n💡 You can now create users and grant privileges to this database."
else
ui_msg "Creation Failed" "Failed to create database '$db_name'.\n\nThis could be due to:\n• Database already exists\n• Invalid name format\n• Permission issues\n• MariaDB access problems"
fi
}
create_database_user_interactive() {
log "Creating database user interactively"
if ! systemctl is-active --quiet mariadb; then
ui_msg "MariaDB Not Running" "MariaDB service is not running.\n\nStart MariaDB first using the service control option."
return
fi
local username
username=$(ui_input "Username" "Enter username:" "") || return
if [[ -z "$username" ]]; then
ui_msg "Invalid Input" "Username cannot be empty."
return
fi
local password
password=$(ui_input "Password" "Enter password (leave empty for auto-generated):" "") || return
if [[ -z "$password" ]]; then
password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 16)
fi
local database
database=$(ui_input "Database" "Enter database name to grant access to (optional):" "") || return
# Create user with different authentication methods
local root_pass=""
local success=false
# Get root password if available
if [[ -f "/root/.mysql_root_password" ]]; then
root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null)
fi
# Try different authentication methods
if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then
mysql -u root -p"$root_pass" -e "CREATE USER '${username}'@'localhost' IDENTIFIED BY '${password}';" 2>/dev/null && success=true
elif sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then
sudo mysql -e "CREATE USER '${username}'@'localhost' IDENTIFIED BY '${password}';" 2>/dev/null && success=true
else
# Prompt for password if other methods fail
local manual_pass
if manual_pass=$(prompt_mariadb_password); then
mysql -u root -p"$manual_pass" -e "CREATE USER '${username}'@'localhost' IDENTIFIED BY '${password}';" 2>/dev/null && success=true
fi
fi
# Create user
if [[ "$success" == "true" ]]; then
local success_msg="✅ User '$username' created successfully!\n\n• Username: $username\n• Password: $password\n• Host: localhost\n"
# Grant privileges if database specified
if [[ -n "$database" ]]; then
if sudo mysql -e "GRANT ALL PRIVILEGES ON ${database}.* TO '${username}'@'localhost';" 2>/dev/null; then
sudo mysql -e "FLUSH PRIVILEGES;" 2>/dev/null
success_msg+="\n🔐 Privileges granted:\n• Full access to database: $database"
else
success_msg+="\n⚠ Could not grant privileges to database: $database"
fi
else
success_msg+="\n💡 No database privileges granted.\nUse GRANT commands to assign specific permissions."
fi
ui_info "User Created" "$success_msg"
else
ui_msg "Creation Failed" "Failed to create user '$username'.\n\nThis could be due to:\n• User already exists\n• Invalid username format\n• Permission issues\n• MariaDB access problems"
fi
}
backup_database_interactive() {
log "Starting interactive database backup"
if ! systemctl is-active --quiet mariadb; then
ui_msg "MariaDB Not Running" "MariaDB service is not running.\n\nStart MariaDB first using the service control option."
return
fi
# Get list of databases with different authentication methods
local db_list
local root_pass=""
# Get root password if available
if [[ -f "/root/.mysql_root_password" ]]; then
root_pass=$(sudo cat /root/.mysql_root_password 2>/dev/null)
fi
# Try different authentication methods
if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then
db_list=$(mysql -u root -p"$root_pass" -e "SHOW DATABASES;" 2>/dev/null | tail -n +2 | grep -v -E '^(information_schema|performance_schema|mysql|sys)$')
elif sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then
db_list=$(sudo mysql -e "SHOW DATABASES;" 2>/dev/null | tail -n +2 | grep -v -E '^(information_schema|performance_schema|mysql|sys)$')
else
# Prompt for password if other methods fail
local manual_pass
if manual_pass=$(prompt_mariadb_password); then
db_list=$(mysql -u root -p"$manual_pass" -e "SHOW DATABASES;" 2>/dev/null | tail -n +2 | grep -v -E '^(information_schema|performance_schema|mysql|sys)$')
else
ui_msg "Database Access Error" "Cannot connect to MariaDB to list databases."
return
fi
fi
if [[ -z "$db_list" ]]; then
ui_msg "No Databases" "No user databases found to backup.\n\nOnly system databases are present."
return
fi
local database
database=$(ui_input "Database Name" "Enter database name to backup:\n\nAvailable databases:\n$(echo "$db_list" | sed 's/^/• /')" "") || return
if [[ -z "$database" ]]; then
ui_msg "Invalid Input" "Database name cannot be empty."
return
fi
# Check if database exists
if ! echo "$db_list" | grep -q "^${database}$"; then
ui_msg "Database Not Found" "Database '$database' not found.\n\nAvailable databases:\n$(echo "$db_list" | sed 's/^/• /')"
return
fi
local backup_dir="$BACKUP_DIR/database-backups"
mkdir -p "$backup_dir"
local backup_file="$backup_dir/${database}-$(date +%Y%m%d-%H%M%S).sql"
# Perform backup with different authentication methods
local backup_success=false
# Try different authentication methods for mysqldump
if [[ -n "$root_pass" ]] && mysql -u root -p"$root_pass" -e "SELECT 1;" >/dev/null 2>&1; then
mysqldump -u root -p"$root_pass" "$database" > "$backup_file" 2>/dev/null && backup_success=true
elif sudo mysql -e "SELECT 1;" >/dev/null 2>&1; then
sudo mysqldump "$database" > "$backup_file" 2>/dev/null && backup_success=true
else
# Use the manual password if we got one earlier
if [[ -n "$manual_pass" ]]; then
mysqldump -u root -p"$manual_pass" "$database" > "$backup_file" 2>/dev/null && backup_success=true
fi
fi
if [[ "$backup_success" == "true" ]]; then
local file_size
file_size=$(du -h "$backup_file" | cut -f1)
ui_info "Backup Complete" "✅ Database backup successful!\n\n• Database: $database\n• Backup file: $backup_file\n• File size: $file_size\n• Timestamp: $(date)\n\n💾 Backup saved to:\n$backup_file\n\n💡 To restore:\nmysql $database < $backup_file"
else
ui_msg "Backup Failed" "Failed to backup database '$database'.\n\nThis could be due to:\n• Database access issues\n• Insufficient disk space\n• Permission problems\n• MariaDB connection issues"
fi
}
mariadb_service_control() {
log "MariaDB service control"
local status_msg="🔧 MariaDB Service Control\n\n"
if systemctl is-active --quiet mariadb; then
status_msg+="Current Status: 🟢 Running\n\n"
else
status_msg+="Current Status: 🔴 Not Running\n\n"
fi
local menu_items=()
menu_items+=("start" "(.Y.) Start MariaDB")
menu_items+=("stop" "(.Y.) Stop MariaDB")
menu_items+=("restart" "(.Y.) Restart MariaDB")
menu_items+=("status" "(.Y.) Show Detailed Status")
menu_items+=("enable" "(.Y.) Enable Auto-start")
menu_items+=("disable" "(.Y.) Disable Auto-start")
menu_items+=("logs" "(.Y.) View Recent Logs")
menu_items+=("" "(_*_)")
menu_items+=("back" "(B) Back")
local choice
choice=$(ui_menu "MariaDB Service Control" "$status_msg" \
25 80 12 "${menu_items[@]}") || return
case "$choice" in
start)
if sudo systemctl start mariadb 2>/dev/null; then
ui_msg "Service Started" "✅ MariaDB service started successfully!"
else
ui_msg "Start Failed" "❌ Failed to start MariaDB service.\n\nCheck logs for details:\nsudo journalctl -u mariadb -n 20"
fi
;;
stop)
if ui_yesno "Stop MariaDB" "This will stop the MariaDB service.\n\nAll database connections will be terminated.\n\nContinue?"; then
if sudo systemctl stop mariadb 2>/dev/null; then
ui_msg "Service Stopped" "✅ MariaDB service stopped successfully!"
else
ui_msg "Stop Failed" "❌ Failed to stop MariaDB service."
fi
fi
;;
restart)
if sudo systemctl restart mariadb 2>/dev/null; then
ui_msg "Service Restarted" "✅ MariaDB service restarted successfully!"
else
ui_msg "Restart Failed" "❌ Failed to restart MariaDB service.\n\nCheck logs for details:\nsudo journalctl -u mariadb -n 20"
fi
;;
status)
local detailed_status
detailed_status=$(sudo systemctl status mariadb 2>/dev/null || echo "Status unavailable")
ui_info "MariaDB Status" "$detailed_status"
;;
enable)
if sudo systemctl enable mariadb 2>/dev/null; then
ui_msg "Auto-start Enabled" "✅ MariaDB will now start automatically on boot!"
else
ui_msg "Enable Failed" "❌ Failed to enable MariaDB auto-start."
fi
;;
disable)
if ui_yesno "Disable Auto-start" "This will prevent MariaDB from starting automatically on boot.\n\nContinue?"; then
if sudo systemctl disable mariadb 2>/dev/null; then
ui_msg "Auto-start Disabled" "✅ MariaDB auto-start disabled!"
else
ui_msg "Disable Failed" "❌ Failed to disable MariaDB auto-start."
fi
fi
;;
logs)
local recent_logs
recent_logs=$(sudo journalctl -u mariadb -n 30 --no-pager 2>/dev/null || echo "Logs unavailable")
ui_info "MariaDB Logs" "$recent_logs"
;;
esac
}
# ==============================================================================
# MAIN PROGRAM
# ==============================================================================
main() {
# Show ASCII art splash screen first
show_ascii_splash
# Parse command line arguments
parse_arguments "$@"
# 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