Files
dotfiles/config/scripts/.local/scripts/niri-force-kill-window
T
2026-04-25 12:19:51 +02:00

183 lines
6.3 KiB
Bash
Executable File
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.
#!/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. 获取真实的底层 PIDWayland 直接取用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"