#!/bin/bash # ============================================================================== # 04-niri-setup.sh - Niri Desktop (Restored FZF & Robust AUR) # ============================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PARENT_DIR="$(dirname "$SCRIPT_DIR")" source "$SCRIPT_DIR/00-utils.sh" VERIFY_LIST="/tmp/shorin_install_verify.list" rm -f "$VERIFY_LIST" DEBUG=${DEBUG:-0} CN_MIRROR=${CN_MIRROR:-0} UNDO_SCRIPT="$SCRIPT_DIR/de-undochange.sh" check_root # --- [HELPER FUNCTIONS] --- # 2. Critical Failure Handler (The "Big Red Box") # 2. Critical Failure Handler (The "Big Red Box") critical_failure_handler() { local failed_reason="$1" trap - ERR echo "" echo -e "\033[0;31m################################################################\033[0m" echo -e "\033[0;31m# #\033[0m" echo -e "\033[0;31m# CRITICAL INSTALLATION FAILURE DETECTED #\033[0m" echo -e "\033[0;31m# #\033[0m" echo -e "\033[0;31m# Reason: $failed_reason\033[0m" echo -e "\033[0;31m# #\033[0m" echo -e "\033[0;31m# OPTIONS: #\033[0m" echo -e "\033[0;31m# 1. Restore snapshot (Undo changes & Exit) #\033[0m" echo -e "\033[0;31m# 2. Retry / Re-run script #\033[0m" echo -e "\033[0;31m# 3. Abort (Exit immediately) #\033[0m" echo -e "\033[0;31m# #\033[0m" echo -e "\033[0;31m################################################################\033[0m" echo "" while true; do read -p "Select an option [1-3]: " -r choice case "$choice" in 1) # Option 1: Restore Snapshot if [ -f "$UNDO_SCRIPT" ]; then warn "Executing recovery script..." bash "$UNDO_SCRIPT" exit 1 else error "Recovery script missing! You are on your own." exit 1 fi ;; 2) # Option 2: Re-run Script warn "Restarting installation script..." echo "-----------------------------------------------------" sleep 1 exec "$0" "$@" ;; 3) # Option 3: Exit warn "User chose to abort." warn "Please fix the issue manually before re-running." error "Installation aborted." exit 1 ;; *) echo "Invalid input. Please enter 1, 2, or 3." ;; esac done } # 3. Robust Package Installation with Retry Loop ensure_package_installed() { local pkg="$1" local context="$2" # e.g., "Repo" or "AUR" local max_attempts=3 local attempt=1 local install_success=false # 1. Check if already installed if pacman -Q "$pkg" &>/dev/null; then return 0 fi # 2. Retry Loop while [ $attempt -le $max_attempts ]; do if [ $attempt -gt 1 ]; then warn "Retrying '$pkg' ($context)... (Attempt $attempt/$max_attempts)" sleep 3 # Cooldown else log "Installing '$pkg' ($context)..." fi # Try installation if as_user yay -S --noconfirm --needed --answerdiff=None --answerclean=None "$pkg"; then install_success=true break else warn "Attempt $attempt/$max_attempts failed for '$pkg'." fi ((attempt++)) done # 3. Final Verification if [ "$install_success" = true ] && pacman -Q "$pkg" &>/dev/null; then success "Installed '$pkg'." else critical_failure_handler "Failed to install '$pkg' after $max_attempts attempts." fi } section "Phase 4" "Niri Desktop Environment" # ============================================================================== # STEP 0: Safety Checkpoint # ============================================================================== # Enable Trap trap 'critical_failure_handler "Script Error at Line $LINENO"' ERR # ============================================================================== # STEP 1: Identify User & DM Check # ============================================================================== detect_target_user info_kv "Target" "$TARGET_USER" # DM Check check_dm_conflict # ============================================================================== # STEP 2: Core Components # ============================================================================== section "Step 1/9" "Core Components" PKGS="niri xdg-desktop-portal-gnome fuzzel kitty firefox libnotify mako polkit-gnome" # 記錄到清單 echo "$PKGS" >> "$VERIFY_LIST" exe pacman -S --noconfirm --needed $PKGS log "Configuring Firefox Policies..." POL_DIR="/etc/firefox/policies" exe mkdir -p "$POL_DIR" cat << 'EOF' > "$POL_DIR/policies.json" { "policies": { "Extensions": { "Install": [ "https://addons.mozilla.org/firefox/downloads/latest/pywalfox/latest.xpi", "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi" ] } } } EOF exe chmod 755 "$POL_DIR" && exe chmod 644 "$POL_DIR/policies.json" # ============================================================================== # STEP 3: File Manager # ============================================================================== section "Step 2/9" "File Manager" FM_PKGS1="ffmpegthumbnailer gvfs-smb nautilus-open-any-terminal file-roller gnome-keyring gst-plugins-base gst-plugins-good gst-libav nautilus" FM_PKGS2="xdg-desktop-portal-gtk thunar tumbler ffmpegthumbnailer poppler-glib gvfs-smb file-roller thunar-archive-plugin gnome-keyring thunar-volman gvfs-mtp gvfs-gphoto2 webp-pixbuf-loader libgsf" # 記錄到清單 echo "$FM_PKGS1" >> "$VERIFY_LIST" echo "$FM_PKGS2" >> "$VERIFY_LIST" exe pacman -S --noconfirm --needed $FM_PKGS1 exe pacman -S --noconfirm --needed $FM_PKGS2 # 默认终端处理 echo "xdg-terminal-exec" >> "$VERIFY_LIST" exe as_user paru -S --noconfirm --needed xdg-terminal-exec if ! grep -q "kitty" "$HOME_DIR/.config/xdg-terminals.list"; then echo 'kitty.desktop' >> "$HOME_DIR/.config/xdg-terminals.list" fi # if [ ! -f /usr/local/bin/gnome-terminal ] || [ -L /usr/local/bin/gnome-terminal ]; then # exe ln -sf /usr/bin/kitty /usr/local/bin/gnome-terminal # fi sudo -u "$TARGET_USER" dbus-run-session gsettings set com.github.stunkymonkey.nautilus-open-any-terminal terminal kitty # Nautilus Nvidia/Input Fix configure_nautilus_user section "Step 3/9" "Temp sudo file" SUDO_TEMP_FILE="/etc/sudoers.d/99_shorin_installer_temp" echo "$TARGET_USER ALL=(ALL) NOPASSWD: ALL" >"$SUDO_TEMP_FILE" chmod 440 "$SUDO_TEMP_FILE" log "Temp sudo file created..." # ============================================================================== # STEP 5: Dependencies (RESTORED FZF) # ============================================================================== section "Step 4/9" "Dependencies" LIST_FILE="$PARENT_DIR/niri-applist.txt" # Ensure tools command -v fzf &>/dev/null || pacman -S --noconfirm fzf >/dev/null 2>&1 if [ -f "$LIST_FILE" ]; then mapfile -t DEFAULT_LIST < <(grep -vE "^\s*#|^\s*$" "$LIST_FILE" | sed 's/#.*//; s/AUR://g' | xargs -n1) if [ ${#DEFAULT_LIST[@]} -eq 0 ]; then warn "App list is empty. Skipping." PACKAGE_ARRAY=() else echo -e "\n ${H_YELLOW}>>> Default installation in 60s. Press ANY KEY to customize...${NC}" if read -t 60 -n 1 -s -r; then # --- [RESTORED] Original FZF Selection Logic --- clear log "Loading package list..." SELECTED_LINES=$(grep -vE "^\s*#|^\s*$" "$LIST_FILE" | sed -E 's/[[:space:]]+#/\t#/' | fzf --multi \ --layout=reverse \ --border \ --margin=1,2 \ --prompt="Search Pkg > " \ --pointer=">>" \ --marker="* " \ --delimiter=$'\t' \ --with-nth=1 \ --bind 'load:select-all' \ --bind 'ctrl-a:select-all,ctrl-d:deselect-all' \ --info=inline \ --header="[TAB] TOGGLE | [ENTER] INSTALL | [CTRL-D] DE-ALL | [CTRL-A] SE-ALL" \ --preview "echo {} | cut -f2 -d$'\t' | sed 's/^# //'" \ --preview-window=down:50%:wrap \ --color=dark \ --color=fg+:white,bg+:black \ --color=hl:blue,hl+:blue:bold \ --color=header:yellow:bold \ --color=info:magenta \ --color=prompt:cyan,pointer:cyan:bold,marker:green:bold \ --color=spinner:yellow) clear if [ -z "$SELECTED_LINES" ]; then warn "User cancelled selection. Installing NOTHING." PACKAGE_ARRAY=() else PACKAGE_ARRAY=() while IFS= read -r line; do raw_pkg=$(echo "$line" | cut -f1 -d$'\t' | xargs) clean_pkg="${raw_pkg#AUR:}" [ -n "$clean_pkg" ] && PACKAGE_ARRAY+=("$clean_pkg") done <<<"$SELECTED_LINES" fi # ----------------------------------------------- else log "Auto-confirming ALL packages." PACKAGE_ARRAY=("${DEFAULT_LIST[@]}") fi fi # --- Installation Loop --- if [ ${#PACKAGE_ARRAY[@]} -gt 0 ]; then BATCH_LIST=() AUR_LIST=() info_kv "Target" "${#PACKAGE_ARRAY[@]} packages scheduled." # 記錄到清單 (將陣列展開並寫入) echo "${PACKAGE_ARRAY[@]}" >> "$VERIFY_LIST" for pkg in "${PACKAGE_ARRAY[@]}"; do [ "$pkg" == "imagemagic" ] && pkg="imagemagick" [[ "$pkg" == "AUR:"* ]] && AUR_LIST+=("${pkg#AUR:}") || BATCH_LIST+=("$pkg") done # 1. Batch Install Repo Packages if [ ${#BATCH_LIST[@]} -gt 0 ]; then log "Phase 1: Batch Installing Repo Packages..." as_user yay -Syu --noconfirm --needed --answerdiff=None --answerclean=None "${BATCH_LIST[@]}" || true # Verify Each for pkg in "${BATCH_LIST[@]}"; do ensure_package_installed "$pkg" "Repo" done fi # 2. Sequential AUR Install if [ ${#AUR_LIST[@]} -gt 0 ]; then log "Phase 2: Installing AUR Packages (Sequential)..." for pkg in "${AUR_LIST[@]}"; do ensure_package_installed "$pkg" "AUR" done fi # Waybar fallback if ! command -v waybar &>/dev/null; then warn "Waybar missing. Installing stock..." exe pacman -S --noconfirm --needed waybar fi else warn "No packages selected." fi else warn "niri-applist.txt not found." fi # ============================================================================== # STEP 6: Dotfiles (Smart Recursive Symlink) # ============================================================================== section "Step 5/9" "Deploying Dotfiles" REPO_GITHUB="https://github.com/SHORiN-KiWATA/Shorin-ArchLinux-Guide.git" # 1. 仓库位置:放在 .local/share 下,不污染 home 根目录 DOTFILES_REPO="$HOME_DIR/.local/share/shorin-niri" # --- Smart Linking Function --- # 核心逻辑:只链接“叶子”节点,对于“容器”目录(.config, .local, share)则递归进入 link_recursive() { local src_dir="$1" local dest_dir="$2" local exclude_list="$3" as_user mkdir -p "$dest_dir" find "$src_dir" -mindepth 1 -maxdepth 1 -not -path '*/.git*' | while read -r src_path; do local item_name item_name=$(basename "$src_path") # 0. 排除检查 if echo "$exclude_list" | grep -qw "$item_name"; then log "Skipping excluded: $item_name" continue fi # 1. 判断是否是需要“穿透”的系统目录 local need_recurse=false if [ "$item_name" == ".config" ]; then need_recurse=true elif [ "$item_name" == ".local" ]; then need_recurse=true # 只有当父目录名字是以 .local 结尾时,才穿透 share (bin 被移除了) elif [[ "$src_dir" == *".local" ]] && [ "$item_name" == "share" ]; then need_recurse=true fi if [ "$need_recurse" = true ]; then # 递归进入:传入当前路径作为新的源和目标 log " Entering container: $item_name" link_recursive "$src_path" "$dest_dir/$item_name" "$exclude_list" else # 2. 具体的配置文件夹/文件(如 fcitx5, niri, .zshrc, .local/bin) -> 执行链接 local target_path="$dest_dir/$item_name" # 先清理旧的目标(无论是文件、文件夹还是死链) if [ -e "$target_path" ] || [ -L "$target_path" ]; then log " Overwriting: $item_name" as_user rm -rf "$target_path" fi # 创建软链接 info_kv "Linking" "$item_name -> $dest_dir" as_user ln -sf "$src_path" "$target_path" fi done } # --- Execution --- # 1. 准备仓库 prepare_repository() { local TARGET_DIRS=("dotfiles" "wallpapers") local BRANCH_NAME="main" # --- 1. 检查是否存在旧仓库 --- if [ -d "$DOTFILES_REPO" ]; then if ! as_user git -C "$DOTFILES_REPO" rev-parse --is-inside-work-tree &>/dev/null; then warn "Found incomplete or broken repository folder. Cleaning up..." rm -rf "$DOTFILES_REPO" else log "Repository already exists. Checking for updates..." if ! as_user git -C "$DOTFILES_REPO" pull origin "$BRANCH_NAME"; then warn "Update failed (network issue?), cleaning up..." rm -rf "$DOTFILES_REPO" fi fi fi # --- 2. 初始化新仓库 --- if [ ! -d "$DOTFILES_REPO" ]; then log "Initializing Sparse & Shallow Checkout to $DOTFILES_REPO..." cd "$HOME_DIR" # ================= [修改开始:智能安全创建目录] ================= # 逻辑: # 1. 优先尝试用 as_user (sudo -u) 创建,这是最干净的。 # 2. 如果失败,通常是因为父目录 (.local 或 .local/share) 被 Root 占用了。 # 3. 此时只修正父目录的 Owner (不递归),然后再试。 if ! as_user mkdir -p "$DOTFILES_REPO" 2>/dev/null; then local parent_dir=$(dirname "$DOTFILES_REPO") log "User creation failed. Fixing parent permissions: $parent_dir" # 确保父目录存在,并修正父目录权限 (非递归,瞬间完成) if [ ! -d "$parent_dir" ]; then mkdir -p "$parent_dir" fi chown "$TARGET_USER:" "$parent_dir" # 再次尝试以用户身份创建 if ! as_user mkdir -p "$DOTFILES_REPO"; then # 最后的兜底:实在不行就用 Root 创建,然后只修这个新目录的权限 warn "Fallback to root creation..." mkdir -p "$DOTFILES_REPO" chown -R "$TARGET_USER:" "$DOTFILES_REPO" fi fi # ================= [修改结束] ================= as_user git -C "$DOTFILES_REPO" init # 强制将本地分支名设为 main as_user git -C "$DOTFILES_REPO" branch -m "$BRANCH_NAME" as_user git -C "$DOTFILES_REPO" config core.sparseCheckout true local sparse_file="$DOTFILES_REPO/.git/info/sparse-checkout" for item in "${TARGET_DIRS[@]}"; do log " Configuring sparse-checkout: $item" echo "$item" | as_user tee -a "$sparse_file" >/dev/null done log " Adding remote origin: $REPO_GITHUB" as_user git -C "$DOTFILES_REPO" remote add origin "$REPO_GITHUB" log "Downloading latest snapshot (Github)..." if ! as_user git -C "$DOTFILES_REPO" pull origin "$BRANCH_NAME" --depth 1 ; then error "Failed to download dotfiles." warn "Cleaning up empty directory to prevent errors on retry..." rm -rf "$DOTFILES_REPO" critical_failure_handler "Failed to download dotfiles (Sparse+Shallow failed)." else # 这里可以保留作为双重保险,或者因为上面已经处理好了权限,这行其实是可选的 chown -R "$TARGET_USER:" "$DOTFILES_REPO" as_user git -C "$DOTFILES_REPO" branch --set-upstream-to=origin/main main # 这是一个常见痛点:因为 git 也是 sudo -u 运行的,这步是为了防止 git 报错 "dubious ownership" as_user git config --global --add safe.directory "$DOTFILES_REPO" success "Repository prepared." fi else log "Dotfiles repo already exists. Skipping clone." fi } prepare_repository # 2. 执行链接 if [ -d "$DOTFILES_REPO/dotfiles" ]; then EXCLUDE_LIST="" if [ "$TARGET_USER" != "shorin" ]; then EXCLUDE_FILE="$PARENT_DIR/exclude-dotfiles.txt" if [ -f "$EXCLUDE_FILE" ]; then log "Loading exclusions..." EXCLUDE_LIST=$(grep -vE "^\s*#|^\s*$" "$EXCLUDE_FILE" | tr '\n' ' ') fi fi log "Backing up existing configs..." as_user tar -czf "$HOME_DIR/config_backup_$(date +%s).tar.gz" -C "$HOME_DIR" .config # 调用递归函数:从 dotfiles 根目录开始,目标是 HOME link_recursive "$DOTFILES_REPO/dotfiles" "$HOME_DIR" "$EXCLUDE_LIST" as_user chmod -R +x $HOME_DIR/.local/bin # 创建shorin工具的链接 as_user shorin link # --- Post-Process (防止污染 git 的修正) --- OUTPUT_EXAMPLE_KDL="$HOME_DIR/.config/niri/output-example.kdl" OUTPUT_KDL="$HOME_DIR/.config/niri/output.kdl" if [ "$TARGET_USER" != "shorin" ]; then as_user touch $OUTPUT_KDL # 修复 Bookmarks (转为实体文件并修改) BOOKMARKS_FILE="$HOME_DIR/.config/gtk-3.0/bookmarks" REPO_BOOKMARKS="$DOTFILES_REPO/dotfiles/.config/gtk-3.0/bookmarks" if [ -f "$REPO_BOOKMARKS" ]; then as_user sed -i "s/shorin/$TARGET_USER/g" "$REPO_BOOKMARKS" log "Updated GTK bookmarks." fi else as_user cp "$DOTFILES_REPO/dotfiles/.config/niri/output-example.kdl" "$OUTPUT_KDL" fi # GTK Theme Symlinks (Fix internal links) GTK4="$HOME_DIR/.config/gtk-4.0" THEME="$HOME_DIR/.local/share/themes/adw-gtk3-dark/gtk-4.0" if [ -d "$GTK4" ]; then as_user rm -f "$GTK4/gtk.css" "$GTK4/gtk-dark.css" as_user ln -sf "$THEME/gtk-dark.css" "$GTK4/gtk-dark.css" as_user ln -sf "$THEME/gtk.css" "$GTK4/gtk.css" fi # Flatpak overrides if command -v flatpak &>/dev/null; then as_user flatpak override --user --filesystem=xdg-data/themes as_user flatpak override --user --filesystem="$HOME_DIR/.themes" as_user flatpak override --user --filesystem=xdg-config/gtk-4.0 as_user flatpak override --user --filesystem=xdg-config/gtk-3.0 as_user flatpak override --user --env=GTK_THEME=adw-gtk3-dark as_user flatpak override --user --filesystem=xdg-config/fontconfig fi success "Dotfiles Linked." else warn "Dotfiles missing in repo directory." fi # === niri blur ==== curl -L shorin.xyz/niri-blur-toggle | as_user bash # ============================================================================== # STEP 7: Wallpapers # ============================================================================== section "Step 6/9" "Wallpapers" # 更新引用路径 if [ -d "$DOTFILES_REPO/wallpapers" ]; then as_user ln -sf "$DOTFILES_REPO/wallpapers" "$HOME_DIR/Pictures/Wallpapers" as_user mkdir -p "$HOME_DIR/Templates" as_user touch "$HOME_DIR/Templates/new" echo "#!/bin/bash" | as_user tee "$HOME_DIR/Templates/new.sh" >/dev/null as_user chmod +x "$HOME_DIR/Templates/new.sh" success "Installed." fi # === remove gtk bottom ======= as_user gsettings set org.gnome.desktop.wm.preferences button-layout ":close" # ============================================================================== # STEP 8: Hardware Tools # ============================================================================== section "Step 7/9" "Hardware" if pacman -Q ddcutil &>/dev/null; then gpasswd -a "$TARGET_USER" i2c lsmod | grep -q i2c_dev || echo "i2c-dev" >/etc/modules-load.d/i2c-dev.conf fi if pacman -Q swayosd &>/dev/null; then systemctl enable --now swayosd-libinput-backend.service >/dev/null 2>&1 fi success "Tools configured." section "Config" "Hiding useless .desktop files" log "Hiding useless .desktop files" run_hide_desktop_file rm -f "$SUDO_TEMP_FILE" # === 教程文件 === log "Copying tutorial file on desktop..." as_user cp "$PARENT_DIR/resources/必看-shoirn-Niri使用方法.txt" "$HOME_DIR/必看-Shoirn-Niri使用方法.txt" # ============================================================================== # STEP 9: Cleanup & Auto-Login # ============================================================================== # section "Final" "Cleanup & Boot" # SVC_DIR="$HOME_DIR/.config/systemd/user" # SVC_FILE="$SVC_DIR/niri-autostart.service" # LINK="$SVC_DIR/default.target.wants/niri-autostart.service" # if [ "$SKIP_DM" = true ]; then # log "Auto-login skipped." # as_user rm -f "$LINK" "$SVC_FILE" # else # log "Configuring TTY Auto-login..." # mkdir -p "/etc/systemd/system/getty@tty1.service.d" # echo -e "[Service]\nExecStart=\nExecStart=-/sbin/agetty --noreset --noclear --autologin $TARGET_USER - \${TERM}" >"/etc/systemd/system/getty@tty1.service.d/autologin.conf" # as_user mkdir -p "$(dirname "$LINK")" # cat <"$SVC_FILE" # [Unit] # Description=Niri Session Autostart # After=graphical-session-pre.target # [Service] # ExecStart=/usr/bin/niri-session # Restart=on-failure # [Install] # WantedBy=default.target # EOT # as_user ln -sf "../niri-autostart.service" "$LINK" # chown -R "$TARGET_USER" "$SVC_DIR" # success "Enabled." # fi # ============================================================================== # STEP 9: Display Manager (greetd + tuigreet) & Cleanup # ============================================================================== section "Final" "Cleanup & Boot Configuration" # 1. 清理旧的 TTY 自动登录残留(无论是否启用 greetd,旧版残留都应清除) log "Cleaning up legacy TTY autologin configs..." rm -f /etc/systemd/system/getty@tty1.service.d/autologin.conf 2>/dev/null if [ "$SKIP_DM" = true ]; then log "Display Manager setup skipped (Conflict found or user opted out)." warn "You will need to start your session manually from the TTY." else setup_ly fi trap - ERR log "Module 04 completed."