#!/usr/bin/env bash # alvin-install — Gum TUI Arch Linux installer with LUKS encryption # shellcheck disable=SC2034 set -Eeuo pipefail readonly LOG=/var/log/alvin-install.log readonly PASS_MIN=8 readonly ESP_SIZE="1GiB" readonly TARGET="/mnt" readonly -a PKGS_EXCLUDE=(archiso mkinitcpio-archiso mkinitcpio-nfs-utils syslinux memtest86+) skip() { local x; for x in "${PKGS_EXCLUDE[@]}"; do [[ "$x" == "$1" ]] && return 0; done; return 1; } DISK="" KEYMAP="" USERNAME="" PASS="" HOSTNAME="" GIT_NAME="" GIT_EMAIL="" TIMEZONE="" LOCALE="" LUKS="true" FILESYSTEM="ext4" log() { echo "[$(date '+%H:%M:%S')] $*" >> "$LOG"; } cleanup() { set +e rm -f "${TARGET}/etc/sudoers.d/99-end4-install" 2>/dev/null umount -R "$TARGET" 2>/dev/null cryptsetup close cryptroot 2>/dev/null true } trap cleanup EXIT trap 'gum style --foreground 9 --bold "Failed at line $LINENO. See $LOG"' ERR (( EUID == 0 )) || { echo "Run as root" >&2; exit 1; } die() { gum style --foreground 9 --bold "ERROR: $*"; exit 1; } info() { gum style --foreground 12 --bold "$*"; } ok() { gum style --foreground 10 --bold "$*"; } warn() { gum style --foreground 11 --bold "$*"; } header() { gum style \ --foreground "#FAFAFA" --background "#6C91BF" \ --padding "1 2" --bold --align center --width 70 \ "$1" } divider() { gum style --foreground 8 "──────────────────────────────────────────────────────────" } part() { [[ "$DISK" =~ [0-9]$ ]] && echo "${DISK}p$1" || echo "${DISK}$1"; } valid_username() { [[ "$1" =~ ^[a-z_][a-z0-9_-]*$ ]] && (( ${#1} <= 32 )); } check_prereqs() { command -v gum >/dev/null 2>&1 || die "gum is not installed" [[ -d /sys/firmware/efi ]] || die "UEFI not detected. This installer requires UEFI boot." } # ── Screens ─────────────────────────────────────────────────────────────────── screen_welcome() { clear header "Alvin Arch Linux Installer" echo "" gum style --foreground 15 --align center \ "This will install Arch Linux with:" \ " * LUKS disk encryption (default)" \ " * GRUB bootloader" \ " * NetworkManager" \ " * Your user + git config" \ "" divider echo "" gum confirm --affirmative "Begin installation" --negative "Abort" \ "Ready to begin?" || die "Installation aborted by user." } screen_keymap() { clear header "Keyboard Layout" echo "" gum style --foreground 15 \ "Select the keyboard layout of your physical keyboard." \ "This affects passwords and the LUKS passphrase at boot." \ "" \ "The default is the US layout (us)." echo "" if gum confirm --affirmative "Use us (default)" --negative "Choose another layout" \ "Use US keyboard layout (us)?"; then KEYMAP="us" else KEYMAP=$(localectl list-keymaps | gum filter --header "+ Keymap" --placeholder "Search keymaps...") \ || die "No keymap selected." fi [[ -z "$KEYMAP" ]] && die "No keymap selected." loadkeys "$KEYMAP" 2>/dev/null || true log "Keymap: $KEYMAP" } screen_network() { clear header "Network Setup" echo "" gum style --foreground 15 --align center \ "A network connection is required for installation." \ "" \ "When nmtui opens:" \ " 1. Select 'Activate a connection'" \ " 2. Use arrow keys to pick your WiFi network" \ " 3. Enter the password and press Enter" \ " 4. Press Esc to exit nmtui" echo "" divider echo "" gum confirm --affirmative "Open nmtui" --negative "Skip (already connected)" \ "Open NetworkManager TUI?" && nmtui || true echo "" info "Verifying network connection..." local try=0 while ! ping -c1 -W3 archlinux.org &>/dev/null; do try=$((try + 1)) if (( try >= 5 )); then gum confirm --affirmative "Retry" --negative "Abort" \ "Network check failed. Retry?" || die "No network connection." try=0 fi sleep 2 done ok "Network verified." log "Network: connected" } screen_disk() { clear header "Select Installation Disk" echo "" local live_dev="" if [[ -d /run/archiso/bootmnt ]]; then local src src=$(findmnt -no SOURCE /run/archiso/bootmnt 2>/dev/null || true) [[ -n "$src" ]] && live_dev="/dev/$(lsblk -no PKNAME "$src" 2>/dev/null | head -n1)" fi local disks=() while IFS= read -r line; do local name size model name=$(echo "$line" | awk '{print $1}') size=$(echo "$line" | awk '{print $2}') model=$(echo "$line" | awk '{$1=$2=""; sub(/^[ \t]+/,""); print}') [[ "$name" == "$live_dev" ]] && continue disks+=("${name} ${size} ${model}") done < <(lsblk -dpno NAME,SIZE,MODEL,TYPE | awk '$NF=="disk" {print $1,$2,$3}') (( ${#disks[@]} > 0 )) || die "No disks found." while true; do local choice choice=$(gum choose --header "+ Target disk (live USB hidden)" "${disks[@]}") \ || die "No disk selected." DISK=$(echo "$choice" | awk '{print $1}') gum confirm --affirmative "Wipe ${DISK}" --negative "Go back" \ "WARNING: ALL data on ${DISK} will be destroyed!" && break done log "Disk: $DISK" } screen_luks() { clear header "Disk Encryption" echo "" gum style --foreground 15 --align center \ "LUKS encrypts your root filesystem." \ "The passphrase is the same as your login password." \ "Your data stays protected if the device is lost or stolen." echo "" echo "" if gum confirm --affirmative "Enable LUKS encryption" --negative "No encryption" \ "Enable LUKS disk encryption? (default: yes)"; then LUKS="true" else LUKS="false" fi log "LUKS: $LUKS" } screen_filesystem() { clear header "Filesystem" echo "" FILESYSTEM=$(gum choose --header "+ Root filesystem" "ext4" "btrfs") \ || die "No filesystem selected." log "Filesystem: $FILESYSTEM" } screen_username() { clear header "Create User" echo "" local input input=$(gum input --placeholder "username" --char-limit 32 --header "+ Username") \ || die "No username entered." [[ -z "$input" ]] && die "Username cannot be empty." valid_username "$input" || die "Invalid username. Use lowercase letters, digits, _ or -." USERNAME="$input" log "Username: $USERNAME" } screen_password() { clear header "Set Password" echo "" gum style --foreground 15 \ "This password will be used for login and LUKS disk encryption." echo "" local pass1 pass2 pass1=$(gum input --password --header "+ Password") || die "No password entered." (( ${#pass1} >= PASS_MIN )) || die "Password must be at least ${PASS_MIN} characters." pass2=$(gum input --password --header "+ Confirm password") || die "No confirmation entered." [[ "$pass1" == "$pass2" ]] || die "Passwords do not match." PASS="$pass1" log "Password: set (${#PASS} chars)" } screen_hostname() { clear header "Hostname" echo "" HOSTNAME=$(gum input --placeholder "archlinux" --char-limit 64 --header "+ Hostname") \ || die "No hostname entered." [[ -z "$HOSTNAME" ]] && HOSTNAME="archlinux" log "Hostname: $HOSTNAME" } screen_git() { clear header "Git Configuration" echo "" GIT_NAME=$(gum input --value "$USERNAME" --header "+ Git name") \ || die "No git name entered." GIT_EMAIL=$(gum input --placeholder "user@example.com" --header "+ Git email") \ || die "No git email entered." [[ -z "$GIT_EMAIL" ]] && die "Git email cannot be empty." log "Git: $GIT_NAME <$GIT_EMAIL>" } screen_timezone() { clear header "Timezone" echo "" TIMEZONE=$(timedatectl list-timezones | gum filter --header "+ Timezone" --placeholder "Search...") \ || die "No timezone selected." log "Timezone: $TIMEZONE" } screen_locale() { clear header "Locale" echo "" local locales locales=$(sed -n 's/^#\?\([a-z][a-z]*_[A-Z][A-Z]\.UTF-8\) UTF-8.*/\1/p' /etc/locale.gen | sort -u) [[ -z "$locales" ]] && locales="en_US.UTF-8" LOCALE=$(echo "$locales" | gum filter --header "+ Locale" --placeholder "Search...") \ || die "No locale selected." log "Locale: $LOCALE" } screen_summary() { clear header "Installation Summary" echo "" local enc_status="No" [[ "$LUKS" == "true" ]] && enc_status="Yes (uses login password)" gum style --foreground 15 --border normal --border-foreground 8 \ --padding "0 2" --width 60 \ "Disk: ${DISK}" \ "Encryption: ${enc_status}" \ "Filesystem: ${FILESYSTEM}" \ "Username: ${USERNAME}" \ "Hostname: ${HOSTNAME}" \ "Keymap: ${KEYMAP}" \ "Locale: ${LOCALE}" \ "Timezone: ${TIMEZONE}" \ "Git: ${GIT_NAME} <${GIT_EMAIL}>" echo "" gum confirm --affirmative "Install" --negative "Abort" \ "Proceed with installation?" || die "Installation aborted by user." } # ── Install steps ───────────────────────────────────────────────────────────── install_partition() { info "Partitioning ${DISK}..." log "Partitioning $DISK" wipefs -af "$DISK" 2>>"$LOG" sgdisk --zap-all "$DISK" 2>>"$LOG" parted -s "$DISK" \ mklabel gpt \ mkpart ESP fat32 1MiB "${ESP_SIZE}" \ set 1 esp on \ mkpart root "${ESP_SIZE}" 100% partprobe "$DISK" udevadm settle --timeout=30 wipefs -af "$(part 1)" "$(part 2)" ok "Partitioned." } install_luks() { [[ "$LUKS" != "true" ]] && return info "Setting up LUKS encryption on $(part 2)..." log "LUKS format $(part 2)" printf '%s' "$PASS" | cryptsetup luksFormat \ --batch-mode --type luks2 --key-file=- "$(part 2)" printf '%s' "$PASS" | cryptsetup open \ --key-file=- "$(part 2)" cryptroot ok "LUKS unlocked." } install_format_mount() { info "Formatting..." mkfs.fat -F32 -n ESP "$(part 1)" 2>>"$LOG" local root_dev if [[ "$LUKS" == "true" ]]; then root_dev="/dev/mapper/cryptroot" else root_dev="$(part 2)" fi if [[ "$FILESYSTEM" == "btrfs" ]]; then mkfs.btrfs -f -L root "$root_dev" 2>>"$LOG" else mkfs.ext4 -F -L root "$root_dev" 2>>"$LOG" fi info "Mounting..." mount "$root_dev" "$TARGET" mkdir -p "${TARGET}/boot" mount "$(part 1)" "${TARGET}/boot" ok "Formatted and mounted." } install_pacstrap() { info "Building package list..." local pkglist="/run/archiso/bootmnt/arch/pkglist.x86_64.txt" local pkgs=() local dropped=() local filter_ok=false if [[ -f "$pkglist" ]]; then # The medium's pkglist comes from `pacman -Q`: lines are "name version". # Parse only the first field, then drop names not resolvable in the live # session's repos (e.g. local-aur packages are not configured there). info "Syncing package databases for resolvability check..." if pacman -Syy --noconfirm >>"$LOG" 2>&1; then declare -A available=() local db_line while IFS= read -r db_line; do [[ -n "$db_line" ]] && available["$db_line"]=1 done < <(pacman -Slq 2>>"$LOG" || true) (( ${#available[@]} > 0 )) && filter_ok=true else warn "pacman -Syy failed (see $LOG) — installing without resolvability filter." fi local line pkg while IFS= read -r line; do [[ -z "$line" ]] && continue pkg=${line%% *} skip "$pkg" && continue if $filter_ok && [[ -z "${available[$pkg]:-}" ]]; then dropped+=("$pkg") continue fi pkgs+=("$pkg") done < "$pkglist" fi if (( ${#dropped[@]} > 0 )); then warn "Skipped ${#dropped[@]} pkglist package(s) not found in configured repos:" gum style --foreground 11 "${dropped[@]/#/ - }" local d for d in "${dropped[@]}"; do log "Pacstrap: skipped unresolvable: $d" done fi # Hardcoded essentials are never filtered — they must be in allowed repos. for p in base linux linux-firmware linux-cachyos linux-cachyos-headers \ networkmanager grub efibootmgr sudo git intel-ucode amd-ucode \ mkinitcpio cryptsetup dosfstools e2fsprogs btrfs-progs \ cachyos-keyring cachyos-mirrorlist; do local found=false for existing in "${pkgs[@]+"${pkgs[@]}"}"; do [[ "$existing" == "$p" ]] && found=true && break done $found || pkgs+=("$p") done info "Installing ${#pkgs[@]} packages (this may take a while)..." log "Pacstrap: ${#pkgs[@]} packages, ${#dropped[@]} skipped" pacstrap "$TARGET" "${pkgs[@]}" --noconfirm 2>>"$LOG" ok "Packages installed." } install_fstab() { info "Generating fstab..." genfstab -U "$TARGET" >> "${TARGET}/etc/fstab" ok "fstab generated." } install_cachyos_repo() { info "Configuring CachyOS repository..." if ! grep -q '\[cachyos\]' "${TARGET}/etc/pacman.conf" 2>/dev/null; then cat >> "${TARGET}/etc/pacman.conf" <<'REPO' [cachyos] Include = /etc/pacman.d/cachyos-mirrorlist REPO fi # [local-aur] lives only on the live medium (file:///opt/localrepo). The # target's pacman.conf does not normally contain it - pacstrap is run # without -P, so it keeps the stock file shipped by the pacman package - # but drop it anyway in case that ever changes. sed -i '/^\[local-aur\]$/,/^$/d' "${TARGET}/etc/pacman.conf" 2>/dev/null || true ok "CachyOS repo configured." } install_timezone() { info "Setting timezone..." arch-chroot "$TARGET" ln -sf "/usr/share/zoneinfo/${TIMEZONE}" /etc/localtime arch-chroot "$TARGET" hwclock --systohc || true ok "Timezone set." } install_locale() { info "Setting locale..." arch-chroot "$TARGET" sed -i "s/^#${LOCALE}/${LOCALE}/" /etc/locale.gen arch-chroot "$TARGET" locale-gen echo "LANG=${LOCALE}" > "${TARGET}/etc/locale.conf" echo "KEYMAP=${KEYMAP}" > "${TARGET}/etc/vconsole.conf" ok "Locale and keymap set." } install_hostname() { info "Setting hostname..." echo "$HOSTNAME" > "${TARGET}/etc/hostname" cat > "${TARGET}/etc/hosts" </dev/null; then mkdir -p "${TARGET}/etc/NetworkManager/system-connections" cp -a "$nm_dir"/* "${TARGET}/etc/NetworkManager/system-connections/" chmod 600 "${TARGET}/etc/NetworkManager/system-connections"/* log "WiFi profiles copied." fi ok "NetworkManager enabled." } install_user() { info "Creating user..." cp -rT /etc/skel "${TARGET}/etc/skel" arch-chroot "$TARGET" useradd -m -G wheel -s /bin/bash "$USERNAME" printf '%s:%s\n' "$USERNAME" "$PASS" | arch-chroot "$TARGET" chpasswd arch-chroot "$TARGET" passwd -l root echo '%wheel ALL=(ALL:ALL) ALL' > "${TARGET}/etc/sudoers.d/10-wheel" chmod 440 "${TARGET}/etc/sudoers.d/10-wheel" ok "User ${USERNAME} created (wheel group, sudo enabled). Root locked." } install_git() { info "Configuring git..." arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ git config --global user.name "$GIT_NAME" arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ git config --global user.email "$GIT_EMAIL" ok "Git configured for ${USERNAME}." } install_end4_dots() { if [[ ! -d /opt/dots-hyprland ]]; then warn "No staged dots-hyprland at /opt/dots-hyprland — skipping end-4 dotfiles." return fi info "Installing end-4 dotfiles (this may take a while)..." local home_in_target="/home/${USERNAME}" local repo_in_target="${home_in_target}/.dots-hyprland" local repo_on_host="${TARGET}${repo_in_target}" local dots_ok=true local setups_done=true cp -a /opt/dots-hyprland "$repo_on_host" chown -R "${USERNAME}:${USERNAME}" "$repo_on_host" echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "${TARGET}/etc/sudoers.d/99-end4-install" chmod 440 "${TARGET}/etc/sudoers.d/99-end4-install" # Everything appended to $LOG from here belongs to this attempt; used # further down to report which steps end-4 had to skip. local log_mark=0 if [[ -f "$LOG" ]]; then log_mark=$(wc -l < "$LOG") fi # stdin feeds "i" to end-4's x() [R/e:i] failure prompt, which otherwise # blocks forever on a TTY whose prompt text is redirected to $LOG. # Its systemd lines cannot work from inside this chroot: the medium's /run # is bind-mounted in, so `systemctl --machine=…@.host --user enable … --now` # has no target session (and --now would start services on the live medium # anyway) - they fail on every install. "i" lets the run carry on with the # venv, groups and config sync instead of aborting at the first such line; # the usable part of that step is re-applied below and every skip is # reported afterwards. # --skip-alldeps: every dependency was already installed at ISO build time # (customize_airootfs.sh runs ./setup install-deps), so this only copies # configs, sets up services and builds the python venv from the warm cache. if ! arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ env HOME="$home_in_target" bash -c \ "cd ${repo_in_target} && ./setup install -f -s --skip-alldeps" \ < <(yes i) >>"$LOG" 2>&1; then warn "end-4 setup failed, retrying files only (see $LOG)." setups_done=false arch-chroot "$TARGET" runuser -u "$USERNAME" -- \ env HOME="$home_in_target" bash -c \ "cd ${repo_in_target} && ./setup install -f -s --skip-alldeps --skip-allsetups" \ < <(yes i) >>"$LOG" 2>&1 || dots_ok=false fi rm -f "${TARGET}/etc/sudoers.d/99-end4-install" if ! arch-chroot "$TARGET" id -nG "$USERNAME" | grep -qw input; then arch-chroot "$TARGET" groupadd -f i2c arch-chroot "$TARGET" usermod -aG video,i2c,input "$USERNAME" warn "end-4 group setup was missing and has been applied." fi # ── steps end-4 cannot take from inside the chroot ─────────────────────── # `enable` alone is a pure file operation and needs no running session, so # redo those here - without --now, since the target is not booted yet. local mods_conf="${TARGET}/etc/modules-load.d/i2c-dev.conf" if [[ ! -f "$mods_conf" ]]; then mkdir -p "$(dirname "$mods_conf")" echo i2c-dev > "$mods_conf" fi if [[ -f "${TARGET}/usr/lib/systemd/system/bluetooth.service" ]]; then arch-chroot "$TARGET" systemctl enable bluetooth.service >>"$LOG" 2>&1 \ || warn "Could not enable bluetooth.service (see $LOG)." fi if [[ -f "${TARGET}/usr/lib/systemd/user/ydotool.service" ]]; then arch-chroot "$TARGET" runuser -u "$USERNAME" -- env HOME="$home_in_target" \ systemctl --user enable ydotool.service >>"$LOG" 2>&1 \ || warn "Could not enable ydotool.service for ${USERNAME} (see $LOG)." fi # Everything end-4 answered "i" to. systemctl/gsettings are the lines known # to be unusable in this chroot; anything else it had to skip is worth # telling the user about. local -a ignored=() if ((log_mark > 0)); then mapfile -t ignored < <( sed -n "$((log_mark + 1)),\$p" "$LOG" | sed $'s/\x1b\\[[0-9;]*m//g' | sed -n 's/^.*Command "\(.*\)" has failed but ignored by user\..*$/\1/p' ) fi if ((${#ignored[@]} > 0)); then log "end-4 skipped ${#ignored[@]} step(s): ${ignored[*]}" local step local -a skipped=() for step in "${ignored[@]}"; do case "$step" in *systemctl* | *gsettings*) ;; *) skipped+=("$step") ;; esac done if ((${#skipped[@]} > 0)); then warn "end-4 had to skip: ${skipped[*]} (see $LOG)" fi fi if ! $setups_done; then warn "end-4 setup did not complete — rerun as ${USERNAME}: cd ~/.dots-hyprland && ./setup install-setups" fi # end-4's install step rsync --delete's the whole ~/.config/quickshell # directory (sdata/subcmd-install/3.files-legacy.sh), which drops the # end4-pC fork skel put into the user's home - restore it afterwards. local fork_src="/etc/skel/.config/quickshell/end4-pC" local fork_dst="${TARGET}${home_in_target}/.config/quickshell/end4-pC" if [[ -d "$fork_src" ]]; then mkdir -p "$(dirname "$fork_dst")" rm -rf "$fork_dst" cp -a "$fork_src" "$fork_dst" chown -R "${USERNAME}:${USERNAME}" "$fork_dst" fi # Same for fish: end-4 syncs ~/.config/fish with rsync --delete, keeping # only what upstream ships (auto-Hypr.fish, config.fish, fish_variables), # which drops skel's extras (fisher/nvm functions, completions, # fish_plugins) and restores its own `alias q 'qs -c ii'`. The default # quickshell config on this ISO is end4-pC, so put the extras back and # repoint that one alias. local fish_src="/etc/skel/.config/fish" local fish_dst="${TARGET}${home_in_target}/.config/fish" if [[ -d "$fish_src" && -d "$fish_dst" ]]; then rsync -a --exclude config.fish --exclude fish_variables \ "$fish_src/" "$fish_dst/" if [[ -f "${fish_dst}/config.fish" ]]; then sed -i "s/alias q 'qs -c ii'/alias q 'qs -c end4-pC'/" "${fish_dst}/config.fish" grep -qF "alias q 'qs -c end4-pC'" "${fish_dst}/config.fish" \ || warn "fish 'q' alias still points at the upstream ii config (see $LOG)." fi chown -R "${USERNAME}:${USERNAME}" "$fish_dst" fi local configs_ok=true [[ -e "${TARGET}${home_in_target}/.config/hypr/hyprland.lua" \ && -d "${TARGET}${home_in_target}/.config/quickshell/ii" \ && -d "${TARGET}${home_in_target}/.config/quickshell/end4-pC" ]] || configs_ok=false # Files alone are not enough: with --skip-alldeps the dependencies must # have come from the ISO. hyprland and quickshell are installed at ISO # build time, so their binaries missing here means pacstrap dropped them # (they were not resolvable in the live repos). The venv is created from # the image's uv cache at install time, so a missing python means the # python step of end-4's setup never got to run. if $configs_ok; then [[ -x "${TARGET}/usr/bin/hyprctl" && -x "${TARGET}/usr/bin/quickshell" ]] \ || configs_ok=false [[ -x "${TARGET}${home_in_target}/.local/state/quickshell/.venv/bin/python" ]] \ || configs_ok=false fi if [[ -d /etc/skel/.config/hypr/custom ]]; then cp -rfT /etc/skel/.config/hypr/custom "${TARGET}${home_in_target}/.config/hypr/custom" chown -R "${USERNAME}:${USERNAME}" "${TARGET}${home_in_target}/.config/hypr/custom" fi # Presets ship with absolute paths from the build machine; repoint them at # the home that was just created (the images they name are not shipped). local presets="${TARGET}${home_in_target}/.config/illogical-impulse/presets" local preset for preset in "$presets"/*.json; do [[ -f "$preset" ]] || continue sed -i "s|/home/alvin/|/home/${USERNAME}/|g" "$preset" done if $dots_ok && $configs_ok; then log "end-4 dotfiles installed for ${USERNAME} (setups_done=${setups_done} skipped=${#ignored[@]})." ok "end-4 dotfiles installed." else log "end-4 dotfiles incomplete for ${USERNAME} (setup_exit_ok=${dots_ok} configs=${configs_ok} setups=${setups_done} skipped=${#ignored[@]})." warn "end-4 dotfiles incomplete (see $LOG) — as ${USERNAME}, run: cd ~/.dots-hyprland && ./setup install" fi } install_mkinitcpio() { info "Generating initramfs..." if [[ "$LUKS" == "true" ]]; then mkdir -p "${TARGET}/etc/mkinitcpio.conf.d" cat > "${TARGET}/etc/mkinitcpio.conf.d/alvin.conf" </dev/null || true unset PASS log "Installation complete." ok "Installation complete!" echo "" gum style --foreground 10 --align center \ "Arch Linux installed successfully." \ "" \ "Run 'reboot' to boot into your new system." echo "" } # ── Main ────────────────────────────────────────────────────────────────────── main() { check_prereqs screen_welcome screen_keymap screen_network screen_disk screen_luks screen_filesystem screen_username screen_password screen_hostname screen_git screen_timezone screen_locale screen_summary install_partition install_luks install_format_mount install_pacstrap install_fstab install_cachyos_repo install_timezone install_locale install_hostname install_network install_user install_git install_end4_dots install_mkinitcpio install_bootloader install_cleanup } main "$@"