#!/usr/bin/env bash # ═══════════════════════════════════════════════════════════════════ # Unit-3 installer — Hyprland + Quickshell + Waybar rice # Usage: bash <(curl -fsSL https://raw.githubusercontent.com/samyns/Unit-3/main/install.sh) # ═══════════════════════════════════════════════════════════════════ set -euo pipefail # ─── Configuration ────────────────────────────────────────────────── readonly REPO_URL="https://github.com/samyns/Unit-3.git" readonly REPO_BRANCH="${UNIT3_BRANCH:-main}" readonly CLONE_DIR="${TMPDIR:-/tmp}/Unit-3-install-$$" BACKUP_DIR="$HOME/.config-backup-$(date +%Y%m%d-%H%M%S)" readonly BACKUP_DIR readonly CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" # Add support for --pinned flag PINNED_MODE=false for arg in "$@"; do case "$arg" in --pinned) PINNED_MODE=true ;; --latest) PINNED_MODE=false ;; --help|-h) cat <&2; } fatal() { err "$*"; exit 1; } ask_yn() { local prompt="$1" default="${2:-n}" reply hint="[y/N]" [[ "$default" == "y" ]] && hint="[Y/n]" while true; do read -rp "$(printf '%s[?]%s %s %s ' "$C_YELLOW" "$C_RESET" "$prompt" "$hint")" reply reply="${reply:-$default}" case "${reply,,}" in y|yes) return 0 ;; n|no) return 1 ;; *) warn "Please answer y or n." ;; esac done } cleanup() { [[ -d "$CLONE_DIR" ]] && rm -rf "$CLONE_DIR"; } trap cleanup EXIT # ─── Pre-flight ───────────────────────────────────────────────────── preflight() { log "Running pre-flight checks…" [[ $EUID -ne 0 ]] || fatal "Do not run this script as root. Run as your normal user; sudo will be invoked when needed." command -v pacman >/dev/null || fatal "pacman not found — this script is for Arch Linux only." command -v sudo >/dev/null || fatal "sudo is required." command -v git >/dev/null || sudo pacman -S --needed --noconfirm git log "Asking for sudo password upfront…" sudo -v || fatal "sudo authentication failed." # Keep sudo alive in background while true; do sudo -n true; sleep 60; kill -0 $$ 2>/dev/null || exit; done 2>/dev/null & log "Checking internet connectivity…" ping -c 1 -W 3 archlinux.org >/dev/null 2>&1 || fatal "No internet connection." if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then warn "You are running this from inside Hyprland. You will need to log out / restart your session at the end." fi ok "All checks passed." } # ─── User prompts ─────────────────────────────────────────────────── collect_choices() { echo log "I'll ask a few questions before starting." echo BACKUP_OLD=true; ask_yn "Backup existing configs to $BACKUP_DIR?" y || BACKUP_OLD=false INSTALL_AUR=true; ask_yn "Install AUR packages (quickshell-git, awww)? Highly recommended." y || INSTALL_AUR=false INSTALL_WALLPAPERS=true; ask_yn "Install default wallpapers to ~/Pictures/wallpapers?" y || INSTALL_WALLPAPERS=false INSTALL_BASHRC=true; ask_yn "Install Unit-3 .bashrc (welcome banner + NieR prompt)?" y || INSTALL_BASHRC=false ENABLE_SERVICES=true; ask_yn "Enable system services (NetworkManager, pipewire)?" y || ENABLE_SERVICES=false echo } # ─── Base setup ───────────────────────────────────────────────────── install_base() { log "Installing base-devel + git…" sudo pacman -S --needed --noconfirm base-devel git } bootstrap_aur_helper() { if command -v yay >/dev/null; then ok "yay is already installed."; return; fi if command -v paru >/dev/null; then ok "paru is already installed."; return; fi log "Bootstrapping yay (AUR helper)…" local d; d=$(mktemp -d) git clone --depth=1 https://aur.archlinux.org/yay-bin.git "$d/yay-bin" (cd "$d/yay-bin" && makepkg -si --noconfirm) rm -rf "$d" ok "yay installed." } # ─── Clone ────────────────────────────────────────────────────────── clone_repo() { log "Cloning Unit-3 ($REPO_BRANCH)…" git clone --depth=1 --branch "$REPO_BRANCH" "$REPO_URL" "$CLONE_DIR" } # ─── Packages ─────────────────────────────────────────────────────── read_pkg_list() { local file="$1" [[ -f "$file" ]] || return 0 grep -vE '^\s*(#|$)' "$file" } install_packages() { local pacman_list="$CLONE_DIR/packages/pacman.txt" local aur_list="$CLONE_DIR/packages/aur.txt" if $PINNED_MODE; then local pinned_pacman="$CLONE_DIR/packages/pinned-pacman.txt" local pinned_aur="$CLONE_DIR/packages/pinned-aur.txt" log "Pinned mode: installing exact tested versions from Arch Archive." if [[ -f "$pinned_pacman" ]]; then log "Installing pinned pacman packages…" install_pinned_from_archive "$pinned_pacman" fi if $INSTALL_AUR && [[ -f "$pinned_aur" ]]; then warn "AUR packages cannot be reliably pinned — falling back to latest." mapfile -t aur_pkgs < <(grep -vE '^\s*(#|$)' "$aur_list") local helper; helper=$(command -v yay || command -v paru) "$helper" -S --needed --noconfirm "${aur_pkgs[@]}" fi else # Latest mode (default) local pacman_pkgs aur_pkgs mapfile -t pacman_pkgs < <(grep -vE '^\s*(#|$)' "$pacman_list") if (( ${#pacman_pkgs[@]} > 0 )); then log "Installing ${#pacman_pkgs[@]} pacman packages (latest)…" sudo pacman -S --needed --noconfirm "${pacman_pkgs[@]}" fi if $INSTALL_AUR && [[ -f "$aur_list" ]]; then mapfile -t aur_pkgs < <(grep -vE '^\s*(#|$)' "$aur_list") if (( ${#aur_pkgs[@]} > 0 )); then log "Installing ${#aur_pkgs[@]} AUR packages (latest)…" local helper; helper=$(command -v yay || command -v paru) "$helper" -S --needed --noconfirm "${aur_pkgs[@]}" fi fi fi } install_pinned_from_archive() { local pinned_file="$1" local archive_base="https://archive.archlinux.org/packages" local urls=() while IFS='=' read -r pkg version; do [[ "$pkg" =~ ^[[:space:]]*# ]] && continue [[ -z "$pkg" || -z "$version" ]] && continue # Archive structure: /packages///--x86_64.pkg.tar.zst local first="${pkg:0:1}" local url="$archive_base/$first/$pkg/$pkg-$version-x86_64.pkg.tar.zst" urls+=("$url") done < "$pinned_file" if (( ${#urls[@]} > 0 )); then sudo pacman -U --noconfirm "${urls[@]}" fi } # ─── Deploy configs ───────────────────────────────────────────────── deploy_configs() { mkdir -p "$CONFIG_HOME" for name in "${MANAGED_DIRS[@]}"; do local src="$CLONE_DIR/config/$name" local dest="$CONFIG_HOME/$name" [[ -d "$src" ]] || { warn "Skipping $name (not in repo)."; continue; } if [[ -e "$dest" ]]; then if $BACKUP_OLD; then mkdir -p "$BACKUP_DIR" log "Backing up $dest → $BACKUP_DIR/$name" mv "$dest" "$BACKUP_DIR/$name" else warn "Removing existing $dest (no backup)." rm -rf "$dest" fi fi log "Installing config: $name" cp -r "$src" "$dest" done # Make all .sh / .py executable log "Setting executable bits on scripts…" find "$CONFIG_HOME/hypr" "$CONFIG_HOME/quickshell" "$CONFIG_HOME/waybar" \ -type f \( -name '*.sh' -o -name '*.py' \) -exec chmod +x {} + 2>/dev/null || true # Create the user override file (NEVER overwritten on update) local user_conf="$CONFIG_HOME/hypr/user.conf" if [[ ! -f "$user_conf" ]]; then cat > "$user_conf" <<'EOF' # ═══════════════════════════════════════════════════════════════════ # Personal Hyprland overrides # This file is created empty by the installer and is NEVER overwritten # by future updates. Put your monitor configs, custom binds, env vars, # theme tweaks, etc. here. # # Examples: # monitor = DP-1, 2560x1440@144, 0x0, 1 # bind = SUPER, B, exec, firefox # env = GTK_THEME, Adwaita-dark # ═══════════════════════════════════════════════════════════════════ EOF ok "Created empty user.conf for your personal overrides." fi } # ─── System files (PAM, etc.) ─────────────────────────────────────── deploy_system_files() { local pam_src="$CLONE_DIR/config/system/pam.d" if [[ ! -d "$pam_src" ]]; then warn "No system files to install." return fi log "Installing system PAM configurations…" for f in "$pam_src"/*; do [[ -f "$f" ]] || continue local name; name=$(basename "$f") sudo install -D -m 644 "$f" "/etc/pam.d/$name" ok "Installed PAM config: /etc/pam.d/$name" done } # ─── Shell config (.bashrc with welcome banner) ──────────────────── deploy_shell_config() { $INSTALL_BASHRC || { warn "Skipping .bashrc installation."; return; } local bashrc_src="$CLONE_DIR/config/bash/.bashrc" local bashrc_dest="$HOME/.bashrc" [[ -f "$bashrc_src" ]] || { warn "No bundled .bashrc found in repo."; return; } # Backup existing .bashrc if it's not already a Unit-3 one if [[ -f "$bashrc_dest" ]] && ! grep -q "Unit-3 default .bashrc" "$bashrc_dest"; then if $BACKUP_OLD; then mkdir -p "$BACKUP_DIR" cp "$bashrc_dest" "$BACKUP_DIR/.bashrc" log "Backed up existing ~/.bashrc" fi fi log "Installing Unit-3 .bashrc (welcome banner enabled)…" cp "$bashrc_src" "$bashrc_dest" # Create empty user override if missing if [[ ! -f "$HOME/.bashrc.local" ]]; then cat > "$HOME/.bashrc.local" <<'OVR' # Unit-3 user overrides — never touched by updates. # Put your personal aliases, functions, exports here. # # Examples: # alias ll='ls -la' # export EDITOR=nano # export PATH="$PATH:$HOME/.local/bin" OVR ok "Created empty ~/.bashrc.local for your personal overrides." fi ok "Bashrc installed." } # ─── Wallpapers & Pictures dir ───────────────────────────────────── setup_user_dirs() { mkdir -p "$HOME/Pictures/wallpapers" "$HOME/Screenshots" if $INSTALL_WALLPAPERS && [[ -d "$CLONE_DIR/assets/wallpapers" ]]; then log "Installing default wallpapers…" cp -n "$CLONE_DIR/assets/wallpapers/"* "$HOME/Pictures/wallpapers/" 2>/dev/null || true fi } # ─── Services ────────────────────────────────────────────────────── enable_services() { $ENABLE_SERVICES || { warn "Skipping service activation."; return; } log "Enabling system services…" sudo systemctl enable --now NetworkManager.service 2>/dev/null || warn "NetworkManager: skipped." log "Enabling user audio services…" systemctl --user enable --now pipewire pipewire-pulse wireplumber 2>/dev/null || warn "pipewire: skipped (this is fine if you'll start it from the session)." } # ─── Final message ───────────────────────────────────────────────── finalize() { echo ok "${C_BOLD}Installation complete!${C_RESET}" echo echo " ${C_BOLD}Next steps:${C_RESET}" echo " 1. Reboot or log out, then log back into Hyprland." echo " 2. Customise via ~/.config/hypr/user.conf — never edit hyprland.conf directly." echo " 3. Bashrc personal overrides go in ~/.bashrc.local" echo " 4. Wallpapers go in ~/Pictures/wallpapers/ (use SUPER+P to pick one)." if [[ -d "$BACKUP_DIR" ]]; then echo echo " ${C_BOLD}Backup of your old configs:${C_RESET}" echo " $BACKUP_DIR" fi echo echo " ${C_BOLD}Docs & support:${C_RESET}" echo " https://github.com/samyns/Unit-3#readme" echo } main() { preflight collect_choices install_base $INSTALL_AUR && bootstrap_aur_helper clone_repo install_packages deploy_configs deploy_system_files deploy_shell_config setup_user_dirs enable_services finalize } main "$@"