#!/bin/bash export SHELL=$(command -v bash) # ============================================================================== # Shorin Arch Setup - Main Installer (v1.2) # ============================================================================== BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPTS_DIR="$BASE_DIR/scripts" STATE_FILE="$BASE_DIR/.install_progress" # --- Source Visual Engine --- if [ -f "$SCRIPTS_DIR/00-utils.sh" ]; then source "$SCRIPTS_DIR/00-utils.sh" else echo "Error: 00-utils.sh not found." exit 1 fi # --- Global Cleanup on Exit --- cleanup() { rm -f "/tmp/shorin_install_user" } trap cleanup EXIT # --- Global Trap (Restore Cursor on Exit) --- cleanup_on_exit() { tput cnorm } trap cleanup_on_exit EXIT # --- Environment --- export DEBUG=${DEBUG:-0} export CN_MIRROR=${CN_MIRROR:-0} check_root chmod +x "$SCRIPTS_DIR"/*.sh # --- ASCII Banners --- banner1() { cat << "EOF" ██████ ██ ██ ██████ ███████ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ███████ ██ ██ ██████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██████ ██ ██ ██████ ██ ██ ██ ██ ████ EOF } export SHORIN_BANNER_IDX=0 show_banner() { clear echo -e "${H_CYAN}" case $SHORIN_BANNER_IDX in 0) banner1 ;; esac echo -e "${NC}" echo -e "${DIM} :: Arch Linux Automation ::${NC}" echo -e "" } # --- Desktop Selection Menu (FZF Powered) --- select_desktop() { if ! command -v fzf &> /dev/null; then echo -e " ${DIM}Installing fzf for interactive menu...${NC}" pacman -Sy --noconfirm --needed fzf >/dev/null 2>&1 fi local MENU_ITEMS=( "No_Desktop|none" "Surprise Me!|random" "" "KDE_Plasma ${H_YELLOW}(Recommended)${NC}|kde" "" "Shorin_DMS_Niri ${H_YELLOW}(Recommended)${NC}|shorindmsgit" "Shorin_Noctalia_Niri|shorinnocniri" "Shorin_Niri|shorinniri" "Minimal_Niri|minimalniri" "Shorin_DMS_Hyprland_Scrolling|hyprniri" "" "GNOME |gnome" "" "Quickshell: End4--illogical_impulse|end4" "Quickshell: DMS--DankMaterialShell|dms" "Quickshell: Caelestia|caelestia" ) while true; do show_banner local fzf_list=() local idx=1 for item in "${MENU_ITEMS[@]}"; do [[ -z "$item" ]] && continue local name="${item%%|*}" local val="${item##*|}" local colored_idx="${H_CYAN}[${idx}]${NC}" if [ $idx -lt 10 ]; then fzf_list+=("${colored_idx} ${name}\t${val}\t${name}") else fzf_list+=("${colored_idx} ${name}\t${val}\t${name}") fi ((idx++)) done local selected selected=$(printf "%b\n" "${fzf_list[@]}" | sed '/^[[:space:]]*$/d' | fzf \ --ansi \ --delimiter='\t' \ --with-nth=1 \ --info=hidden \ --layout=reverse \ --border="rounded" \ --border-label=" Select Desktop Environment " \ --border-label-pos=5 \ --color="marker:cyan,pointer:cyan,label:yellow" \ --header=" [J/K] Select | [Enter] confirm" \ --pointer=">" \ --bind 'j:down,k:up,ctrl-c:abort,esc:abort' \ --height=~20) local fzf_status=$? if [ $fzf_status -eq 130 ]; then echo -e "\n ${H_RED}>>> Installation aborted by user.${NC}" exit 130 fi if [ -z "$selected" ]; then continue; fi export DESKTOP_ENV="$(echo "$selected" | awk -F'\t' '{print $2}')" local selected_name="$(echo "$selected" | awk -F'\t' '{print $3}')" if [ "$DESKTOP_ENV" == "random" ]; then local POOL=() for item in "${MENU_ITEMS[@]}"; do [[ -z "$item" ]] && continue local oid="${item##*|}" if [[ "$oid" != "none" && "$oid" != "random" ]]; then POOL+=("$item") fi done local rand_idx=$(( RANDOM % ${#POOL[@]} )) local final_item="${POOL[$rand_idx]}" local final_name="${final_item%%|*}" export DESKTOP_ENV="${final_item##*|}" echo -e "\n ${H_CYAN}>>> Randomly selected:${NC} ${BOLD}${final_name}${NC}" read -p "$(echo -e " ${H_YELLOW}Continue with this selection? [Y/n]: ${NC}")" confirm if [[ "${confirm,,}" == "n" ]]; then continue; else break; fi else log "Selected: ${selected_name}" sleep 0.5 break fi done } # --- Optional Modules Selection Menu (FZF Powered) --- select_optional_modules() { local OPTIONAL_MENU=( "Windows Linux Dualboot Setup|02a-dualboot-fix.sh" "GPU Drivers|03b-gpu-driver.sh" "Grub Themes|07-grub-theme.sh" "Common Apps|99-apps.sh" ) show_banner local fzf_list=() for item in "${OPTIONAL_MENU[@]}"; do local name="${item%%|*}" local val="${item##*|}" fzf_list+=(" ${name}\t${val}") done # 核心修复:引入 --expect=ctrl-x,enter 来拦截按键动作 local selected_raw selected_raw=$(printf "%b\n" "${fzf_list[@]}" | fzf \ --multi \ --delimiter='\t' \ --with-nth=1 \ --layout=reverse \ --border="rounded" \ --border-label=" Select Optional Modules " \ --border-label-pos=5 \ --color="marker:cyan,pointer:cyan,label:yellow" \ --header=" [TAB]: Toggle | [CTRL-X]: Skip All | [ENTER]: Confirm " \ --pointer=">" \ --expect=ctrl-x,enter \ --bind 'start:select-all,ctrl-a:select-all,ctrl-d:deselect-all,ctrl-c:abort,esc:abort,j:down,k:up' \ --height=~20) local fzf_status=$? if [ $fzf_status -eq 130 ]; then echo -e "\n ${H_RED}>>> Installation aborted by user.${NC}" exit 130 fi OPTIONAL_MODULES=() if [ -n "$selected_raw" ]; then # 解析 FZF 输出:第一行是按下的键,后面是选中的内容 local key key=$(head -n 1 <<< "$selected_raw") local selected_items selected_items=$(sed '1d' <<< "$selected_raw") # 完美解决“回车默认选中光标项”的问题:用户直接按 Ctrl-X 即可退出并清空 if [[ "$key" == "ctrl-x" ]]; then log "Skipping all optional modules..." sleep 0.5 else if [ -n "$selected_items" ]; then # 利用 awk 过滤掉空行,防止产生空元素 mapfile -t OPTIONAL_MODULES < <(echo "$selected_items" | awk -F'\t' '{if ($2 != "") print $2}') fi fi fi } sys_dashboard() { echo -e "${H_BLUE}╔════ SYSTEM DIAGNOSTICS ══════════════════════════════╗${NC}" echo -e "${H_BLUE}║${NC} ${BOLD}Kernel${NC} : $(uname -r)" echo -e "${H_BLUE}║${NC} ${BOLD}User${NC} : $(whoami)" echo -e "${H_BLUE}║${NC} ${BOLD}Desktop${NC} : ${H_CYAN}${DESKTOP_ENV^^}${NC}" echo -e "${H_BLUE}║${NC} ${BOLD}Modules${NC} : ${#OPTIONAL_MODULES[@]} optional module(s) selected" if [ "$CN_MIRROR" == "1" ]; then echo -e "${H_BLUE}║${NC} ${BOLD}Network${NC} : ${H_YELLOW}CN Optimized (Manual)${NC}" elif [ "$DEBUG" == "1" ]; then echo -e "${H_BLUE}║${NC} ${BOLD}Network${NC} : ${H_RED}DEBUG FORCE (CN Mode)${NC}" else echo -e "${H_BLUE}║${NC} ${BOLD}Network${NC} : Global Default" fi if [ -f "$STATE_FILE" ]; then done_count=$(wc -l < "$STATE_FILE") echo -e "${H_BLUE}║${NC} ${BOLD}Progress${NC} : Resuming ($done_count steps recorded)" fi echo -e "${H_BLUE}╚══════════════════════════════════════════════════════╝${NC}" echo "" } # --- Main Execution --- select_desktop select_optional_modules clear show_banner sys_dashboard MANDATORY_MODULES=( "00-btrfs-init.sh" "01-base.sh" "02-musthave.sh" "03-user.sh" "03c-snapshot-before-desktop.sh" "05-verify-desktop.sh" ) ALL_MODULES=("${MANDATORY_MODULES[@]}" "${OPTIONAL_MODULES[@]}") case "$DESKTOP_ENV" in shorinniri) ALL_MODULES+=("04-niri-setup.sh") ;; minimalniri) ALL_MODULES+=("04j-minimal-niri.sh") ;; kde) ALL_MODULES+=("04b-kdeplasma-setup.sh") ;; end4) ALL_MODULES+=("04e-illogical-impulse-end4-quickshell.sh") ;; dms) ALL_MODULES+=("04c-dms-quickshell.sh") ;; shorindmsgit) ALL_MODULES+=("04h-shorindms-quickshell.sh"); export SHORIN_DMS_GIT=1 ;; hyprniri) ALL_MODULES+=("04i-shorin-hyprniri-quickshell.sh") ;; shorinnocniri) ALL_MODULES+=("04k-shorin-noctalia-quickshell.sh") ;; caelestia) ALL_MODULES+=("04g-caelestia-quickshell.sh") ;; gnome) ALL_MODULES+=("04d-gnome.sh") ;; none) log "Skipping Desktop Environment installation." ;; *) warn "Unknown selection, skipping desktop setup." ;; esac mapfile -t MODULES < <(printf "%s\n" "${ALL_MODULES[@]}" | sort -u) if [ ! -f "$STATE_FILE" ]; then touch "$STATE_FILE"; fi TOTAL_STEPS=${#MODULES[@]} CURRENT_STEP=0 log "Initializing installer sequence..." sleep 0.5 # --- Reflector Mirror Update (State Aware) --- section "Pre-Flight" "Mirrorlist Optimization" if grep -q "^REFLECTOR_DONE$" "$STATE_FILE"; then echo -e " ${H_GREEN}✔${NC} Mirrorlist previously optimized." echo -e " ${DIM} Skipping Reflector steps (Resume Mode)...${NC}" else log "Checking Reflector..." exe pacman -S --noconfirm --needed reflector CURRENT_TZ=$(readlink -f /etc/localtime) REFLECTOR_ARGS="--protocol https -a 12 -f 10 --sort rate --save /etc/pacman.d/mirrorlist --verbose" if [[ "$CURRENT_TZ" == *"Shanghai"* ]]; then echo "" echo -e "${H_YELLOW}╔══════════════════════════════════════════════════════════════════╗${NC}" echo -e "${H_YELLOW}║ DETECTED TIMEZONE: Asia/Shanghai ║${NC}" echo -e "${H_YELLOW}║ Refreshing mirrors in China can be slow. ║${NC}" echo -e "${H_YELLOW}║ Do you want to force refresh mirrors with Reflector? ║${NC}" echo -e "${H_YELLOW}╚══════════════════════════════════════════════════════════════════╝${NC}" echo "" read -t 60 -p "$(echo -e " ${H_CYAN}Run Reflector?[y/N] (Default No in 60s): ${NC}")" choice if [ $? -ne 0 ]; then echo ""; fi choice=${choice:-N} if [[ "$choice" =~ ^[Yy]$ ]]; then log "Running Reflector for China..." if exe reflector $REFLECTOR_ARGS -c China; then success "Mirrors updated." else warn "Reflector failed. Continuing with existing mirrors." fi else log "Skipping mirror refresh." fi else log "Detecting location for optimization..." COUNTRY_CODE=$(curl -s --max-time 2 https://ipinfo.io/country) if [ -n "$COUNTRY_CODE" ]; then info_kv "Country" "$COUNTRY_CODE" "(Auto-detected)" log "Running Reflector for $COUNTRY_CODE..." if ! exe reflector $REFLECTOR_ARGS -c "$COUNTRY_CODE"; then warn "Country specific refresh failed. Trying global speed test..." exe reflector $REFLECTOR_ARGS fi else warn "Could not detect country. Running global speed test..." exe reflector $REFLECTOR_ARGS --latest 25 fi success "Mirrorlist optimized." fi echo "REFLECTOR_DONE" >> "$STATE_FILE" fi # ---- update keyring----- section "Pre-Flight" "Update Keyring" exe pacman -Sy exe pacman -S --noconfirm archlinux-keyring # --- Global Update --- section "Pre-Flight" "System update" log "Ensuring system is up-to-date..." if exe pacman -Syu --noconfirm; then success "System Updated." else error "System update failed. Check your network." exit 1 fi # --- Module Loop --- for module in "${MODULES[@]}"; do [[ -z "$module" ]] && continue CURRENT_STEP=$((CURRENT_STEP + 1)) script_path="$SCRIPTS_DIR/$module" if [ ! -f "$script_path" ]; then error "Module not found: $module" continue fi if grep -q "^${module}$" "$STATE_FILE"; then echo -e " ${H_GREEN}✔${NC} Module ${BOLD}${module}${NC} already completed." echo -e " ${DIM} Skipping... (Delete .install_progress to force run)${NC}" continue fi section "Module ${CURRENT_STEP}/${TOTAL_STEPS}" "$module" bash "$script_path" exit_code=$? if [ $exit_code -eq 0 ]; then echo "$module" >> "$STATE_FILE" success "Module $module completed." elif [ $exit_code -eq 130 ]; then echo "" warn "Script interrupted by user (Ctrl+C)." log "Exiting without rollback. You can resume later." exit 130 else write_log "FATAL" "Module $module failed with exit code $exit_code" error "Module execution failed." exit 1 fi done # ------------------------------------------------------------------------------ # Final Cleanup # ------------------------------------------------------------------------------ section "Completion" "System Cleanup" clean_intermediate_snapshots() { local config_name="$1" local start_marker="Before Shorin Setup" local KEEP_MARKERS=( "Before Desktop Environments" "Before Niri Setup" ) if ! snapper -c "$config_name" list &>/dev/null; then return fi log "Scanning junk snapshots in: $config_name..." local start_id start_id=$(snapper -c "$config_name" list --columns number,description | grep -F "$start_marker" | awk '{print $1}' | tail -n 1) if [ -z "$start_id" ]; then warn "Marker '$start_marker' not found in '$config_name'. Skipping cleanup." return fi local IDS_TO_KEEP=() for marker in "${KEEP_MARKERS[@]}"; do local found_id found_id=$(snapper -c "$config_name" list --columns number,description | grep -F "$marker" | awk '{print $1}' | tail -n 1) if [ -n "$found_id" ]; then IDS_TO_KEEP+=("$found_id") log "Found protected snapshot: '$marker' (ID: $found_id)" fi done local snapshots_to_delete=() while IFS= read -r line; do local id local type id=$(echo "$line" | awk '{print $1}') type=$(echo "$line" | awk '{print $3}') if [[ "$id" =~ ^[0-9]+$ ]]; then if [ "$id" -gt "$start_id" ]; then local skip=false for keep in "${IDS_TO_KEEP[@]}"; do if [[ "$id" == "$keep" ]]; then skip=true break fi done if [ "$skip" = true ]; then continue fi if [[ "$type" == "pre" || "$type" == "post" ]]; then snapshots_to_delete+=("$id") fi fi fi done < <(snapper -c "$config_name" list --columns number,type) if [ ${#snapshots_to_delete[@]} -gt 0 ]; then log "Deleting ${#snapshots_to_delete[@]} junk snapshots in '$config_name'..." if exe snapper -c "$config_name" delete "${snapshots_to_delete[@]}"; then success "Cleaned $config_name." fi else log "No junk snapshots found in '$config_name'." fi } log "Cleaning Pacman/Yay cache..." exe pacman -Sc --noconfirm clean_intermediate_snapshots "root" clean_intermediate_snapshots "home" DETECTED_USER=$(awk -F: '$3 == 1000 {print $1}' /etc/passwd) TARGET_USER="${DETECTED_USER:-$(read -p "Target user: " u && echo $u)}" HOME_DIR="/home/$TARGET_USER" for dir in /var/cache/pacman/pkg/download-*/; do if [ -d "$dir" ]; then echo "Found residual directory: $dir, cleaning up..." rm -rf "$dir" fi done VERIFY_LIST="/tmp/shorin_install_verify.list" rm -f "$VERIFY_LIST" log "Regenerating final GRUB configuration..." exe env LANG=en_US.UTF-8 grub-mkconfig -o /boot/grub/grub.cfg # --- Completion --- clear show_banner echo -e "${H_GREEN}╔══════════════════════════════════════════════════════╗${NC}" echo -e "${H_GREEN}║ INSTALLATION COMPLETE ║${NC}" echo -e "${H_GREEN}╚══════════════════════════════════════════════════════╝${NC}" echo "" if [ -f "$STATE_FILE" ]; then rm "$STATE_FILE"; fi log "Archiving log..." if [ -f "/tmp/shorin_install_user" ]; then FINAL_USER=$(cat /tmp/shorin_install_user) else FINAL_USER=$(awk -F: '$3 == 1000 {print $1}' /etc/passwd) fi if [ -n "$FINAL_USER" ]; then FINAL_DOCS="/home/$FINAL_USER/Documents" mkdir -p "$FINAL_DOCS" if [ -f "${TEMP_LOG_FILE:-/tmp/shorin.log}" ]; then cp "${TEMP_LOG_FILE:-/tmp/shorin.log}" "$FINAL_DOCS/log-shorin-arch-setup.txt" chown -R "$FINAL_USER:$FINAL_USER" "$FINAL_DOCS" echo -e " ${H_BLUE}●${NC} Log Saved : ${BOLD}$FINAL_DOCS/log-shorin-arch-setup.txt${NC}" fi fi echo "" echo -e "${H_YELLOW}>>> System requires a REBOOT.${NC}" while read -r -t 0; do read -r; done for i in {10..1}; do echo -ne "\r ${DIM}Auto-rebooting in ${i}s... (Press 'n' to cancel)${NC}" read -t 1 -n 1 input if [ $? -eq 0 ]; then if [[ "$input" == "n" || "$input" == "N" ]]; then echo -e "\n\n ${H_BLUE}>>> Reboot cancelled.${NC}" exit 0 else break fi fi done echo -e "\n\n ${H_GREEN}>>> Rebooting...${NC}" systemctl reboot