#!/usr/bin/env bash # Interactive Arch Linux + i3/Xorg workstation installer. # Run as root from an official Arch Linux live ISO. # # This installer ERASES the selected disk. set -Eeuo pipefail IFS=$'\n\t' LOG=/tmp/arch-i3-install.log exec > >(tee -a "$LOG") 2>&1 trap 'echo "ERROR near line $LINENO. See $LOG" >&2' ERR BOLD=$'\e[1m'; RED=$'\e[31m'; GREEN=$'\e[32m' YELLOW=$'\e[33m'; PURPLE=$'\e[35m'; RESET=$'\e[0m' say() { printf "\n%s==>%s %s\n" "$PURPLE" "$RESET" "$*"; } warn() { printf "%sWARNING:%s %s\n" "$YELLOW" "$RESET" "$*" >&2; } die() { printf "%sERROR:%s %s\n" "$RED" "$RESET" "$*" >&2; exit 1; } ask() { local prompt=$1 default=${2-} answer if [[ -n "$default" ]]; then read -r -p "$prompt [$default]: " answer printf '%s' "${answer:-$default}" else read -r -p "$prompt: " answer printf '%s' "$answer" fi } yesno() { local prompt=$1 default=${2:-y} answer marker [[ "$default" == y ]] && marker='[Y/n]' || marker='[y/N]' while true; do read -r -p "$prompt $marker " answer answer=${answer:-$default} case "${answer,,}" in y|yes) return 0 ;; n|no) return 1 ;; esac done } choose() { local prompt=$1; shift local opts=("$@") n answer printf "\n%s\n" "$prompt" >&2 for n in "${!opts[@]}"; do printf " %d) %s\n" "$((n+1))" "${opts[$n]}" >&2 done while true; do read -r -p "Choose [1-${#opts[@]}]: " answer [[ "$answer" =~ ^[0-9]+$ ]] || continue (( answer >= 1 && answer <= ${#opts[@]} )) || continue printf '%s' "${opts[$((answer-1))]}" return done } part_path() { [[ "$1" =~ (nvme|mmcblk|loop) ]] && printf '%sp%s' "$1" "$2" || printf '%s%s' "$1" "$2" } [[ $EUID -eq 0 ]] || die "Run this as root." [[ -d /sys/firmware/efi/efivars ]] || die "Boot the Arch ISO in UEFI mode." command -v pacstrap >/dev/null || die "Use the official Arch Linux ISO." ping -c 1 -W 3 archlinux.org >/dev/null 2>&1 || die "No network connection. Ethernet must be connected." timedatectl set-ntp true clear cat <<'EOF' ╭──────────────────────────────────────────────────────────────╮ │ Arch Linux + i3/Xorg Workstation Installer │ │ Native packages • Paru • Picom • keyboard-first desktop │ ╰──────────────────────────────────────────────────────────────╯ EOF warn "This installer permanently erases the disk you select." # --------------------------------------------------------------------------- # Identity # --------------------------------------------------------------------------- while true; do USERNAME=$(ask "Username" "owen") [[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]*$ ]] && break warn "Use a lowercase Unix username." done FULLNAME=$(ask "Display name" "Owen Rummage") HOSTNAME=$(ask "Hostname" "workstation") TIMEZONE=$(ask "Timezone" "America/Chicago") [[ -e "/usr/share/zoneinfo/$TIMEZONE" ]] || die "Unknown timezone." KEYMAP=$(ask "Console keyboard layout" "us") XKB_LAYOUT=$(ask "Xorg keyboard layout" "us") # --------------------------------------------------------------------------- # Disk # --------------------------------------------------------------------------- say "Available disks" lsblk -dpo NAME,SIZE,MODEL,TRAN,TYPE | awk '$NF=="disk"{print}' while true; do DISK=$(ask "Exact disk to erase, such as /dev/nvme0n1") [[ -b "$DISK" ]] || { warn "Not a block device."; continue; } [[ "$(lsblk -dno TYPE "$DISK")" == disk ]] || { warn "Choose a whole disk."; continue; } break done ENCRYPT=false yesno "Encrypt the root filesystem with LUKS?" y && ENCRYPT=true KERNELS=$(choose "Kernel selection" \ "linux + linux-lts" \ "linux only" \ "linux-lts only") SWAP_SIZE=$(ask "Swapfile size in GiB; enter 0 for none" "16") [[ "$SWAP_SIZE" =~ ^[0-9]+$ ]] || die "Swap size must be a whole number." # --------------------------------------------------------------------------- # Visual choices # --------------------------------------------------------------------------- THEME=$(choose "Color preset" \ "Purple dusk" \ "Blue graphite" \ "Green slate" \ "Warm amber" \ "Custom") case "$THEME" in "Purple dusk") ACCENT="#b48ead"; ACCENT2="#c099ff"; BG="#111318"; PANEL="#191c24"; FG="#e6e9ef"; MUTED="#9aa3b2" ;; "Blue graphite") ACCENT="#7aa2f7"; ACCENT2="#89b4fa"; BG="#101216"; PANEL="#181b22"; FG="#e5e9f0"; MUTED="#929aaa" ;; "Green slate") ACCENT="#8fbc8f"; ACCENT2="#a6e3a1"; BG="#101411"; PANEL="#182019"; FG="#e3e9e4"; MUTED="#93a097" ;; "Warm amber") ACCENT="#e5b567"; ACCENT2="#f9c74f"; BG="#15120e"; PANEL="#211b14"; FG="#eee7dc"; MUTED="#a89d8c" ;; *) ACCENT=$(ask "Primary accent hex" "#b48ead") ACCENT2=$(ask "Bright accent hex" "#c099ff") BG=$(ask "Background hex" "#111318") PANEL=$(ask "Panel hex" "#191c24") FG=$(ask "Foreground hex" "#e6e9ef") MUTED=$(ask "Muted text hex" "#9aa3b2") ;; esac for c in "$ACCENT" "$ACCENT2" "$BG" "$PANEL" "$FG" "$MUTED"; do [[ "$c" =~ ^#[0-9A-Fa-f]{6}$ ]] || die "Invalid color: $c" done CORNER_RADIUS=$(ask "Picom corner radius" "12") GAP_INNER=$(ask "Inner gap" "8") GAP_OUTER=$(ask "Outer gap" "14") BLUR=false yesno "Enable Picom blur and subtle transparency?" y && BLUR=true ANIMATIONS=false yesno "Enable Picom window animations? They may be less consistent than macOS." n && ANIMATIONS=true MODIFIER=$(choose "Primary modifier" "Super/Windows" "Alt") [[ "$MODIFIER" == "Super/Windows" ]] && I3MOD=Mod4 || I3MOD=Mod1 TERM_BIND=$(choose "Terminal shortcut" "$MODIFIER + Enter" "$MODIFIER + T") [[ "$TERM_BIND" == *Enter ]] && TERM_KEY=Return || TERM_KEY=t FOCUS_FOLLOWS=false yesno "Focus follows mouse?" n && FOCUS_FOLLOWS=true # --------------------------------------------------------------------------- # Monitor setup # --------------------------------------------------------------------------- MONITOR_MODE=$(choose "Initial two-monitor layout" \ "Auto; configure later with arandr" \ "Secondary monitor is right of primary" \ "Secondary monitor is left of primary" \ "Secondary monitor is above primary" \ "Secondary monitor is below primary") PRIMARY_OUTPUT=$(ask "Primary Xorg output, or auto (examples: DisplayPort-0, HDMI-A-0)" "auto") SECONDARY_OUTPUT=$(ask "Secondary Xorg output, or auto" "auto") FIXED_WORKSPACES=false yesno "Pin workspaces 1-5 to primary and 6-10 to secondary?" y && FIXED_WORKSPACES=true # --------------------------------------------------------------------------- # Software # --------------------------------------------------------------------------- BROWSER=$(choose "Browser" "Firefox" "Chromium" "Both") FILE_MANAGER=$(choose "File manager" "Thunar" "Nautilus") SHELL=$(choose "Shell" "Zsh" "Bash" "Fish") INSTALL_VESKTOP=true yesno "Install Vesktop using paru?" y || INSTALL_VESKTOP=false INSTALL_CODE=true yesno "Install Code - OSS?" y || INSTALL_CODE=false INSTALL_OFFICE=true yesno "Install LibreOffice?" y || INSTALL_OFFICE=false INSTALL_DOCKER=true yesno "Install Docker and Docker Compose?" y || INSTALL_DOCKER=false INSTALL_VIRT=false yesno "Install KVM/libvirt and Virt Manager?" n && INSTALL_VIRT=true INSTALL_DEV=true yesno "Install web, Go, Rust, Python, and C/C++ development tools?" y || INSTALL_DEV=false INSTALL_ESP=true yesno "Install ESP-IDF from Espressif upstream?" y || INSTALL_ESP=false INSTALL_PRINT=false yesno "Install printing support?" n && INSTALL_PRINT=true LOGIN=$(choose "Login behavior" \ "LightDM graphical login" \ "Console login and startx") # --------------------------------------------------------------------------- # Confirmation # --------------------------------------------------------------------------- clear cat </dev/null || true cryptsetup close cryptroot 2>/dev/null || true wipefs -af "$DISK" sgdisk --zap-all "$DISK" sgdisk -n 1:0:+2G -t 1:ef00 -c 1:ARCH_EFI "$DISK" sgdisk -n 2:0:0 -t 2:8309 -c 2:ARCH_ROOT "$DISK" partprobe "$DISK" udevadm settle EFI_PART=$(part_path "$DISK" 1) ROOT_PART=$(part_path "$DISK" 2) mkfs.fat -F32 -n ARCH_EFI "$EFI_PART" if $ENCRYPT; then cryptsetup luksFormat --type luks2 "$ROOT_PART" cryptsetup open "$ROOT_PART" cryptroot ROOT_DEVICE=/dev/mapper/cryptroot else ROOT_DEVICE=$ROOT_PART fi mkfs.btrfs -f -L ARCH_ROOT "$ROOT_DEVICE" mount "$ROOT_DEVICE" /mnt for sub in @ @home @snapshots @var_log @var_cache_pacman; do btrfs subvolume create "/mnt/$sub" done umount /mnt BTRFS_OPTS="noatime,compress=zstd:3,ssd,discard=async,space_cache=v2" mount -o "$BTRFS_OPTS,subvol=@" "$ROOT_DEVICE" /mnt mkdir -p /mnt/{boot,home,.snapshots,var/log,var/cache/pacman/pkg} mount -o "$BTRFS_OPTS,subvol=@home" "$ROOT_DEVICE" /mnt/home mount -o "$BTRFS_OPTS,subvol=@snapshots" "$ROOT_DEVICE" /mnt/.snapshots mount -o "$BTRFS_OPTS,subvol=@var_log" "$ROOT_DEVICE" /mnt/var/log mount -o "$BTRFS_OPTS,subvol=@var_cache_pacman" "$ROOT_DEVICE" /mnt/var/cache/pacman/pkg mount "$EFI_PART" /mnt/boot # --------------------------------------------------------------------------- # Bootstrap # --------------------------------------------------------------------------- say "Bootstrapping the base system" BOOTSTRAP=(base base-devel linux-firmware amd-ucode btrfs-progs sudo git rust) case "$KERNELS" in "linux + linux-lts") BOOTSTRAP+=(linux linux-headers linux-lts linux-lts-headers) ;; "linux only") BOOTSTRAP+=(linux linux-headers) ;; "linux-lts only") BOOTSTRAP+=(linux-lts linux-lts-headers) ;; esac pacstrap -K /mnt "${BOOTSTRAP[@]}" genfstab -U /mnt >> /mnt/etc/fstab printf '%s\n' "$HOSTNAME" > /mnt/etc/hostname cat > /mnt/etc/hosts < /mnt/etc/locale.conf echo "KEYMAP=$KEYMAP" > /mnt/etc/vconsole.conf sed -i 's/^#Color/Color/' /mnt/etc/pacman.conf sed -i 's/^#ParallelDownloads.*/ParallelDownloads = 10/' /mnt/etc/pacman.conf grep -q '^ILoveCandy' /mnt/etc/pacman.conf || sed -i '/^\[options\]/a ILoveCandy' /mnt/etc/pacman.conf sed -i '/^\#\[multilib\]/,/^#Include/ s/^#//' /mnt/etc/pacman.conf arch-chroot /mnt useradd -m -G wheel,audio,video,input,storage "$USERNAME" printf 'root:%s\n%s:%s\n' "$ROOT_PASSWORD" "$USERNAME" "$USER_PASSWORD" | arch-chroot /mnt chpasswd unset ROOT_PASSWORD ROOT_PASSWORD2 USER_PASSWORD USER_PASSWORD2 echo '%wheel ALL=(ALL:ALL) ALL' > /mnt/etc/sudoers.d/10-wheel echo "$USERNAME ALL=(ALL:ALL) NOPASSWD: ALL" > /mnt/etc/sudoers.d/99-installer chmod 440 /mnt/etc/sudoers.d/{10-wheel,99-installer} # --------------------------------------------------------------------------- # Paru and application packages # --------------------------------------------------------------------------- say "Building paru" arch-chroot /mnt runuser -u "$USERNAME" -- bash -lc ' cd /tmp rm -rf paru git clone https://aur.archlinux.org/paru.git cd paru makepkg -si --noconfirm ' REPO_PACKAGES=( networkmanager openssh reflector pacman-contrib man-db man-pages bash-completion curl wget rsync pipewire pipewire-alsa pipewire-pulse pipewire-jack wireplumber mesa vulkan-radeon libva-mesa-driver mesa-vdpau xorg-server xorg-xinit xorg-xrandr xorg-xsetroot xorg-xprop xorg-xinput i3-wm i3lock i3status polybar rofi dunst dex feh flameshot xclip xsel arandr polkit-gnome gvfs gvfs-smb udiskie udisks2 ghostty pavucontrol pamixer playerctl fastfetch tmux fzf ripgrep fd jq tree unzip zip p7zip noto-fonts noto-fonts-emoji ttf-dejavu ttf-liberation ttf-jetbrains-mono-nerd papirus-icon-theme adwaita-cursors gtk3 gtk4 imagemagick snapper ) case "$BROWSER" in Firefox) REPO_PACKAGES+=(firefox) ;; Chromium) REPO_PACKAGES+=(chromium) ;; Both) REPO_PACKAGES+=(firefox chromium) ;; esac case "$FILE_MANAGER" in Thunar) REPO_PACKAGES+=(thunar thunar-archive-plugin tumbler file-roller) ;; Nautilus) REPO_PACKAGES+=(nautilus file-roller) ;; esac case "$SHELL" in Zsh) REPO_PACKAGES+=(zsh zsh-completions) ;; Fish) REPO_PACKAGES+=(fish) ;; esac $INSTALL_CODE && REPO_PACKAGES+=(code) $INSTALL_OFFICE && REPO_PACKAGES+=(libreoffice-fresh) $INSTALL_DOCKER && REPO_PACKAGES+=(docker docker-compose docker-buildx) $INSTALL_VIRT && REPO_PACKAGES+=(qemu-full libvirt virt-manager dnsmasq bridge-utils edk2-ovmf) $INSTALL_DEV && REPO_PACKAGES+=(go rustup nodejs npm pnpm python python-pip python-pipx cmake ninja gcc gdb clang llvm lldb make) $INSTALL_ESP && REPO_PACKAGES+=(python python-pip python-virtualenv cmake ninja ccache dfu-util libusb) $INSTALL_PRINT && REPO_PACKAGES+=(cups cups-pdf system-config-printer) [[ "$LOGIN" == LightDM* ]] && REPO_PACKAGES+=(lightdm lightdm-gtk-greeter) $ANIMATIONS || REPO_PACKAGES+=(picom) mapfile -t REPO_PACKAGES < <(printf '%s\n' "${REPO_PACKAGES[@]}" | awk '!seen[$0]++') say "Installing packages through paru" printf -v PKG_STRING '%q ' "${REPO_PACKAGES[@]}" arch-chroot /mnt runuser -u "$USERNAME" -- bash -lc "paru -S --needed --noconfirm $PKG_STRING" if $INSTALL_VESKTOP; then say "Installing Vesktop from the AUR" arch-chroot /mnt runuser -u "$USERNAME" -- bash -lc 'paru -S --needed --noconfirm vesktop-bin' fi if $ANIMATIONS; then say "Installing the animation-capable Picom fork from the AUR" arch-chroot /mnt runuser -u "$USERNAME" -- bash -lc 'paru -S --needed --noconfirm picom-ftlabs-git' fi rm -f /mnt/etc/sudoers.d/99-installer # --------------------------------------------------------------------------- # Boot setup # --------------------------------------------------------------------------- if $ENCRYPT; then sed -i 's/^HOOKS=.*/HOOKS=(base udev autodetect microcode modconf kms keyboard keymap consolefont block encrypt filesystems fsck)/' /mnt/etc/mkinitcpio.conf else sed -i 's/^HOOKS=.*/HOOKS=(base udev autodetect microcode modconf kms keyboard keymap consolefont block filesystems fsck)/' /mnt/etc/mkinitcpio.conf fi arch-chroot /mnt mkinitcpio -P arch-chroot /mnt bootctl install ROOT_UUID=$(blkid -s UUID -o value "$ROOT_PART") BTRFS_UUID=$(blkid -s UUID -o value "$ROOT_DEVICE") PARAMS="root=UUID=$BTRFS_UUID rootflags=subvol=@ rw quiet loglevel=3 amd_pstate=active" $ENCRYPT && PARAMS="cryptdevice=UUID=$ROOT_UUID:cryptroot $PARAMS" cat > /mnt/boot/loader/loader.conf < "/mnt/boot/loader/entries/$1.conf" < "$HOME_DIR/.xinitrc" <<'EOF' #!/bin/sh if [ -d /etc/X11/xinit/xinitrc.d ]; then for f in /etc/X11/xinit/xinitrc.d/?*.sh; do [ -x "$f" ] && . "$f" done unset f fi export GTK_THEME=Adwaita:dark export XDG_CURRENT_DESKTOP=i3 exec i3 EOF chmod +x "$HOME_DIR/.xinitrc" FOCUS_MOUSE=no $FOCUS_FOLLOWS && FOCUS_MOUSE=yes MONITOR_COMMANDS="" if [[ "$PRIMARY_OUTPUT" != auto && "$SECONDARY_OUTPUT" != auto ]]; then case "$MONITOR_MODE" in *right*) MONITOR_COMMANDS="exec --no-startup-id xrandr --output $PRIMARY_OUTPUT --auto --primary --output $SECONDARY_OUTPUT --auto --right-of $PRIMARY_OUTPUT" ;; *left*) MONITOR_COMMANDS="exec --no-startup-id xrandr --output $PRIMARY_OUTPUT --auto --primary --output $SECONDARY_OUTPUT --auto --left-of $PRIMARY_OUTPUT" ;; *above*) MONITOR_COMMANDS="exec --no-startup-id xrandr --output $PRIMARY_OUTPUT --auto --primary --output $SECONDARY_OUTPUT --auto --above $PRIMARY_OUTPUT" ;; *below*) MONITOR_COMMANDS="exec --no-startup-id xrandr --output $PRIMARY_OUTPUT --auto --primary --output $SECONDARY_OUTPUT --auto --below $PRIMARY_OUTPUT" ;; esac fi WORKSPACE_OUTPUTS="" if $FIXED_WORKSPACES && [[ "$PRIMARY_OUTPUT" != auto && "$SECONDARY_OUTPUT" != auto ]]; then for n in 1 2 3 4 5; do WORKSPACE_OUTPUTS+="workspace $n output $PRIMARY_OUTPUT"$'\n'; done for n in 6 7 8 9 10; do WORKSPACE_OUTPUTS+="workspace $n output $SECONDARY_OUTPUT"$'\n'; done fi BROWSER_CMD=firefox [[ "$BROWSER" == Chromium ]] && BROWSER_CMD=chromium FILE_CMD=thunar [[ "$FILE_MANAGER" == Nautilus ]] && FILE_CMD=nautilus cat > "$CFG/i3/config" < "$CFG/picom/picom.conf" < "$CFG/polybar/config.ini" < label-focused = %index% label-focused-background = \${colors.background-alt} label-focused-foreground = \${colors.secondary} label-focused-padding = 2 label-unfocused = %index% label-unfocused-foreground = \${colors.muted} label-unfocused-padding = 2 label-urgent = %index% label-urgent-background = \${colors.alert} label-urgent-padding = 2 [module/xwindow] type = internal/xwindow label = %title:0:50:...% label-foreground = \${colors.muted} [module/date] type = internal/date interval = 1 date = %a %b %d time = %I:%M %p label = %date% %time% [module/filesystem] type = internal/fs interval = 30 mount-0 = / label-mounted = 󰋊 %percentage_used%% [module/cpu] type = internal/cpu interval = 2 label = 󰻠 %percentage%% [module/memory] type = internal/memory interval = 2 label = 󰍛 %percentage_used%% [module/pulseaudio] type = internal/pulseaudio format-volume = 󰕾 label-muted = 󰖁 muted click-right = pavucontrol [module/eth] type = internal/network interface-type = wired interval = 3 label-connected = 󰈀 %local_ip% label-disconnected = 󰈂 offline [module/powermenu] type = custom/text content = 󰐥 content-foreground = \${colors.secondary} click-left = ~/.local/bin/power-menu [settings] screenchange-reload = true pseudo-transparency = true EOF cat > "$CFG/polybar/launch.sh" <<'EOF' #!/usr/bin/env bash killall -q polybar || true while pgrep -u "$UID" -x polybar >/dev/null; do sleep 0.2; done if command -v xrandr >/dev/null; then mapfile -t monitors < <(xrandr --query | awk '/ connected/{print $1}') for monitor in "${monitors[@]}"; do MONITOR="$monitor" polybar --reload main & done else polybar --reload main & fi EOF chmod +x "$CFG/polybar/launch.sh" cat > "$CFG/rofi/config.rasi" < "$CFG/dunst/dunstrc" < "$CFG/ghostty/config" < "$CFG/gtk-3.0/settings.ini" < "$HOME_DIR/.local/bin/power-menu" <<'EOF' #!/usr/bin/env bash choice=$(printf 'Lock\nLog out\nSuspend\nReboot\nShut down' | rofi -dmenu -p Power) case "$choice" in Lock) i3lock -c 111318 ;; "Log out") i3-msg exit ;; Suspend) systemctl suspend ;; Reboot) systemctl reboot ;; "Shut down") systemctl poweroff ;; esac EOF chmod +x "$HOME_DIR/.local/bin/power-menu" # Wallpaper: generate a high-resolution PNG locally. cat > /tmp/wallpaper.svg < EOF cp /tmp/wallpaper.svg "$HOME_DIR/.local/share/wallpapers/workstation.svg" arch-chroot /mnt magick /home/"$USERNAME"/.local/share/wallpapers/workstation.svg \ /home/"$USERNAME"/.local/share/wallpapers/workstation.png # Shell configuration. cat > "$HOME_DIR/.bashrc" <<'EOF' [[ $- != *i* ]] && return alias ll='ls -lah --color=auto' alias update='paru -Syu' alias install='paru -S' alias remove='paru -Rns' alias search='paru -Ss' export EDITOR=code export PATH="$HOME/.local/bin:$PATH" eval "$(fzf --bash)" fastfetch EOF cat > "$HOME_DIR/.zshrc" <<'EOF' autoload -Uz compinit && compinit setopt autocd hist_ignore_dups share_history HISTFILE=$HOME/.zsh_history HISTSIZE=10000 SAVEHIST=10000 bindkey -e alias ll='ls -lah --color=auto' alias update='paru -Syu' alias install='paru -S' alias remove='paru -Rns' alias search='paru -Ss' export EDITOR=code export PATH="$HOME/.local/bin:$PATH" source <(fzf --zsh) PROMPT='%F{magenta}%n%f@%F{blue}%m%f %F{cyan}%~%f %# ' fastfetch EOF mkdir -p "$CFG/fish" cat > "$CFG/fish/config.fish" <<'EOF' alias ll='ls -lah --color=auto' alias update='paru -Syu' alias install='paru -S' alias remove='paru -Rns' alias search='paru -Ss' set -gx EDITOR code fish_add_path ~/.local/bin if status is-interactive fastfetch end EOF cat > "$HOME_DIR/.gitconfig" <> "$HOME_DIR/.bashrc" <<'EOF' alias get_idf='. "$HOME/esp/esp-idf/export.sh"' EOF cat >> "$HOME_DIR/.zshrc" <<'EOF' alias get_idf='. "$HOME/esp/esp-idf/export.sh"' EOF cat > /mnt/etc/udev/rules.d/99-esp32.rules <<'EOF' SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", MODE="0660", GROUP="uucp" SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", MODE="0660", GROUP="uucp" SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", MODE="0660", GROUP="uucp" SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", MODE="0660", GROUP="uucp" EOF arch-chroot /mnt usermod -aG uucp "$USERNAME" fi # Snapper. umount /mnt/.snapshots rmdir /mnt/.snapshots arch-chroot /mnt snapper -c root create-config / btrfs subvolume delete /mnt/.snapshots mkdir /mnt/.snapshots mount -o "$BTRFS_OPTS,subvol=@snapshots" "$ROOT_DEVICE" /mnt/.snapshots chmod 750 /mnt/.snapshots arch-chroot /mnt systemctl enable snapper-timeline.timer snapper-cleanup.timer # Swapfile. if (( SWAP_SIZE > 0 )); then mkdir -p /mnt/swap btrfs subvolume create /mnt/swap/swapvol chattr +C /mnt/swap/swapvol btrfs filesystem mkswapfile --size "${SWAP_SIZE}G" /mnt/swap/swapvol/swapfile echo '/swap/swapvol/swapfile none swap defaults 0 0' >> /mnt/etc/fstab fi # Reflector, Ethernet-only NetworkManager behavior. mkdir -p /mnt/etc/xdg/reflector cat > /mnt/etc/xdg/reflector/reflector.conf <<'EOF' --save /etc/pacman.d/mirrorlist --protocol https --country US --latest 20 --sort rate EOF mkdir -p /mnt/etc/NetworkManager/conf.d cat > /mnt/etc/NetworkManager/conf.d/10-ethernet.conf <<'EOF' [main] plugins=keyfile [device] wifi.scan-rand-mac-address=no EOF arch-chroot /mnt chown -R "$USERNAME:$USERNAME" "/home/$USERNAME" arch-chroot /mnt bash -n "/home/$USERNAME/.local/bin/power-menu" arch-chroot /mnt bash -n "/home/$USERNAME/.config/polybar/launch.sh" say "Installation complete" cat < Install a package paru -Ss Search paru -Rns Remove a package First login: • LightDM starts automatically if selected. • Otherwise log in and run: startx • Open Ghostty with $MODIFIER + $( [[ "$TERM_KEY" == Return ]] && echo Enter || echo T ) • Open Rofi with $MODIFIER + Space • Configure monitors graphically with: arandr No syncing, Wi-Fi utilities, or Bluetooth utilities were installed. NetworkManager remains installed for wired Ethernet configuration. Log: $LOG EOF if yesno "Unmount now?" y; then sync umount -R /mnt $ENCRYPT && cryptsetup close cryptroot fi yesno "Reboot now?" n && reboot