#!/bin/bash # ============================================================================== # 00-utils.sh - The "TUI" Visual Engine (v4.0) # ============================================================================== # --- 1. 颜色与样式定义 (ANSI) --- # 注意:这里定义的是字面量字符串,需要 echo -e 来解析 export NC='\033[0m' export BOLD='\033[1m' export DIM='\033[2m' export ITALIC='\033[3m' export UNDER='\033[4m' export H_MAGENTA='\033[1;35m' # 常用高亮色 export H_RED='\033[1;31m' export H_GREEN='\033[1;32m' export H_YELLOW='\033[1;33m' export H_BLUE='\033[1;34m' export H_PURPLE='\033[1;35m' export H_CYAN='\033[1;36m' export H_WHITE='\033[1;37m' export H_GRAY='\033[1;90m' # 背景色 (用于标题栏) export BG_BLUE='\033[44m' export BG_PURPLE='\033[45m' # 符号定义 export TICK="${H_GREEN}✔${NC}" export CROSS="${H_RED}✘${NC}" export INFO="${H_BLUE}ℹ${NC}" export WARN="${H_YELLOW}⚠${NC}" export ARROW="${H_CYAN}➜${NC}" check_root() { if [ "$EUID" -ne 0 ]; then echo -e "${H_RED} $CROSS CRITICAL ERROR: Script must be run as root.${NC}" exit 1 fi } check_root # ============================================================================== # detect_target_user - 识别目标用户 (支持 1-based 序号与回车默认选择) # ============================================================================== detect_target_user() { # 1. 缓存检查 if [[ -f "/tmp/shorin_install_user" ]]; then TARGET_USER=$(cat "/tmp/shorin_install_user") HOME_DIR="/home/$TARGET_USER" export TARGET_USER HOME_DIR return 0 fi log "Detecting system users..." # 2. 提取系统中所有普通用户 (UID 1000-60000) mapfile -t HUMAN_USERS < <(awk -F: '$3 >= 1000 && $3 < 60000 {print $1}' /etc/passwd) # 3. 核心决策逻辑 if [[ ${#HUMAN_USERS[@]} -gt 1 ]]; then echo -e " ${H_YELLOW}>>> Multiple users detected. Who is the target?${NC}" local default_user="" local default_idx="" # 遍历用户,生成 1 开始的序号,并捕获当前 Sudo 用户作为默认值 for i in "${!HUMAN_USERS[@]}"; do local mark="" local display_idx=$((i + 1)) if [[ "${HUMAN_USERS[$i]}" == "${SUDO_USER:-}" ]]; then mark="${H_CYAN}*${NC}" default_user="${HUMAN_USERS[$i]}" default_idx="$display_idx" fi echo -e " [${display_idx}] ${mark}${HUMAN_USERS[$i]}" done while true; do # 动态生成提示词 if [[ -n "$default_user" ]]; then echo -ne " ${H_CYAN}Select user ID [1-${#HUMAN_USERS[@]}] (Default ${default_idx}): ${NC}" else echo -ne " ${H_CYAN}Select user ID [1-${#HUMAN_USERS[@]}]: ${NC}" fi read -r idx # 处理直接回车:如果有默认用户,直接采纳 if [[ -z "$idx" && -n "$default_user" ]]; then TARGET_USER="$default_user" log "Defaulting to current user: ${H_CYAN}${TARGET_USER}${NC}" break fi # 验证输入是否为合法数字 (1 到 数组长度) if [[ "$idx" =~ ^[0-9]+$ ]] && [ "$idx" -ge 1 ] && [ "$idx" -le "${#HUMAN_USERS[@]}" ]; then # 数组索引需要减 1 还原 TARGET_USER="${HUMAN_USERS[$((idx - 1))]}" break else warn "Invalid selection. Please enter a valid number or press Enter for default." fi done elif [[ ${#HUMAN_USERS[@]} -eq 1 ]]; then TARGET_USER="${HUMAN_USERS[0]}" log "Single user detected: ${H_CYAN}${TARGET_USER}${NC}" else if [[ -n "${SUDO_USER:-}" && "$SUDO_USER" != "root" ]]; then TARGET_USER="$SUDO_USER" else echo -ne " ${H_YELLOW}No standard user found. Enter intended username:${NC} " read -r TARGET_USER fi fi # 4. 最终验证与持久化 if [[ -z "$TARGET_USER" ]]; then error "Target user cannot be empty." exit 1 fi echo "$TARGET_USER" > "/tmp/shorin_install_user" HOME_DIR="/home/$TARGET_USER" export TARGET_USER HOME_DIR } # 日志文件 export TEMP_LOG_FILE="/tmp/log-shorin-arch-setup.txt" [ ! -f "$TEMP_LOG_FILE" ] && touch "$TEMP_LOG_FILE" && chmod 666 "$TEMP_LOG_FILE" # --- 2. 基础工具 --- write_log() { # Strip ANSI colors for log file local clean_msg=$(echo -e "$2" | sed 's/\x1b\[[0-9;]*m//g') echo "[$(date '+%H:%M:%S')] [$1] $clean_msg" >> "$TEMP_LOG_FILE" } # --- 3. 视觉组件 (TUI Style) --- # 绘制分割线 hr() { printf "${H_GRAY}%*s${NC}\n" "${COLUMNS:-80}" '' | tr ' ' '─' } # 绘制大标题 (Section) section() { local title="$1" local subtitle="$2" echo "" echo -e "${H_PURPLE}╭──────────────────────────────────────────────────────────────────────────────╮${NC}" echo -e "${H_PURPLE}│${NC} ${BOLD}${H_WHITE}$title${NC}" echo -e "${H_PURPLE}│${NC} ${H_CYAN}$subtitle${NC}" echo -e "${H_PURPLE}╰──────────────────────────────────────────────────────────────────────────────╯${NC}" write_log "SECTION" "$title - $subtitle" } # 绘制键值对信息 info_kv() { local key="$1" local val="$2" local extra="$3" printf " ${H_BLUE}●${NC} %-15s : ${BOLD}%s${NC} ${DIM}%s${NC}\n" "$key" "$val" "$extra" write_log "INFO" "$key=$val" } # 普通日志 log() { echo -e " $ARROW $1" write_log "LOG" "$1" } # 成功日志 success() { echo -e " $TICK ${H_GREEN}$1${NC}" write_log "SUCCESS" "$1" } # 警告日志 (突出显示) warn() { echo -e " $WARN ${H_YELLOW}${BOLD}WARNING:${NC} ${H_YELLOW}$1${NC}" write_log "WARN" "$1" } # 错误日志 (非常突出) error() { echo -e "" echo -e "${H_RED} ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓${NC}" echo -e "${H_RED} ┃ ERROR: $1${NC}" echo -e "${H_RED} ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛${NC}" echo -e "" write_log "ERROR" "$1" } # --- 4. 核心:命令执行器 (Command Exec) --- exe() { local full_command="$*" # Visual: 显示正在运行的命令 echo -e " ${H_GRAY}┌──[ ${H_MAGENTA}EXEC${H_GRAY} ]────────────────────────────────────────────────────${NC}" echo -e " ${H_GRAY}│${NC} ${H_CYAN}$ ${NC}${BOLD}$full_command${NC}" write_log "EXEC" "$full_command" # Run the command "$@" local status=$? if [ $status -eq 0 ]; then echo -e " ${H_GRAY}└──────────────────────────────────────────────────────── ${H_GREEN}OK${H_GRAY} ─┘${NC}" else echo -e " ${H_GRAY}└────────────────────────────────────────────────────── ${H_RED}FAIL${H_GRAY} ─┘${NC}" write_log "FAIL" "Exit Code: $status" return $status fi } # 静默执行 exe_silent() { "$@" > /dev/null 2>&1 } # --- 5. 可复用逻辑块 --- # 动态选择 Flathub 镜像源 (修复版:使用 echo -e 处理颜色变量) select_flathub_mirror() { # 1. 索引数组保证顺序 local names=( "SJTU (Shanghai Jiao Tong)" "USTC (Univ of Sci & Tech of China)" "FlatHub Offical" ) local urls=( "https://mirror.sjtu.edu.cn/flathub" "https://mirrors.ustc.edu.cn/flathub" "https://dl.flathub.org/repo/" ) # 2. 动态计算菜单宽度 (基于无颜色的纯文本) local max_len=0 local title_text="Select Flathub Mirror (60s Timeout)" max_len=${#title_text} for name in "${names[@]}"; do # 预估显示长度:"[x] Name - Recommended" local item_len=$((${#name} + 4 + 14)) if (( item_len > max_len )); then max_len=$item_len fi done # 菜单总宽度 local menu_width=$((max_len + 4)) # --- 3. 渲染菜单 (使用 echo -e 确保颜色变量被解析) --- echo "" # 生成横线 local line_str="" printf -v line_str "%*s" "$menu_width" "" line_str=${line_str// /─} # 打印顶部边框 echo -e "${H_PURPLE}╭${line_str}╮${NC}" # 打印标题 (计算居中填充) local title_padding_len=$(( (menu_width - ${#title_text}) / 2 )) local right_padding_len=$((menu_width - ${#title_text} - title_padding_len)) # 生成填充空格 local t_pad_l=""; printf -v t_pad_l "%*s" "$title_padding_len" "" local t_pad_r=""; printf -v t_pad_r "%*s" "$right_padding_len" "" echo -e "${H_PURPLE}│${NC}${t_pad_l}${BOLD}${title_text}${NC}${t_pad_r}${H_PURPLE}│${NC}" # 打印中间分隔线 echo -e "${H_PURPLE}├${line_str}┤${NC}" # 打印选项 for i in "${!names[@]}"; do local name="${names[$i]}" local display_idx=$((i+1)) # 1. 构造用于显示的带颜色字符串 local color_str="" # 2. 构造用于计算长度的无颜色字符串 local raw_str="" if [ "$i" -eq 0 ]; then raw_str=" [$display_idx] $name - Recommended" color_str=" ${H_CYAN}[$display_idx]${NC} ${name} - ${H_GREEN}Recommended${NC}" else raw_str=" [$display_idx] $name" color_str=" ${H_CYAN}[$display_idx]${NC} ${name}" fi # 计算右侧填充空格 local padding=$((menu_width - ${#raw_str})) local pad_str=""; if [ "$padding" -gt 0 ]; then printf -v pad_str "%*s" "$padding" "" fi # 打印:边框 + 内容 + 填充 + 边框 echo -e "${H_PURPLE}│${NC}${color_str}${pad_str}${H_PURPLE}│${NC}" done # 打印底部边框 echo -e "${H_PURPLE}╰${line_str}╯${NC}" echo "" # --- 4. 用户交互 --- local choice # 提示符 read -t 60 -p "$(echo -e " ${H_YELLOW}Enter choice [1-${#names[@]}]: ${NC}")" choice if [ $? -ne 0 ]; then echo ""; fi choice=${choice:-1} if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt "${#names[@]}" ]; then log "Invalid choice or timeout. Defaulting to SJTU..." choice=1 fi local index=$((choice-1)) local selected_name="${names[$index]}" local selected_url="${urls[$index]}" log "Setting Flathub mirror to: ${H_GREEN}$selected_name${NC}" # 执行修改 (仅修改 flathub,不涉及 github) if exe flatpak remote-modify flathub --url="$selected_url"; then success "Mirror updated." else error "Failed to update mirror." fi } as_user() { runuser -u "$TARGET_USER" -- "$@" } hide_desktop_file() { local source_file="$1" local filename=$(basename "$source_file") local user_dir="$HOME_DIR/.local/share/applications" local target_file="$user_dir/$filename" mkdir -p "$user_dir" if [[ -f "$source_file" ]]; then cp -fv "$source_file" "$target_file" if grep -q "^NoDisplay=" "$target_file"; then sed -i 's/^NoDisplay=.*/NoDisplay=true/' "$target_file" else echo "NoDisplay=true" >> "$target_file" fi chown "$TARGET_USER:" "$target_file" fi } # 批量执行 run_hide_desktop_file() { local apps_to_hide=( "avahi-discover.desktop" "qv4l2.desktop" "qvidcap.desktop" "bssh.desktop" "org.fcitx.Fcitx5.desktop" "org.fcitx.fcitx5-migrator.desktop" "xgps.desktop" "xgpsspeed.desktop" "gvim.desktop" "kbd-layout-viewer5.desktop" "bvnc.desktop" "yazi.desktop" "btop.desktop" "vim.desktop" "nvim.desktop" "nvtop.desktop" "mpv.desktop" "org.gnome.Settings.desktop" "thunar-settings.desktop" "thunar-bulk-rename.desktop" "thunar-volman-settings.desktop" "clipse-gui.desktop" "waypaper.desktop" "xfce4-about.desktop" "cmake-gui.desktop" "assistant.desktop" "qdbusviewer.desktop" "linguist.desktop" "designer.desktop" "org.kde.drkonqi.coredump.gui.desktop" "org.kde.kwrite.desktop" "org.freedesktop.MalcontentControl.desktop" "org.gnome.Nautilus.desktop" ) echo "正在隐藏不需要的桌面图标..." # 用一个 for 循环搞定所有调用 for app in "${apps_to_hide[@]}"; do hide_desktop_file "/usr/share/applications/$app" done chown -R "$TARGET_USER:" "$HOME_DIR/.local/share/applications" echo "图标隐藏完成!" } configure_nautilus_user() { local sys_file="/usr/share/applications/org.gnome.Nautilus.desktop" local user_dir="$HOME_DIR/.local/share/applications" local user_file="$user_dir/org.gnome.Nautilus.desktop" # 1. 检查系统文件是否存在 if [ -f "$sys_file" ]; then local need_modify=0 local env_vars="env" # --- 逻辑 1: Niri 检测 (输入法修复) --- if command -v niri >/dev/null 2>&1; then # 只要有 niri,就强制使用 fcitx 模块 env_vars="$env_vars GTK_IM_MODULE=fcitx" need_modify=1 log "检测到 Niri 环境,准备注入 GTK_IM_MODULE=fcitx" fi # --- 逻辑 2: 双显卡 NVIDIA 检测 (GSK 渲染修复) --- local gpu_count=$(lspci | grep -E -i "vga|3d" | wc -l) local has_nvidia=$(lspci | grep -E -i "nvidia" | wc -l) if [ "$gpu_count" -gt 1 ] && [ "$has_nvidia" -gt 0 ]; then # 叠加 GSK 渲染变量 env_vars="$env_vars GSK_RENDERER=gl" need_modify=1 log "检测到双显卡 NVIDIA,准备注入 GSK_RENDERER=gl" # 额外操作: 创建 gsk.conf local env_conf_dir="$HOME_DIR/.config/environment.d" if [ ! -f "$env_conf_dir/gsk.conf" ]; then mkdir -p "$env_conf_dir" echo "GSK_RENDERER=gl" > "$env_conf_dir/gsk.conf" # 修复权限 if [ -n "$TARGET_USER" ]; then chown -R "$TARGET_USER" "$env_conf_dir" fi log "已添加用户级环境变量配置: $env_conf_dir/gsk.conf" fi fi # --- 3. 执行修改 (如果命中了任意一个逻辑) --- if [ "$need_modify" -eq 1 ]; then # 准备目录并复制 mkdir -p "$user_dir" cp "$sys_file" "$user_file" # 修复所有者 if [ -n "$TARGET_USER" ]; then chown "$TARGET_USER" "$user_file" fi # 修改 Desktop 文件 # env_vars 此时可能是: # - "env GTK_IM_MODULE=fcitx" (仅Niri) # - "env GSK_RENDERER=gl" (仅双显卡) # - "env GTK_IM_MODULE=fcitx GSK_RENDERER=gl" (两者都有) sed -i "s|^Exec=|Exec=$env_vars |" "$user_file" log "已生成 Nautilus 用户配置: $user_file (参数: $env_vars)" fi fi } force_copy() { local src="$1" local target_dir="$2" if [[ -z "$src" || -z "$target_dir" ]]; then warn "force_copy: Missing arguments" return 1 fi if [[ -d "${src%/}" ]]; then (cd "$src" && find . -type d) | while read -r d; do as_user rm -f "$target_dir/$d" 2>/dev/null done fi exe as_user cp -rf "$src" "$target_dir" } # ============================================================================== # check_dm_conflict - 检测现有的显示管理器冲突,并让用户选择是否启用新 DM # ============================================================================== # 使用方法: check_dm_conflict # 结果: 设置全局变量 $SKIP_DM (true/false) check_dm_conflict() { local KNOWN_DMS=( "cdm" "console-tdm" "emptty" "lemurs" "lidm" "loginx" "ly" "nodm" "tbsm" "entrance-git" "gdm" "lightdm" "lxdm" "plasma-login-manager" "sddm" "slim" "xorg-xdm" "greetd" ) SKIP_DM=false local DM_FOUND="" for dm in "${KNOWN_DMS[@]}"; do if pacman -Q "$dm" &>/dev/null; then DM_FOUND="$dm" break fi done if [ -n "$DM_FOUND" ]; then info_kv "Conflict" "${H_RED}$DM_FOUND${NC}" SKIP_DM=true else # read -t 20 等待 20 秒,超时默认 Y read -t 20 -p "$(echo -e " ${H_CYAN}Enable Display Manager ? [Y/n] (Default Y): ${NC}")" choice || true if [[ "${choice:-Y}" =~ ^[Yy]$ ]]; then SKIP_DM=false else SKIP_DM=true fi fi } # ============================================================================== # setup_greetd_tuigreet - 安装并配置 greetd + tuigreet # ============================================================================== # 使用方法: setup_greetd_tuigreet setup_greetd_tuigreet() { log "Installing greetd and tuigreet..." exe pacman -S --noconfirm --needed greetd greetd-tuigreet # 禁用可能存在的默认 getty@tty1,把 TTY1 彻底让给 greetd systemctl disable getty@tty1.service 2>/dev/null # 配置 greetd (覆盖写入 config.toml) log "Configuring /etc/greetd/config.toml..." local GREETD_CONF="/etc/greetd/config.toml" cat < "$GREETD_CONF" [terminal] # 绑定到 TTY1 vt = 1 [default_session] # 使用 tuigreet 作为前端 # 自动扫描 /usr/share/wayland-sessions/,支持时间显示、密码星号、记住上次选择 command = "tuigreet --time --user-menu --remember --remember-user-session --asterisks" user = "greeter" EOF # 修复 tuigreet 的 --remember 缓存目录权限 log "Ensuring cache directory permissions for tuigreet..." mkdir -p /var/cache/tuigreet chown -R greeter:greeter /var/cache/tuigreet chmod 755 /var/cache/tuigreet # 启用服务 log "Enabling greetd service..." systemctl enable greetd.service success "greetd with tuigreet frontend has been successfully configured!" } # ============================================================================== # setup_ly - 安装并配置 ly 显示管理器 # ============================================================================== # 功能列表: # 1. 安装 ly 软件包 # 2. 禁用其他可能冲突的 TTY 登录服务 (getty/greetd) # 3. 编辑 /etc/ly/config.ini,开启 Matrix (代码雨) 背景动画 # 4. 启用 ly.service 开机自启 # 使用方法: setup_ly setup_ly() { log "Installing ly display manager..." exe pacman -S --noconfirm --needed ly # # 配置 ly (非破坏性修改 config.ini) # log "Configuring /etc/ly/config.ini for Matrix animation..." # local LY_CONF="/etc/ly/config.ini" # if [[ -f "$LY_CONF" ]]; then # # 使用 sed 精准替换: # # 1. 将注释掉的或现有的 animation = none 替换为 animation = matrix # sed -i 's/^[#[:space:]]*animation[[:space:]]*=.*/animation = matrix/' "$LY_CONF" # else # log "Warning: $LY_CONF not found! Please check ly installation." # fi # 启用服务 log "Enabling ly service..." systemctl enable ly@tty1 success "ly display manager with Matrix animation has been successfully configured!" }