diff --git a/config/niri/.config/niri/config/binds.kdl b/config/niri/.config/niri/config/binds.kdl index ee2428f..6cd2664 100644 --- a/config/niri/.config/niri/config/binds.kdl +++ b/config/niri/.config/niri/config/binds.kdl @@ -65,10 +65,11 @@ binds { XF86MonBrightnessDown allow-when-locked=true { spawn "qs" "ipc" "call" "brightness" "down"; } // Window management - Mod+Tab repeat=false { toggle-overview; } + Mod+Tab repeat=false { toggle-overview; } - Mod+Q repeat=false { close-window; } - Alt+F4 repeat=false { close-window; } // can't imagine this does not come as default + Mod+Q repeat=false { close-window; } + Mod+Shift+Q repeat=false { spawn "niri-force-kill-window"; } + Alt+F4 repeat=false { close-window; } // can't imagine this does not come as default Mod+Left { focus-column-left; } Mod+Down { focus-window-or-workspace-down; } @@ -161,7 +162,7 @@ binds { Mod+Escape allow-inhibiting=false repeat=false { toggle-keyboard-shortcuts-inhibit; } // Session - Mod+Shift+Q allow-inhibiting=false repeat=false { quit; } + Mod+K allow-inhibiting=false repeat=false { quit; } Mod+Shift+P allow-inhibiting=false repeat=false { spawn-sh "hyprlock & niri msg action power-off-monitors"; } Mod+L allow-inhibiting=false repeat=false { spawn "loginctl" "lock-session"; } diff --git a/config/scripts/.local/scripts/niri-force-kill-window b/config/scripts/.local/scripts/niri-force-kill-window new file mode 100755 index 0000000..171d771 --- /dev/null +++ b/config/scripts/.local/scripts/niri-force-kill-window @@ -0,0 +1,182 @@ +#!/usr/bin/env bash + +# https://github.com/SHORiN-KiWATA/shorin-contrib +# +# niri-force-kill-window +# 通过鼠标点击强制结束 (SIGKILL) 任意窗口。 +# 完美支持 Wayland 原生与 XWayland 代理窗口的独立精准击杀。 +# 具备多语言自适应 (i18n) 与智能依赖提示功能。 +# +# 实现原理: +# 1. 通过 niri msg pick-window 点选窗口 +# 2. 利用窗口的 PID,通过进程名称判断它是 xwayland 还是 wayland +# 3. 获取真实的底层 PID(Wayland 直接取用,XWayland 利用 xprop 获取焦点窗口的真实 PID) +# 4. 向上追溯该真实 PID,找到所属应用的根进程 (App Root),并利用防火墙逻辑避开系统进程 +# 5. 从根进程向下遍历收集所有子孙进程,利用 kill -9 执行“九头蛇绞杀”,防止多进程软件死灰复燃 + +# ========================================== +# 0. 多语言 (Locale) 自适应支持 +# ========================================== +if [[ "${LANG}" == zh_* ]]; then + STR_ERR_DEP_TITLE="依赖缺失" + STR_ERR_DEP_MSG="缺少必需命令: %s\n请尝试安装包: %s" + STR_ERR_INFO_TITLE="获取信息失败" + STR_ERR_INFO_MSG="无法获取窗口或进程信息。" + STR_ERR_KILL_TITLE="结束失败" + STR_ERR_KILL_MSG="无法提取目标真实 PID。" + STR_SUCC_TITLE="强制杀死 (%s)" + STR_SUCC_MSG="应用: %s\n连根拔起: 成功摧毁 %s 个相关进程。" + STR_UNKNOWN_APP="未知应用" +else + STR_ERR_DEP_TITLE="Missing Dependency" + STR_ERR_DEP_MSG="Command not found: %s\nPlease install package: %s" + STR_ERR_INFO_TITLE="Info Error" + STR_ERR_INFO_MSG="Could not determine window or process information." + STR_ERR_KILL_TITLE="Kill Failed" + STR_ERR_KILL_MSG="Could not extract target real PID." + STR_SUCC_TITLE="Headshot! (%s)" + STR_SUCC_MSG="App: %s\nHydra Kill: %s processes terminated." + STR_UNKNOWN_APP="Unknown App" +fi + +# ========================================== +# 1. 环境与依赖检查 +# ========================================== +check_dependency() { + local cmd="$1" + local pkg="$2" + if ! command -v "$cmd" &> /dev/null; then + local msg + printf -v msg "$STR_ERR_DEP_MSG" "$cmd" "$pkg" + + # 如果 notify-send 存在,则发送桌面通知;否则输出到终端标准错误 + if command -v notify-send &> /dev/null; then + notify-send "$STR_ERR_DEP_TITLE" "$msg" -u critical -i dialog-error + else + echo -e "[${STR_ERR_DEP_TITLE}]\n${msg}" >&2 + fi + exit 1 + fi +} + +# 检查三大核心依赖并提示对应软件包 +check_dependency "niri" "niri" +check_dependency "notify-send" "libnotify" +check_dependency "xprop" "xorg-xprop" + +# ========================================== +# 2. 辅助函数:发送通知与音效 (非阻塞) +# ========================================== +notify_and_play() { + local title="$1" + local msg="$2" + notify-send "$title" "$msg" -a "Window Killer" -i application-exit + + # pw-play 不是强依赖,如果有则播放音效,放入后台运行防止阻塞 + if command -v pw-play &> /dev/null; then + pw-play /usr/share/sounds/freedesktop/stereo/dialog-error.oga >/dev/null 2>&1 & + fi +} + +# ========================================== +# 3. 抓取目标窗口信息 +# ========================================== +# 执行命令并捕获退出码。如果用户按 Esc 取消,返回非零退出码,静默退出 +if ! output=$(niri msg pick-window 2>/dev/null); then + exit 0 +fi + +# 如果没有任何输出,直接退出 +if [[ -z "$output" ]]; then + exit 0 +fi + +# 提取 Niri 视角下的 PID 和 App ID +pid=$(grep -oP 'PID:\s*\K\d+' <<< "$output") +app_id=$(grep -oP 'App ID:\s*"\K[^"]+' <<< "$output") +app_name="${app_id:-$STR_UNKNOWN_APP}" + +# 如果正则没有提取到 PID(未命中合法窗口),静默退出 +if [[ -z "$pid" ]]; then + exit 0 +fi + +# 如果确实抓到了 PID,但此时进程已不存在,发出异常通知 +if [[ ! -f "/proc/$pid/comm" ]]; then + notify-send "$STR_ERR_INFO_TITLE" "$STR_ERR_INFO_MSG" -a "Window Killer" -i dialog-error + exit 1 +fi + +# ========================================== +# 4. 判定协议类型并获取真实 PID +# ========================================== +process_name=$(cat "/proc/$pid/comm") +process_name_lower="${process_name,,}" + +if [[ "$process_name_lower" == *"xwayland"* ]]; then + proto_str="XWayland" + # 给内核与 X11 服务端预留 50 毫秒的时间传递和同步焦点 + sleep 0.05 + # 顺着焦点,询问 X11 当前活动的窗口 ID + active_wid=$(xprop -root -notype _NET_ACTIVE_WINDOW 2>/dev/null | grep -o '0x[0-9a-fA-F]\+') + # 顺藤摸瓜:提取这个 X11 窗口绑定的真实底层 Linux PID + real_pid=$(xprop -id "$active_wid" -notype _NET_WM_PID 2>/dev/null | grep -oP '\d+') +else + proto_str="Wayland" + real_pid="$pid" +fi + +if [[ -z "$real_pid" ]]; then + notify-send "$STR_ERR_KILL_TITLE" "$STR_ERR_KILL_MSG" -a "Window Killer" -i dialog-error + exit 1 +fi + +# ========================================== +# 5. 九头蛇绞杀逻辑 (Hydra Kill) +# ========================================== +# 向上追溯,寻找进程家族的老祖宗 (App Root) +app_root=$real_pid +current=$real_pid + +while true; do + ppid=$(ps -o ppid= -p "$current" 2>/dev/null | tr -d ' ') + + # 如果找不到父进程,或者父进程是系统最高层 PID 1,停止追溯 + if [[ -z "$ppid" || "$ppid" == "1" ]]; then + break + fi + + pname=$(ps -o comm= -p "$ppid" 2>/dev/null) + + # 【核心防火墙】:遇到桌面环境、终端、系统核心服务,立刻停止溯源! + if [[ "$pname" =~ ^(systemd|niri|bash|zsh|fish|tmux|screen|xwayland.*|sshd|login|init|sway|hyprland)$ ]]; then + break + fi + + app_root=$ppid + current=$ppid +done + +# 向下递归,收集家族所有子孙 PID +get_descendants() { + local p=$1 + echo "$p" + # pgrep -P 获取直接子进程 + for c in $(pgrep -P "$p" 2>/dev/null); do + get_descendants "$c" + done +} + +family_pids=$(get_descendants "$app_root") +pid_count=$(echo "$family_pids" | wc -w) + +# 执行联合绞杀:把所有收集到的 PID 一次性全部强制终止 +kill -9 $family_pids 2>/dev/null + +# ========================================== +# 6. 发送战果通知与音效 +# ========================================== +printf -v final_title "$STR_SUCC_TITLE" "$proto_str" +printf -v final_msg "$STR_SUCC_MSG" "$app_name" "$pid_count" + +notify_and_play "$final_title" "$final_msg"