Compare commits

...
Sign in to create a new pull request.

6 commits
dev ... main

20 changed files with 1667 additions and 471 deletions

105
README.md
View file

@ -1,52 +1,103 @@
# Atlas Desktop installer # Atlas Desktop
This project is an automated installer for the Atlas Desktop. A fully automated Archlinux setup for people that care about their computer.
## Disclaimer ```sh
git clone https://forge.oblic-parallels.fr/guillm/atlas-desktop.git
cd atlas-desktop
bash scripts/installer.sh
```
### ⚠ Use at your own risk.
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. ## What's included
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. - **Window manager**: Hyprland
More generally give a look to the [Quick start guide](https://hedgedoc.oblic-parallels.fr/s/JSR33pjd_) - **Status bar**: Waybar
- **Display manager**: Ly
If something doesn't work or behaves anormally don't hesitate to contact me so that it will be fixed for future users. - **Launcher**: Rofi
- **Notifications center**: Swaync
- **Terminal**: Foot
- **Shell**: Zsh + Zinit + Powerlevel10k
## Requirements ## Requirements
You'll just need an already functional (even basic) **Arch Linux** system with `sudo` installed. - A basic Arch Linux install (cli is enough, no DE required)
Setup will take care of configuring AppArmor and Zram if it hasn't been done yet. - 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.
## Usage ```sh
git clone https://forge.oblic-parallels.fr/guillm/atlas-desktop.git
Very simple, clone the project (or download it directly from the web interface) cd atlas-desktop
```sh= bash scripts/installer.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 ! Then just follow the instructions and you should be good to go !
If you encounter any error or bug, don't hesitate to open an issue on this repo. 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.
## Configuration ## Configuration
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 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.
I particularly think of helix which is used as the default text editor but may not suit some people needs. 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.
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. 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`. If you need to modify the layout, go to `~/.config/hypr/config/default/kb_layouts`.
### Defaults
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. ## 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 `<file>.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 &
```
## Future improvements ## Future improvements
- Waybar switch script
- A defaults system allowing you to easily choose your default programs
- Nvidia cards handling - Nvidia cards handling
- Cleaner way to enable/disable some packages / configuratuions

View file

@ -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;

View file

@ -71,7 +71,7 @@ layerrule {
layerrule { layerrule {
name = status-bar name = status-bar
blur = off blur = on
animation = slide up animation = slide up
match:namespace = ^(waybar)$ match:namespace = ^(waybar)$
} }

View file

@ -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;
}
}

View file

@ -1 +0,0 @@
Boussole/config.jsonc

View file

@ -1 +0,0 @@
Boussole/style.css

57
doc/Developpers.md Normal file
View file

@ -0,0 +1,57 @@
# 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
│ └── <group>.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/<group>`. 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.

1
packages/amd-cpu.pkgs Normal file
View file

@ -0,0 +1 @@
amd-ucode

View file

@ -2,7 +2,7 @@ foot
grim grim
hypridle hypridle
hyprland hyprland
hyprland-qtutils hyprland-guiutils
hyprlock hyprlock
hyprpaper hyprpaper
hyprpicker hyprpicker

1
packages/intel-cpu.pkgs Normal file
View file

@ -0,0 +1 @@
intel-ucode

View file

@ -1,2 +1 @@
intel-media-driver intel-media-driver
intel-ucode

0
packages/optional.groups Normal file
View file

965
scripts/install.sh Normal file
View file

