diff --git a/README.md b/README.md index 8367b7d..4880934 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,52 @@ -# Atlas Desktop +# Atlas Desktop installer -A fully automated Archlinux setup for people that care about their computer. +This project is an automated installer for the Atlas Desktop. -```sh -curl -fsSL https://forge.oblic-parallels.fr/guillm/atlas-desktop/raw/branch/main/scripts/install.sh | bash -``` +## Disclaimer +### ⚠ Use at your own risk. -## What's included - -- **Window manager**: Hyprland -- **Status bar**: Waybar -- **Display manager**: Ly -- **Launcher**: Rofi -- **Notifications center**: Swaync -- **Terminal**: Foot -- **Shell**: Zsh + Zinit + Powerlevel10k +Some platforms may not yet be supported such as Nvidia graphic cards for which you'll have to install all the drivers yourself. Sadly I don't have any equipment to test that out. + +Also you will very probably miss some software you're used to. I'll strongly recommend you to check the [Additionnal packages](https://hedgedoc.oblic-parallels.fr/s/JSR33pjd_#Additional-packages) section to know about some open source utilities that fit perfectly within the desktop. +More generally give a look to the [Quick start guide](https://hedgedoc.oblic-parallels.fr/s/JSR33pjd_) + +If something doesn't work or behaves anormally don't hesitate to contact me so that it will be fixed for future users. ## Requirements -- A basic Arch Linux install (cli is enough, no DE required) -- A user account with `sudo` access -- `git` installed (`sudo pacman -Sy git`) -- Internet connection - -Setup will take care of configuring AppArmor and Zram or even firewall if you haven't done it yet. - -## Installation - -**Disclaimer**: Some platforms may not yet be supported such as Nvidia graphic cards for which you'll have to install all the drivers yourself. Sadly I don't have any equipment to test that out. +You'll just need an already functional (even basic) **Arch Linux** system with `sudo` installed. +Setup will take care of configuring AppArmor and Zram if it hasn't been done yet. -```sh -curl -fsSL https://forge.oblic-parallels.fr/guillm/atlas-desktop/raw/branch/main/scripts/install.sh | bash +## Usage + +Very simple, clone the project (or download it directly from the web interface) +```sh= +git clone https://forge.oblic-parallels.fr/guillm/atlas-install ``` +And run `setup.sh` +```sh= +./setup.sh +``` + Then just follow the instructions and you should be good to go ! -After the setup finishes you may need to reboot in order to login: -```sh -sudo reboot -``` - -> **Note**: If you encounter any error or bug, don't hesitate to open an issue on this repo. - -> **Note:** After installation the original clone is no longer needed. -> `~/.dotfiles` is the real repo and is what the updater tracks. - +If you encounter any error or bug, don't hesitate to open an issue on this repo. ## Configuration -I suggest you to give a look to the [Quick start guide](https://hedgedoc.oblic-parallels.fr/s/JSR33pjd_) that explains how the system works if you're not used to it. -As you will very probably miss some software you're used to. I'll strongly recommend you to check the [Additionnal packages](https://hedgedoc.oblic-parallels.fr/s/JSR33pjd_#Additional-packages) section to know about some open source utilities that fit perfectly within the desktop. +I suggest you to give a look to the packages that will be installed on your system as it's important to know how things will work or to disable some things you wouldn't necessarely want. +I particularly think of helix which is used as the default text editor but may not suit some people needs. Note that the default keyboard layout is QWERTY (us-fr). You can switch with the AZERTY layout by using `Mod+Ctrl+Space`. That said, the default layout already has french accents if you need them. If you need to modify the layout, go to `~/.config/hypr/config/default/kb_layouts`. +### Defaults -## Staying up to date - -```sh -atlas-update -``` - -This will: - -- Fetch origin/main and show new commits -- Detect conflicts between upstream changes and local edits, asking you which to keep per file -- Pull and re-stow config/ -- Re-prompt for any new optional groups added upstream - - -## Troubleshooting - -**Stow reports conflicts on first install** -- Existing real files that clash with stow links are automatically renamed to `.bak`. - -**An AUR package failed to install** -- The installer reports it and continues. It won't be saved to state, so `atlas-update` will retry it next run. - -**`atlas-update` says "already up to date" but a package is missing** -- Delete its state file to force a re-sync: -```bash -rm ~/.atlas-dotfiles/.state/packages/mygroup.pkgs -atlas-update -``` - -**Waybar doesn't reload after switching layout** -- Reload desktop using `Mod+Shift+R` or run the following command: -```bash -pkill waybar && waybar & -``` - +I'm working on a system to easily change the default programs. I don't know yet if it will result into anything but it's worth trying. ## Future improvements -- Waybar switch script -- A defaults system allowing you to easily choose your default programs - Nvidia cards handling +- Cleaner way to enable/disable some packages / configuratuions diff --git a/config/.config/colors/colors.css b/config/.config/colors/colors.css new file mode 100644 index 0000000..8e0278d --- /dev/null +++ b/config/.config/colors/colors.css @@ -0,0 +1,18 @@ +/* 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; diff --git a/config/.config/waybar/vertical/config.jsonc b/config/.config/waybar/Atlas/config.jsonc similarity index 100% rename from config/.config/waybar/vertical/config.jsonc rename to config/.config/waybar/Atlas/config.jsonc diff --git a/config/.config/waybar/vertical/style.css b/config/.config/waybar/Atlas/style.css similarity index 100% rename from config/.config/waybar/vertical/style.css rename to config/.config/waybar/Atlas/style.css diff --git a/config/.config/waybar/Atlas/style_minimal.css b/config/.config/waybar/Atlas/style_minimal.css new file mode 100644 index 0000000..8c01bc1 --- /dev/null +++ b/config/.config/waybar/Atlas/style_minimal.css @@ -0,0 +1,292 @@ +@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; + } +} diff --git a/config/.config/waybar/horizontal/config.jsonc b/config/.config/waybar/Boussole/config.jsonc similarity index 100% rename from config/.config/waybar/horizontal/config.jsonc rename to config/.config/waybar/Boussole/config.jsonc diff --git a/config/.config/waybar/horizontal/style.css b/config/.config/waybar/Boussole/style.css similarity index 100% rename from config/.config/waybar/horizontal/style.css rename to config/.config/waybar/Boussole/style.css diff --git a/config/.config/waybar/config.jsonc b/config/.config/waybar/config.jsonc new file mode 120000 index 0000000..1b9ff12 --- /dev/null +++ b/config/.config/waybar/config.jsonc @@ -0,0 +1 @@ +Boussole/config.jsonc \ No newline at end of file diff --git a/config/.config/waybar/style.css b/config/.config/waybar/style.css new file mode 120000 index 0000000..e7230cf --- /dev/null +++ b/config/.config/waybar/style.css @@ -0,0 +1 @@ +Boussole/style.css \ No newline at end of file diff --git a/doc/Developpers.md b/doc/Developpers.md deleted file mode 100644 index a965e50..0000000 --- a/doc/Developpers.md +++ /dev/null @@ -1,57 +0,0 @@ -# Developpers documentation - -## Repository structure - -``` -dotfiles/ -├── install.sh -├── scripts/ -│ └── update.sh ← dotfiles-update -├── packages/ -│ ├── core.pkgs ← always installed -│ ├── desktop.pkgs ← always installed (automatic) -│ ├── aur.pkgs ← AUR packages (paru) -│ ├── intel-cpu.pkgs ← auto: Intel CPU -│ ├── amd-cpu.pkgs ← auto: AMD CPU -│ ├── intel-gpu.pkgs ← auto: Intel GPU -│ ├── amd-gpu.pkgs ← auto: AMD GPU -│ ├── nvidia.pkgs ← auto: NVIDIA GPU -│ ├── gaming.pkgs ← optional -│ ├── development.pkgs ← optional -│ ├── ... -│ ├── optional.groups ← lists which groups are optional -│ └── .desc ← one-line description shown in installer -└── config/ ← stow package; mirrors $HOME - ├── .config/ - │ ├── hypr/ - │ ├── waybar/ - │ │ ├── vertical/ - │ │ └── horizontal/ - │ ├── rofi/ - │ └── ... - └── .local/ - └── bin/ - └── atlas-update -``` - -### Package group conventions - -| File | When installed | -|---|---| -| `core.pkgs` | Always | -| Any unlisted `*.pkgs` (e.g. `desktop.pkgs`) | Always (automatic) | -| `aur.pkgs` | Always, via paru | -| `intel-gpu.pkgs`, `amd-gpu.pkgs`, etc. | Auto-detected via `lspci` / `/proc/cpuinfo` | -| Groups listed in `optional.groups` | User-chosen during install, remembered on updates | - -To add a new automatic group: create `packages/mygroup.pkgs` and commit. -To add a new optional group: also add its name to `packages/optional.groups` and optionally a `packages/mygroup.desc` one-liner. - - -## Package state tracking - -After each install or update, a snapshot of every installed group is saved to `~/.dotfiles/.state/packages/`. On the next run, each group is diffed: - -- **New packages** in the `.pkgs` file → installed automatically -- **Removed packages** → you are asked before uninstalling -Delete `~/.dotfiles/.state/` to reset all state and force a full reinstall on the next update. diff --git a/packages/amd-cpu.pkgs b/packages/amd-cpu.pkgs deleted file mode 100644 index 35a60e1..0000000 --- a/packages/amd-cpu.pkgs +++ /dev/null @@ -1 +0,0 @@ -amd-ucode diff --git a/packages/aur.pkgs b/packages/aur.pkgs index 7105d9d..9caabcc 100644 --- a/packages/aur.pkgs +++ b/packages/aur.pkgs @@ -1 +1 @@ -rofi-bluetooth-git + rofi-bluetooth-git diff --git a/packages/intel-cpu.pkgs b/packages/intel-cpu.pkgs deleted file mode 100644 index 2052a8a..0000000 --- a/packages/intel-cpu.pkgs +++ /dev/null @@ -1 +0,0 @@ -intel-ucode diff --git a/packages/intel-gpu.pkgs b/packages/intel.pkgs similarity index 63% rename from packages/intel-gpu.pkgs rename to packages/intel.pkgs index c36c61d..2d9fee7 100644 --- a/packages/intel-gpu.pkgs +++ b/packages/intel.pkgs @@ -1 +1,2 @@ intel-media-driver +intel-ucode \ No newline at end of file diff --git a/packages/optional.groups b/packages/optional.groups deleted file mode 100644 index e69de29..0000000 diff --git a/scripts/install.sh b/scripts/install.sh deleted file mode 100644 index e4b1cfc..0000000 --- a/scripts/install.sh +++ /dev/null @@ -1,965 +0,0 @@ -#!/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 .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 - # Note: AUR packages are installed one-by-one so a single failure does not abort - # the whole group. Official repo packages are bulk-installed with a per- - # package fallback. Only successfully installed packages are saved to 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)" - - local succeeded=() failed=() - - if [[ $group == aur ]]; then - for pkg in "${pkgs[@]}"; do - local tmp; tmp=$(mktemp) - if [[ $IS_TTY == true ]]; then - spinner_start " [aur] $pkg" - if paru -S --needed --noconfirm "$pkg" >"$tmp" 2>&1; then - spinner_stop ok "[aur] $pkg"; succeeded+=("$pkg") - else - spinner_stop fail "[aur] $pkg" - warn "paru output:"; cat "$tmp" | indent; failed+=("$pkg") - fi - else - printf " ... [aur] %s\n" "$pkg" - if paru -S --needed --noconfirm "$pkg" >"$tmp" 2>&1; then - ok "[aur] $pkg"; succeeded+=("$pkg") - else - err "[aur] $pkg" - warn "paru output:"; cat "$tmp" | indent; failed+=("$pkg") - fi - fi - rm -f "$tmp" - done - else - run_quiet " Installing ${group}" pacman_install "${pkgs[@]}" - local tmp; tmp=$(mktemp) - if pacman_install "${pkgs[@]}" >"$tmp" 2>&1; then - succeeded=("${pkgs[@]}"); rm -f "$tmp" - else - rm -f "$tmp" - warn "Bulk install failed for group '${group}' — retrying per package" - for pkg in "${pkgs[@]}"; do - local tmp2; tmp2=$(mktemp) - if [[ $IS_TTY == true ]]; then - spinner_start " [${group}] $pkg" - if pacman_install "$pkg" >"$tmp2" 2>&1; then - spinner_stop ok "[${group}] $pkg"; succeeded+=("$pkg") - else - spinner_stop fail "[${group}] $pkg" - warn "pacman output:"; cat "$tmp2" | indent; failed+=("$pkg") - fi - else - printf " ... [%s] %s\n" "$group" "$pkg" - if pacman_install "$pkg" >"$tmp2" 2>&1; then - ok "[${group}] $pkg"; succeeded+=("$pkg") - else - err "[${group}] $pkg" - warn "pacman output:"; cat "$tmp2" | indent; failed+=("$pkg") - fi - fi - rm -f "$tmp2" - done - fi - fi - - if (( ${#succeeded[@]} )); then - save_pkg_list "$group" "${succeeded[@]}" - fi - - if (( ${#failed[@]} )); then - warn "Failed in group '${group}' (will retry on next update):" - for pkg in "${failed[@]}"; do - printf " - %s\n" "$pkg" - done - fi - return 0 - } - - # stow itself (needed for the next step) - run_quiet "Ensuring stow is present" sudo pacman -S --needed --noconfirm stow - - # Build the set of groups that are handled specially so we can exclude them - # when iterating all .pkgs files for automatic groups. - local hw_groups=("${HW_CPU}-cpu" "${HW_GPU}-gpu") - local optional_group_names=() - local optional_file="$PACKAGES_DIR/optional.groups" - if [[ -f $optional_file ]]; then - mapfile -t optional_group_names < <(grep -v '^\s*#' "$optional_file" | grep -v '^\s*$') - fi - - is_special_group() { - local g="$1" - # Returns 0 if the group is hw, optional, aur, or security (handled separately) - [[ $g == aur || $g == security ]] && return 0 - for hw in "${hw_groups[@]}"; do [[ $g == "$hw" ]] && return 0; done - for og in "${optional_group_names[@]}"; do [[ $g == "$og" ]] && return 0; done - return 1 - } - - # 1. Core - install_group "core" - - # 2. Hardware (auto-detected) - install_group "${HW_CPU}-cpu" - install_group "${HW_GPU}-gpu" - - # 3. All remaining automatic groups (every .pkgs file that is not special) - while IFS= read -r pkg_file; do - local group; group=$(basename "$pkg_file" .pkgs) - is_special_group "$group" && continue - [[ $group == core ]] && continue # already done above - install_group "$group" - done < <(find "$PACKAGES_DIR" -maxdepth 1 -name "*.pkgs" | sort) - - - # 4. 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 - - # 5. Optional chosen groups - for group in "${CHOSEN_OPTIONAL_GROUPS[@]}"; do - install_group "$group" - done - - # 6. AUR group - install_group "aur" - - phase_done -} - -# ───────────────────────────────────────────────────────────────────────────── -# STEP 9 — Clone into ~/.atlas-dotfiles and stow -# ───────────────────────────────────────────────────────────────────────────── -stow_dotfiles() { - phase "Cloning and stowing dotfiles" - - # ~/.atlas-dotfiles is the permanent home of the repo. You 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 ${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 "$@" diff --git a/scripts/update.sh b/scripts/update.sh deleted file mode 100644 index 52dd255..0000000 --- a/scripts/update.sh +++ /dev/null @@ -1,561 +0,0 @@ -#!/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_DIR="$DOTFILES_DIR" -STOW_PKG="config" -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" -} - -# ── Change detection ──────────────────────────────────────────────────────────────── - -# Files changed inside config/ (stow package) — used for conflict detection. -# Paths are stripped of the "config/" prefix so they match $HOME-relative targets. -collect_changed_config_files() { - git -C "$DOTFILES_DIR" diff --name-only "${OLD_HASH}" "${NEW_HASH}" \ - | grep '^config/' \ - | sed 's|^config/||' \ - || true -} - -# Returns 0 (true) if any file under packages/ changed between the two commits. -packages_changed() { - git -C "$DOTFILES_DIR" diff --name-only "${OLD_HASH}" "${NEW_HASH}" \ - | grep -q '^packages/' -} - -# Returns 0 (true) if any file under scripts/ changed between the two commits. -scripts_changed() { - git -C "$DOTFILES_DIR" diff --name-only "${OLD_HASH}" "${NEW_HASH}" \ - | grep -q '^scripts/' -} - -handle_conflicts() { - local repo_changed_files - mapfile -t repo_changed_files < <(collect_changed_config_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" - - local err_file; err_file=$(mktemp) - if stow --dir="$STOW_DIR" --target="$STOW_TARGET" --restow "$STOW_PKG" 2>"$err_file"; then - ok "stow: $STOW_PKG" - else - warn "stow: $STOW_PKG (errors below)" - while IFS= read -r line; do - echo " ${DIM}${line}${RESET}" - done < "$err_file" - fi - rm -f "$err_file" -} - -# ── 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} Atlas Desktop Updater${RESET}" - echo "${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 - - # Re-stow config/ whenever dotfiles changed - apply_stow - - # Re-stow scripts/ when it changed (keeps ~/.local/bin helpers up to date) - if scripts_changed; then - info "scripts/ changed — re-linking binaries" - local err_file; err_file=$(mktemp) - if stow --dir="$DOTFILES_DIR" --target="$HOME" --restow scripts \ - 2>"$err_file"; then - ok "stow: scripts" - else - warn "stow: scripts (errors below)" - while IFS= read -r line; do - echo " ${DIM}${line}${RESET}" - done < "$err_file" - fi - rm -f "$err_file" - fi - - state_set "last_commit" "$NEW_HASH" - - # Re-process packages whenever packages/ changed, even if no git update - # produced new dotfiles — package lists are independent of stowed files. - if packages_changed; then - info "packages/ changed — running full package sync" - fi - fi - - # Package sync runs on every invocation (diff vs saved state catches changes). - # This also handles the case where a previous run partially failed. - 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 "$@"