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

612 lines
20 KiB
Bash
Raw Permalink 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
# ==============================================================================
# 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 <<EOF > "$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!"
}