atlas-desktop/scripts/install.sh

873 lines
33 KiB
Bash

#!/usr/bin/env bash
# =============================================================================
# Atlas Desktop Installer
# for Arch Linux
# =============================================================================
# By Gu://em_
# Co-Authored with Claude Sonnet 4.6
set -euo pipefail
# ── Constants ─────────────────────────────────────────────────────────────────
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CONFIG_DIR="$REPO_DIR/config"
PACKAGES_DIR="$REPO_DIR/packages"
SCRIPTS_DIR="$REPO_DIR/scripts"
DOTFILES_DIR="$HOME/.atlas-dotfiles"
STATE_DIR="$DOTFILES_DIR/.state"
PKG_STATE_DIR="$STATE_DIR/packages"
WAYBAR_CFG_DIR="$HOME/.config/waybar"
# ── Colours & styles ──────────────────────────────────────────────────────────
ESC=$'\e'
BOLD="${ESC}[1m" DIM="${ESC}[2m" RESET="${ESC}[0m"
BLACK="${ESC}[30m" RED="${ESC}[31m" GREEN="${ESC}[32m"
YELLOW="${ESC}[33m" BLUE="${ESC}[34m" MAGENTA="${ESC}[35m"
CYAN="${ESC}[36m" WHITE="${ESC}[37m"
BG_BLACK="${ESC}[40m" BG_BLUE="${ESC}[44m"
HIDE_CURSOR="${ESC}[?25l" SHOW_CURSOR="${ESC}[?25h"
CLEAR_LINE="${ESC}[2K\r"
# Ensure cursor is restored on exit
trap 'printf "%s" "$SHOW_CURSOR"; tput cnorm 2>/dev/null || true' EXIT
trap 'printf "%s" "$SHOW_CURSOR"; tput cnorm 2>/dev/null || true; exit 1' INT TERM
# ── Logging helpers ───────────────────────────────────────────────────────────
PHASE_NUM=0
ok() { printf " ${GREEN}${BOLD}${RESET} %s\n" "$*"; }
info() { printf " ${CYAN}·${RESET} %s\n" "$*"; }
warn() { printf " ${YELLOW}${RESET} %s\n" "$*"; }
err() { printf " ${RED}${BOLD}${RESET} %s\n" "$*" >&2; }
die() { err "$*"; exit 1; }
blank() { echo; }
indent() { sed 's/^/ /'; }
phase() {
(( PHASE_NUM++ )) || true
blank
printf "${BOLD}${BLUE}┌─ Phase %d ─ %s${RESET}\n" "$PHASE_NUM" "$*"
}
step() {
printf "${BLUE}${RESET} ${BOLD}%s${RESET}\n" "$*"
}
phase_done() {
printf "${BLUE}└─${GREEN} done${RESET}\n"
}
# ── Spinner ───────────────────────────────────────────────────────────────────
_SPINNER_PID=""
_SPINNER_FRAMES=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
spinner_start() {
local msg="$1"
printf "%s" "$HIDE_CURSOR"
(
local i=0
while true; do
printf "${CLEAR_LINE} ${CYAN}${_SPINNER_FRAMES[$((i % ${#_SPINNER_FRAMES[@]}))]}${RESET} %s" "$msg"
sleep 0.08
(( i++ )) || true
done
) &
_SPINNER_PID=$!
}
spinner_stop() {
local status="${1:-ok}" # ok | fail
if [[ -n $_SPINNER_PID ]]; then
kill "$_SPINNER_PID" 2>/dev/null; wait "$_SPINNER_PID" 2>/dev/null || true
_SPINNER_PID=""
fi
printf "%s" "$SHOW_CURSOR"
printf "%s" "$CLEAR_LINE"
if [[ $status == ok ]]; then
ok "$2"
else
err "$2"
fi
}
run_quiet() {
# run_quiet "label" cmd [args...]
local label="$1"; shift
spinner_start "$label"
local tmp; tmp=$(mktemp)
if "$@" >"$tmp" 2>&1; then
spinner_stop ok "$label"
rm -f "$tmp"
else
local rc=$?
spinner_stop fail "$label"
warn "Output:"
cat "$tmp" | indent
rm -f "$tmp"
return $rc
fi
}
# ── UI primitives ─────────────────────────────────────────────────────────────
header() {
clear
printf "${BOLD}${BLUE}"
cat << 'EOF'
╔══════════════════════════════════════════════════════════════════╗
║ _ _ _____ _ _ ║
║ /\ | | | | | __ \ | | | | ║
║ / \ | |_| | __ _ ___ | | | | ___ ___| | _| |_ ___ _ __ ║
║ / /\ \| __| |/ _` / __| | | | |/ _ \/ __| |/ / __/ _ \| '_ \ ║
║ / ____ \ |_| | (_| \__ \ | |__| | __/\__ \ <| || (_) | |_) | ║
║ /_/ \_\__|_|\__,_|___/ |_____/ \___||___/_|\_\\__\___/| .__/ ║
║ | | ║
║ |_| ║
╠══════════════════════════════════════════════════════════════════╣
║ Installer · Arch Linux ║
╚══════════════════════════════════════════════════════════════════╝
EOF
printf "${RESET}\n"
}
divider() {
printf " ${DIM}%s${RESET}\n" "──────────────────────────────────────────────────────"
}
confirm() {
local msg="$1" default="${2:-y}"
local prompt; [[ $default == y ]] && prompt="${GREEN}Y${RESET}/n" || prompt="y/${RED}N${RESET}"
local ans
read -rp " ${YELLOW}?${RESET} $msg [${prompt}] " ans
ans="${ans:-$default}"
[[ ${ans,,} == y ]]
}
press_enter() {
read -rp " ${DIM}Press Enter to continue…${RESET}" _
blank
}
# ── State helpers ─────────────────────────────────────────────────────────────
state_get() { grep -m1 "^${1}=" "$STATE_DIR/install.state" 2>/dev/null | cut -d= -f2- || true; }
state_set() {
local key="$1" val="$2"
local f="$STATE_DIR/install.state"
if grep -q "^${key}=" "$f" 2>/dev/null; then
sed -i "s|^${key}=.*|${key}=${val}|" "$f"
else
echo "${key}=${val}" >> "$f"
fi
}
# ── Package helpers ───────────────────────────────────────────────────────────
read_pkg_file() {
local file="$1"
[[ -f $file ]] || return
grep -v '^\s*#' "$file" | grep -v '^\s*$' | sort -u
}
save_pkg_list() {
local group="$1"; shift
mkdir -p "$PKG_STATE_DIR"
printf '%s\n' "$@" > "$PKG_STATE_DIR/${group}"
}
pacman_install() {
sudo pacman -S --needed --noconfirm "$@"
}
paru_install() {
paru -S --needed --noconfirm "$@"
}
pkg_installed() {
pacman -Q "$1" &>/dev/null
}
# ── Hardware detection ────────────────────────────────────────────────────────
detect_hardware() {
HW_GPU="unknown"
HW_CPU="unknown"
if grep -qi intel /proc/cpuinfo 2>/dev/null; then HW_CPU="intel"; fi
if grep -qi amd /proc/cpuinfo 2>/dev/null; then HW_CPU="amd"; fi
if lspci 2>/dev/null | grep -qi "nvidia"; then HW_GPU="nvidia"
elif lspci 2>/dev/null | grep -qi "amd.*radeon\|advanced micro"; then HW_GPU="amd"
elif lspci 2>/dev/null | grep -qi "intel.*graphics"; then HW_GPU="intel"
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 0 — Preflight
# ─────────────────────────────────────────────────────────────────────────────
preflight() {
phase "Preflight checks"
# Arch Linux check
step "Checking system"
if [[ ! -f /etc/arch-release ]]; then
err "This installer only supports Arch Linux."
exit 1
fi
ok "Arch Linux detected"
# Not root
if [[ $EUID -eq 0 ]]; then
die "Do not run this script as root. It will use sudo when needed."
fi
ok "Running as user: ${BOLD}${USER}${RESET}"
# sudo available
if ! sudo -v 2>/dev/null; then
die "sudo is required. Make sure $USER is in the sudoers group."
fi
ok "sudo access confirmed"
# git
command -v git &>/dev/null || die "git is not installed."
ok "git found"
# Config dir
if [[ ! -d $CONFIG_DIR ]]; then
die "config/ directory not found in $REPO_DIR"
fi
ok "Repo looks good: ${DIM}$REPO_DIR${RESET}"
detect_hardware
info "CPU: ${BOLD}${HW_CPU^^}${RESET} GPU: ${BOLD}${HW_GPU^^}${RESET}"
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 1 — Welcome & choices
# ─────────────────────────────────────────────────────────────────────────────
welcome() {
header
cat << EOF
Welcome! This script will set up the Atlas Desktop on your machine.
It will:
${CYAN}·${RESET} Install required packages (pacman + paru for AUR)
${CYAN}·${RESET} Link your dotfiles into ${BOLD}~/${RESET} using GNU Stow
${CYAN}·${RESET} Enable necessary system services
${CYAN}·${RESET} Configure the updater so you stay in sync
Nothing will be modified until you confirm below.
EOF
divider
blank
confirm "Ready to begin?" y || { echo; info "Goodbye!"; exit 0; }
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 2 — Waybar layout picker
# ─────────────────────────────────────────────────────────────────────────────
choose_waybar() {
phase "Waybar layout"
blank
# Vertical preview
printf " ${BOLD}${CYAN}1) Vertical${RESET} ${DIM}(sidebar, left or right)${RESET}\n\n"
printf "${DIM}"
cat << 'EOF'
┌────┐
│ 1 │ ← workspace indicators
│ 2 │
│ │
│ │
│ 󰕾 │ ← volume
│ 󰖩 │ ← network
│ 12 │
│ 34 │ ← clock
│  │ ← power
└────┘
EOF
printf "${RESET}\n"
# Horizontal preview
printf " ${BOLD}${CYAN}2) Horizontal${RESET} ${DIM}(bottom bar)${RESET}\n\n"
printf "${DIM}"
cat << 'EOF'
┌─────────────────────────────────────────────┐
│ ● ● ○ ○ 12:34 󰕾 󰖩  │
└─────────────────────────────────────────────┘
EOF
printf "${RESET}\n"
divider
local choice
while true; do
read -rp " ${YELLOW}?${RESET} Choose a layout ${BOLD}[1/2]${RESET}: " choice
case "$choice" in
1) WAYBAR_LAYOUT="vertical"; ok "Vertical layout selected"; break ;;
2) WAYBAR_LAYOUT="horizontal"; ok "Horizontal layout selected"; break ;;
*) warn "Please enter 1 or 2." ;;
esac
done
state_set "waybar_layout" "$WAYBAR_LAYOUT"
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 3 — Security profile
# ─────────────────────────────────────────────────────────────────────────────
choose_security() {
phase "Security & stability"
blank
cat << EOF
The following extras are ${BOLD}recommended for most users${RESET} but are
optional for those who manage their own setup:
${GREEN}·${RESET} ${BOLD}UFW${RESET} — simple firewall (blocks unsolicited inbound traffic)
${GREEN}·${RESET} ${BOLD}AppArmor${RESET} — mandatory access control (limits process capabilities)
${GREEN}·${RESET} ${BOLD}Zram${RESET} — compressed swap-in-RAM (better performance, less disk wear)
${DIM}Recommended unless you already manage these yourself.${RESET}
EOF
if confirm "Install security & stability extras?" y; then
INSTALL_SECURITY=true
ok "Security extras will be installed"
else
INSTALL_SECURITY=false
info "Skipping — you can install them later"
fi
state_set "install_security" "$INSTALL_SECURITY"
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 4 — Optional package groups
# ─────────────────────────────────────────────────────────────────────────────
choose_optional_groups() {
phase "Optional packages"
blank
local optional_file="$PACKAGES_DIR/optional.groups"
if [[ ! -f $optional_file ]]; then
info "No optional.groups file found — skipping"
CHOSEN_OPTIONAL_GROUPS=()
phase_done
return
fi
mapfile -t ALL_OPTIONAL < <(grep -v '^\s*#' "$optional_file" | grep -v '^\s*$')
if (( ${#ALL_OPTIONAL[@]} == 0 )); then
info "No optional groups defined — skipping"
CHOSEN_OPTIONAL_GROUPS=()
phase_done
return
fi
info "Select which optional groups to install:"
blank
CHOSEN_OPTIONAL_GROUPS=()
for group in "${ALL_OPTIONAL[@]}"; do
# Show a short description if <group>.desc file exists
local desc_file="$PACKAGES_DIR/${group}.desc"
local desc=""
[[ -f $desc_file ]] && desc=" ${DIM}$(cat "$desc_file")${RESET}"
if confirm " Install ${BOLD}${group}${RESET}${desc}?" n; then
CHOSEN_OPTIONAL_GROUPS+=("$group")
fi
done
if (( ${#CHOSEN_OPTIONAL_GROUPS[@]} )); then
ok "Selected: ${CHOSEN_OPTIONAL_GROUPS[*]}"
else
info "No optional groups selected"
fi
# Save choices
local joined; joined=$(IFS=','; echo "${CHOSEN_OPTIONAL_GROUPS[*]}")
state_set "optional_groups" "$joined"
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 5 — Summary & confirm
# ─────────────────────────────────────────────────────────────────────────────
confirm_summary() {
phase "Summary"
blank
printf " Here's what will happen:\n\n"
printf " ${BOLD}Dotfiles${RESET}\n"
printf " ${DIM} %s → %s${RESET}\n" "$CONFIG_DIR" "$HOME"
blank
printf " ${BOLD}Waybar layout:${RESET} %s\n" "${WAYBAR_LAYOUT}"
blank
printf " ${BOLD}Hardware packages:${RESET}\n"
printf " CPU: %s · GPU: %s\n" "${HW_CPU}" "${HW_GPU}"
blank
if [[ $INSTALL_SECURITY == true ]]; then
printf " ${GREEN}${RESET} Security extras: UFW, AppArmor, Zram\n"
else
printf " ${DIM}· Security extras: skipped${RESET}\n"
fi
blank
if (( ${#CHOSEN_OPTIONAL_GROUPS[@]} )); then
printf " ${BOLD}Optional groups:${RESET} %s\n" "${CHOSEN_OPTIONAL_GROUPS[*]}"
else
printf " ${DIM}· No optional groups${RESET}\n"
fi
blank
printf " ${BOLD}Services to enable:${RESET}\n"
printf " ly.service"
[[ $INSTALL_SECURITY == true ]] && printf " · apparmor.service · ufw · systemd-zram-setup@zram0"
printf "\n"
blank
divider
blank
confirm "Proceed with installation?" y || { info "Aborted — nothing was changed."; exit 0; }
blank
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 6 — System update
# ─────────────────────────────────────────────────────────────────────────────
system_update() {
phase "System update"
step "Syncing pacman databases and updating system"
run_quiet "Updating system" sudo pacman -Syu --noconfirm
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 7 — Bootstrap paru
# ─────────────────────────────────────────────────────────────────────────────
bootstrap_paru() {
phase "AUR helper (paru)"
if pkg_installed paru; then
ok "paru is already installed"
phase_done
return
fi
step "Installing paru from AUR"
info "Requires: base-devel, git, cargo (rust)"
run_quiet "Installing base-devel" sudo pacman -S --needed --noconfirm base-devel git
# Install rust if needed (paru needs cargo)
if ! command -v cargo &>/dev/null; then
run_quiet "Installing rust" sudo pacman -S --needed --noconfirm rust
fi
local tmp_dir; tmp_dir=$(mktemp -d)
spinner_start "Cloning paru…"
git clone --depth=1 https://aur.archlinux.org/paru.git "$tmp_dir/paru" &>/dev/null
spinner_stop ok "Cloned paru"
step "Building paru (this may take a moment)"
(
cd "$tmp_dir/paru"
run_quiet "Building paru" makepkg -si --noconfirm
)
rm -rf "$tmp_dir"
if pkg_installed paru; then
ok "paru installed successfully"
else
die "paru installation failed"
fi
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 8 — Install packages
# ─────────────────────────────────────────────────────────────────────────────
install_packages() {
phase "Installing packages"
local pkg_file pkgs group
# Helper: install a group and save state
install_group() {
local group="$1"
local pkg_file="$PACKAGES_DIR/${group}.pkgs"
[[ -f $pkg_file ]] || { info "No file for group '${group}' — skipping"; return; }
mapfile -t pkgs < <(read_pkg_file "$pkg_file")
(( ${#pkgs[@]} == 0 )) && { info "${group}: empty — skipping"; return; }
step "Group: ${group} (${#pkgs[@]} packages)"
if [[ $group == aur ]]; then
run_quiet " Installing ${group}" paru_install "${pkgs[@]}"
else
run_quiet " Installing ${group}" pacman_install "${pkgs[@]}"
fi
save_pkg_list "$group" "${pkgs[@]}"
}
# stow itself (needed for the next step)
run_quiet "Ensuring stow is present" sudo pacman -S --needed --noconfirm stow
# Core
install_group "core"
# Hardware
install_group "${HW_CPU}-cpu" 2>/dev/null || true
install_group "${HW_GPU}-gpu" 2>/dev/null || true
# Security extras
if [[ $INSTALL_SECURITY == true ]]; then
local sec_pkgs=(ufw apparmor zram-generator)
step "Security extras"
run_quiet " Installing security packages" pacman_install "${sec_pkgs[@]}"
save_pkg_list "security" "${sec_pkgs[@]}"
fi
# Optional chosen groups
for group in "${CHOSEN_OPTIONAL_GROUPS[@]}"; do
install_group "$group"
done
# AUR group (always, if paru is present)
install_group "aur"
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 9 — Clone into ~/.atlas-dotfiles and stow
# ─────────────────────────────────────────────────────────────────────────────
stow_dotfiles() {
phase "Copying and stowing dotfiles"
# ~/.atlas-dotfiles is the permanent home of the repo. They can delete the original
# clone after installation without breaking anything.
local remote_url
remote_url=$(git -C "$REPO_DIR" remote get-url origin 2>/dev/null || true)
if [[ -z $remote_url ]]; then
die "Could not determine remote URL from $REPO_DIR (is origin set?)"
fi
step "Setting up ~/.atlas-dotfiles as the permanent repo"
info "Remote: $remote_url"
if [[ -d $DOTFILES_DIR/.git ]]; then
# Already a git repo — verify it points to the same remote
local existing_remote
existing_remote=$(git -C "$DOTFILES_DIR" remote get-url origin 2>/dev/null || true)
if [[ $existing_remote == $remote_url ]]; then
ok "~/.atlas-dotfiles already cloned from correct remote"
run_quiet "Pulling latest" git -C "$DOTFILES_DIR" pull --ff-only origin main
else
warn "~/.atlas-dotfiles exists but points to a different remote:"
warn " existing: $existing_remote"
warn " expected: $remote_url"
if confirm "Replace it with the correct repo?" y; then
rm -rf "$DOTFILES_DIR"
run_quiet "Cloning dotfiles" git clone "$remote_url" "$DOTFILES_DIR"
else
die "Aborted — ~/.atlas-dotfiles remote mismatch"
fi
fi
elif [[ -e $DOTFILES_DIR ]]; then
warn "~/.atlas-dotfiles exists but is not a git repo — moving to ~/.atlas-dotfiles.bak"
mv "$DOTFILES_DIR" "${DOTFILES_DIR}.bak"
run_quiet "Cloning dotfiles" git clone "$remote_url" "$DOTFILES_DIR"
else
run_quiet "Cloning dotfiles" git clone "$remote_url" "$DOTFILES_DIR"
fi
#TODO Checkout ?
ok "~/.atlas-dotfiles → $remote_url"
# Backup any real files that would conflict with stow links
local stow_config_dir="$DOTFILES_DIR/config"
local conflicts=()
while IFS= read -r link; do
local rel="${link#$stow_config_dir/}"
local target="$HOME/$rel"
if [[ -e $target && ! -L $target ]]; then
conflicts+=("$target")
fi
done < <(find "$stow_config_dir" -type f 2>/dev/null)
if (( ${#conflicts[@]} )); then
warn "${#conflicts[@]} existing file(s) will be backed up (.bak):"
for f in "${conflicts[@]}"; do
info " $f${f}.bak"
mv "$f" "${f}.bak"
done
fi
# Stow from ~/.atlas-dotfiles treating config/ as the stow package
run_quiet "Stowing config" \
stow --dir="$DOTFILES_DIR" --target="$HOME" --restow config
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 10 — Waybar config linking
# ─────────────────────────────────────────────────────────────────────────────
setup_waybar() {
phase "Waybar configuration"
local layout_dir="$WAYBAR_CFG_DIR/${WAYBAR_LAYOUT}"
if [[ ! -d $layout_dir ]]; then
warn "Waybar layout directory not found: $layout_dir"
warn "Make sure config/.config/waybar/${WAYBAR_LAYOUT}/ exists in the repo"
phase_done
return
fi
step "Linking ${WAYBAR_LAYOUT} layout as active config"
# Remove existing active config/style links (not the layout dirs themselves)
rm -f "$WAYBAR_CFG_DIR/config" "$WAYBAR_CFG_DIR/style.css" \
"$WAYBAR_CFG_DIR/config.jsonc"
# Link config file (support config, config.json, config.jsonc)
local cfg_file=""
for name in config config.json config.jsonc; do
[[ -f "$layout_dir/$name" ]] && { cfg_file="$name"; break; }
done
if [[ -n $cfg_file ]]; then
ln -sf "$layout_dir/$cfg_file" "$WAYBAR_CFG_DIR/config"
ok "config → ${WAYBAR_LAYOUT}/${cfg_file}"
else
warn "No config file found in $layout_dir"
fi
# Link style.css
if [[ -f "$layout_dir/style.css" ]]; then
ln -sf "$layout_dir/style.css" "$WAYBAR_CFG_DIR/style.css"
ok "style.css → ${WAYBAR_LAYOUT}/style.css"
else
warn "No style.css found in $layout_dir"
fi
state_set "waybar_layout" "$WAYBAR_LAYOUT"
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 11 — Enable services
# ─────────────────────────────────────────────────────────────────────────────
enable_services() {
phase "Enabling services"
enable_service() {
local svc="$1"
if systemctl is-enabled "$svc" &>/dev/null; then
ok "$svc (already enabled)"
else
if sudo systemctl enable "$svc" &>/dev/null; then
ok "$svc"
else
warn "Failed to enable $svc (service may not be installed)"
fi
fi
}
# Display manager
step "Display manager"
# Disable any currently-enabled DM first to avoid conflicts
for dm in gdm sddm lightdm lxdm greetd; do
systemctl is-enabled "${dm}.service" &>/dev/null && \
sudo systemctl disable "${dm}.service" &>/dev/null || true
done
enable_service "ly.service"
# Security services (if chosen)
if [[ $INSTALL_SECURITY == true ]]; then
blank
step "Security & stability"
enable_service "apparmor.service"
enable_service "ufw.service"
# UFW default policy
spinner_start "Configuring UFW defaults"
sudo ufw default deny incoming &>/dev/null
sudo ufw default allow outgoing &>/dev/null
sudo ufw --force enable &>/dev/null
spinner_stop ok "UFW configured (deny incoming, allow outgoing)"
# Zram — write generator config if not present
local zram_conf="/etc/systemd/zram-generator.conf"
if [[ ! -f $zram_conf ]]; then
spinner_start "Writing zram config"
printf '[zram0]\nzram-size = ram / 2\ncompression-algorithm = zstd\n' \
| sudo tee "$zram_conf" >/dev/null
spinner_stop ok "Zram configured (50% RAM, zstd)"
else
ok "Zram config already exists"
fi
enable_service "systemd-zram-setup@zram0.service"
fi
# Pipewire (audio)
blank
step "Audio (Pipewire)"
systemctl --user enable --now pipewire.service &>/dev/null && ok "pipewire.service" || warn "pipewire.service not found"
systemctl --user enable --now pipewire-pulse.service &>/dev/null && ok "pipewire-pulse.service" || warn "pipewire-pulse not found"
systemctl --user enable --now wireplumber.service &>/dev/null && ok "wireplumber.service" || warn "wireplumber not found"
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 12 — Install updater
# ─────────────────────────────────────────────────────────────────────────────
install_updater() {
phase "Dotfiles updater"
local updater_in_dotfiles="$DOTFILES_DIR/scripts/update.sh"
local updater_link="$HOME/.local/bin/dotfiles-update"
if [[ ! -f $updater_in_dotfiles ]]; then
warn "update.sh not found in ~/.atlas-dotfiles/scripts/ — skipping"
info "Expected: $updater_in_dotfiles"
phase_done
return
fi
chmod +x "$updater_in_dotfiles"
mkdir -p "$HOME/.local/bin"
# Symlink into PATH — stow may also handle this if scripts/ is a stow pkg,
# but we do it explicitly here so it works regardless of stow layout.
ln -sf "$updater_in_dotfiles" "$updater_link"
ok "dotfiles-update → $updater_in_dotfiles"
info "Run ${BOLD}dotfiles-update${RESET} any time to sync with upstream"
# Initialise state inside the permanent repo
local state_dir="$DOTFILES_DIR/.state"
mkdir -p "$state_dir"
# Point state file at the permanent repo, not the installer's location
grep -q "^repo_dir=" "$state_dir/update.state" 2>/dev/null || echo "repo_dir=$DOTFILES_DIR" >> "$state_dir/update.state"
# Stamp the current HEAD so the first `dotfiles-update` knows the baseline
local head; head=$(git -C "$DOTFILES_DIR" rev-parse HEAD)
if grep -q "^last_commit=" "$state_dir/update.state" 2>/dev/null; then
sed -i "s|^last_commit=.*|last_commit=${head}|" "$state_dir/update.state"
else
echo "last_commit=$head" >> "$state_dir/update.state"
fi
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 13 — Final screen
# ─────────────────────────────────────────────────────────────────────────────
finish() {
blank
printf "${BOLD}${GREEN}"
cat << 'EOF'
╔══════════════════════════════════════════════════════════╗
║ ║
║ ✔ Installation complete! ║
║ ║
╚══════════════════════════════════════════════════════════╝
EOF
printf "${RESET}\n"
cat << EOF
${BOLD}What to do next:${RESET}
${CYAN}·${RESET} ${BOLD}Reboot${RESET} to start Ly and load your Hyprland session
${DIM}sudo reboot${RESET}
${CYAN}·${RESET} ${BOLD}Update dotfiles${RESET} any time by running:
${DIM}atlas-update${RESET}
${CYAN}·${RESET} ${BOLD}Switch Waybar layout${RESET} (vertical ↔ horizontal):
${DIM}atlas-waybar [vertical|horizontal]${RESET}
${CYAN}·${RESET} Your chosen settings are saved in:
${DIM}$STATE_DIR/install.state${RESET}
EOF
if [[ $INSTALL_SECURITY == true ]]; then
printf " ${GREEN}·${RESET} ${BOLD}UFW${RESET} is active. To allow a port: ${DIM}sudo ufw allow <port>${RESET}\n"
blank
fi
divider
printf " ${DIM}Atlas Desktop — %s${RESET}\n\n" "$(git -C "$REPO_DIR" rev-parse --short HEAD)"
}
# ─────────────────────────────────────────────────────────────────────────────
# Waybar switcher (can be called directly: atlas-waybar vertical)
# ─────────────────────────────────────────────────────────────────────────────
waybar_switch() {
local layout="${1:-}"
if [[ -z $layout ]]; then
local current; current=$(state_get "waybar_layout" || echo "unknown")
echo "Current layout: $current"
echo "Usage: atlas-waybar [vertical|horizontal]"
return
fi
case "$layout" in
vertical|horizontal) ;;
*) die "Unknown layout '$layout'. Choose: vertical, horizontal" ;;
esac
WAYBAR_LAYOUT="$layout"
setup_waybar
ok "Waybar switched to ${layout}. Restart waybar to apply."
}
# ─────────────────────────────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────────────────────────────
main() {
# Allow standalone waybar switching
if [[ "${1:-}" == "waybar" ]]; then
shift
waybar_switch "$@"
exit 0
fi
mkdir -p "$STATE_DIR" "$PKG_STATE_DIR"
[[ -f "$STATE_DIR/install.state" ]] || touch "$STATE_DIR/install.state"
# Init arrays
CHOSEN_OPTIONAL_GROUPS=()
INSTALL_SECURITY=false
WAYBAR_LAYOUT="horizontal"
HW_CPU="unknown"
HW_GPU="unknown"
welcome
preflight
choose_waybar
choose_security
choose_optional_groups
confirm_summary
# From here on we're actually making changes
system_update
bootstrap_paru
install_packages
stow_dotfiles
setup_waybar
enable_services
install_updater
finish
}
main "$@"