#!/usr/bin/env bash set -Eeuo pipefail readonly PROJECT="niri-beyond-glass" readonly NIRI_TAG="v26.04" readonly XWS_TAG="v0.8.2" readonly RUST_TOOLCHAIN="1.85.1" readonly DMS_REV="069ddab041c738236a8910e4c39b65d9628d3018" readonly NIXPKGS_REV="d407951447dcd00442e97087bf374aad70c04cea" readonly ARCHIVE_URL="https://git.k1412.top/wuyang/niri-beyond-glass/archive/main.tar.gz" wallpaper="" config_only=0 dry_run=0 rebuild=0 source_dir="${NBG_SOURCE_DIR:-}" tmp_dir="" selected_wallpaper="" usage() { cat <<'EOF' 用法:install.sh [选项] --wallpaper PATH 使用指定的静态壁纸 --config-only 只更新配置(要求 DMS 运行层已经安装) --rebuild 强制重新编译 Niri 和 Xwayland Satellite --dry-run 显示将执行的主要步骤,不修改系统 -h, --help 显示帮助 EOF } log() { printf '\033[1;36m[Niri]\033[0m %s\n' "$*" } die() { printf '\033[1;31m错误:\033[0m %s\n' "$*" >&2 exit 1 } run() { if (( dry_run )); then printf '+' printf ' %q' "$@" printf '\n' else "$@" fi } cleanup() { if [[ -n "$tmp_dir" && -d "$tmp_dir" ]]; then rm -rf "$tmp_dir" fi } trap cleanup EXIT while (($#)); do case "$1" in --wallpaper) (($# >= 2)) || die "--wallpaper 缺少路径" wallpaper="$2" shift 2 ;; --config-only) config_only=1 shift ;; --rebuild) rebuild=1 shift ;; --dry-run) dry_run=1 shift ;; -h|--help) usage exit 0 ;; *) die "未知参数:$1" ;; esac done [[ ${EUID:-$(id -u)} -ne 0 ]] || die "请用普通用户运行,脚本会在需要时调用 sudo。" [[ -z "$wallpaper" || -f "$wallpaper" ]] || die "壁纸不存在:$wallpaper" if [[ -r /etc/os-release ]]; then # shellcheck disable=SC1091 . /etc/os-release else die "无法识别操作系统。" fi if [[ "${ID:-}" != "ubuntu" || "${VERSION_ID:-}" != "24.04" ]]; then die "只支持 Ubuntu 24.04;当前是 ${PRETTY_NAME:-未知系统}。" fi case "$(uname -m)" in x86_64|aarch64) ;; *) die "目前只支持 x86_64 和 aarch64。" ;; esac fetch_source() { if [[ -n "$source_dir" ]]; then [[ -f "$source_dir/config/niri/config.kdl" ]] || die "NBG_SOURCE_DIR 不是有效源码目录。" return fi tmp_dir="$(mktemp -d)" log "下载配置仓库" curl -fL --retry 3 "$ARCHIVE_URL" -o "$tmp_dir/source.tar.gz" mkdir -p "$tmp_dir/source" tar -xzf "$tmp_dir/source.tar.gz" -C "$tmp_dir/source" --strip-components=1 source_dir="$tmp_dir/source" } install_packages() { log "安装 Ubuntu 24.04 依赖" run sudo apt-get update run sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y \ build-essential clang git curl ca-certificates pkg-config \ meson ninja-build cmake hwdata \ libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev \ libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev \ libseat-dev libpipewire-0.3-dev libpango1.0-dev \ libdisplay-info-dev libcairo2-dev libglib2.0-dev \ libxcb1-dev libxcb-composite0-dev libxcb-randr0-dev \ libxcb-res0-dev libxcb-xfixes0-dev libxcb-cursor-dev \ xwayland xdg-desktop-portal-gnome xdg-desktop-portal-gtk \ alacritty \ nautilus policykit-1-gnome pipewire-pulse wireplumber \ brightnessctl playerctl grim slurp wl-clipboard libnotify-bin \ accountsservice jq nix-bin nix-setup-systemd \ fonts-noto-core fonts-noto-cjk fonts-noto-color-emoji fonts-font-awesome \ fonts-inter fonts-jetbrains-mono } install_dms_runtime() { local state_home="${XDG_STATE_HOME:-$HOME/.local/state}" local profile="$state_home/$PROJECT/nix-profile" local dms_flake="github:AvengeMedia/DankMaterialShell/$DMS_REV" local nixpkgs="github:NixOS/nixpkgs/$NIXPKGS_REV" if [[ -x "$profile/bin/dms" && -x "$profile/bin/quickshell" && -x "$profile/bin/matugen" && -x "$profile/bin/dgop" && -x "$profile/bin/dsearch" ]]; then log "DMS 运行层已经安装,跳过" return fi log "安装隔离的 DMS 1.5.3 与 Quickshell 0.3.0" run sudo usermod -aG nix-users "$USER" run sudo systemctl enable --now nix-daemon.socket run mkdir -p "$state_home/$PROJECT" if (( dry_run )); then printf '+ sudo -u %q -g nix-users nix profile install --profile %q ...\n' \ "$USER" "$profile" return fi local attempt for attempt in 1 2 3 4 5; do if sudo -u "$USER" -g nix-users env \ HOME="$HOME" USER="$USER" \ NIX_CONFIG="experimental-features = nix-command flakes" \ nix profile install --profile "$profile" \ "$dms_flake#dms-shell" \ "$nixpkgs#quickshell" \ "$nixpkgs#matugen" \ "$nixpkgs#dgop" \ "$nixpkgs#dsearch"; then break fi (( attempt < 5 )) || die "DMS 运行层安装失败,请检查网络后重新运行脚本。" log "下载中断,正在重试($attempt/5)" sleep 3 done [[ -x "$profile/bin/dms" && -x "$profile/bin/quickshell" ]] || die "DMS 或 Quickshell 安装不完整。" } install_rust() { export RUSTUP_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/$PROJECT/rustup" export CARGO_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/$PROJECT/cargo" export PATH="$CARGO_HOME/bin:$PATH" if [[ ! -x "$CARGO_HOME/bin/rustup" ]]; then log "安装隔离的 Rust $RUST_TOOLCHAIN 工具链" if (( dry_run )); then printf '+ curl https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal --default-toolchain %q\n' "$RUST_TOOLCHAIN" return fi local rustup_installer rustup_installer="$(mktemp)" curl --proto '=https' --tlsv1.2 -fsSL --retry 5 --retry-all-errors \ https://sh.rustup.rs -o "$rustup_installer" RUSTUP_MAX_RETRIES=5 sh "$rustup_installer" -y --no-modify-path \ --profile minimal --default-toolchain "$RUST_TOOLCHAIN" rm -f "$rustup_installer" elif ! "$CARGO_HOME/bin/rustup" run "$RUST_TOOLCHAIN" rustc --version >/dev/null 2>&1; then RUSTUP_MAX_RETRIES=5 run "$CARGO_HOME/bin/rustup" toolchain install \ "$RUST_TOOLCHAIN" --profile minimal fi run "$CARGO_HOME/bin/rustup" set auto-self-update disable run "$CARGO_HOME/bin/rustup" default "$RUST_TOOLCHAIN" } version_ge() { [[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" == "$2" ]] } install_libdisplay_info() { local installed="" installed="$(pkg-config --modversion libdisplay-info 2>/dev/null || true)" if [[ -n "$installed" ]] && version_ge "$installed" "0.3.0"; then return fi log "编译 libdisplay-info 0.3.0(Ubuntu 24.04 自带版本较旧)" local build_root="${XDG_CACHE_HOME:-$HOME/.cache}/$PROJECT/build" local src="$build_root/libdisplay-info" run mkdir -p "$build_root" if [[ ! -d "$src/.git" ]]; then run git clone --depth 1 --branch 0.3.0 \ https://gitlab.freedesktop.org/emersion/libdisplay-info.git "$src" fi run meson setup "$src/build" "$src" --prefix=/usr/local --buildtype=release --wipe run ninja -C "$src/build" run sudo ninja -C "$src/build" install run sudo ldconfig } install_niri() { local current="" current="$(niri --version 2>/dev/null || true)" if (( ! rebuild )) && [[ "$current" == *"26.4"* || "$current" == *"26.04"* ]]; then log "已安装兼容的 $current,跳过编译" return fi log "编译 Niri $NIRI_TAG" local build_root="${XDG_CACHE_HOME:-$HOME/.cache}/$PROJECT/build" local src="$build_root/niri" run mkdir -p "$build_root" if [[ ! -d "$src/.git" ]]; then run git clone --depth 1 --branch "$NIRI_TAG" https://github.com/niri-wm/niri.git "$src" else run git -C "$src" fetch --depth 1 origin "refs/tags/$NIRI_TAG:refs/tags/$NIRI_TAG" run git -C "$src" checkout -f "$NIRI_TAG" fi run cargo build --manifest-path "$src/Cargo.toml" --release --locked run sudo install -Dm755 "$src/target/release/niri" /usr/local/bin/niri run sudo install -Dm755 "$src/resources/niri-session" /usr/local/bin/niri-session run sudo install -Dm644 "$src/resources/niri.service" /usr/local/lib/systemd/user/niri.service run sudo install -Dm644 "$src/resources/niri-shutdown.target" /usr/local/lib/systemd/user/niri-shutdown.target run sudo install -Dm644 "$src/resources/niri.desktop" /usr/local/share/wayland-sessions/niri.desktop run sudo install -Dm644 "$src/resources/niri-portals.conf" /usr/local/share/xdg-desktop-portal/niri-portals.conf } install_xwayland_satellite() { if (( ! rebuild )) && command -v xwayland-satellite >/dev/null 2>&1; then return fi log "编译 Xwayland Satellite $XWS_TAG" local build_root="${XDG_CACHE_HOME:-$HOME/.cache}/$PROJECT/build" local src="$build_root/xwayland-satellite" run mkdir -p "$build_root" if [[ ! -d "$src/.git" ]]; then run git clone --depth 1 --branch "$XWS_TAG" \ https://github.com/Supreeeme/xwayland-satellite.git "$src" else run git -C "$src" fetch --depth 1 origin "refs/tags/$XWS_TAG:refs/tags/$XWS_TAG" run git -C "$src" checkout -f "$XWS_TAG" fi run cargo build --manifest-path "$src/Cargo.toml" --release --locked run sudo install -Dm755 "$src/target/release/xwayland-satellite" /usr/local/bin/xwayland-satellite } backup_and_install_config() { local config_home="${XDG_CONFIG_HOME:-$HOME/.config}" local state_home="${XDG_STATE_HOME:-$HOME/.local/state}" local stamp stamp="$(date +%Y%m%d-%H%M%S)" local backup_root="$state_home/$PROJECT/backups/$stamp" local backup="$backup_root/config" local original="$state_home/$PROJECT/original" local names=(niri waybar fuzzel mako swaylock niri-beyond-glass DankMaterialShell alacritty) select_wallpaper "$config_home/niri-beyond-glass/wallpaper" log "备份并安装用户配置" run mkdir -p "$backup" local name for name in "${names[@]}"; do if [[ -e "$config_home/$name" ]]; then run cp -a "$config_home/$name" "$backup/$name" fi done if [[ -d "$state_home/DankMaterialShell" ]]; then run mkdir -p "$backup_root/state" run cp -a "$state_home/DankMaterialShell" "$backup_root/state/DankMaterialShell" fi run mkdir -p "$original/config" "$original/absent" for name in "${names[@]}"; do if [[ ! -e "$original/config/$name" && ! -e "$original/absent/$name" ]]; then if [[ -e "$config_home/$name" ]]; then run cp -a "$config_home/$name" "$original/config/$name" else run touch "$original/absent/$name" fi fi done run touch "$original/.captured" if (( dry_run )); then printf '+ install DMS, Niri and Alacritty style while preserving wallpaper %q\n' \ "$selected_wallpaper" return fi mkdir -p \ "$config_home/niri" \ "$config_home/niri/dms" \ "$config_home/niri-beyond-glass" \ "$config_home/DankMaterialShell/themes" \ "$config_home/alacritty" \ "$config_home/systemd/user" \ "$state_home/DankMaterialShell" \ "$HOME/.local/bin" \ "$HOME/Pictures/Screenshots" install -m644 "$source_dir/config/niri/config.kdl" "$config_home/niri/config.kdl" sed "s|__HOME__|$HOME|g" "$source_dir/config/niri/beyond-glass.kdl" \ > "$config_home/niri/beyond-glass.kdl" install -m644 "$source_dir"/config/niri/dms/*.kdl "$config_home/niri/dms/" local theme_file="$config_home/DankMaterialShell/themes/beyond-glass.json" install -m644 "$source_dir/config/dms/beyond-glass.json" "$theme_file" local settings_tmp session_tmp settings_tmp="$(mktemp)" session_tmp="$(mktemp)" jq --arg wallpaper "$selected_wallpaper" --arg theme "$theme_file" ' .customThemeFile = $theme | .greeterWallpaperPath = $wallpaper | .lockScreenWallpaperPath = $wallpaper | .lockScreenVideoPath = "" ' "$source_dir/config/dms/settings.json" > "$settings_tmp" jq --arg wallpaper "$selected_wallpaper" ' .wallpaperPath = $wallpaper | .wallpaperPathLight = $wallpaper | .wallpaperPathDark = $wallpaper | .monitorWallpapers = {} | .monitorWallpapersLight = {} | .monitorWallpapersDark = {} | .perMonitorWallpaper = false ' "$source_dir/config/dms/session.json" > "$session_tmp" install -m644 "$settings_tmp" "$config_home/DankMaterialShell/settings.json" install -m644 "$session_tmp" "$state_home/DankMaterialShell/session.json" rm -f "$settings_tmp" "$session_tmp" touch "$config_home/DankMaterialShell/.firstlaunch" \ "$config_home/DankMaterialShell/.changelog-1.5" install -m644 "$source_dir/config/alacritty/alacritty.toml" \ "$config_home/alacritty/alacritty.toml" install -m644 "$source_dir/config/systemd/dms.service" \ "$config_home/systemd/user/dms.service" install -m755 "$source_dir/scripts/niri-beyond-browser" \ "$HOME/.local/bin/niri-beyond-browser" install -m755 "$source_dir/scripts/niri-beyond-refresh" \ "$HOME/.local/bin/niri-beyond-refresh" local profile="$state_home/$PROJECT/nix-profile" local binary for binary in dms quickshell qs matugen dgop dsearch; do if [[ -x "$profile/bin/$binary" ]]; then ln -sfn "$profile/bin/$binary" "$HOME/.local/bin/$binary" fi done printf '%s\n' "$selected_wallpaper" > "$config_home/niri-beyond-glass/wallpaper" } select_wallpaper() { local state_file="$1" local selected="$wallpaper" if [[ -z "$selected" ]]; then local existing="" if [[ -r "$state_file" ]]; then existing="$(head -n1 "$state_file")" fi if [[ -n "$existing" && -f "$existing" ]]; then selected="$existing" fi fi if [[ -z "$selected" ]]; then local session="${XDG_STATE_HOME:-$HOME/.local/state}/DankMaterialShell/session.json" local existing="" if [[ -r "$session" ]]; then existing="$(jq -r '.wallpaperPath // empty' "$session" 2>/dev/null || true)" fi if [[ -n "$existing" && -f "$existing" ]]; then selected="$existing" fi fi if [[ -z "$selected" ]] && command -v gsettings >/dev/null 2>&1; then local uri="" uri="$(gsettings get org.gnome.desktop.background picture-uri-dark 2>/dev/null \ | tr -d "'" || true)" uri="${uri#file://}" if [[ -n "$uri" && -f "$uri" ]]; then selected="$uri" fi fi if [[ -z "$selected" ]]; then local candidates=( /usr/share/backgrounds/warty-final-ubuntu.png /usr/share/backgrounds/ubuntu-default-greyscale-wallpaper.png ) local candidate for candidate in "${candidates[@]}"; do if [[ -f "$candidate" ]]; then selected="$candidate" break fi done fi if [[ -z "$selected" ]]; then selected="$(find /usr/share/backgrounds -maxdepth 2 -type f \ \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) \ -print -quit 2>/dev/null || true)" fi [[ -n "$selected" && -f "$selected" ]] || die "没有找到可用壁纸;可用 --wallpaper PATH 指定目标机上的图片。" selected_wallpaper="$selected" log "保留目标机壁纸:$selected_wallpaper" } activate_dms() { local profile="${XDG_STATE_HOME:-$HOME/.local/state}/$PROJECT/nix-profile" if (( dry_run )); then printf '+ stop legacy shell processes and enable dms.service\n' return fi if ! systemctl --user daemon-reload 2>/dev/null; then log "当前没有可用的 systemd 用户会话;DMS 将在下次 Niri 登录时启动" return fi systemctl --user disable --now waybar.service mako.service 2>/dev/null || true pkill -x waybar 2>/dev/null || true pkill -x mako 2>/dev/null || true pkill -x swaybg 2>/dev/null || true pkill -x swayidle 2>/dev/null || true systemctl --user enable dms.service "$profile/bin/dms" auth resolve-lock --quiet >/dev/null 2>&1 || true if [[ -n "${WAYLAND_DISPLAY:-}" ]]; then systemctl --user restart dms.service fi } validate_config() { if (( dry_run )); then printf '+ niri validate -c %q\n' "${XDG_CONFIG_HOME:-$HOME/.config}/niri/config.kdl" return fi command -v niri >/dev/null 2>&1 || die "找不到 niri,无法验证配置。" niri validate -c "${XDG_CONFIG_HOME:-$HOME/.config}/niri/config.kdl" } main() { fetch_source if (( ! config_only )); then install_packages install_rust install_libdisplay_info install_niri install_xwayland_satellite install_dms_runtime else local profile="${XDG_STATE_HOME:-$HOME/.local/state}/$PROJECT/nix-profile" [[ -x "$profile/bin/dms" && -x "$profile/bin/quickshell" ]] || die "--config-only 需要先完成一次完整安装。" fi backup_and_install_config validate_config activate_dms log "安装完成" printf '\nDMS 个性化已迁移,目标机原壁纸和显示器布局未修改。\n' printf '当前若在 Niri 中,顶栏会自动重启;否则注销后选择“Niri”登录。\n' printf 'GNOME 和 GDM 均未修改;快捷键说明:Super+Shift+/。\n' } main