When the world says, ‘Give up,’ hope whispers, ‘Try it one more time’, Lyndon B. Johnson

With the core graphical environment (Niri, greetd, and Noctalia) installed, the focus shifts to system organization and maintenance. A robust Arch Linux setup relies on version-controlled configuration files and an optimized package manager.
The following scripts finalize the post-installation process: the first creates a symlink farm, linking your dotfiles (stored in a git repository) directly to their expected system locations. This ensures that changes made in your dotfiles directory are immediately live. The second script optimizes pacman itself—enabling color output, parallel downloads, and the multilib repository—while using reflector to identify the fastest mirrors and installing a suite of essential command-line tools.
#!/usr/bin/env bash
# ── 05-symlinks.sh ────────────────────────────────────────────────────────────
# Symlink dotfiles/myArch/config/* into the live config locations.
#
# Philosophy:
# Config files live in dotfiles/myArch/config/ (version controlled).
# ~/.config/ and /etc/ contain SYMLINKS to those files.
# Editing dotfiles/myArch/config/niri/config.kdl IS editing the live config.
# git diff always shows your real config state.
# Features: Idempotent operation (safe to run multiple times)
# Symlink map:
# config/niri/config.kdl → ~/.config/niri/config.kdl
# config/noctalia/settings.json → ~/.config/noctalia/settings.json
# config/fastfetch/config.jsonc → ~/.config/fastfetch/config.jsonc
# config/alacritty/dracula.toml → ~/.config/alacritty/dracula.toml
# config/rofi/rounded-dark.rasi → ~/.config/rofi/rounded-dark.rasi
# config/rofi/rounded-common.rasi → ~/.config/rofi/rounded-common.rasi
# config/espanso/base.yml → ~/.config/espanso/match/base.yml
# config/greetd/config.toml → /etc/greetd/config.toml (sudo)
# /mnt/data/dotfiles/wallpaper/ → ~/wallpaper/ (copied, not linked)
#
# Run: bash 05-symlinks.sh
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
# ── Paths ─────────────────────────────────────────────────────────────────────
DOTFILES="$HOME/dotfiles/myArch/config"
WALLPAPERSDIR="/mnt/data/dotfiles/wallpapers"
# ── Helper ────────────────────────────────────────────────────────────────────
# link SOURCE TARGET
# Creates TARGET as a symlink to SOURCE.
# If TARGET already exists (file, dir, or broken symlink) it is backed up
# with a .bak suffix before the new symlink is created.
link() {
local src="$1" # src = the real file inside your dotfiles repo
local tgt="$2" # tgt = where the symlink should be created
# Ensure the source file actually exists in dotfiles
# This prevents creating a broken symlink if you mistype a filename or forget to include a file in your repo.
if [[ ! -e "$src" ]]; then
echo " ⚠️ Source not found, skipping: $src"
return 0
fi
# Create parent directory of target if it doesn't exist
# If you want to link something into ~/.config/nvim/init.lua, but ~/.config/nvim/ doesn’t exist yet, this creates it automatically
mkdir -p "$(dirname "$tgt")"
# Back up any existing file/symlink at the target path
# This makes the script safe and reversible
if [[ -e "$tgt" || -L "$tgt" ]]; then
local backup="${tgt}.bak"
echo " 📦 Backing up existing: $tgt → $backup"
mv "$tgt" "$backup"
fi
# Create the symlink and print a friendly message.
ln -s "$src" "$tgt"
echo " 🔗 $tgt → $src"
}
# ── User config symlinks ──────────────────────────────────────────────────────
echo "── Linking user configs..."
# ── Compositor ────────────────────────────────────────────────────────────────
link "$DOTFILES/niri/config.kdl" \
"$HOME/.config/niri/config.kdl"
# ── Shell ─────────────────────────────────────────────────────────────────────
link "$DOTFILES/noctalia/settings.json" \
"$HOME/.config/noctalia/settings.json"
link "$DOTFILES/fastfetch/config.jsonc" \
"$HOME/.config/fastfetch/config.jsonc"
# ── Alacritty theme ───────────────────────────────────────────────────────────
link "$DOTFILES/alacritty/dracula.toml" \
"$HOME/.config/alacritty/dracula.toml"
# ── Rofi themes ───────────────────────────────────────────────────────────────
link "$DOTFILES/rofi/rounded-dark.rasi" \
"$HOME/.config/rofi/rounded-dark.rasi"
link "$DOTFILES/rofi/rounded-common.rasi" \
"$HOME/.config/rofi/rounded-common.rasi"
# ── Espanso ───────────────────────────────────────────────────────────────────
# Espanso looks for matches in ~/.config/espanso/match/
link "$DOTFILES/espanso/base.yml" \
"$HOME/.config/espanso/match/base.yml"
# ── Wallpapers ────────────────────────────────────────────────────────────────
# Copy (not symlink) wallpapers so they're available even without dotfiles
install -d "$HOME/wallpaper"
if [[ -d "$WALLPAPERSDIR" ]]; then
cp -rnt "$HOME/wallpaper" "$WALLPAPERSDIR"/* 2>/dev/null || true
# A trailing slash on the source is required to say "copy the content"
echo " 🖼️ Copy my wallpapers to ~/wallpaper/"
fi
# Noctalia's expected directory for wallpapers is $HOME/Pictures/Wallpapers
link "$HOME/wallpaper" "$HOME/Pictures/Wallpapers"
# ── System config symlinks (require sudo) ─────────────────────────────────────
echo "── Linking system configs (sudo required)..."
# greetd config lives in /etc — not user-writable.
# We symlink it so it stays in version control.
# NOTE: if you edit /etc/greetd/config.toml via sudo, the change goes
# directly into dotfiles/myArch/config/greetd/config.toml — no copying needed.
sudo mkdir -p /etc/greetd
sudo ln -sf "$DOTFILES/greetd/config.toml" /etc/greetd/config.toml
echo " 🔗 /etc/greetd/config.toml → $DOTFILES/greetd/config.toml"
# ── Verify all symlinks ───────────────────────────────────────────────────────
echo
echo "── Verification:"
for path in \
"$HOME/.config/niri/config.kdl" \
"$HOME/.config/noctalia/settings.json" \
"$HOME/.config/fastfetch/config.jsonc" \
"$HOME/.config/alacritty/dracula.toml" \
"$HOME/.config/rofi/rounded-dark.rasi" \
"$HOME/.config/rofi/rounded-common.rasi" \
"$HOME/.config/espanso/match/base.yml" \
"/etc/greetd/config.toml"
do
if [[ -L "$path" ]]; then
echo " ✅ $path → $(readlink "$path")"
else
echo " ❌ $path is NOT a symlink"
fi
done
echo
echo "✅ 05-symlinks: done"
echo " Reboot and log in via greetd → select Niri session"
#!/usr/bin/env bash
# ── 06-pacman.sh ──────────────────────────────────────────────────────────────
# Configure pacman, refresh mirrors with reflector, and install base essentials.
#
# IDEMPOTENT — safe to run multiple times. Every step checks current state
# before acting, so re-running produces no side effects.
#
# What this script does:
# 1. Tweak /etc/pacman.conf — colour output, ILoveCandy progress bar,
# enable the [multilib] repo (needed for 32-bit libs, Steam, Wine)
# 2. Refresh pacman keyring
# 3. Set up reflector for fast Spanish mirrors and run a full system update
# 4. Install a curated set of essential CLI tools
# 5. Configure passwordless sudo for your user
#
# Run: bash 06-pacman.sh
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
USERNAME="${SUDO_USER:-$(whoami)}" # works whether called with sudo or not
# ── Idempotency helper ────────────────────────────────────────────────────────
# already_in FILE PATTERN
# Returns 0 (true) if PATTERN exists as a non-commented line in FILE.
# Used before every sed insertion to avoid duplicating lines.
already_in() {
local file="$1" pattern="$2"
grep -qE "^[[:space:]]*${pattern}" "$file" 2>/dev/null
}
# ── 1. pacman.conf tweaks ─────────────────────────────────────────────────────
echo "── Configuring /etc/pacman.conf..."
# Enable Color output
# sed guard: only uncomment if the line is currently commented out
if ! already_in /etc/pacman.conf "Color"; then
sudo sed -i '/^#Color/s/^#//' /etc/pacman.conf
echo " ✅ Color enabled"
else
echo " ⏭️ Color already enabled"
fi
# ILoveCandy: replaces [####...] with a Pac-Man animation.
# IDEMPOTENCY: check before inserting
if ! already_in /etc/pacman.conf "ILoveCandy"; then
sudo sed -i '/^Color/a ILoveCandy' /etc/pacman.conf
echo " ✅ ILoveCandy enabled"
else
echo " ⏭️ ILoveCandy already enabled"
fi
# Enable [multilib] repository (32-bit libs, Steam, Wine).
# The block appears as:
# #[multilib]
# #Include = /etc/pacman.d/mirrorlist
# sed matches from [multilib] to the Include line and removes leading #.
# Idempotent: if already uncommented, s/^#// matches nothing and does nothing.
if ! already_in /etc/pacman.conf '\[multilib\]'; then
sudo sed -i "/\[multilib\]/,/Include/"'s/^#//' /etc/pacman.conf
echo " ✅ [multilib] enabled"
else
echo " ⏭️ [multilib] already enabled"
fi
# Enable ParallelDownloads — download up to 5 packages simultaneously.
# Cuts update times by 60-80% on fast connections.
# Idempotent: only matches the commented-out form (#ParallelDownloads = 5).
if ! already_in /etc/pacman.conf "ParallelDownloads"; then
sudo sed -i 's/^#ParallelDownloads = 5/ParallelDownloads = 5/' /etc/pacman.conf
echo " ✅ ParallelDownloads enabled"
else
echo " ⏭️ ParallelDownloads already enabled"
fi
echo " ✅ pacman.conf configured"
# ── 2. Refresh pacman keyring ─────────────────────────────────────────────────
# The keyring can become stale on a fresh install — rotating Arch keys can
# cause "invalid or corrupted package" errors during installation.
# --refresh-keys fetches updated keys from the keyserver.
# Idempotent: re-running only refreshes keys, never breaks existing ones.
echo "── Refreshing pacman keyring..."
sudo pacman-key --init
sudo pacman-key --populate archlinux
sudo pacman-key --refresh-keys 2>/dev/null \
|| echo " ⚠️ Key refresh had warnings — usually harmless"
# ── 3. Reflector — fastest mirrors for Spain ──────────────────────────────────
# reflector benchmarks mirror candidates and writes the fastest to mirrorlist.
# Idempotent: always overwrites /etc/pacman.d/mirrorlist with fresh results.
#
# Flags:
# --country 'Spain' only benchmark Spanish mirrors
# --latest 20 from the 20 most recently synced (avoids stale mirrors)
# --sort rate sort by download speed, fastest first
# --save write to mirrorlist
echo "── Installing and configuring reflector..."
sudo pacman -S --needed --noconfirm reflector
sudo reflector \
--country Spain,Portugal,France,Germany \
--protocol https \
--latest 20 \
--sort rate \
--save /etc/pacman.d/mirrorlist
echo " ✅ Mirrorlist updated with fastest Spanish mirrors"
# Enable reflector.timer so mirrors are refreshed weekly automatically.
# Idempotent: enable --now is safe to run on an already-enabled timer.
sudo systemctl enable --now reflector.timer
# ── 4. Full system update ─────────────────────────────────────────────────────
# Always run AFTER changing the mirrorlist.
# Partial upgrades (updating mirrorlist but not packages) cause library mismatches.
# -Syu: sync databases (-S), refresh (-y), upgrade all (-u).
# Idempotent: if everything is up to date, pacman does nothing.
echo "── Running full system update..."
sudo pacman -Syu --noconfirm
# ── 5. Essential packages ─────────────────────────────────────────────────────
# --needed makes this idempotent: already-installed packages are skipped.
echo "── Installing essential packages..."
BASE=(
base-devel # make, gcc, binutils — required for AUR and compilation
git # version control
rsync # fast file sync / backup (used by maintain-arch.sh)
wget # non-interactive download
curl # HTTP client
unzip # archive extraction
p7zip # 7z archives
tree # directory tree display
)
SHELL=(
zsh # shell
bash-completion # tab completion for bash (fallback shell)
starship # cross-shell prompt
)
EDITORS=(
neovim # Text editor
)
SYSTEM=(
fastfetch # system info display (neofetch successor)
btop # resource monitor
man-db # man pages
man-pages # Linux man pages
tldr # simplified man pages
)
CLI=(
eza # modern ls with icons and colours
bat # modern cat with syntax highlighting
fd # modern find
ripgrep # modern grep
fzf # fuzzy finder
zoxide # smart cd that learns your habits
jq # JSON processor
)
sudo pacman -S --needed --noconfirm \
"${BASE[@]}" \
"${SHELL[@]}" \
"${EDITORS[@]}" \
"${SYSTEM[@]}" \
"${CLI[@]}"
echo " ✅ Essential packages installed"
# ── 6. Passwordless sudo ──────────────────────────────────────────────────────
# Adds a sudoers drop-in granting your user NOPASSWD for all commands.
#
# WHY a drop-in in /etc/sudoers.d/ instead of editing /etc/sudoers directly:
# A syntax error in /etc/sudoers locks you out of sudo permanently.
# Drop-in files are validated independently and can be deleted if broken.
#
# IDEMPOTENCY: the file is only written if it does not already exist,
# OR if it exists but does not contain the expected NOPASSWD line.
# This prevents overwriting a customised sudoers entry on re-runs.
#
SUDOERS_FILE="/etc/sudoers.d/99-${USERNAME}-nopasswd"
SUDOERS_LINE="${USERNAME} ALL=(ALL:ALL) NOPASSWD: ALL"
echo "── Configuring passwordless sudo for: $USERNAME"
if [[ -f "$SUDOERS_FILE" ]] && grep -qF "$SUDOERS_LINE" "$SUDOERS_FILE" 2>/dev/null; then
echo " ⏭️ Passwordless sudo already configured for $USERNAME"
else
echo "$SUDOERS_LINE" | sudo tee "$SUDOERS_FILE" > /dev/null
# sudoers files MUST be mode 0440 — sudo refuses world-writable drop-ins
sudo chmod 0440 "$SUDOERS_FILE"
# Validate before accepting — syntax errors are caught here, not at next login
if sudo visudo -c -f "$SUDOERS_FILE" 2>/dev/null; then
echo " ✅ Passwordless sudo enabled for $USERNAME"
else
echo " ❌ Syntax error in sudoers drop-in — removing it"
sudo rm -f "$SUDOERS_FILE"
exit 1
fi
fi
echo
echo "✅ 06-pacman: done"
echo " Next: bash 07-locale.sh"