# shellcheck shell=bash #------------------------------------------ # lib/common.sh — shared helpers # # Sourced by setup.sh and by every module. Modules source it directly, so a # module can be run standalone for testing: # DE=none TWM=niri bash modules/twm/niri.sh #------------------------------------------ # Guard against double-sourcing (setup.sh and a module can both pull it in) if [[ -n "${_SB_COMMON_LOADED:-}" ]]; then return 0 fi _SB_COMMON_LOADED=1 #------------------------------------------ # Colors (printf-safe, no tput) #------------------------------------------ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' CYAN='\033[0;36m' PURPLE='\033[0;35m' BLUE='\033[0;34m' GRAY='\033[0;37m' BOLD='\033[1m' RESET='\033[0m' # Disable colors if output is not a terminal # (PURPLE/BLUE/BOLD defined for future use; shellcheck has no way to know that) # shellcheck disable=SC2034 if [ ! -t 1 ]; then RED='' GREEN='' YELLOW='' CYAN='' PURPLE='' BLUE='' GRAY='' BOLD='' RESET='' fi color_yellow() { printf "%b%s%b\n" "$YELLOW" "$*" "$RESET"; } color_cyan() { printf "%b%s%b\n" "$CYAN" "$*" "$RESET"; } color_red() { printf "%b%s%b\n" "$RED" "$*" "$RESET"; } color_green() { printf "%b%s%b\n" "$GREEN" "$*" "$RESET"; } color_gray() { printf "%b%s%b\n" "$GRAY" "$*" "$RESET"; } #------------------------------------------ # Log levels #------------------------------------------ info() { color_cyan " $*"; } ok() { color_green " $*"; } warn() { color_yellow " $*"; } err() { color_red " $*" >&2; } step() { printf "\n"; color_yellow "$*"; } rule() { printf "%s\n" "----------------------------------------------------------------"; } #------------------------------------------ # Debug pausing #------------------------------------------ is_debug() { [[ "${DEBUG_MODE:-false}" == "true" ]]; } pause_if_debug() { if is_debug; then printf "\n" color_yellow "----------------------------------------------------------------" read -rp "Section complete. Press [Enter] to continue, or [q] to abort: " debug_choice if [[ "${debug_choice,,}" == "q" ]]; then color_red "Script aborted by user." exit 1 fi color_yellow "----------------------------------------------------------------" printf "\n" fi } #------------------------------------------ # Package helpers (dnf5) #------------------------------------------ install_packages() { # -y : no prompts # weak deps on : keep default behaviour, stated explicitly so it is greppable sudo dnf install -y --setopt=install_weak_deps=True "$@" } # Same, but a missing/renamed package warns instead of aborting the whole run. # Use for anything that is a nice-to-have rather than load-bearing. install_optional() { sudo dnf install -y --setopt=install_weak_deps=True "$@" \ || warn "Optional packages not installed: $*" } pkg_installed() { local p for p in "$@"; do rpm -q "$p" &>/dev/null || return 1 done return 0 } remove_packages() { local packages_to_remove=() for pkg in "$@"; do if pkg_installed "$pkg"; then packages_to_remove+=("$pkg") else color_gray " Package '$pkg' not installed. Skipping removal." fi done if [ ${#packages_to_remove[@]} -gt 0 ]; then info "Removing packages: ${packages_to_remove[*]}" sudo dnf remove -y "${packages_to_remove[@]}" fi } # dnf comps group. Fail loudly with the real group list rather than letting # dnf's generic "no match" scroll past. install_group() { local group="$1" if ! dnf group info "$group" >/dev/null 2>&1; then err "No such dnf group: @$group" err "Available groups on this system:" dnf group list --hidden 2>/dev/null | sed 's/^/ /' >&2 || true return 1 fi sudo dnf group install -y "$group" } # Try each candidate group/environment in turn; first one that exists wins. install_group_any() { local g for g in "$@"; do if dnf group info "$g" >/dev/null 2>&1; then info "Using dnf group: @$g" sudo dnf group install -y "$g" return 0 fi done err "None of these dnf groups exist: $*" dnf group list --hidden 2>/dev/null | sed 's/^/ /' >&2 || true return 1 } #------------------------------------------ # Third-party repos #------------------------------------------ add_terra_repo() { if pkg_installed terra-release; then ok "Terra repository already installed." return 0 fi info "Adding Terra repository (Fyra Labs)..." # Terra's own instructions use --nogpgcheck for the bootstrap package; # once terra-release is in place the repo itself is signature-checked. sudo dnf install -y --nogpgcheck \ --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' \ terra-release } add_rpmfusion_repo() { local rel rel=$(rpm -E %fedora) if pkg_installed rpmfusion-free-release rpmfusion-nonfree-release; then ok "RPM Fusion already installed." return 0 fi info "Enabling RPM Fusion (free + nonfree) for Fedora ${rel}..." sudo dnf install -y \ "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-${rel}.noarch.rpm" \ "https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-${rel}.noarch.rpm" } #------------------------------------------ # Fonts #------------------------------------------ # Nerd Fonts are not in the Fedora repos, so pull the release zip from GitHub # into the user's font dir. Idempotent: skipped if the family is already there. install_nerd_font() { local family="${1:-JetBrainsMono}" local dest="${HOME}/.local/share/fonts/NerdFonts-${family}" local url="https://github.com/ryanoasis/nerd-fonts/releases/latest/download/${family}.zip" if [[ -d "$dest" ]] && compgen -G "$dest/*.ttf" >/dev/null; then ok "Nerd Font ${family} already installed." return 0 fi info "Installing Nerd Font ${family}..." local tmp tmp=$(mktemp -d) if ! curl -fsSL -o "$tmp/${family}.zip" "$url"; then warn "Could not download ${family} Nerd Font. Skipping." rm -rf "$tmp" return 0 fi if ! command -v unzip >/dev/null 2>&1; then install_packages unzip fi mkdir -p "$dest" unzip -qo "$tmp/${family}.zip" -d "$dest" rm -rf "$tmp" fc-cache -f "$dest" >/dev/null 2>&1 || true ok "Nerd Font ${family} installed to $dest" } #------------------------------------------ # System: SELinux, Btrfs, snapper #------------------------------------------ # Permanently disable SELinux. Destructive-ish: warn loudly, and remind the # user a reboot is needed for the change to take effect. disable_selinux() { # FSTAB/SELINUX file overridable for testing on non-Fedora hosts local cfg="${SELINUX_CONFIG:-/etc/selinux/config}" if [[ -f "$cfg" ]] && grep -q '^SELINUX=disabled' "$cfg"; then ok "SELinux already disabled in $cfg." return 0 fi warn "Disabling SELinux permanently. This removes a significant layer of" warn "system protection. Only do this if you specifically need it." if [[ -f "$cfg" ]]; then sudo cp "$cfg" "${cfg}.bkp.$(date +%F)" sudo sed -i 's/^SELINUX=.*/SELINUX=disabled/' "$cfg" else printf 'SELINUX=disabled\nSELINUXTYPE=targeted\n' | sudo tee "$cfg" >/dev/null fi ok "SELINUX=disabled written to $cfg (backup saved). Reboot to apply." } # Show the btrfs layout, then make sure every btrfs fstab entry compresses # with zstd. The sed variant from the SysGuides guide appends ,compress=zstd:1 # after the subvol= option; we guard it so re-runs cannot stack duplicates. ensure_btrfs_compression() { local fstab="${FSTAB_FILE:-/etc/fstab}" info "Current btrfs subvolumes on /:" sudo btrfs subvolume list / 2>/dev/null | sed 's/^/ /' || warn "btrfs subvolume list failed (not btrfs?)" info "fstab btrfs entries:" grep ' btrfs ' "$fstab" | sed 's/^/ /' || true local fixed=0 line # Only touch btrfs lines that have a subvol= option and no compress= yet. while IFS= read -r line; do if grep -q ' btrfs ' <<<"$line" && grep -q 'subvol=' <<<"$line" && ! grep -q 'compress=' <<<"$line"; then fixed=$((fixed+1)) fi done < "$fstab" if (( fixed == 0 )); then ok "All btrfs fstab entries already carry a compress= option (or none exist)." return 0 fi info "Adding compress=zstd:1 to ${fixed} btrfs fstab entrie(s)..." # Backup once per run, then apply the SysGuides sed to every qualifying line. sudo cp "$fstab" "$fstab.bkp.$(date +%F-%H%M)" sudo sed -i '/ btrfs / s/subvol=[^ ,]*/&,compress=zstd:1/' "$fstab" ok "fstab updated. Remount or reboot for compression to take effect:" info " sudo mount -o remount / && sudo mount -o remount /home" } # Full snapshot setup via the SysGuides installer (snapper + grub-btrfs + # DNF5 pre/post snapshot actions). The installer re-validates btrfs itself. setup_snapper() { if pkg_installed snapper && [[ -d /.snapshots ]]; then ok "Snapper already configured (config for / exists)." return 0 fi if ! findmnt -n -o FSTYPE / 2>/dev/null | grep -q btrfs; then warn "Root filesystem is not btrfs; skipping snapshot setup." return 0 fi info "Setting up snapper + grub-btrfs (SysGuides installer)..." local tmp tmp=$(mktemp -d) git clone --depth 1 https://github.com/SysGuides/sysguides-snapper-fedora "$tmp/sysguides" \ || { err "Could not clone the SysGuides installer."; rm -rf "$tmp"; return 1; } ( cd "$tmp/sysguides" && ./install.sh ) local rc=$? rm -rf "$tmp" if (( rc == 0 )); then ok "Snapper + grub-btrfs installed. Every dnf transaction now gets a pre/post snapshot." info "Try it: snapper ls | snapper status 1..2 | sudo snapper undochange 1..2" fi return $rc }