目标与现状
问题:在 KVM 里操作旧 Win10 时,宿主机与 guest 的快捷键互相干扰 —— 有的键被宿主机抢先处理,有的在 guest 里无效。
最终诉求(用户 2026-10-15 定稿,只保留这一条):只有 Win / Win+E / Win+Esc 三个键需要「激活窗口优先」 —— VM 控制台窗口聚焦时只进 guest,宿主窗口聚焦时只由宿主机处理;其余快捷键不在本机制范围内,用户自行克服。
现状:已达成。机制是一条 KWin 窗口规则(by_focus 层),不需要任何额外软件。所有快捷键的行为只分两类:host 处理 或 激活窗口处理。
前置事实(实测)
| 项 | 实测值 |
|---|---|
| VM 控制台窗口识别 | virt-manager 控制台 WM_CLASS .virt-manager-wrapped(wmname QEMU/KVM);virt-viewer WM_CLASS .virt-viewer-wrapped |
| 压制机制 | KWin 窗口规则动作 disableglobalshortcuts → kwin/src/workspace.cpp:disableGlobalShortcutsForClient() → D-Bus org.kde.KGlobalAccel.blockGlobalShortcuts(true) ⇒ 该窗口聚焦期间 KGlobalAccel 整块失效 |
| 粒度 | 窗口级,不是按键级 —— 该窗口内 KGlobalAccel 全有全无,做不到「只放开某几个键」 |
| 规则生效的必要条件 | 每个动作必须同时写 <动作>=值 与 <动作>rule=<1..4> 策略键;缺策略键 ⇒ 整个动作被静默忽略(1=Apply 2=Force 3=Remember 4=ApplyNow) |
| 本机环境 | Plasma / KWin 6(X11 会话);实时配置文件 ~/.config/kwinrulesrc(KWin 只从 KConfig 读,系统级 /etc/xdg 的规则列表合并不可靠) |
实现过程
数据(单一数据源)
/etc/nixos/modules/system/service/kwin-data/kwin-hotkey-router-rules.yml 的 window_rules 段,两条:
window_rules:
- uuid: "248e7dde-…" # virt-viewer
description: "virt-viewer 窗口:忽略全局快捷键(Win 等交给 guest)"
actions: { disableglobalshortcuts: true }
match: { wmclass: "virt-viewer", wmclassmatch: "2" }
- uuid: "4b55d2a4-…" # virt-manager 控制台
description: "virt-manager 控制台窗口:忽略全局快捷键(Win 等交给 guest)"
actions: { disableglobalshortcuts: true }
match: { wmclass: "virt-manager", wmclassmatch: "2", wmname: "QEMU/KVM", wmnamematch: "2" }
生成
kwin.nix 里的 kwinRulesMerge(python + pyyaml,rebuild 激活期运行)把 yml 渲染成 KWin 的 kconfig 段,并为每个动作自动补 <动作>rule=2(Force),写进 ~/.config/kwinrulesrc,随后调 org.kde.KWin.reconfigure 让运行中的 KWin 当场重载。
覆盖语义
段级整体覆盖:yml 里出现的 uuid 段被整体替换,未定义的段原样保留 —— 用户在 KDE 界面里自己加的规则不受影响(例如 gwenview 的 Meta+Shift+E 规则)。
生效逻辑
控制台窗口聚焦 ──► KWin 规则命中 ──► blockGlobalShortcuts(true) ──► Win / Win+E / Win+Esc 流给 guest
其它窗口聚焦 ──► 规则不命中 ──► KGlobalAccel 正常工作 ──► 宿主机处理
实测证据
| 检查 | 结果 |
|---|---|
~/.config/kwinrulesrc | 两条 nix 规则均为 disableglobalshortcuts=true + disableglobalshortcutsrule=2;count=3(含用户自己的 gwenview 规则) |
| VM 控制台窗口内按 Win / Win+E / Win+Esc | 只由 guest 响应,宿主机无动作 ✅ |
| 宿主窗口内按 Win / Win+E / Win+Esc | 只由宿主机响应 ✅ |
| 宿主侧原有快捷键 | 恢复原键(CenterFitWindow=Meta+Shift+X、Window Maximize=Meta+PgUp、Switch One Desktop to the Left=Meta+Ctrl+Left 等)✅ |
管理命令
| 用途 | 命令 |
|---|---|
看规则是否真的生效(必须有 rule= 行) | grep -nE "disableglobalshortcuts" ~/.config/kwinrulesrc |
| 改规则 | 编辑上述 yml → /run/wrappers/bin/sudo -n nixos-rebuild switch(自动重写规则 + 重载 KWin) |
| 只让 KWin 重载配置(不 rebuild) | qdbus org.kde.KWin /KWin org.kde.KWin.reconfigure |
| 看当前 KWin 会话信息 / 规则文件 | qdbus org.kde.KWin /KWin supportInformation | head;head ~/.config/kwinrulesrc |
机制说明:yml 是数据,kwin.nix 是唯一生成器,rebuild 是唯一落地时机 —— 手改 kwinrulesrc 会在下次 rebuild 被 nix 定义的段覆盖(未定义段不受影响)。
踩坑记录
① 规则只有 =true、缺 rule= 策略键 ⇒ 整条动作被静默忽略
- 现象:以为规则在生效,实际热键「两头都响应」——
kwinrulesrc里明明有disableglobalshortcuts=true却毫无作用。 - 成因:KWin 规则的每个动作都要配策略键
<动作>rule=<1..4>,缺省等于不应用。 - 改法:生成器为每个动作补
<动作>rule=2(Force;仍不生效可试 1=Apply)。在 KDE 界面里手建的规则是好的(界面会自动写rule=),换成脚本生成后就静默失效了 —— 这是排查时最容易走偏的一步。
② 窗口规则是窗口级、不是按键级
- 现象:想「只让某几个键交给 guest、其余宿主优先」时无法表达。
- 成因:
blockGlobalShortcuts是布尔整块开关。 - 处置:本需求只保留三个键,窗口级正好够用(用户 2026-10-15 定稿「其余自己克服」)。
③ evdev 层方案(keyd)会误伤宿主侧绑定
- 现象:宿主侧
Win+Shift+X/Z、Win+Ctrl+方向全部失效,且裸Win在 VM 内同时触发两侧。 - 成因:在 evdev 层介入按键,与 KGlobalAccel 的作用域交叉抢占。
- 处置:整个 keyd 方案撤回(
modules/system/service/keyd.nix已删除),只用窗口规则。
④ sxhkd + 解绑 KGlobalAccel 方案在 VM 窗口内无效
- 现象:宿主窗口里生效,进 VM 控制台窗口一律失效(连应用自己抓的键也失效)。
- 成因:SPICE 控制台控件(virt-manager 控制台与 virt-viewer 是同一个 widget)聚焦时持 X 键盘 active grab,压过所有 X 层抓键(KGlobalAccel、XGrabKey、应用自身抓键)。
- 处置:撤回;本需求用窗口规则即可,不需要 X 层抓键。
⑤ KWin 保存自身配置会覆盖外部写入的规则段
- 现象:
count一度从 3 掉到 2,一条 nix 规则凭空消失(随后 rebuild 自愈)。 - 成因:KWin 按内存态重写整个
kwinrulesrc。 - 处置:以 rebuild 为自愈手段,需要时重跑一次重建。
边界
- 本机制只覆盖
Win/Win+E/Win+Esc的激活窗口优先,不覆盖其它快捷键。 - 不覆盖「在 VM 窗口内也让宿主机响应」的键 —— 那需要 evdev 层方案,已判定会误伤宿主绑定,不做。
- 不覆盖应用自身的 X11 抓键(如微信的自定义键,在控制台窗口内受 active grab 影响)。
- 反检测 / 性能分档等其它主题不在本篇范围。
- 「用起来对不对」属人工验收项:按上表三条实测判据自测。
相关:完整取证在 nixos 项目台账 docs/legacy-win-vm.md §13–§19;可复用流程见 skill kwin-hotkey-routing。