@ -0,0 +1,965 @@
#!/usr/bin/env bash
# =============================================================================
# Atlas Desktop Installer
# for Arch Linux
# =============================================================================
# By Gu://em_
# Co-Authored with Claude Sonnet 4.6
set -euo pipefail
# ── Constants ─────────────────────────────────────────────────────────────────
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CONFIG_DIR="$REPO_DIR/config"
PACKAGES_DIR="$REPO_DIR/packages"
SCRIPTS_DIR="$REPO_DIR/scripts"
DOTFILES_DIR="$HOME/.atlas-dotfiles"
STATE_DIR="$DOTFILES_DIR/.state"
PKG_STATE_DIR="$STATE_DIR/packages"
WAYBAR_CFG_DIR="$HOME/.config/waybar"
# ── Colours & styles ──────────────────────────────────────────────────────────
ESC=$'\e'
BOLD="${ESC}[1m" DIM="${ESC}[2m" RESET="${ESC}[0m"
BLACK="${ESC}[30m" RED="${ESC}[31m" GREEN="${ESC}[32m"
YELLOW="${ESC}[33m" BLUE="${ESC}[34m" MAGENTA="${ESC}[35m"
CYAN="${ESC}[36m" WHITE="${ESC}[37m"
BG_BLACK="${ESC}[40m" BG_BLUE="${ESC}[44m"
HIDE_CURSOR="${ESC}[?25l" SHOW_CURSOR="${ESC}[?25h"
CLEAR_LINE="${ESC}[2K\r"
# Ensure cursor is restored on exit
trap 'printf "%s" "$SHOW_CURSOR"; tput cnorm 2>/dev/null || true' EXIT
trap 'printf "%s" "$SHOW_CURSOR"; tput cnorm 2>/dev/null || true; exit 1' INT TERM
# ── Logging helpers ───────────────────────────────────────────────────────────
PHASE_NUM=0
ok() { printf " ${GREEN}${BOLD}${RESET} %s\n" "$*"; }
info() { printf " ${CYAN}·${RESET} %s\n" "$*"; }
warn() { printf " ${YELLOW}${RESET} %s\n" "$*"; }
err() { printf " ${RED}${BOLD}${RESET} %s\n" "$*" >&2; }
die() { err "$*"; exit 1; }
blank() { echo; }
indent() { sed 's/^/ /'; }
phase() {
(( PHASE_NUM++ )) || true
blank
printf "${BOLD}${BLUE}┌─ Phase %d ─ %s${RESET}\n" "$PHASE_NUM" "$*"
}
step() {
printf "${BLUE}${RESET} ${BOLD}%s${RESET}\n" "$*"
}
phase_done() {
printf "${BLUE}└─${GREEN} done${RESET}\n"
}
# ── Spinner ───────────────────────────────────────────────────────────────────
_SPINNER_PID=""
_SPINNER_FRAMES=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
spinner_start() {
local msg="$1"
printf "%s" "$HIDE_CURSOR"
(
local i=0
while true; do
printf "${CLEAR_LINE} ${CYAN}${_SPINNER_FRAMES[$((i % ${#_SPINNER_FRAMES[@]}))]}${RESET} %s" "$msg"
sleep 0.08
(( i++ )) || true
done
) &
_SPINNER_PID=$!
}
spinner_stop() {
local status="${1:-ok}" # ok | fail
if [[ -n $_SPINNER_PID ]]; then
kill "$_SPINNER_PID" 2>/dev/null; wait "$_SPINNER_PID" 2>/dev/null || true
_SPINNER_PID=""
fi
printf "%s" "$SHOW_CURSOR"
printf "%s" "$CLEAR_LINE"
if [[ $status == ok ]]; then
ok "$2"
else
err "$2"
fi
}
run_quiet() {
# run_quiet "label" cmd [args...]
local label="$1"; shift
spinner_start "$label"
local tmp; tmp=$(mktemp)
if "$@" >"$tmp" 2>&1; then
spinner_stop ok "$label"
rm -f "$tmp"
else
local rc=$?
spinner_stop fail "$label"
warn "Output:"
cat "$tmp" | indent
rm -f "$tmp"
return $rc
fi
}
# ── UI primitives ─────────────────────────────────────────────────────────────
header() {
clear
printf "${BOLD}${BLUE}"
cat << 'EOF'
╔══════════════════════════════════════════════════════════════════╗
║ _ _ _____ _ _ ║
║ /\ | | | | | __ \ | | | |
║ / \ | |_| | __ _ ___ | | | | ___ ___| | _| |_ ___ _ __ ║
║ / /\ \| __| |/ _` / __| | | | |/ _ \/ __| |/ / __/ _ \| '_ \
║ / ____ \ |_| | (_| \__ \ | |__| | __/\__ \ <| || (_) | |_) |
║ /_/ \_\__|_|\__,_|___/ |_____/ \___||___/_|\_\\__\___/| .__/ ║
| |
|_|
╠══════════════════════════════════════════════════════════════════╣
║ Installer · Arch Linux ║
╚══════════════════════════════════════════════════════════════════╝
EOF
printf "${RESET}\n"
}
divider() {
printf " ${DIM}%s${RESET}\n" "──────────────────────────────────────────────────────"
}
confirm() {
local msg="$1" default="${2:-y}"
local prompt; [[ $default == y ]] && prompt="${GREEN}Y${RESET}/n" || prompt="y/${RED}N${RESET}"
local ans
read -rp " ${YELLOW}?${RESET} $msg [${prompt}] " ans
ans="${ans:-$default}"
[[ ${ans,,} == y ]]
}
press_enter() {
read -rp " ${DIM}Press Enter to continue…${RESET}" _
blank
}
# ── State helpers ─────────────────────────────────────────────────────────────
state_get() { grep -m1 "^${1}=" "$STATE_DIR/install.state" 2>/dev/null | cut -d= -f2- || true; }
state_set() {
local key="$1" val="$2"
local f="$STATE_DIR/install.state"
if grep -q "^${key}=" "$f" 2>/dev/null; then
sed -i "s|^${key}=.*|${key}=${val}|" "$f"
else
echo "${key}=${val}" >> "$f"
fi
}
# ── Package helpers ───────────────────────────────────────────────────────────
read_pkg_file() {
local file="$1"
[[ -f $file ]] || return
grep -v '^\s*#' "$file" | grep -v '^\s*$' | sort -u
}
save_pkg_list() {
local group="$1"; shift
mkdir -p "$PKG_STATE_DIR"
printf '%s\n' "$@" > "$PKG_STATE_DIR/${group}"
}
pacman_install() {
sudo pacman -S --needed --noconfirm "$@"
}
paru_install() {
paru -S --needed --noconfirm "$@"
}
pkg_installed() {
pacman -Q "$1" &>/dev/null
}
# ── Hardware detection ────────────────────────────────────────────────────────
detect_hardware() {
HW_GPU="unknown"
HW_CPU="unknown"
if grep -qi intel /proc/cpuinfo 2>/dev/null; then HW_CPU="intel"; fi
if grep -qi amd /proc/cpuinfo 2>/dev/null; then HW_CPU="amd"; fi
if lspci 2>/dev/null | grep -qi "nvidia"; then HW_GPU="nvidia"
elif lspci 2>/dev/null | grep -qi "amd.*radeon\|advanced micro"; then HW_GPU="amd"
elif lspci 2>/dev/null | grep -qi "intel.*graphics"; then HW_GPU="intel"
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 0 — Preflight
# ─────────────────────────────────────────────────────────────────────────────
preflight() {
phase "Preflight checks"
# Arch Linux check
step "Checking system"
if [[ ! -f /etc/arch-release ]]; then
err "This installer only supports Arch Linux."
exit 1
fi
ok "Arch Linux detected"
# Not root
if [[ $EUID -eq 0 ]]; then
die "Do not run this script as root. It will use sudo when needed."
fi
ok "Running as user: ${BOLD}${USER}${RESET}"
# sudo available
if ! sudo -v 2>/dev/null; then
die "sudo is required. Make sure $USER is in the sudoers group."
fi
ok "sudo access confirmed"
# git
command -v git &>/dev/null || die "git is not installed."
ok "git found"
# Config dir
if [[ ! -d $CONFIG_DIR ]]; then
die "config/ directory not found in $REPO_DIR"
fi
ok "Repo looks good: ${DIM}$REPO_DIR${RESET}"
detect_hardware
info "CPU: ${BOLD}${HW_CPU^^}${RESET} GPU: ${BOLD}${HW_GPU^^}${RESET}"
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 1 — Welcome & choices
# ─────────────────────────────────────────────────────────────────────────────
welcome() {
header
cat << EOF
Welcome! This script will set up the Atlas Desktop on your machine.
It will:
${CYAN}·${RESET} Install required packages (pacman + paru for AUR)
${CYAN}·${RESET} Link your dotfiles into ${BOLD}~/${RESET} using GNU Stow
${CYAN}·${RESET} Enable necessary system services
${CYAN}·${RESET} Configure the updater so you stay in sync
Nothing will be modified until you confirm below.
EOF
divider
blank
confirm "Ready to begin?" y || { echo; info "Goodbye!"; exit 0; }
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 2 — Waybar layout picker
# ─────────────────────────────────────────────────────────────────────────────
choose_waybar() {
phase "Waybar layout"
blank
# Vertical preview
printf " ${BOLD}${CYAN}1) Vertical${RESET} ${DIM}(sidebar, left or right)${RESET}\n\n"
printf "${DIM}"
cat << 'EOF'
┌────┐
1 │ ← workspace indicators
2
│ │
│ │
│ 󰕾 │ ← volume
│ 󰖩 │ ← network
12
34 │ ← clock
│  │ ← power
└────┘
EOF
printf "${RESET}\n"
# Horizontal preview
printf " ${BOLD}${CYAN}2) Horizontal${RESET} ${DIM}(bottom bar)${RESET}\n\n"
printf "${DIM}"
cat << 'EOF'
┌─────────────────────────────────────────────┐
│ ● ● ○ ○ 12:34 󰕾 󰖩  │
└─────────────────────────────────────────────┘
EOF
printf "${RESET}\n"
divider
local choice
while true; do
read -rp " ${YELLOW}?${RESET} Choose a layout ${BOLD}[1/2]${RESET}: " choice
case "$choice" in
1) WAYBAR_LAYOUT="vertical"; ok "Vertical layout selected"; break ;;
2) WAYBAR_LAYOUT="horizontal"; ok "Horizontal layout selected"; break ;;
*) warn "Please enter 1 or 2." ;;
esac
done
state_set "waybar_layout" "$WAYBAR_LAYOUT"
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 3 — Security profile
# ─────────────────────────────────────────────────────────────────────────────
choose_security() {
phase "Security & stability"
blank
cat << EOF
The following extras are ${BOLD}recommended for most users${RESET} but are
optional for those who manage their own setup:
${GREEN}·${RESET} ${BOLD}UFW${RESET} — simple firewall (blocks unsolicited inbound traffic)
${GREEN}·${RESET} ${BOLD}AppArmor${RESET} — mandatory access control (limits process capabilities)
${GREEN}·${RESET} ${BOLD}Zram${RESET} — compressed swap-in-RAM (better performance, less disk wear)
${DIM}Recommended unless you already manage these yourself.${RESET}
EOF
if confirm "Install security & stability extras?" y; then
INSTALL_SECURITY=true
ok "Security extras will be installed"
else
INSTALL_SECURITY=false
info "Skipping — you can install them later"
fi
state_set "install_security" "$INSTALL_SECURITY"
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 4 — Optional package groups
# ─────────────────────────────────────────────────────────────────────────────
choose_optional_groups() {
phase "Optional packages"
blank
local optional_file="$PACKAGES_DIR/optional.groups"
if [[ ! -f $optional_file ]]; then
info "No optional.groups file found — skipping"
CHOSEN_OPTIONAL_GROUPS=()
phase_done
return
fi
mapfile -t ALL_OPTIONAL < <(grep -v '^\s*#' "$optional_file" | grep -v '^\s*$')
if (( ${#ALL_OPTIONAL[@]} == 0 )); then
info "No optional groups defined — skipping"
CHOSEN_OPTIONAL_GROUPS=()
phase_done
return
fi
info "Select which optional groups to install:"
blank
CHOSEN_OPTIONAL_GROUPS=()
for group in "${ALL_OPTIONAL[@]}"; do
# Show a short description if <group>.desc file exists
local desc_file="$PACKAGES_DIR/${group}.desc"
local desc=""
[[ -f $desc_file ]] && desc=" ${DIM}$(cat "$desc_file")${RESET}"
if confirm " Install ${BOLD}${group}${RESET}${desc}?" n; then
CHOSEN_OPTIONAL_GROUPS+=("$group")
fi
done
if (( ${#CHOSEN_OPTIONAL_GROUPS[@]} )); then
ok "Selected: ${CHOSEN_OPTIONAL_GROUPS[*]}"
else
info "No optional groups selected"
fi
# Save choices
local joined; joined=$(IFS=','; echo "${CHOSEN_OPTIONAL_GROUPS[*]}")
state_set "optional_groups" "$joined"
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 5 — Summary & confirm
# ─────────────────────────────────────────────────────────────────────────────
confirm_summary() {
phase "Summary"
blank
printf " Here's what will happen:\n\n"
printf " ${BOLD}Dotfiles${RESET}\n"
printf " ${DIM} %s → %s${RESET}\n" "$CONFIG_DIR" "$HOME"
blank
printf " ${BOLD}Waybar layout:${RESET} %s\n" "${WAYBAR_LAYOUT}"
blank
printf " ${BOLD}Hardware packages:${RESET}\n"
printf " CPU: %s · GPU: %s\n" "${HW_CPU}" "${HW_GPU}"
blank
if [[ $INSTALL_SECURITY == true ]]; then
printf " ${GREEN}${RESET} Security extras: UFW, AppArmor, Zram\n"
else
printf " ${DIM}· Security extras: skipped${RESET}\n"
fi
blank
if (( ${#CHOSEN_OPTIONAL_GROUPS[@]} )); then
printf " ${BOLD}Optional groups:${RESET} %s\n" "${CHOSEN_OPTIONAL_GROUPS[*]}"
else
printf " ${DIM}· No optional groups${RESET}\n"
fi
blank
printf " ${BOLD}Services to enable:${RESET}\n"
printf " ly.service"
[[ $INSTALL_SECURITY == true ]] && printf " · apparmor.service · ufw · systemd-zram-setup@zram0"
printf "\n"
blank
divider
blank
confirm "Proceed with installation?" y || { info "Aborted — nothing was changed."; exit 0; }
blank
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 6 — System update
# ─────────────────────────────────────────────────────────────────────────────
system_update() {
phase "System update"
step "Syncing pacman databases and updating system"
run_quiet "Updating system" sudo pacman -Syu --noconfirm
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 7 — Bootstrap paru
# ─────────────────────────────────────────────────────────────────────────────
bootstrap_paru() {
phase "AUR helper (paru)"
if pkg_installed paru; then
ok "paru is already installed"
phase_done
return
fi
step "Installing paru from AUR"
info "Requires: base-devel, git, cargo (rust)"
run_quiet "Installing base-devel" sudo pacman -S --needed --noconfirm base-devel git
# Install rust if needed (paru needs cargo)
if ! command -v cargo &>/dev/null; then
run_quiet "Installing rust" sudo pacman -S --needed --noconfirm rust
fi
local tmp_dir; tmp_dir=$(mktemp -d)
spinner_start "Cloning paru…"
git clone --depth=1 https://aur.archlinux.org/paru.git "$tmp_dir/paru" &>/dev/null
spinner_stop ok "Cloned paru"
step "Building paru (this may take a moment)"
(
cd "$tmp_dir/paru"
run_quiet "Building paru" makepkg -si --noconfirm
)
rm -rf "$tmp_dir"
if pkg_installed paru; then
ok "paru installed successfully"
else
die "paru installation failed"
fi
phase_done
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 8 — Install packages
# ─────────────────────────────────────────────────────────────────────────────
install_packages() {
phase "Installing packages"
local pkg_file pkgs group
# Helper: install a group and save state
# 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 <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 "$@"

561
scripts/update.sh Normal file
View file

@ -0,0 +1,561 @@
#!/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 "$@"

127
setup.sh
View file

@ -1,127 +0,0 @@
#!/bin/sh
version="Beta 26.04.27-1"
echo "Atlas Desktop Installer - ${version}"
echo
# Definitions
## Vars
script_path=$PWD
pkgs_path="${script_path}/packages"
config_path="${script_path}/config"
CPU_vendor=$(lscpu | grep Vendor | awk '{print $NF}')
dotfiles_path="$HOME/.dotfiles"
## Functions
install() {
echo
echo "Downloading $1 packages..."
echo
sudo pacman -Syu --noconfirm --color auto "$(cat "$pkgs_path"/"$1".pkgs)"
printf "\n\nDone\n\n"
}
install_aur() {
echo
echo "Downloading $1 packages..."
echo
paru -Syu --noconfirm --color auto "$(cat "$pkgs_path"/"$1".pkgs)"
printf "\n\nDone\n\n"
}
# Checks
## Sudo installed
if ! pacman -Q sudo > /dev/null; then
echo "Please install 'sudo' first"
exit 1
fi
## Scripts presence
if [ ! -d "$pkgs_path" ]; then
echo "Couldn't find 'packages' folder inside ${script_path}"
exit 1
fi
if [ ! -d "$config_path" ]; then
echo "Couldn't find 'config' folder inside ${script_path}"
exit 1
fi
# Installation
echo "--- Installation ---"
## Main packages
install core
install cli-dev-tools
install fonts
install desktop
install theming
## Vendor specific packages
if [ "$CPU_vendor" = "Genuine Intel" ]; then
install intel
fi
## Paru
git clone https://aur.archlinux.org/paru.git /tmp/paru &&
echo "Installing paru" &&
cd /tmp/paru &&
makepkg -si
cd "$script_path" || exit 1
## AUR packages
install_aur aur
# Configuration
printf "\n--- Configuration ---\n\n"
## Dotfiles
echo "Copying configuration files"
cp -r "$config_path" "$dotfiles_path" &&
cd "$dotfiles_path" &&
stow .
cd "$script_path" || exit 1
## Swap (Zram)
is_zram_active=$(sudo systemctl is-active systemd-zram-setup@zram0.service)
if [ "$is_zram_active" != "active" ]; then
printf "\n========================================\n"
echo "WARNING: you don't have Zram enabled"
echo "Since this script is not yet capable of activating zram, you'll have to do it yourself"
echo "Please refer to https://wiki.archlinux.org/title/Zram#Using_zram-generator"
printf "========================================\n\n"
fi
## Mandatory Access Control (Apparmor)
is_aa_active=$(sudo systemctl is-active apparmor.service)
if [ "$is_aa_active" != "active" ]; then
printf "\n========================================\n"
echo "WARNING: you don't have AppArmor enabled"
echo "Since this script is not yet capable of managing kernel parameters, you'll have to do it yourself"
echo "Please refer to https://wiki.archlinux.org/title/AppArmor"
printf "========================================\n\n"
fi
## Display manager (Ly)
sudo systemctl enable ly@tty1.service
## Firewall (UFW)
sudo systemctl enable ufw
sudo ufw enable
# Finish
fastfetch
echo
echo "Installation finished"
echo "Please check the previous outputs for warnings"
echo "You can make final adjustements before rebooting"
echo "- In particular, if you are using a qwerty keyboard, changing input-fr to input-en in ~/.config/hypr/hyprland.conf"