Files
www/public/scripts/archinstall.sh
T
2026-07-10 06:36:42 -05:00

1086 lines
34 KiB
Bash

#!/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 <<EOF
${BOLD}Installation summary${RESET}
Disk: $DISK — WILL BE ERASED
Encryption: $ENCRYPT
Kernel: $KERNELS
Swap: ${SWAP_SIZE} GiB
User: $USERNAME
Hostname: $HOSTNAME
Desktop: i3-wm on Xorg with Picom
Theme: $THEME
Modifier: $MODIFIER
Browser: $BROWSER
File manager: $FILE_MANAGER
Shell: $SHELL
Package UI: paru
Vesktop: $INSTALL_VESKTOP
Syncing: not installed
Bluetooth: not installed
Wi-Fi tools: not installed
EOF
printf "\nType ${BOLD}ERASE %s${RESET} exactly: " "$DISK"
read -r CONFIRM
[[ "$CONFIRM" == "ERASE $DISK" ]] || die "Cancelled."
while true; do
read -r -s -p "Root password: " ROOT_PASSWORD; echo
read -r -s -p "Repeat root password: " ROOT_PASSWORD2; echo
[[ -n "$ROOT_PASSWORD" && "$ROOT_PASSWORD" == "$ROOT_PASSWORD2" ]] && break
warn "Passwords did not match."
done
while true; do
read -r -s -p "Password for $USERNAME: " USER_PASSWORD; echo
read -r -s -p "Repeat password: " USER_PASSWORD2; echo
[[ -n "$USER_PASSWORD" && "$USER_PASSWORD" == "$USER_PASSWORD2" ]] && break
warn "Passwords did not match."
done
# ---------------------------------------------------------------------------
# Partitioning
# ---------------------------------------------------------------------------
say "Partitioning and formatting"
swapoff -a || true
umount -R /mnt 2>/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 <<EOF
127.0.0.1 localhost
::1 localhost
127.0.1.1 $HOSTNAME.localdomain $HOSTNAME
EOF
ln -sf "/usr/share/zoneinfo/$TIMEZONE" /mnt/etc/localtime
arch-chroot /mnt hwclock --systohc
sed -i 's/^#en_US.UTF-8/en_US.UTF-8/' /mnt/etc/locale.gen
arch-chroot /mnt locale-gen
echo 'LANG=en_US.UTF-8' > /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 <<EOF
default arch.conf
timeout 3
console-mode max
editor no
EOF
boot_entry() {
cat > "/mnt/boot/loader/entries/$1.conf" <<EOF
title $2
linux /vmlinuz-$3
initrd /amd-ucode.img
initrd /$4
options $PARAMS
EOF
}
case "$KERNELS" in
"linux + linux-lts")
boot_entry arch "Arch Linux" linux initramfs-linux.img
boot_entry arch-lts "Arch Linux LTS" linux-lts initramfs-linux-lts.img ;;
"linux only")
boot_entry arch "Arch Linux" linux initramfs-linux.img ;;
"linux-lts only")
boot_entry arch-lts "Arch Linux LTS" linux-lts initramfs-linux-lts.img
sed -i 's/default arch.conf/default arch-lts.conf/' /mnt/boot/loader/loader.conf ;;
esac
# ---------------------------------------------------------------------------
# Services
# ---------------------------------------------------------------------------
arch-chroot /mnt systemctl enable NetworkManager
arch-chroot /mnt systemctl enable sshd
arch-chroot /mnt systemctl enable systemd-timesyncd
arch-chroot /mnt systemctl enable fstrim.timer
arch-chroot /mnt systemctl enable paccache.timer
arch-chroot /mnt systemctl enable reflector.timer
$INSTALL_DOCKER && {
arch-chroot /mnt systemctl enable docker
arch-chroot /mnt usermod -aG docker "$USERNAME"
}
$INSTALL_VIRT && {
arch-chroot /mnt systemctl enable libvirtd
arch-chroot /mnt usermod -aG libvirt "$USERNAME"
}
$INSTALL_PRINT && arch-chroot /mnt systemctl enable cups
[[ "$LOGIN" == LightDM* ]] && arch-chroot /mnt systemctl enable lightdm
case "$SHELL" in
Zsh) arch-chroot /mnt usermod -s /bin/zsh "$USERNAME" ;;
Fish) arch-chroot /mnt usermod -s /usr/bin/fish "$USERNAME" ;;
esac
# ---------------------------------------------------------------------------
# User desktop configuration
# ---------------------------------------------------------------------------
say "Writing i3, Picom, Polybar, Rofi, and terminal configuration"
HOME_DIR=/mnt/home/$USERNAME
CFG=$HOME_DIR/.config
mkdir -p "$CFG"/{i3,picom,polybar,rofi,dunst,ghostty,gtk-3.0}
mkdir -p "$HOME_DIR"/{Desktop,Documents,Downloads,Music,Pictures,Public,Templates,Videos,Projects,Screenshots}
mkdir -p "$HOME_DIR/.local/bin" "$HOME_DIR/.local/share/wallpapers"
cat > "$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" <<EOF
set \$mod $I3MOD
set \$term ghostty
set \$browser $BROWSER_CMD
set \$files $FILE_CMD
font pango:JetBrainsMono Nerd Font 10
floating_modifier \$mod
focus_follows_mouse $FOCUS_MOUSE
workspace_auto_back_and_forth yes
popup_during_fullscreen smart
default_border pixel 2
default_floating_border pixel 2
hide_edge_borders smart
gaps inner $GAP_INNER
gaps outer $GAP_OUTER
smart_gaps on
smart_borders on
client.focused $ACCENT $ACCENT $BG $ACCENT2 $ACCENT
client.focused_inactive $PANEL $PANEL $FG $MUTED $PANEL
client.unfocused $PANEL $PANEL $MUTED $PANEL $PANEL
client.urgent #bf616a #bf616a #ffffff #bf616a #bf616a
$MONITOR_COMMANDS
$WORKSPACE_OUTPUTS
exec --no-startup-id picom --config ~/.config/picom/picom.conf
exec --no-startup-id ~/.config/polybar/launch.sh
exec --no-startup-id dunst
exec --no-startup-id feh --no-fehbg --bg-fill ~/.local/share/wallpapers/workstation.png
exec --no-startup-id /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1
exec --no-startup-id dex --autostart --environment i3
exec --no-startup-id udiskie --tray
exec --no-startup-id xset s 600 600
exec --no-startup-id xset dpms 0 0 900
bindsym \$mod+$TERM_KEY exec \$term
bindsym \$mod+b exec \$browser
bindsym \$mod+e exec \$files
bindsym \$mod+space exec rofi -show drun
bindsym \$mod+q kill
bindsym \$mod+Shift+q exec "i3-nagbar -t warning -m 'Exit i3?' -B 'Exit' 'i3-msg exit'"
bindsym \$mod+f fullscreen toggle
bindsym \$mod+v floating toggle
bindsym \$mod+Shift+Escape exec i3lock -c ${BG#\#}
bindsym Print exec flameshot gui
bindsym \$mod+Print exec flameshot full -p ~/Screenshots
bindsym \$mod+h focus left
bindsym \$mod+j focus down
bindsym \$mod+k focus up
bindsym \$mod+l focus right
bindsym \$mod+Left focus left
bindsym \$mod+Down focus down
bindsym \$mod+Up focus up
bindsym \$mod+Right focus right
bindsym \$mod+Shift+h move left
bindsym \$mod+Shift+j move down
bindsym \$mod+Shift+k move up
bindsym \$mod+Shift+l move right
bindsym \$mod+Shift+Left move left
bindsym \$mod+Shift+Down move down
bindsym \$mod+Shift+Up move up
bindsym \$mod+Shift+Right move right
bindsym \$mod+1 workspace number 1
bindsym \$mod+2 workspace number 2
bindsym \$mod+3 workspace number 3
bindsym \$mod+4 workspace number 4
bindsym \$mod+5 workspace number 5
bindsym \$mod+6 workspace number 6
bindsym \$mod+7 workspace number 7
bindsym \$mod+8 workspace number 8
bindsym \$mod+9 workspace number 9
bindsym \$mod+0 workspace number 10
bindsym \$mod+Shift+1 move container to workspace number 1
bindsym \$mod+Shift+2 move container to workspace number 2
bindsym \$mod+Shift+3 move container to workspace number 3
bindsym \$mod+Shift+4 move container to workspace number 4
bindsym \$mod+Shift+5 move container to workspace number 5
bindsym \$mod+Shift+6 move container to workspace number 6
bindsym \$mod+Shift+7 move container to workspace number 7
bindsym \$mod+Shift+8 move container to workspace number 8
bindsym \$mod+Shift+9 move container to workspace number 9
bindsym \$mod+Shift+0 move container to workspace number 10
bindsym XF86AudioRaiseVolume exec --no-startup-id pamixer -i 5
bindsym XF86AudioLowerVolume exec --no-startup-id pamixer -d 5
bindsym XF86AudioMute exec --no-startup-id pamixer -t
bindsym XF86AudioPlay exec --no-startup-id playerctl play-pause
bindsym XF86AudioNext exec --no-startup-id playerctl next
bindsym XF86AudioPrev exec --no-startup-id playerctl previous
for_window [class="Pavucontrol|Arandr"] floating enable, move position center
EOF
PICOM_BLUR=false
$BLUR && PICOM_BLUR=true
PICOM_ANIM=false
$ANIMATIONS && PICOM_ANIM=true
cat > "$CFG/picom/picom.conf" <<EOF
backend = "glx";
vsync = true;
use-damage = true;
corner-radius = $CORNER_RADIUS;
round-borders = 1;
shadow = true;
shadow-radius = 18;
shadow-offset-x = -8;
shadow-offset-y = -8;
shadow-opacity = 0.35;
fading = true;
fade-in-step = 0.045;
fade-out-step = 0.045;
inactive-opacity = $( $BLUR && echo 0.95 || echo 1.0 );
active-opacity = 1.0;
frame-opacity = 1.0;
blur-method = "$( $BLUR && echo dual_kawase || echo none )";
blur-strength = 4;
animations = $PICOM_ANIM;
animation-stiffness = 220.0;
animation-dampening = 28.0;
animation-clamping = true;
animation-mass = 0.8;
animation-for-open-window = "zoom";
animation-for-unmap-window = "zoom";
opacity-rule = [
"100:class_g = 'Firefox'",
"100:class_g = 'Chromium'",
"100:class_g = 'Code'"
];
wintypes:
{
tooltip = { fade = true; shadow = true; opacity = 0.96; focus = true; };
dock = { shadow = false; };
dnd = { shadow = false; };
};
EOF
cat > "$CFG/polybar/config.ini" <<EOF
[colors]
background = $PANEL
background-alt = $BG
foreground = $FG
muted = $MUTED
primary = $ACCENT
secondary = $ACCENT2
alert = #bf616a
[bar/main]
width = 99%
height = 34
offset-x = 0.5%
offset-y = 8
radius = 12
fixed-center = true
background = \${colors.background}
foreground = \${colors.foreground}
line-size = 2
border-size = 0
padding-left = 2
padding-right = 2
module-margin = 1
font-0 = JetBrainsMono Nerd Font:size=10;2
modules-left = i3 xwindow
modules-center = date
modules-right = filesystem cpu memory pulseaudio eth powermenu
tray-position = right
tray-padding = 2
wm-restack = i3
override-redirect = false
enable-ipc = true
[module/i3]
type = internal/i3
pin-workspaces = true
show-urgent = true
format = <label-state>
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-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" <<EOF
configuration {
modi: "drun,run,window";
show-icons: true;
display-drun: "Apps";
font: "Noto Sans 13";
}
* {
bg: $BG;
panel: $PANEL;
fg: $FG;
muted: $MUTED;
accent: $ACCENT;
}
window {
width: 42%;
border: 2px;
border-color: @accent;
border-radius: 14px;
background-color: @panel;
}
mainbox { padding: 12px; background-color: transparent; }
inputbar {
padding: 10px;
border-radius: 10px;
background-color: @bg;
text-color: @fg;
}
listview { margin: 10px 0 0; lines: 8; spacing: 4px; }
element { padding: 9px; border-radius: 9px; }
element selected { background-color: @accent; text-color: @bg; }
element-text { text-color: inherit; }
EOF
cat > "$CFG/dunst/dunstrc" <<EOF
[global]
monitor = 0
follow = mouse
width = 360
height = 120
origin = top-right
offset = 14x54
notification_limit = 5
progress_bar = true
font = Noto Sans 10
frame_width = 2
frame_color = "$ACCENT"
corner_radius = 12
separator_color = frame
padding = 14
horizontal_padding = 14
icon_position = left
max_icon_size = 48
[urgency_low]
background = "$PANEL"
foreground = "$FG"
timeout = 4
[urgency_normal]
background = "$PANEL"
foreground = "$FG"
timeout = 6
[urgency_critical]
background = "#3b2027"
foreground = "#ffffff"
frame_color = "#bf616a"
timeout = 0
EOF
cat > "$CFG/ghostty/config" <<EOF
font-family = JetBrainsMono Nerd Font
font-size = 12
background = $BG
foreground = $FG
selection-background = $ACCENT
selection-foreground = $BG
cursor-color = $ACCENT2
cursor-text = $BG
background-opacity = 0.97
window-padding-x = 12
window-padding-y = 10
window-decoration = none
confirm-close-surface = false
copy-on-select = clipboard
EOF
cat > "$CFG/gtk-3.0/settings.ini" <<EOF
[Settings]
gtk-theme-name=Adwaita-dark
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Noto Sans 10
gtk-cursor-theme-name=Adwaita
gtk-cursor-theme-size=24
gtk-application-prefer-dark-theme=1
EOF
cat > "$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
<svg xmlns="http://www.w3.org/2000/svg" width="3840" height="2160">
<defs>
<radialGradient id="a" cx="72%" cy="18%" r="75%">
<stop offset="0" stop-color="$ACCENT" stop-opacity=".42"/>
<stop offset=".55" stop-color="$PANEL" stop-opacity=".35"/>
<stop offset="1" stop-color="$BG"/>
</radialGradient>
<radialGradient id="b" cx="12%" cy="88%" r="55%">
<stop offset="0" stop-color="$ACCENT2" stop-opacity=".18"/>
<stop offset="1" stop-color="$BG" stop-opacity="0"/>
</radialGradient>
</defs>
<rect width="100%" height="100%" fill="$BG"/>
<rect width="100%" height="100%" fill="url(#a)"/>
<rect width="100%" height="100%" fill="url(#b)"/>
</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" <<EOF
[user]
name = $FULLNAME
[init]
defaultBranch = main
[core]
editor = code --wait
[color]
ui = auto
EOF
# ESP-IDF.
if $INSTALL_ESP; then
say "Installing ESP-IDF"
arch-chroot /mnt runuser -u "$USERNAME" -- bash -lc '
mkdir -p "$HOME/esp"
git clone --recursive --depth 1 https://github.com/espressif/esp-idf.git "$HOME/esp/esp-idf"
"$HOME/esp/esp-idf/install.sh" esp32,esp32s2,esp32s3,esp32c3,esp32c6
'
cat >> "$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 <<EOF
${GREEN}${BOLD}Arch Linux with i3/Xorg is installed.${RESET}
Package management:
paru -Syu Update official and AUR packages
paru -S <package> Install a package
paru -Ss <query> Search
paru -Rns <package> 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