Files
arch/scripts/04-niri-setup.sh
2026-03-31 20:13:15 +08:00

617 lines
22 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 <<EOT >"$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."