Files
debianito-post-install/modules/utils.sh
T
stornic56 2f11abbd64 LXDE: add Custom packages option and respect existing display manager
- New 'LXDE Custom (Choose packages)' menu entry: fixed base
  (lxde-core + xserver-xorg, lightdm only when no display manager
  is configured) plus a data-driven checklist of 16 extras.
- lxterminal, polkitd, notification-daemon and light-locker stay out
  of the checklist: they arrive via lxde-core Recommends (verified
  against packages.debian.org/trixie, 2026-10-16).
- Full/Core/Custom now honor an already-configured display manager
  via _existing_display_manager (GNOME/KDE pattern): no debconf
  preseed, no lightdm install, no enable over an existing DM.
- Extract _select_packages_checklist helper to utils.sh; XFCE
  Custom consumes it with no functional change.
2026-10-21 15:29:18 -05:00

987 lines
32 KiB
Bash

#!/usr/bin/env bash
# Common utility functions for the post-install script
# ------------------
# Global variables
# ------------------
# Session state shared with every lazily-loaded module:
# exported so shellcheck sees the cross-file usage (SC2034).
export CPU_SUMMARY=""
export RAM_SUMMARY=""
export GPU_TYPE=""
export GPU_DESC=""
export GPU_VERSION=""
export INTEL_GPU_DEVICE_ID=""
export NVIDIA_GPU_DEVICE_ID=""
export HAS_NVIDIA=false
export HAS_AMD=false
export HAS_INTEL=false
export HAS_AMD_LEGACY_GCN=false
export KERNEL_VERSION=""
export DISPLAY_SERVER="unknown"
export STORAGE_SUMMARY=""
export WIFI_CHIPSET=""
export DESKTOP_ENV=""
export AUDIO_SERVER=""
# OS identity populated by detect_debian_version(); read by
# every module (SC2034).
export DEBIAN_VERSION=""
# APT update deduplication flag (set to 1 after the first successful apt-get update)
APT_UPDATED=0
# Cached output of `lspci -nn` for the whole session (populated once via _init_lspci_cache)
LSPCI_OUTPUT=""
# Deferred hardware state: the detection battery runs on
# first need (_ensure_state_detected) instead of at boot.
STATE_DETECTED=0
# Network check result cache: 0 = online, 1 = offline,
# empty = not checked yet (checked on first use).
_NET_STATUS=""
# Pre-apply summary support: countdown of installs whose
# per-package prompt was already confirmed by
# _confirm_install_list. 0 = every _install_pkg asks
# individually (the default, safe behavior).
_SELECTION_CONFIRMED=0
# Lazy module registry (presence of a key = loaded).
# Populated by _load_module() so every module is
# sourced at most once per session.
declare -A _LOADED_MODULES=()
# ── Lazy module loading ──────────────────────────
# Sources a module on first use and remembers it in
# _LOADED_MODULES. Call at the top of a main_menu()
# case branch, before calling into the module:
# _load_module <name> || continue
# Cross-module dependencies (functions called at
# runtime that live in another module) are declared
# in the deps case below, so a module never calls
# an unloaded function.
_load_module() {
# Defensive: an empty name would produce a bad
# array subscript under `set -u`.
local name="${1:-}"
[ -n "$name" ] || return 1
# Already loaded in this session?
[ -n "${_LOADED_MODULES[$name]+x}" ] && return 0
# Module file path. Most modules live directly in
# modules/; a few have a dedicated subdirectory.
local path
case "$name" in
system_prefs) path="${MODULES_DIR}/system/system_prefs.sh" ;;
audio) path="${MODULES_DIR}/system/audio.sh" ;;
java) path="${MODULES_DIR}/extras/java.sh" ;;
repo_detect) path="${MODULES_DIR}/repos/repo_detect.sh" ;;
gnome) path="${MODULES_DIR}/desktops/desktop_gnome.sh" ;;
kde) path="${MODULES_DIR}/desktops/desktop_kde.sh" ;;
xfce) path="${MODULES_DIR}/desktops/desktop_xfce.sh" ;;
lxde) path="${MODULES_DIR}/desktops/desktop_lxde.sh" ;;
*) path="${MODULES_DIR}/${name}.sh" ;;
esac
# Cross-module dependencies, loaded first.
local -a deps=()
case "$name" in
system_prefs) deps=(audio) ;;
repos) deps=(repo_detect) ;;
firmware) deps=(repo_detect bluetooth repos) ;;
gaming) deps=(repo_detect java repos) ;;
extras) deps=(java) ;;
desktop_display) deps=(repos audio) ;;
gnome) deps=(desktop_display audio) ;;
kde) deps=(desktop_display audio) ;;
xfce) deps=(desktop_display audio) ;;
lxde) deps=(desktop_display audio) ;;
esac
local dep
for dep in "${deps[@]}"; do
_load_module "$dep" || return 1
done
if [ ! -f "$path" ]; then
echo -e "${RED}[-] Module not found: ${path}${NC}" >&2
return 1
fi
# shellcheck disable=SC1090
source "$path"
_LOADED_MODULES["$name"]=1
return 0
}
# --------------------------
# Pre-flight checks
# --------------------------
check_root() {
if [ "$EUID" -eq 0 ]; then
echo -e "${RED}Do not run this script as root. Use a normal user with sudo.${NC}"
exit 1
fi
}
check_sudo() {
if ! sudo -v; then
echo -e "${RED}This script requires sudo privileges.${NC}"
exit 1
fi
}
# -------------------------------------------------------------------
# Robust time sync: NTP + timezone validation + service restart
# -------------------------------------------------------------------
_ensure_time_synced() {
command -v timedatectl &>/dev/null || return
# ── Step 1: Force NTP active ──
sudo timedatectl set-ntp true --no-ask-password 2>/dev/null || true
# ── Step 2: Validate timezone ──
local tz
tz=$(timedatectl show -p Timezone --value 2>/dev/null || echo "")
if [ -z "$tz" ] || [ "$tz" = "n/a" ] || [ "$tz" = "Etc/UTC" ]; then
if [ -n "${DISPLAY:-}" ] || [ -n "${SSH_TTY:-}" ]; then
_msg "Timezone" \
"Your system timezone is not set or is set to UTC.\n\nThe script will now open the timezone\nconfiguration tool to set your local timezone." 12 60
sudo env LC_ALL=C LANGUAGE=C dpkg-reconfigure tzdata || true
echo -e "${GREEN}Timezone configured: $(timedatectl show -p Timezone --value 2>/dev/null)${NC}"
else
echo -e "${YELLOW}Timezone not set. Run 'sudo dpkg-reconfigure tzdata' later.${NC}"
fi
fi
# ── Step 3: Install/ensure systemd-timesyncd ──
if ! is_installed systemd-timesyncd; then
sudo DEBIAN_FRONTEND=noninteractive apt install -y systemd-timesyncd || true
fi
sudo systemctl enable systemd-timesyncd 2>/dev/null || true
sudo systemctl restart systemd-timesyncd 2>/dev/null || true
# ── Step 4: Verify result ──
sleep 2
if timedatectl show --property=NTPSynchronized --value 2>/dev/null | grep -q yes; then
echo -e "${GREEN}Time synchronized: $(date '+%Y-%m-%d %H:%M')${NC}"
else
echo -e "${YELLOW}NTP sync did not complete yet (may need a moment).${NC}"
fi
}
# --------------------------------
# Debian version detection
# --------------------------------
detect_debian_version() {
if ! command -v lsb_release &>/dev/null; then
if [ -f /etc/os-release ]; then
DEBIAN_CODENAME=$(grep -oP 'VERSION_CODENAME=\K\w+' /etc/os-release 2>/dev/null || echo "")
fi
if [ -z "$DEBIAN_CODENAME" ]; then
_ensure_time_synced || true
sudo apt update -qq 2>/dev/null && sudo apt install -y -qq lsb-release || true
fi
fi
if [ -z "$DEBIAN_CODENAME" ]; then
DEBIAN_CODENAME=$(lsb_release -cs 2>/dev/null || echo "")
fi
case "$DEBIAN_CODENAME" in
bullseye) DEBIAN_VERSION="11" ;;
bookworm) DEBIAN_VERSION="12" ;;
trixie) DEBIAN_VERSION="13" ;;
*)
echo -e "${RED}Unsupported Debian version: '$DEBIAN_CODENAME'. Only 11 (bullseye), 12 (bookworm) and 13 (trixie) are supported.${NC}"
exit 1
;;
esac
}
# ----------------------------------
# CPU and RAM info (cosmetic)
# ----------------------------------
detect_cpu_ram() {
CPU_SUMMARY=$(grep -m1 'model name' /proc/cpuinfo | sed 's/.*: //' || true)
RAM_KB=$(grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}') || RAM_KB=0
# BH-001: If /proc/meminfo is unavailable or grep finds no MemTotal,
# RAM_KB would be empty under set -u. Assign 0 as safe default.
[ -z "$RAM_KB" ] && RAM_KB=0
RAM_GB=$(awk -v kb="$RAM_KB" 'BEGIN { printf "%.2f", kb / 1048576 }')
RAM_SUMMARY="${RAM_GB} GB"
}
# ----------------------------------
# Check if running backports kernel
# ----------------------------------
is_backports_kernel() {
local kver
kver=$(uname -r)
if echo "$kver" | grep -q 'bpo'; then
echo true
else
echo false
fi
}
# ----------------------------------
# Package installed check
# ----------------------------------
is_installed() {
dpkg -l "$1" 2>/dev/null | grep -q '^ii'
}
# ----------------------------------
# Package version helpers
# ----------------------------------
# Get the stable version of a package from apt-cache policy
# Returns: version string or "" if not found
_get_pkg_version() {
local pkg="$1"
apt-cache policy "$pkg" 2>/dev/null | awk 'NR==3 {print $2; exit}'
}
# Get the installed version of a package from dpkg
# Returns: version string or "" if not installed
_get_installed_version() {
local pkg="$1"
dpkg -l "$pkg" 2>/dev/null | awk '/^ii/{print $3; exit}'
}
# Get the backports version of a package
# Returns: version string or "" if not found
_get_backports_version() {
local pkg="$1"
local codename="${2:-$DEBIAN_CODENAME}"
apt-cache madison "$pkg" 2>/dev/null |
grep "${codename}-backports" | awk '{print $3}' | head -1
}
_state() {
is_installed "$1" && echo "ON" || echo "OFF"
}
# ----------------------------------
# Kernel version
# ----------------------------------
detect_kernel() {
KERNEL_VERSION=$(uname -r)
}
# ----------------------------------
# lspci output cache
# ----------------------------------
# Populates LSPCI_OUTPUT once per session. `lspci -nn` includes the textual PCI
# class (VGA/3D/Ethernet/Network/Bluetooth controller) and the device IDs
# (e.g. 14e4:), so a single capture covers every grep used across modules.
_init_lspci_cache() {
[ -n "${LSPCI_OUTPUT:-}" ] && return
LSPCI_OUTPUT=$(timeout 2 lspci -nn 2>/dev/null || true)
}
_lspci_desc() {
# Strip the "bus domain] device ID" and "(rev XX)" fields,
# keeping the human-readable device name.
echo "$1" | sed -E 's/^.*\]: //; s/ \[[0-9a-fA-F]{4}:[0-9a-fA-F]{4}\]//; s/ \(rev [0-9a-fA-F]+\)//'
}
# GPU detection
# ----------------------------------
detect_gpu() {
local gpu_lines
gpu_lines=$(echo "$LSPCI_OUTPUT" | grep -E "VGA|3D") || true
if [ -z "$gpu_lines" ]; then
GPU_TYPE="unknown"
GPU_DESC="No GPU detected"
return
fi
local has_nvidia=false has_amd=false has_intel=false
local desc_lines="" nvidia_dev_id="" intel_dev_id=""
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" ]] && continue
local desc
desc=$(echo "$line" | sed -E 's/.*: //; s/ *\(rev.*//')
[ -n "$desc_lines" ] && desc_lines+=" + "
desc_lines+="$desc"
if echo "$line" | grep -qi "nvidia"; then
has_nvidia=true
[ -z "$nvidia_dev_id" ] && nvidia_dev_id=$(echo "$line" | grep -oP '10de:\K[0-9a-fA-F]+' | head -n1)
elif echo "$line" | grep -qi "amd"; then
has_amd=true
HAS_AMD_LEGACY_GCN=$(is_amd_legacy_gcn)
elif echo "$line" | grep -qi "intel"; then
has_intel=true
[ -z "$intel_dev_id" ] && intel_dev_id=$(echo "$line" | grep -oP '8086:\K[0-9a-fA-F]+' | head -n1)
fi
done <<<"$gpu_lines"
GPU_DESC="$desc_lines"
HAS_NVIDIA=$has_nvidia
HAS_AMD=$has_amd
HAS_INTEL=$has_intel
if $has_nvidia; then
GPU_TYPE="nvidia"
[ -n "$nvidia_dev_id" ] && NVIDIA_GPU_DEVICE_ID="$nvidia_dev_id"
elif $has_amd; then
GPU_TYPE="amd"
elif $has_intel; then
GPU_TYPE="intel"
if [ -n "$intel_dev_id" ]; then
INTEL_GPU_DEVICE_ID="0x${intel_dev_id,,}"
fi
else
GPU_TYPE="unknown"
fi
if [ "$GPU_TYPE" = "nvidia" ]; then
local nv_ver
nv_ver=$(timeout 3 nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -1) || true
if [ -z "$nv_ver" ]; then
nv_ver=$(_get_installed_version "nvidia-driver" | sed 's/-.*//') || true
fi
[ -n "$nv_ver" ] && GPU_VERSION="NVIDIA $nv_ver"
fi
if [ -z "$GPU_VERSION" ]; then
local mesa_ver
mesa_ver=$(_get_installed_version "libgl1-mesa-dri" | sed 's/-.*//')
[ -n "$mesa_ver" ] && GPU_VERSION="Mesa $mesa_ver"
fi
}
get_gpu_summary() {
if [ -n "$GPU_DESC" ]; then
echo "$GPU_DESC"
else
echo "Unknown/Rare"
fi
}
# -------------------------------------
# Network adapter detection
# -------------------------------------
WIFI_CHIPSET=""
WIFI_DESC=""
ETH_DESC=""
declare -a ETH_NAMES=()
declare -a ETH_DESCS=()
declare -a ETH_STATES=()
declare -a ETH_IPS=()
declare -a WIFI_NAMES=()
declare -a WIFI_DESCS=()
declare -a WIFI_STATES=()
declare -a WIFI_IPS=()
declare -a WIFI_SSIDS=()
detect_network() {
# BH-013: Reset all network arrays at the start.
# Without this, if detect_network() is called more than once (e.g. from
# refresh_system_state()), the arrays ETH_NAMES, WIFI_NAMES, etc. would
# accumulate duplicates instead of being reset, causing incorrect data.
ETH_NAMES=()
ETH_STATES=()
ETH_IPS=()
ETH_DESCS=()
WIFI_NAMES=()
WIFI_STATES=()
WIFI_IPS=()
WIFI_SSIDS=()
WIFI_DESCS=()
local eth_line
eth_line=$(echo "$LSPCI_OUTPUT" | grep -i 'Ethernet controller' | head -n1) || true
if [ -n "$eth_line" ]; then
ETH_DESC=$(_lspci_desc "$eth_line")
fi
local wifi_line
# Layer 1: grep by PCI class description text
wifi_line=$(echo "$LSPCI_OUTPUT" | grep -iE 'network controller|wireless|wi-fi|wlan|802\.11' | head -n1) || true
# Layer 2: grep by exact PCI class code 0x0280 (Network controller)
if [ -z "$wifi_line" ]; then
wifi_line=$(echo "$LSPCI_OUTPUT" | grep -i 'network controller' | head -n1) || true
fi
# Layer 3: Broadcom vendor ID fallback (14e4)
if [ -z "$wifi_line" ]; then
wifi_line=$(echo "$LSPCI_OUTPUT" | grep -i '14e4:' | head -n1) || true
fi
if [ -n "$wifi_line" ]; then
WIFI_CHIPSET="$wifi_line"
WIFI_DESC=$(_lspci_desc "$wifi_line")
fi
# Layer 4: USB WiFi adapter (no PCI device)
if [ -z "$wifi_line" ] && command -v lsusb &>/dev/null; then
local usb_wifi
usb_wifi=$(lsusb 2>/dev/null | grep -iE 'wireless|wifi|wlan|802\.11' | head -n1) || true
if [ -n "$usb_wifi" ]; then
WIFI_CHIPSET="$usb_wifi"
WIFI_DESC=$(echo "$usb_wifi" | sed 's/^.*ID //')
fi
fi
# ── Safeguard: if ip is not installed, skip runtime parsing ──
if ! command -v ip &>/dev/null; then
return
fi
local iface state ip4 ssid
while IFS= read -r line; do
iface=$(echo "$line" | awk -F': ' '{print $2}' | sed 's/@.*//')
state=$(echo "$line" | awk '{print $9}')
case "$iface" in
eth* | enp* | ens* | enx* | eno*)
ip4=$(timeout 1 ip -4 -o addr show "$iface" 2>/dev/null | awk '{print $4}')
ETH_NAMES+=("$iface")
ETH_STATES+=("$state")
ETH_IPS+=("${ip4:-}")
ETH_DESCS+=("${ETH_DESC:-}")
;;
wl*)
ip4=$(timeout 1 ip -4 -o addr show "$iface" 2>/dev/null | awk '{print $4}')
ssid=""
[ "$state" = "UP" ] && ssid=$(timeout 1 iwgetid -r "$iface" 2>/dev/null || true)
WIFI_NAMES+=("$iface")
WIFI_STATES+=("$state")
WIFI_IPS+=("${ip4:-}")
WIFI_SSIDS+=("${ssid:-}")
WIFI_DESCS+=("${WIFI_DESC:-}")
;;
esac
done < <(timeout 2 ip -o link show 2>/dev/null)
}
# ---------------------------------------
# Display Server detection (Wayland / X11 / tty)
# ---------------------------------------
detect_displayserver() {
local st="${XDG_SESSION_TYPE:-}"
case "$st" in
wayland) DISPLAY_SERVER="Wayland" ;;
x11) DISPLAY_SERVER="X11" ;;
tty) DISPLAY_SERVER="none (tty)" ;;
*)
if [ -n "${WAYLAND_DISPLAY:-}" ]; then
DISPLAY_SERVER="Wayland"
elif [ -n "${DISPLAY:-}" ]; then
DISPLAY_SERVER="X11"
else
DISPLAY_SERVER="unknown"
fi
;;
esac
}
# ---------------------------------------
# Storage summary via lsblk (NVMe / SSD / HDD / USB-SD)
# ---------------------------------------
detect_storage() {
local parts=()
local name size rota type rm
while read -r name size rota type; do
[ "$name" = "NAME" ] && continue
echo "$name" | grep -q "zram" && continue
[ "$type" = "loop" ] || [ "$type" = "rom" ] && continue
if echo "$name" | grep -q "nvme"; then
type="NVMe"
elif [ "$rota" = "1" ]; then
type="HDD"
else
rm=$(timeout 2 cat /sys/block/"$name"/removable 2>/dev/null || echo 0)
if [ "$rm" = "1" ]; then
type="USB/SD"
else
type="SSD"
fi
fi
parts+=("${size} ${type}")
done < <(timeout 2 lsblk -d -o NAME,SIZE,ROTA,TYPE -e 7,11 2>/dev/null || true)
if [ ${#parts[@]} -eq 0 ]; then
STORAGE_SUMMARY="No disks detected"
return
fi
local result=""
local p
for p in "${parts[@]}"; do
[ -n "$result" ] && result+=" + "
result+="$p"
done
STORAGE_SUMMARY="$result"
}
# ---------------------------------------
# Desktop environment detection
# ---------------------------------------
detect_desktop_environment() {
case "${XDG_CURRENT_DESKTOP:-}" in
*GNOME*) DESKTOP_ENV="gnome" ;;
*KDE*) DESKTOP_ENV="kde" ;;
*XFCE*) DESKTOP_ENV="xfce" ;;
*) DESKTOP_ENV="other" ;;
esac
}
# ---------------------------------------
# Audio server detection (PipeWire / PulseAudio)
# ---------------------------------------
detect_audio_server() {
if command -v pw-cli &>/dev/null && timeout 2 pw-cli info &>/dev/null 2>&1; then
AUDIO_SERVER="pipewire"
elif command -v pactl &>/dev/null; then
AUDIO_SERVER="pulseaudio"
else
AUDIO_SERVER="none"
fi
}
# ---------------------------------------
# Intel HD Graphics generation detection
# ---------------------------------------
# Returns "gen7-" if Device ID < 0x1600, else "gen8+"
get_intel_generation() {
if [ -z "$INTEL_GPU_DEVICE_ID" ]; then
# fallback: assume gen8+
echo "gen8+"
return
fi
local dev_int
dev_int=$(printf "%d" "$INTEL_GPU_DEVICE_ID")
if [ "$dev_int" -lt 5632 ]; then # 0x1600 = 5632
echo "gen7-"
else
echo "gen8+"
fi
}
# ----------------------------------------------------------------------
# Check if backports repository is enabled (active line without #)
# Note: the Debian 12 (Bookworm) NVIDIA flow no longer uses this function
# (backports EOL 2026-08-09). Still active for kernels, desktop/greetd,
# gaming, audio, communication and Mesa (AMD/Intel).
# ----------------------------------------------------------------------
is_backports_enabled() {
local codename="${DEBIAN_CODENAME:-}"
[ -z "$codename" ] && {
echo false
return
}
local c_pattern="^[^#]*${codename}-backports[[:space:]]+"
local d_pattern="Suites:.*${codename}-backports"
local apt_dir="${APT_DIR:-/etc/apt}"
# Classic embedded (sources.list)
if [ -f "$apt_dir/sources.list" ] && grep -Eq "$c_pattern" "$apt_dir/sources.list" 2>/dev/null; then
echo true
return
fi
# Classic standalone (any .list file in sources.list.d)
if [ -d "$apt_dir/sources.list.d" ] && grep -qrE "$c_pattern" "$apt_dir/sources.list.d/"*.list 2>/dev/null; then
echo true
return
fi
# Deb822 any .sources file
if [ -d "$apt_dir/sources.list.d" ] && grep -qr "$d_pattern" "$apt_dir/sources.list.d/"*.sources 2>/dev/null; then
echo true
return
fi
echo false
}
install_backports_or_stable() {
local pkg="$1"
local pkg_desc="${2:-$pkg}"
local bpo_ver=""
if [ "$(is_backports_enabled)" == true ]; then
bpo_ver=$(_get_backports_version "$pkg")
fi
if is_installed "$pkg"; then
if [ -n "$bpo_ver" ]; then
local current_ver
current_ver=$(_get_installed_version "$pkg")
if _confirm "Backports: ${pkg}" \
"${pkg} ${current_ver} installed.\nUpgrade to backports ${bpo_ver}?"; then
_run_cmd "Backports" \
"sudo DEBIAN_FRONTEND=noninteractive apt install -y -t ${DEBIAN_CODENAME}-backports $pkg" \
"Upgrading $pkg..."
return
fi
fi
echo "$pkg already installed."
return
fi
if [ -n "$bpo_ver" ]; then
local stable_ver
stable_ver=$(_get_pkg_version "$pkg")
if _confirm_custom "${pkg}" "Install ${pkg_desc}?\n\n Backports: ${bpo_ver} (newer, recommended for gaming/newer HW)\n Stable: ${stable_ver:-N/A}\n\nChoose version:" "Backports" "Stable"; then
_run_cmd "Backports" \
"sudo DEBIAN_FRONTEND=noninteractive apt install -y -t ${DEBIAN_CODENAME}-backports $pkg" \
"Installing $pkg from backports..."
return
fi
_run_cmd "APT" "sudo DEBIAN_FRONTEND=noninteractive apt install -y $pkg" "Installing $pkg from stable..."
return
fi
local stable_ver
stable_ver=$(_get_pkg_version "$pkg")
if _confirm "Install: ${pkg}" "Install ${pkg} ${stable_ver:-}?"; then
_run_cmd "APT" "sudo DEBIAN_FRONTEND=noninteractive apt install -y $pkg" "Installing $pkg..."
fi
}
# ----------------------------------------------------------------------
# Whiptail helpers (4-block pattern)
# ----------------------------------------------------------------------
_confirm() {
# --yes (unattended mode): auto-accept without
# invoking whiptail; the prompt gate is the flag itself.
[ "${AUTO_YES:-0}" = "1" ] && return 0
whiptail --title "$1" --yes-button "Yes" --no-button "No" \
--yesno "$2" "${3:-10}" "${4:-65}"
}
_confirm_custom() {
local title="$1" text="$2" yes_btn="$3" no_btn="$4"
shift 4
local height="${1:-20}"
local width="${2:-78}"
whiptail --title "$title" --yes-button "$yes_btn" --no-button "$no_btn" \
--yesno "$text" "$height" "$width"
}
_msg() {
local _msg_title="$1"
local _msg_text="$2"
# BH-014: Escape '%' to prevent whiptail from interpreting them as printf format.
_msg_text="${_msg_text//%/%%}"
whiptail --title "$_msg_title" --msgbox "$_msg_text" "${3:-10}" "${4:-65}" || true
}
_msg_red() {
whiptail --colors --title "\Z1$1\Zn" --msgbox "\Z1$2\Zn" "${3:-12}" "${4:-70}" || true
}
_menu() {
local title="$1" text="$2" h="$3" w="$4" lh="$5"
shift 5
whiptail --title "$title" --menu "$text" "$h" "$w" "$lh" "$@" 3>&1 1>&2 2>&3 || true
}
_checklist() {
local title="$1" text="$2" h="$3" w="$4" lh="$5"
shift 5
whiptail --title "$title" --ok-button "Apply" --checklist "$text" "$h" "$w" "$lh" "$@" 3>&1 1>&2 2>&3 || true
}
_radiolist() {
local title="$1" text="$2" h="$3" w="$4" lh="$5"
shift 5
whiptail --title "$title" --ok-button "Install" --radiolist "$text" "$h" "$w" "$lh" "$@" 3>&1 1>&2 2>&3 || true
}
_inputbox() {
whiptail --title "$1" --ok-button "Apply" --inputbox "$2" "${3:-10}" "${4:-60}" "${5:-}" 3>&1 1>&2 2>&3 || true
}
_validate_sudoers() {
local content="$1" dest="$2"
local tmpfile
tmpfile=$(mktemp) || return 1
echo "$content" >"$tmpfile"
if ! /usr/sbin/visudo -cf "$tmpfile" &>/dev/null; then
local err
err=$(/usr/sbin/visudo -cf "$tmpfile" 2>&1 || true)
rm -f "$tmpfile"
_msg "Sudoers Error" "Invalid sudoers syntax in:\n\n${err}\n\nFile was NOT written.\nThis prevents broken sudo access." 12 70
return 1
fi
sudo cp "$tmpfile" "$dest"
sudo chmod 0440 "$dest"
rm -f "$tmpfile"
}
# shellcheck disable=SC2120 # Optional prompt arg used by GPU modules.
_pause() {
local msg="${1:-Press Enter to continue...}"
echo -e "$msg"
read -r || true
}
# Blocks 2-4: run → pause
# Returns the exit code of the executed command so callers can abort
# or adjust state (e.g. NVIDIA_DRIVER_MODE) when a step fails.
_run_cmd() {
local title="$1" command="$2" success_msg="${3:-Running...}"
echo -e "${GREEN}[+]${NC} $success_msg"
echo "──────────────────────────────────────────────"
local rc=0
bash -c "$command" || rc=$?
echo "──────────────────────────────────────────────"
if [ $rc -eq 0 ]; then
echo -e "${GREEN}[+]${NC} Done."
else
echo -e "${RED}[-]${NC} Failed (exit code: $rc)."
fi
_pause
return "$rc"
}
# Blocks 1-4: confirm → run → pause
_run() {
if _confirm "$1" "$2"; then
_run_cmd "$1" "$3" "$4"
fi
}
_is_headless() {
[ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]
}
_run_install_batch() {
local pkgs=("$@")
[ ${#pkgs[@]} -eq 0 ] && return 0
local ver_list=""
for pkg in "${pkgs[@]}"; do
local ver
ver=$(_get_pkg_version "$pkg")
ver_list+=" - ${pkg} ${ver:-unknown}\n"
done
if _confirm "Install" "Install these packages?\n${ver_list}"; then
_run_cmd "Install" "sudo DEBIAN_FRONTEND=noninteractive apt install -y ${pkgs[*]}" "Installing..."
fi
}
# Pre-apply summary: confirm a parsed selection before
# any apt transaction starts. On success it arms
# _SELECTION_CONFIRMED (a countdown) so _install_pkg
# skips its per-package prompt; checklist flows reset
# the counter to 0 after their install loop.
_confirm_install_list() {
local title="$1"
shift
local -a items=("$@")
[ ${#items[@]} -eq 0 ] && return 1
local list=""
local shown=0
local max_show=15
local item
for item in "${items[@]}"; do
if [ "$shown" -lt "$max_show" ]; then
list+=" - ${item}\n"
shown=$((shown + 1))
fi
done
local hidden=$(( ${#items[@]} - shown ))
if [ "$hidden" -gt 0 ]; then
list+="\n ... and ${hidden} more package(s)\n"
fi
list+="\nProceed with the installation?"
local height=$(( shown + 8 ))
[ "$height" -gt 24 ] && height=24
if _confirm "$title" "$list" "$height" 65; then
_SELECTION_CONFIRMED=${#items[@]}
return 0
fi
return 1
}
# Show a multi-select checklist of packages and return the
# selection as a space-separated string ("" on cancel/Esc).
# Args: title prompt "pkg|description" [pkg|description ...]
# Already-installed packages are pre-checked via _state.
_select_packages_checklist() {
local title="$1" prompt="$2"
shift 2
local -a items=() entry
for entry in "$@"; do
local pkg="${entry%%|*}"
items+=("$pkg" "${entry#*|}" "$(_state "$pkg")")
done
local choices
# _checklist swallows whiptail's cancel (Esc) exit code
# via `|| true`, so a cancelled dialog looks like an empty
# selection here; an empty result means "nothing to install".
choices=$(_checklist "$title" "$prompt" \
$TUI_ALTO $TUI_ANCHO $TUI_ALTO_LISTA "${items[@]}")
choices=$(echo "$choices" | tr -d '"')
echo "$choices"
}
# Install a package with confirmation prompt.
# Handles set -e: failures are caught and reported, not fatal.
_install_pkg() {
local pkg="$1"
local ver
ver=$(_get_pkg_version "$pkg")
[ -z "$ver" ] && ver="(version unknown)"
# Pre-apply summary support: when a checklist flow has
# confirmed its full selection (_confirm_install_list),
# the per-package prompt is already answered — the
# countdown expires after exactly N installs.
if [ "$_SELECTION_CONFIRMED" -gt 0 ]; then
_SELECTION_CONFIRMED=$((_SELECTION_CONFIRMED - 1))
elif ! _confirm "Install: ${pkg}" "Install ${pkg}\nVersion: ${ver}?"; then
return 0
fi
_run_cmd "Install" "sudo DEBIAN_FRONTEND=noninteractive apt install -y $pkg" "Installing $pkg..."
}
# Install a package if not already installed.
# Returns: 0 if already installed, 1 if install failed, 2 if cancelled
_install_if_missing() {
local pkg="$1"
if is_installed "$pkg"; then
echo -e "${GREEN}[+]${NC} $pkg already installed."
return 0
fi
_run_cmd "Install" "sudo DEBIAN_FRONTEND=noninteractive apt install -y $pkg" \
"Installing $pkg..."
return $?
}
# _run_install() wrapper: redirect to _install_pkg for consistency
_run_install() {
_install_pkg "$@"
}
# ----------------------------------
# Language helpers
# ----------------------------------
_detect_lang() {
local sys_lang
sys_lang=$(echo "${LANG:-en}" | cut -c1-2 | tr '[:upper:]' '[:lower:]')
echo "$sys_lang"
}
_detect_lang_pkg() {
local base="$1"
local lang2
lang2=$(_detect_lang)
[ "$lang2" = "en" ] && echo "" && return
# Defensive: LANG may be unset in minimal environments; do not trip set -u.
local full="${LANG:-C}"
full="${full%%.*}"
local hyphenated_full
hyphenated_full=$(echo "$full" | tr '[:upper:]' '[:lower:]' | tr '_' '-')
local pkg
pkg=$(apt-cache search "^${base}-${hyphenated_full}$" 2>/dev/null | awk 'NR==1{print $1}')
[ -z "$pkg" ] && pkg=$(apt-cache search "^${base}-${lang2}$" 2>/dev/null | awk 'NR==1{print $1}')
[ -z "$pkg" ] && pkg=$(apt-cache search "^${base}-all$" 2>/dev/null | awk 'NR==1{print $1}')
echo "$pkg"
}
# ----------------------------------
# Network connectivity check
# ----------------------------------
# shellcheck disable=SC2120 # Optional probe target (default: deb.debian.org).
_check_network() {
local target="${1:-deb.debian.org}"
# Single bounded probe (worst case 3 s; the old
# ping/wget/curl cascade could block up to 13 s).
[[ "$target" =~ ^[a-zA-Z0-9.-]+$ ]] || return 1
timeout 3 bash -c "</dev/tcp/${target}/80" &>/dev/null
}
# ── On-demand network check (non-blocking boot) ──
# The connectivity banner moved from boot to the first
# apt operation. _NET_STATUS caches the result so the
# probe runs at most once per session.
_require_network() {
if [ -z "$_NET_STATUS" ]; then
if _check_network; then
_NET_STATUS=0
else
_NET_STATUS=1
echo -e "${YELLOW}──────────────────────────────────────────${NC}"
echo -e "${YELLOW} No internet connectivity detected.${NC}"
echo -e "${YELLOW} Package installation will fail without network.${NC}"
echo -e "${YELLOW}──────────────────────────────────────────${NC}"
fi
fi
return "$_NET_STATUS"
}
# ----------------------------------
# APT update deduplication
# ----------------------------------
# Runs `apt-get update` at most once per session. Subsequent calls bypass the
# network refresh. Returns 0 on success, 1 if apt-get update fails.
_ensure_apt_updated() {
if [ "$APT_UPDATED" -eq 1 ]; then
echo -e "${GREEN}[+]${NC} APT package lists already refreshed this session."
return 0
fi
# Advisory: probe the network once (cached in
# _NET_STATUS) before attempting an apt operation.
_require_network || true
# A correct clock is required for HTTPS certificate
# validation; only runs when apt is actually used.
_ensure_time_synced
echo -e "${GREEN}[+]${NC} Refreshing APT package lists..."
if sudo apt-get update; then
APT_UPDATED=1
return 0
fi
echo -e "${RED}[-]${NC} apt-get update failed."
return 1
}
# ── Deferred hardware state (lazy pre-flight) ──
# The detection battery runs on first need instead of at
# boot. Call _ensure_state_detected() before reading any
# DETECT_* global (LSPCI_OUTPUT, GPU_*, ETH_NAMES,
# DESKTOP_ENV, AUDIO_SERVER, ...).
_run_detection_battery() {
_init_lspci_cache
detect_gpu
detect_network
detect_displayserver
detect_storage
detect_desktop_environment
detect_audio_server
detect_cpu_ram
detect_kernel
}
_ensure_state_detected() {
[ "$STATE_DETECTED" -eq 1 ] && return 0
_run_detection_battery
STATE_DETECTED=1
}
# ── Lazy system state refresh ──
refresh_system_state() {
detect_debian_version
# Forced re-run: a menu action may have changed the
# hardware or repo state since the first detection.
_run_detection_battery
STATE_DETECTED=1
}