Brand new installer and updaters
This commit is contained in:
parent
4a479c566a
commit
c51330c1c2
14 changed files with 1373 additions and 314 deletions
|
|
@ -1,18 +0,0 @@
|
||||||
/* Swaync */
|
|
||||||
@define-color active #e64e4e;
|
|
||||||
@define-color background_dark #1c2128;
|
|
||||||
@define-color background_light #22272e;
|
|
||||||
@define-color background_transparent alpha(#18191c, 0.7);
|
|
||||||
@define-color foreground #e4e8ed;
|
|
||||||
@define-color border #333940;
|
|
||||||
@define-color alert #ea4545;
|
|
||||||
|
|
||||||
|
|
||||||
/* Waybar */
|
|
||||||
@define-color foreground #e4e8ed;
|
|
||||||
@define-color background #18191c;
|
|
||||||
|
|
||||||
/* @define-color accentColor #b6c8d3; */
|
|
||||||
@define-color accentColor #ec775c;
|
|
||||||
|
|
||||||
@define-color charging #6bc46d;
|
|
||||||
|
|
@ -1,292 +0,0 @@
|
||||||
@import "colors.css";
|
|
||||||
@define-color active @accentColor;
|
|
||||||
|
|
||||||
* {
|
|
||||||
font-size: 16px;
|
|
||||||
font-family: "JetBrainsMono Nerd Font,JetBrainsMono NF";
|
|
||||||
min-width: 8px;
|
|
||||||
min-height: 0px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
text-shadow: none;
|
|
||||||
padding: 0px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
window#waybar {
|
|
||||||
transition-property: background-color;
|
|
||||||
transition-duration: 0.5s;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid alpha(@active, 0.2);
|
|
||||||
background: @background;
|
|
||||||
background: alpha(@background, 0.7);
|
|
||||||
color: lighter(@active);
|
|
||||||
}
|
|
||||||
|
|
||||||
menu,
|
|
||||||
tooltip {
|
|
||||||
border-radius: 2px;
|
|
||||||
padding: 2px;
|
|
||||||
border: 1px solid @active;
|
|
||||||
background: @background;
|
|
||||||
|
|
||||||
color: lighter(@active);
|
|
||||||
}
|
|
||||||
|
|
||||||
menu label,
|
|
||||||
tooltip label {
|
|
||||||
font-size: 14px;
|
|
||||||
color: lighter(@active);
|
|
||||||
}
|
|
||||||
|
|
||||||
#submap,
|
|
||||||
#tray>.needs-attention {
|
|
||||||
animation-name: blink-active;
|
|
||||||
animation-duration: 1s;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-direction: alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modules-right {
|
|
||||||
margin: 0px 6px 4px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: alpha(@background, 0);
|
|
||||||
color: lighter(@active);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modules-left {
|
|
||||||
transition-property: background-color;
|
|
||||||
transition-duration: 0.5s;
|
|
||||||
margin: 6px 5px 6px 5px; /* Testing with 5 pixels on left/right */
|
|
||||||
border-radius: 4px;
|
|
||||||
background: alpha(@background, 0.0);
|
|
||||||
color: lighter(@active);
|
|
||||||
border: 1px solid alpha(@active, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#gcpu,
|
|
||||||
#custom-github,
|
|
||||||
#memory,
|
|
||||||
#disk,
|
|
||||||
#together,
|
|
||||||
#submap,
|
|
||||||
#custom-weather,
|
|
||||||
#custom-recorder,
|
|
||||||
#connection,
|
|
||||||
#cnoti,
|
|
||||||
#brightness,
|
|
||||||
#power,
|
|
||||||
#custom-updates,
|
|
||||||
#tray,
|
|
||||||
/*#audio,*/ /* Duplicate with #sound */
|
|
||||||
#sound,
|
|
||||||
#privacy { /*Controls all the right modules for some reason*/
|
|
||||||
border-radius: 2px;
|
|
||||||
margin: 2px 1px 3px 1px;
|
|
||||||
background: alpha(darker(@active), 0.0);
|
|
||||||
border: 1px solid alpha(darker(@active), 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Override specific parameters*/
|
|
||||||
|
|
||||||
#brightness,
|
|
||||||
#sound {
|
|
||||||
padding: 1px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-notifications {
|
|
||||||
padding-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-hotspot,
|
|
||||||
#custom-github,
|
|
||||||
#custom-notifications {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-hotspot {
|
|
||||||
padding-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-vpn,
|
|
||||||
#custom-hotspot {
|
|
||||||
background: alpha(darker(@active), 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#privacy-item {
|
|
||||||
padding: 6px 0px 6px 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#gcpu {
|
|
||||||
padding: 8px 0px 8px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-cpu-icon {
|
|
||||||
font-size: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-cputemp,
|
|
||||||
#disk,
|
|
||||||
#memory,
|
|
||||||
#cpu {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-github {
|
|
||||||
padding-top: 2px;
|
|
||||||
padding-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-dmark {
|
|
||||||
color: alpha(@foreground, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#submap {
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaces {
|
|
||||||
margin: 0px 2px;
|
|
||||||
padding: 2px 0px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaces button {
|
|
||||||
transition-property: background-color;
|
|
||||||
transition-duration: 0.1s;
|
|
||||||
color: @foreground;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: alpha(@foreground, 0.3);
|
|
||||||
padding: 2px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaces button.urgent {
|
|
||||||
font-weight: bold;
|
|
||||||
color: @foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaces button.active {
|
|
||||||
padding: 2px 0px;
|
|
||||||
background: alpha(@active, 0.4);
|
|
||||||
color: lighter(@active);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#network.wifi {
|
|
||||||
padding-right: 5px;
|
|
||||||
margin: 2px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#network.disconnected {
|
|
||||||
padding-right: 5px;
|
|
||||||
margin: 2px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#network.ethernet {
|
|
||||||
padding-right: 3px;
|
|
||||||
margin: 2px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#submap {
|
|
||||||
min-width: 0px;
|
|
||||||
margin: 4px 6px 4px 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#custom-weather,
|
|
||||||
#tray {
|
|
||||||
padding: 4px 0px 4px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bluetooth {
|
|
||||||
padding-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#battery {
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 4px 0px;
|
|
||||||
margin: 4px 2px 4px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#battery.discharging.warning {
|
|
||||||
animation-name: blink-yellow;
|
|
||||||
animation-duration: 1s;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-direction: alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
#battery.discharging.critical {
|
|
||||||
animation-name: blink-red;
|
|
||||||
animation-duration: 1s;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-direction: alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
#clock {
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 4px 2px 2px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pulseaudio.mic {
|
|
||||||
border-radius: 4px;
|
|
||||||
color: lighter(@active);
|
|
||||||
padding-left: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#backlight-slider slider,
|
|
||||||
#pulseaudio-slider slider {
|
|
||||||
background-color: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#backlight-slider trough,
|
|
||||||
#pulseaudio-slider trough {
|
|
||||||
margin-top: 4px;
|
|
||||||
min-width: 6px;
|
|
||||||
min-height: 60px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: alpha(@background, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
#backlight-slider highlight,
|
|
||||||
#pulseaudio-slider highlight {
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: lighter(@active);
|
|
||||||
}
|
|
||||||
|
|
||||||
#bluetooth.discoverable,
|
|
||||||
#bluetooth.discovering,
|
|
||||||
#bluetooth.pairable {
|
|
||||||
border-radius: 8px;
|
|
||||||
animation-name: blink-active;
|
|
||||||
animation-duration: 1s;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-direction: alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink-active {
|
|
||||||
to {
|
|
||||||
background-color: @active;
|
|
||||||
color: @foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink-red {
|
|
||||||
to {
|
|
||||||
background-color: #c64d4f;
|
|
||||||
color: @foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink-yellow {
|
|
||||||
to {
|
|
||||||
background-color: #cf9022;
|
|
||||||
color: @foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Boussole/config.jsonc
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Boussole/style.css
|
|
||||||
1
packages/amd-cpu.pkgs
Normal file
1
packages/amd-cpu.pkgs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
amd-ucode
|
||||||
|
|
@ -1 +1 @@
|
||||||
rofi-bluetooth-git
|
rofi-bluetooth-git
|
||||||
|
|
|
||||||
1
packages/intel-cpu.pkgs
Normal file
1
packages/intel-cpu.pkgs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
intel-ucode
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
intel-media-driver
|
intel-media-driver
|
||||||
intel-ucode
|
|
||||||
839
scripts/install.sh
Normal file
839
scripts/install.sh
Normal file
|
|
@ -0,0 +1,839 @@
|
||||||
|
#!/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'
|
||||||
|
╔══════════════════════════════════════════════════════════╗
|
||||||
|
║ Some ASCII Art ║
|
||||||
|
╠══════════════════════════════════════════════════════════╣
|
||||||
|
║ 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 — Stow dotfiles
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
stow_dotfiles() {
|
||||||
|
phase "Stowing dotfiles"
|
||||||
|
|
||||||
|
step "Linking ${CONFIG_DIR} → ${HOME}"
|
||||||
|
|
||||||
|
# The config/ dir IS the stow package (its contents mirror $HOME)
|
||||||
|
# We treat config/ as a single stow package named "config"
|
||||||
|
# stow will create symlinks in $HOME for everything inside config/
|
||||||
|
|
||||||
|
# Backup any existing files that would conflict
|
||||||
|
local conflicts=()
|
||||||
|
while IFS= read -r link; do
|
||||||
|
local target="$HOME/${link#$CONFIG_DIR/}"
|
||||||
|
if [[ -e $target && ! -L $target ]]; then
|
||||||
|
conflicts+=("$target")
|
||||||
|
fi
|
||||||
|
done < <(find "$CONFIG_DIR" -type f)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Create DOTFILES_DIR as a symlink to CONFIG_DIR, or use CONFIG_DIR directly
|
||||||
|
# We symlink ~/.atlas-dotfiles → repo's config dir so stow state lives in the repo
|
||||||
|
if [[ -L $DOTFILES_DIR ]]; then
|
||||||
|
local existing_target; existing_target=$(readlink -f "$DOTFILES_DIR")
|
||||||
|
if [[ $existing_target != "$CONFIG_DIR" ]]; then
|
||||||
|
warn "~/.atlas-dotfiles already points elsewhere: $existing_target"
|
||||||
|
warn "Removing and re-linking to $CONFIG_DIR"
|
||||||
|
rm "$DOTFILES_DIR"
|
||||||
|
ln -s "$CONFIG_DIR" "$DOTFILES_DIR"
|
||||||
|
fi
|
||||||
|
elif [[ -d $DOTFILES_DIR ]]; then
|
||||||
|
warn "~/.atlas-dotfiles exists as a real directory. Renaming to ~/.atlas-dotfiles.bak"
|
||||||
|
mv "$DOTFILES_DIR" "${DOTFILES_DIR}.bak"
|
||||||
|
ln -s "$CONFIG_DIR" "$DOTFILES_DIR"
|
||||||
|
else
|
||||||
|
ln -s "$CONFIG_DIR" "$DOTFILES_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "~/.atlas-dotfiles → $CONFIG_DIR"
|
||||||
|
|
||||||
|
# Now run stow from inside the repo's parent, treating config as the package
|
||||||
|
run_quiet "Stowing config" \
|
||||||
|
stow --dir="$REPO_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_src="$SCRIPTS_DIR/update.sh"
|
||||||
|
local updater_link="$HOME/.local/bin/atlas-update"
|
||||||
|
|
||||||
|
if [[ ! -f $updater_src ]]; then
|
||||||
|
warn "Updater not found at $updater_src — skipping"
|
||||||
|
phase_done
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
chmod +x "$updater_src"
|
||||||
|
|
||||||
|
# Create symlink (stow might also handle this if update.sh is in config/)
|
||||||
|
# Here we link directly so it's always in PATH regardless of stow layout
|
||||||
|
ln -sf "$updater_src" "$updater_link"
|
||||||
|
ok "atlas-update → $updater_src"
|
||||||
|
info "Run ${BOLD}atlas-update${RESET} any time to sync with upstream"
|
||||||
|
|
||||||
|
# Save repo path into state so updater knows where it lives
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
state_set "repo_dir" "$REPO_DIR"
|
||||||
|
state_set "last_commit" "$(git -C "$REPO_DIR" rev-parse HEAD)"
|
||||||
|
|
||||||
|
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 "$@"
|
||||||
531
scripts/update.sh
Normal file
531
scripts/update.sh
Normal file
|
|
@ -0,0 +1,531 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Atlas Updater
|
||||||
|
# Arch Linux · GNU Stow · pacman / paru
|
||||||
|
# =============================================================================
|
||||||
|
# By Gu://em_
|
||||||
|
# Co-Authored with Claude Sonnet 4.6
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Paths ────────────────────────────────────────────────────────────────────
|
||||||
|
DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.atlas-dotfiles}"
|
||||||
|
STATE_DIR="$DOTFILES_DIR/.state"
|
||||||
|
PKG_STATE_DIR="$STATE_DIR/packages"
|
||||||
|
STATE_FILE="$STATE_DIR/update.state"
|
||||||
|
PACKAGES_DIR="$DOTFILES_DIR/packages"
|
||||||
|
STOW_TARGET="$HOME"
|
||||||
|
|
||||||
|
# ── Colours ──────────────────────────────────────────────────────────────────
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
BOLD=$'\e[1m'; RESET=$'\e[0m'
|
||||||
|
RED=$'\e[31m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'
|
||||||
|
BLUE=$'\e[34m'; CYAN=$'\e[36m'; DIM=$'\e[2m'
|
||||||
|
else
|
||||||
|
BOLD=''; RESET=''; RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; DIM=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
section() { echo; echo "${BOLD}${BLUE}══ $* ══${RESET}"; }
|
||||||
|
ok() { echo " ${GREEN}✔${RESET} $*"; }
|
||||||
|
info() { echo " ${CYAN}·${RESET} $*"; }
|
||||||
|
warn() { echo " ${YELLOW}⚠${RESET} $*"; }
|
||||||
|
err() { echo " ${RED}✘${RESET} $*" >&2; }
|
||||||
|
die() { err "$*"; exit 1; }
|
||||||
|
dim() { echo "${DIM}$*${RESET}"; }
|
||||||
|
|
||||||
|
# Ask yes/no; default is supplied as 'y' or 'n'
|
||||||
|
confirm() {
|
||||||
|
local msg="$1" default="${2:-y}"
|
||||||
|
local prompt; [[ $default == y ]] && prompt="[Y/n]" || prompt="[y/N]"
|
||||||
|
read -rp " ${YELLOW}?${RESET} $msg $prompt " ans
|
||||||
|
ans="${ans:-$default}"
|
||||||
|
[[ ${ans,,} == y ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Numbered menu; sets $REPLY to chosen index (1-based)
|
||||||
|
pick_one() {
|
||||||
|
local prompt="$1"; shift
|
||||||
|
local options=("$@")
|
||||||
|
echo " ${YELLOW}?${RESET} $prompt"
|
||||||
|
for i in "${!options[@]}"; do
|
||||||
|
printf " %s) %s\n" "$((i+1))" "${options[$i]}"
|
||||||
|
done
|
||||||
|
while true; do
|
||||||
|
read -rp " Choice: " ans
|
||||||
|
if [[ $ans =~ ^[0-9]+$ ]] && (( ans >= 1 && ans <= ${#options[@]} )); then
|
||||||
|
REPLY=$ans; return
|
||||||
|
fi
|
||||||
|
echo " Invalid choice, try again."
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── State helpers ─────────────────────────────────────────────────────────────
|
||||||
|
state_get() { grep -m1 "^${1}=" "$STATE_FILE" 2>/dev/null | cut -d= -f2- || true; }
|
||||||
|
state_set() {
|
||||||
|
local key="$1" val="$2"
|
||||||
|
if grep -q "^${key}=" "$STATE_FILE" 2>/dev/null; then
|
||||||
|
sed -i "s|^${key}=.*|${key}=${val}|" "$STATE_FILE"
|
||||||
|
else
|
||||||
|
echo "${key}=${val}" >> "$STATE_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Dependency check ──────────────────────────────────────────────────────────
|
||||||
|
check_deps() {
|
||||||
|
local missing=()
|
||||||
|
for cmd in git stow pacman; do
|
||||||
|
command -v "$cmd" &>/dev/null || missing+=("$cmd")
|
||||||
|
done
|
||||||
|
if (( ${#missing[@]} )); then
|
||||||
|
die "Missing required tools: ${missing[*]}. Install them first."
|
||||||
|
fi
|
||||||
|
# Prefer paru, fall back to pacman -S for AUR-less installs
|
||||||
|
if command -v paru &>/dev/null; then
|
||||||
|
AUR_HELPER="paru"
|
||||||
|
elif command -v yay &>/dev/null; then
|
||||||
|
AUR_HELPER="yay"
|
||||||
|
else
|
||||||
|
AUR_HELPER=""
|
||||||
|
warn "No AUR helper found (yay/paru). AUR packages will be skipped."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Hardware detection ────────────────────────────────────────────────────────
|
||||||
|
detect_hardware_groups() {
|
||||||
|
local groups=()
|
||||||
|
# CPU
|
||||||
|
if grep -qi intel /proc/cpuinfo 2>/dev/null; then
|
||||||
|
groups+=("intel-cpu")
|
||||||
|
elif grep -qi amd /proc/cpuinfo 2>/dev/null; then
|
||||||
|
groups+=("amd-cpu")
|
||||||
|
fi
|
||||||
|
# GPU
|
||||||
|
if lspci 2>/dev/null | grep -qi "intel.*graphics"; then
|
||||||
|
groups+=("intel-gpu")
|
||||||
|
fi
|
||||||
|
if lspci 2>/dev/null | grep -qi "amd.*radeon\|advanced micro.*display"; then
|
||||||
|
groups+=("amd-gpu")
|
||||||
|
fi
|
||||||
|
if lspci 2>/dev/null | grep -qi "nvidia"; then
|
||||||
|
groups+=("nvidia")
|
||||||
|
fi
|
||||||
|
echo "${groups[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Package-group helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Return the .pkgs file for a group name (strip suffixes variants)
|
||||||
|
pkg_file_for_group() {
|
||||||
|
local group="$1"
|
||||||
|
# Try exact match first, then with .pkgs extension
|
||||||
|
local f
|
||||||
|
for f in "$PACKAGES_DIR/${group}.pkgs" "$PACKAGES_DIR/${group}"; do
|
||||||
|
[[ -f $f ]] && { echo "$f"; return; }
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read packages from a .pkgs file (skip comments & blanks)
|
||||||
|
read_pkg_file() {
|
||||||
|
local file="$1"
|
||||||
|
[[ -f $file ]] || return
|
||||||
|
grep -v '^\s*#' "$file" | grep -v '^\s*$' | sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load previously-saved list for a group
|
||||||
|
saved_pkg_list() {
|
||||||
|
local group="$1"
|
||||||
|
local f="$PKG_STATE_DIR/${group}"
|
||||||
|
[[ -f $f ]] && cat "$f" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save current list for a group
|
||||||
|
save_pkg_list() {
|
||||||
|
local group="$1"
|
||||||
|
shift
|
||||||
|
mkdir -p "$PKG_STATE_DIR"
|
||||||
|
printf '%s\n' "$@" > "$PKG_STATE_DIR/${group}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect all available groups from packages/ dir
|
||||||
|
all_pkg_groups() {
|
||||||
|
[[ -d $PACKAGES_DIR ]] || return
|
||||||
|
find "$PACKAGES_DIR" -maxdepth 1 -name '*.pkgs' -printf '%f\n' \
|
||||||
|
| sed 's/\.pkgs$//' | sort
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read user-chosen optional groups from state
|
||||||
|
chosen_optional_groups() {
|
||||||
|
state_get "optional_groups" | tr ',' '\n' | grep -v '^$' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Git operations ────────────────────────────────────────────────────────────
|
||||||
|
git_fetch_and_check() {
|
||||||
|
section "Checking for updates"
|
||||||
|
cd "$DOTFILES_DIR"
|
||||||
|
|
||||||
|
info "Fetching origin…"
|
||||||
|
git fetch origin main --quiet
|
||||||
|
|
||||||
|
local local_hash remote_hash
|
||||||
|
local_hash=$(git rev-parse HEAD)
|
||||||
|
remote_hash=$(git rev-parse origin/main)
|
||||||
|
LAST_KNOWN_HASH=$(state_get "last_commit" || true)
|
||||||
|
|
||||||
|
if [[ $local_hash == $remote_hash ]]; then
|
||||||
|
ok "Already up to date (${local_hash:0:8})"
|
||||||
|
HAS_UPDATE=false
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "New commits available:"
|
||||||
|
git log --oneline "${local_hash}..${remote_hash}" | while read -r line; do
|
||||||
|
dim " $line"
|
||||||
|
done
|
||||||
|
HAS_UPDATE=true
|
||||||
|
NEW_HASH="$remote_hash"
|
||||||
|
OLD_HASH="$local_hash"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Conflict detection ────────────────────────────────────────────────────────
|
||||||
|
collect_changed_files() {
|
||||||
|
# Files changed between old commit and new commit in the repo
|
||||||
|
git -C "$DOTFILES_DIR" diff --name-only "${OLD_HASH}" "${NEW_HASH}" \
|
||||||
|
| grep -v '^packages/' | grep -v '^\.' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_conflicts() {
|
||||||
|
local repo_changed_files
|
||||||
|
mapfile -t repo_changed_files < <(collect_changed_files)
|
||||||
|
|
||||||
|
if (( ${#repo_changed_files[@]} == 0 )); then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Checking for local conflicts"
|
||||||
|
|
||||||
|
local conflicts=()
|
||||||
|
for rel in "${repo_changed_files[@]}"; do
|
||||||
|
# The stow target path
|
||||||
|
local target="$STOW_TARGET/$rel"
|
||||||
|
# Check if the symlink target (i.e. the real file in dotfiles) was
|
||||||
|
# locally overridden — compare stowed hash vs. live hash
|
||||||
|
if [[ -f $target && ! -L $target ]]; then
|
||||||
|
# File exists but is NOT a symlink → user edited after un-stowing
|
||||||
|
conflicts+=("$rel")
|
||||||
|
elif [[ -L $target ]]; then
|
||||||
|
# It's a symlink — compare live dotfiles version vs. what we last recorded
|
||||||
|
local link_dest
|
||||||
|
link_dest=$(readlink -f "$target" 2>/dev/null || true)
|
||||||
|
local repo_file="$DOTFILES_DIR/$rel"
|
||||||
|
# If the symlink points back into the repo, compare against old hash
|
||||||
|
if [[ $link_dest == $repo_file ]]; then
|
||||||
|
local old_blob new_blob
|
||||||
|
old_blob=$(git -C "$DOTFILES_DIR" show "${OLD_HASH}:${rel}" 2>/dev/null \
|
||||||
|
| sha256sum | cut -d' ' -f1 || echo "")
|
||||||
|
local live_blob
|
||||||
|
live_blob=$(sha256sum "$link_dest" 2>/dev/null | cut -d' ' -f1 || echo "")
|
||||||
|
# If live differs from old-repo → user modified through symlink
|
||||||
|
if [[ -n $old_blob && $old_blob != $live_blob ]]; then
|
||||||
|
conflicts+=("$rel")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( ${#conflicts[@]} == 0 )); then
|
||||||
|
ok "No local conflicts"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "${#conflicts[@]} conflict(s) detected — files modified locally:"
|
||||||
|
for f in "${conflicts[@]}"; do
|
||||||
|
echo " ${YELLOW}${f}${RESET}"
|
||||||
|
done
|
||||||
|
|
||||||
|
for rel in "${conflicts[@]}"; do
|
||||||
|
echo
|
||||||
|
info "Conflict: ${BOLD}${rel}${RESET}"
|
||||||
|
pick_one "Which version to keep?" \
|
||||||
|
"Upstream (repo) — discard local changes" \
|
||||||
|
"Local — keep my version, skip this file"
|
||||||
|
case $REPLY in
|
||||||
|
1)
|
||||||
|
ok "Will use upstream version of $rel"
|
||||||
|
SKIP_FILES+=("__NONE__") # placeholder so array stays parallel
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
warn "Skipping $rel — your local version will be kept"
|
||||||
|
SKIP_FILES+=("$rel")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Apply git update ──────────────────────────────────────────────────────────
|
||||||
|
apply_git_update() {
|
||||||
|
section "Pulling updates"
|
||||||
|
cd "$DOTFILES_DIR"
|
||||||
|
|
||||||
|
# Stash any locally-modified tracked files so pull doesn't fail
|
||||||
|
if ! git diff --quiet HEAD; then
|
||||||
|
info "Stashing local modifications…"
|
||||||
|
git stash push -m "updater-auto-stash" --quiet
|
||||||
|
GIT_STASHED=true
|
||||||
|
else
|
||||||
|
GIT_STASHED=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
git merge --ff-only origin/main --quiet
|
||||||
|
ok "Repository updated to ${NEW_HASH:0:8}"
|
||||||
|
|
||||||
|
# Re-apply stash if we created one (for files user chose to keep locally)
|
||||||
|
if [[ $GIT_STASHED == true ]]; then
|
||||||
|
info "Re-applying local stash…"
|
||||||
|
git stash pop --quiet || {
|
||||||
|
warn "Stash pop had conflicts — your stash is still saved (git stash list)"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Stow ─────────────────────────────────────────────────────────────────────
|
||||||
|
apply_stow() {
|
||||||
|
section "Stowing dotfiles"
|
||||||
|
cd "$DOTFILES_DIR"
|
||||||
|
|
||||||
|
# Discover stow packages (top-level dirs, excluding hidden & packages/)
|
||||||
|
local stow_pkgs=()
|
||||||
|
while IFS= read -r d; do
|
||||||
|
local name
|
||||||
|
name=$(basename "$d")
|
||||||
|
[[ $name == packages ]] && continue
|
||||||
|
stow_pkgs+=("$name")
|
||||||
|
done < <(find "$DOTFILES_DIR" -mindepth 1 -maxdepth 1 -type d ! -name '.*' | sort)
|
||||||
|
|
||||||
|
if (( ${#stow_pkgs[@]} == 0 )); then
|
||||||
|
warn "No stow packages found in $DOTFILES_DIR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
for pkg in "${stow_pkgs[@]}"; do
|
||||||
|
if stow --restow --target="$STOW_TARGET" "$pkg" 2>/tmp/stow_err; then
|
||||||
|
ok "stow: $pkg"
|
||||||
|
else
|
||||||
|
warn "stow: $pkg (see error below)"
|
||||||
|
while IFS= read -r line; do
|
||||||
|
echo " ${DIM}${line}${RESET}"
|
||||||
|
done < /tmp/stow_err
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
rm -f /tmp/stow_err
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Package management ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Install packages via pacman (official) or AUR helper
|
||||||
|
install_pkgs() {
|
||||||
|
local pkgs=("$@")
|
||||||
|
(( ${#pkgs[@]} == 0 )) && return
|
||||||
|
if sudo pacman -S --needed --noconfirm "${pkgs[@]}" 2>/dev/null; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
# Some might be AUR-only — try AUR helper for the remainder
|
||||||
|
if [[ -n $AUR_HELPER ]]; then
|
||||||
|
"$AUR_HELPER" -S --needed --noconfirm "${pkgs[@]}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove packages (orphan-safe: only if nothing else depends on them)
|
||||||
|
remove_pkgs() {
|
||||||
|
local pkgs=("$@")
|
||||||
|
(( ${#pkgs[@]} == 0 )) && return
|
||||||
|
# Filter to only actually-installed packages
|
||||||
|
local installed=()
|
||||||
|
for p in "${pkgs[@]}"; do
|
||||||
|
pacman -Q "$p" &>/dev/null && installed+=("$p")
|
||||||
|
done
|
||||||
|
(( ${#installed[@]} == 0 )) && return
|
||||||
|
sudo pacman -Rns --noconfirm "${installed[@]}" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
process_pkg_group() {
|
||||||
|
local group="$1"
|
||||||
|
local pkg_file
|
||||||
|
pkg_file=$(pkg_file_for_group "$group")
|
||||||
|
|
||||||
|
if [[ -z $pkg_file ]]; then
|
||||||
|
warn "No package file found for group '$group' — skipping"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t new_pkgs < <(read_pkg_file "$pkg_file")
|
||||||
|
mapfile -t old_pkgs < <(saved_pkg_list "$group")
|
||||||
|
|
||||||
|
# Diff
|
||||||
|
mapfile -t to_install < <(comm -23 \
|
||||||
|
<(printf '%s\n' "${new_pkgs[@]}" | sort) \
|
||||||
|
<(printf '%s\n' "${old_pkgs[@]+"${old_pkgs[@]}"}" | sort))
|
||||||
|
mapfile -t to_remove < <(comm -13 \
|
||||||
|
<(printf '%s\n' "${new_pkgs[@]}" | sort) \
|
||||||
|
<(printf '%s\n' "${old_pkgs[@]+"${old_pkgs[@]}"}" | sort))
|
||||||
|
|
||||||
|
local changed=false
|
||||||
|
|
||||||
|
if (( ${#to_install[@]} )); then
|
||||||
|
info " [${group}] Installing: ${to_install[*]}"
|
||||||
|
install_pkgs "${to_install[@]}" && changed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( ${#to_remove[@]} )); then
|
||||||
|
if confirm " [${group}] Remove packages no longer in group: ${to_remove[*]}?" y; then
|
||||||
|
remove_pkgs "${to_remove[@]}" && changed=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $changed == false ]] && (( ${#new_pkgs[@]} )); then
|
||||||
|
ok " [${group}] Up to date"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Always save current list (ensures state file exists after first run)
|
||||||
|
save_pkg_list "$group" "${new_pkgs[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
process_packages() {
|
||||||
|
section "Package management"
|
||||||
|
|
||||||
|
if [[ ! -d $PACKAGES_DIR ]]; then
|
||||||
|
info "No packages/ directory found — skipping"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1. Auto groups (hardware-based)
|
||||||
|
local hw_groups
|
||||||
|
mapfile -t hw_groups < <(detect_hardware_groups)
|
||||||
|
if (( ${#hw_groups[@]} )); then
|
||||||
|
info "Detected hardware groups: ${hw_groups[*]}"
|
||||||
|
for g in "${hw_groups[@]}"; do
|
||||||
|
process_pkg_group "$g"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Core / always-on groups (everything that isn't hw-specific and not optional)
|
||||||
|
# Convention: groups NOT listed in optional.groups file are automatic
|
||||||
|
local optional_file="$PACKAGES_DIR/optional.groups"
|
||||||
|
local optional_group_names=()
|
||||||
|
if [[ -f $optional_file ]]; then
|
||||||
|
mapfile -t optional_group_names < <(grep -v '^\s*#' "$optional_file" | grep -v '^\s*$')
|
||||||
|
fi
|
||||||
|
|
||||||
|
local hw_pattern
|
||||||
|
hw_pattern=$(printf '%s|' "${hw_groups[@]}" | sed 's/|$//')
|
||||||
|
[[ -z $hw_pattern ]] && hw_pattern='__NOMATCH__'
|
||||||
|
|
||||||
|
while IFS= read -r group; do
|
||||||
|
# Skip hw groups already processed
|
||||||
|
echo "$group" | grep -qE "^(${hw_pattern})$" && continue
|
||||||
|
# Skip optional groups (handled below)
|
||||||
|
local is_optional=false
|
||||||
|
for og in "${optional_group_names[@]}"; do
|
||||||
|
[[ $group == $og ]] && { is_optional=true; break; }
|
||||||
|
done
|
||||||
|
$is_optional && continue
|
||||||
|
process_pkg_group "$group"
|
||||||
|
done < <(all_pkg_groups)
|
||||||
|
|
||||||
|
# 3. Optional groups — interactive first-time, then remembered
|
||||||
|
if (( ${#optional_group_names[@]} )); then
|
||||||
|
local chosen
|
||||||
|
mapfile -t chosen < <(chosen_optional_groups)
|
||||||
|
|
||||||
|
if (( ${#chosen[@]} == 0 )); then
|
||||||
|
echo
|
||||||
|
info "Optional package groups available. Choose which to install:"
|
||||||
|
local selected=()
|
||||||
|
for og in "${optional_group_names[@]}"; do
|
||||||
|
if confirm " Install group '${og}'?" n; then
|
||||||
|
selected+=("$og")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if (( ${#selected[@]} )); then
|
||||||
|
state_set "optional_groups" "$(IFS=,; echo "${selected[*]}")"
|
||||||
|
chosen=("${selected[@]}")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
for og in "${chosen[@]}"; do
|
||||||
|
process_pkg_group "$og"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── AUR group (separate because needs AUR helper) ─────────────────────────────
|
||||||
|
process_aur_group() {
|
||||||
|
[[ -z $AUR_HELPER ]] && { warn "Skipping AUR group (no AUR helper)"; return; }
|
||||||
|
local pkg_file
|
||||||
|
pkg_file=$(pkg_file_for_group "aur")
|
||||||
|
[[ -z $pkg_file ]] && return
|
||||||
|
|
||||||
|
section "AUR packages"
|
||||||
|
mapfile -t new_pkgs < <(read_pkg_file "$pkg_file")
|
||||||
|
mapfile -t old_pkgs < <(saved_pkg_list "aur")
|
||||||
|
|
||||||
|
mapfile -t to_install < <(comm -23 \
|
||||||
|
<(printf '%s\n' "${new_pkgs[@]}" | sort) \
|
||||||
|
<(printf '%s\n' "${old_pkgs[@]+"${old_pkgs[@]}"}" | sort))
|
||||||
|
mapfile -t to_remove < <(comm -13 \
|
||||||
|
<(printf '%s\n' "${new_pkgs[@]}" | sort) \
|
||||||
|
<(printf '%s\n' "${old_pkgs[@]+"${old_pkgs[@]}"}" | sort))
|
||||||
|
|
||||||
|
if (( ${#to_install[@]} )); then
|
||||||
|
info "Installing AUR: ${to_install[*]}"
|
||||||
|
"$AUR_HELPER" -S --needed --noconfirm "${to_install[@]}" || true
|
||||||
|
fi
|
||||||
|
if (( ${#to_remove[@]} )); then
|
||||||
|
if confirm "Remove AUR packages no longer listed: ${to_remove[*]}?" y; then
|
||||||
|
remove_pkgs "${to_remove[@]}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
(( ${#to_install[@]} == 0 && ${#to_remove[@]} == 0 )) && ok "AUR packages up to date"
|
||||||
|
save_pkg_list "aur" "${new_pkgs[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
||||||
|
main() {
|
||||||
|
echo
|
||||||
|
echo "${BOLD} Hyprland Dotfiles Updater${RESET} ${DIM}(Arch Linux)${RESET}"
|
||||||
|
echo " ${DIM}Target: ${DOTFILES_DIR}${RESET}"
|
||||||
|
|
||||||
|
[[ -d $DOTFILES_DIR/.git ]] || die "$DOTFILES_DIR is not a git repository."
|
||||||
|
check_deps
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR" "$PKG_STATE_DIR"
|
||||||
|
[[ -f $STATE_FILE ]] || touch "$STATE_FILE"
|
||||||
|
|
||||||
|
HAS_UPDATE=false
|
||||||
|
GIT_STASHED=false
|
||||||
|
SKIP_FILES=()
|
||||||
|
NEW_HASH=""
|
||||||
|
OLD_HASH=""
|
||||||
|
|
||||||
|
git_fetch_and_check
|
||||||
|
|
||||||
|
if [[ $HAS_UPDATE == true ]]; then
|
||||||
|
handle_conflicts
|
||||||
|
apply_git_update
|
||||||
|
apply_stow
|
||||||
|
state_set "last_commit" "$NEW_HASH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
process_packages
|
||||||
|
process_aur_group
|
||||||
|
|
||||||
|
section "Done"
|
||||||
|
ok "Dotfiles are up to date."
|
||||||
|
if [[ $HAS_UPDATE == true ]]; then
|
||||||
|
dim " Updated: ${OLD_HASH:0:8} → ${NEW_HASH:0:8}"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue