#!/bin/bash # LXS - Common library # Sourced by lxs.sh and all sub-scripts. Provides colors, UI helpers, loggers, # spinner, OS guards, and shared utilities (public IP, password generation, # apt lock wait, root re-exec, apt non-interactive setup). # Repo: https://git.hyko.cx/hykocx/lxs # ═══════════════════════════════════════════════════════════════════════════ # Colors — minimal palette: red, cyan, white (+ gray as a white shade) # ═══════════════════════════════════════════════════════════════════════════ RED='\033[38;2;255;64;64m' # errors, destructive actions CYAN='\033[38;2;0;229;255m' # accents, titles, OK, info WHITE='\033[38;2;240;240;240m' # primary text GRAY='\033[38;2;140;140;140m' # secondary text, comments, separators NC='\033[0m' BOLD='\033[1m' DIM='\033[2m' # Legacy aliases — older sub-scripts still reference these. Map onto CYAN # so the palette stays at red/cyan/white without breaking them. YELLOW="$CYAN" GREEN="$CYAN" MAGENTA="$CYAN" PURPLE="$CYAN" # ═══════════════════════════════════════════════════════════════════════════ # UI helpers — title + horizontal rule, no closed box. # # Layout: # show_box_top "TITLE" ["RIGHT_TAG"] → TITLE [ RIGHT ] # ───────────────────────────────── # show_box_mid "SECTION" → ─ SECTION ───────────────────── # show_box_bottom → ─────────────────────────────── # show_separator → ─── (light gray divider) # show_menu_item "01" "LABEL" "desc" → [01] LABEL // desc # show_prompt → > # ═══════════════════════════════════════════════════════════════════════════ _lxs_term_cols() { local cols cols=$(tput cols 2>/dev/null || echo 80) [ "$cols" -gt 100 ] && cols=100 [ "$cols" -lt 60 ] && cols=60 echo "$cols" } show_box_top() { local title="$1" right="${2:-}" local cols pad_len pad cols=$(_lxs_term_cols) if [ -n "$right" ]; then pad_len=$(( cols - 2 - ${#title} - ${#right} - 4 )) [ "$pad_len" -lt 1 ] && pad_len=1 pad=$(printf ' %.0s' $(seq 1 "$pad_len")) printf " ${CYAN}${BOLD}%s${NC}%s${GRAY}[ ${CYAN}%s${GRAY} ]${NC}\n" \ "$title" "$pad" "$right" else printf " ${CYAN}${BOLD}%s${NC}\n" "$title" fi _lxs_hr "$cols" } show_box_mid() { local title="$1" local cols fill_len fill cols=$(_lxs_term_cols) fill_len=$(( cols - ${#title} - 4 )) [ "$fill_len" -lt 2 ] && fill_len=2 fill=$(printf '─%.0s' $(seq 1 "$fill_len")) printf "${GRAY}─ ${CYAN}${BOLD}%s${NC} ${GRAY}%s${NC}\n" "$title" "$fill" } show_box_bottom() { _lxs_hr "$(_lxs_term_cols)" } show_separator() { _lxs_hr "$(_lxs_term_cols)" } _lxs_hr() { local cols=$1 fill fill=$(printf '─%.0s' $(seq 1 "$cols")) printf "${GRAY}%s${NC}\n" "$fill" } show_title() { local title="$1" local subtitle="${2:-}" echo "" if [ -n "$subtitle" ]; then show_box_top "$title" "$subtitle" else show_box_top "$title" fi } # Render a menu line. Pass "exit" as 4th arg to color the key red. # show_menu_item "01" "APPLICATIONS" "deploy stacks" # show_menu_item "00" "EXIT" "quit shell" exit show_menu_item() { local key="$1" label="$2" desc="${3:-}" kind="${4:-}" local key_color="${CYAN}" [ "$kind" = "exit" ] && key_color="${RED}" if [ -n "$desc" ]; then printf " ${key_color}[%s]${NC} ${WHITE}${BOLD}%-18s${NC} ${GRAY}// %s${NC}\n" \ "$key" "$label" "$desc" else printf " ${key_color}[%s]${NC} ${WHITE}${BOLD}%s${NC}\n" "$key" "$label" fi } show_prompt() { echo -e -n " ${CYAN}>${NC} " } info() { echo -e "${CYAN}[..]${NC} $*"; } ok() { echo -e "${CYAN}[OK]${NC} $*"; } warn() { echo -e "${CYAN}[!!]${NC} $*"; } err() { echo -e "${RED}[KO]${NC} $*" >&2; } # ═══════════════════════════════════════════════════════════════════════════ # Spinner — runs a shell command, redirects output to a log file, shows # a spinner with a label, prints ✓/✗ on completion. Returns the command's # exit code. # # Usage: # run_spinner "Installing foo..." "apt-get install -y foo" # # Log file: ${LXS_LOG_FILE:-/tmp/lxs.log} # ═══════════════════════════════════════════════════════════════════════════ run_spinner() { local message=$1 shift local command="$*" local spinstr='|/-\' local log="${LXS_LOG_FILE:-/tmp/lxs.log}" echo -e "${CYAN}[..] ${message}${NC}" eval "$command" > "$log" 2>&1 & local pid=$! while kill -0 "$pid" 2>/dev/null; do local temp=${spinstr#?} printf "\r${CYAN}[%c.]${NC} ${message}" "$spinstr" spinstr=$temp${spinstr%"$temp"} sleep 0.15 done wait "$pid" local exit_code=$? if [ $exit_code -eq 0 ]; then printf "\r${CYAN}[OK]${NC} ${message}\n" else printf "\r${RED}[KO]${NC} ${message}\n" fi return $exit_code } # ═══════════════════════════════════════════════════════════════════════════ # Guards # ═══════════════════════════════════════════════════════════════════════════ require_debian_ubuntu() { if ! grep -qiE 'debian|ubuntu' /etc/os-release 2>/dev/null; then err "This script supports Debian/Ubuntu only." return 1 fi } # Verify enough free disk space before doing apt installs or writing config. # Fails early with a clear message so we don't get half-installed packages or # files truncated mid-write when the disk is full. # # Usage: # require_disk_space # 500MB on /var (apt cache + lists) # require_disk_space 1024 # 1024MB on /var # require_disk_space 1024 /var /opt # 1024MB on each listed path # # Duplicate filesystems (e.g. /var on the same fs as /) are checked once. require_disk_space() { local min_mb=${1:-500} shift 2>/dev/null || true local paths=("$@") [ ${#paths[@]} -eq 0 ] && paths=(/var) local path avail fs seen=" " rc=0 for path in "${paths[@]}"; do fs=$(df -P "$path" 2>/dev/null | awk 'NR==2 {print $1}') if [ -z "$fs" ]; then err "Cannot read disk usage for ${path}" rc=1 continue fi [[ "$seen" == *" $fs "* ]] && continue seen="$seen$fs " avail=$(df -Pm "$path" 2>/dev/null | awk 'NR==2 {print $4}') if [ "${avail:-0}" -lt "$min_mb" ]; then err "Not enough disk space on ${path} (${fs}): ${avail}MB free, ${min_mb}MB required." rc=1 fi done return $rc } # Re-exec the current script via sudo if not already root. Preserves env and # arguments. Call near the top of a sub-script: # require_root "$0" "$@" require_root() { [ "$EUID" -eq 0 ] && return 0 local self=$1 shift exec sudo -E "$self" "$@" } # ═══════════════════════════════════════════════════════════════════════════ # Network / random / apt helpers # ═══════════════════════════════════════════════════════════════════════════ get_public_ip() { local ip="" ip=$(curl -s --max-time 5 ifconfig.me 2>/dev/null) || \ ip=$(curl -s --max-time 5 icanhazip.com 2>/dev/null) || \ ip=$(curl -s --max-time 5 ipinfo.io/ip 2>/dev/null) || \ ip=$(hostname -I 2>/dev/null | awk '{print $1}') echo "$ip" } # Generate a random alphanumeric string. Default length: 32. generate_password() { local length=${1:-32} LC_ALL=C tr -dc 'a-zA-Z0-9' /dev/null 2>&1 || return 0 export DEBIAN_FRONTEND=noninteractive export NEEDRESTART_MODE=a export NEEDRESTART_SUSPEND=1 echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections 2>/dev/null || true } # Add a UFW allow rule, but only if UFW is installed AND active. No-op # otherwise — so app installers can declare the ports they need without # forcing UFW on hosts that don't use it. # # Usage: # ufw_allow 8000/tcp "Coolify dashboard" # ufw_allow 80/tcp ufw_allow() { command -v ufw >/dev/null 2>&1 || return 0 ufw status 2>/dev/null | grep -q "Status: active" || return 0 local rule=$1 comment=${2:-} if [ -n "$comment" ]; then ufw allow "$rule" comment "$comment" >/dev/null else ufw allow "$rule" >/dev/null fi ok "UFW :: allowed ${rule}${comment:+ (${comment})}" } # Wait for other apt/dpkg processes to release their locks. Up to 120s. wait_for_apt() { command -v apt >/dev/null 2>&1 || return 0 local max_wait=120 local wait_count=0 local lock_detected=false while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 || \ fuser /var/lib/dpkg/lock >/dev/null 2>&1 || \ fuser /var/lib/apt/lists/lock >/dev/null 2>&1; do [ "$lock_detected" = false ] && lock_detected=true && \ echo -e "${YELLOW}[!!] Waiting for other package manager to finish...${NC}" wait_count=$((wait_count + 1)) [ $wait_count -ge $max_wait ] && \ echo -e "${RED}[KO] Timeout waiting for package manager${NC}" && return 1 printf "\r${GRAY}[..] Waiting... (%ds/%ds)${NC}" $wait_count $max_wait sleep 1 done [ "$lock_detected" = true ] && \ echo -e "\n${GREEN}[OK] Package manager is now available${NC}" return 0 }