feat(hotkey): 全局快捷键可停用/清空开关 (#576)#592
Conversation
0602 版本默认注册的「切换风格 / 唤起 App」全局快捷键无法关闭,与其他软件冲突 且会拦截软件内快捷键。本 PR 复用 QA 快捷键已有的 `Option<ShortcutBinding>=None` 停用模式,给这两个 action 键加上「可清空停用」能力。 默认仍开启(Some 默认键),对老用户零行为变化,仅新增可关。 后端: - types.rs: 内层 AppPrefs switch_style_hotkey/open_app_hotkey 改 Option;default fn 返回 Some(默认键);Wire::default 改携带 Some(缺字段=启用);from-wire 去掉 unwrap_or_else 直传 Option(None=停用不再被塌缩成默认键——这正是「无法关闭」的根因)。 - coordinator.rs: action_hotkey_binding 返回 Option;supervisor / update 加 None 反注册 分支;reset-latch 对 Option 解包。 - commands.rs: set_switch_style_hotkey/set_open_app_hotkey 接受 Option(None=停用,跳过 validate/overlap);所有 reject_*_overlap 读取处对 Option 解包(停用项不参与冲突检测)。 前端: - types.ts/ipc.ts: switchStyleHotkey/openAppHotkey 与 setter 改 nullable。 - ShortcutsSection: 有值→录制器+「停用」按钮;停用→「启用」按钮恢复默认(仿 QA 行)。 - lib/hotkey.ts: defaultSwitchStyleShortcut/defaultOpenAppShortcut helper。 - 5 份 i18n 加 settings.shortcuts.enable/disable。 测试:默认启用、缺字段启用、None 往返为 null、显式绑定往返、overlap 拒绝(含停用项跳过)。
|
@claude 请审核这个 PR:给「切换风格/唤起 App」全局快捷键加可停用开关(复用 QA 的 Option=None 模式)。重点看:(1) types.rs from-wire 去掉 unwrap_or_else 后,老 preferences.json/缺字段是否仍默认启用(向后兼容);(2) None=停用是否被所有 reject_*_overlap 冲突检测正确跳过;(3) supervisor/update 的 None 反注册分支是否会让停用即时生效且不重复注册;(4) 前端「停用/启用」交互是否覆盖 Some↔None 双向。 |
|
Claude finished @appergb's task in 5m 15s —— View job PR #592 代码审核任务清单
结论:✅ 逻辑正确,可合并,附 2 个轻微建议Focus 1:向后兼容性(types.rs from-wire 去掉
|
| 调用点 | switch_style None 跳过 | open_app None 跳过 |
|---|---|---|
set_qa_hotkey |
if let Some(switch_style) ✅ |
if let Some(open_app) ✅ |
set_dictation_hotkey |
if let Some(switch_style) ✅ |
if let Some(open_app) ✅ |
set_translation_hotkey |
if let Some(switch_style) ✅ |
if let Some(open_app) ✅ |
set_switch_style_hotkey(None) |
整个校验块被 if let Some(binding) 门控 ✅ |
同上 ✅ |
set_open_app_hotkey(None) |
同上 ✅ | 同上 ✅ |
set_combo_hotkey |
if let Some(switch_style) ✅ |
if let Some(open_app) ✅ |
reject_hotkey_collisions |
if let Some(switch_style) ✅ |
if let Some(open_app) ✅ |
| switch_style vs open_app 互检 | if let (Some(s), Some(o)) ✅ |
同上 ✅ |
所有冲突检测对都已正确处理停用状态,无遗漏。
Focus 3:supervisor / update 的 None 反注册分支
结论:立即生效且无重复注册 ✅
即时路径(用户点击「停用」):
// coordinator.rs update_action_hotkey_binding
let Some(binding) = action_hotkey_binding(&self.inner, kind) else {
take_action_hotkey_on_main_thread(&self.inner, kind); // 反注册
log::info!("...");
return; // ← 立即返回,不会再尝试注册
};一次调用 → 反注册 → 返回,无重复注册 ✅
supervisor loop(后台轮询):
let Some(binding) = action_hotkey_binding(&inner, kind) else {
take_action_hotkey_on_main_thread(&inner, kind); // 每 5s 重复一次
std::thread::sleep(std::time::Duration::from_secs(5));
continue;
};这和现有 modifier-only 短路键的处理模式完全一致(is_modifier_only_shortcut 也是每 5s 调一次)。take_action_hotkey_on_main_thread 把 hotkey 句柄置 None 是幂等操作,重复调用无副作用。
⚠️ 轻微建议(不阻塞合并):停用期间 supervisor 每 5 秒向 main thread 派发一次 no-op。可加一个「已停用」状态位,避免无意义的主线程派发。但这与现有 modifier-only 行为完全一致,不是新引入的问题,可作为后续优化。
重新启用的竞态:用户点「启用」→ update_action_hotkey_binding 立即注册 → supervisor 在下次醒来(≤5s)读到 Some(binding) 后走正常注册路径(会替换已有句柄,取决于实现,但预期是幂等的)。这是现有设计的已知权衡,不是本 PR 引入的问题。✅
Focus 4:前端「停用/启用」是否覆盖 Some↔None 双向
结论:双向均已覆盖 ✅
Some → None(停用):
{prefs.switchStyleHotkey ? (
<>
<ShortcutRecorder ... onSave={binding => setSwitchStyleHotkey(binding)} />
<button onClick={() => { setSwitchStyleHotkey(null); savePrefs({...prefs, switchStyleHotkey: null}); }}>
{t('settings.shortcuts.disable')}
</button>
</>
)}None → Some(启用):
{!prefs.switchStyleHotkey && (
<button onClick={() => {
const binding = defaultSwitchStyleShortcut();
setSwitchStyleHotkey(binding);
savePrefs({...prefs, switchStyleHotkey: binding});
}}>
{t('settings.shortcuts.enable')}
</button>
)}defaultSwitchStyleShortcut() / defaultOpenAppShortcut() 的平台判断(isMac ? ['cmd','shift'] : ['ctrl','shift'])与后端 default_app_shortcut_modifiers() 逻辑一致 ✅
5 种语言 i18n 键(enable/disable)全部补齐 ✅
⚠️ 轻微建议(不阻塞合并):「启用」按钮的onClick没有 try/catch。若默认键与现有快捷键冲突,后端返回错误后 UI 没有反馈(错误被 async event handler 静默吞掉)。不过现有录制器的onSave也没有显式错误处理,默认键冲突概率极低(Cmd+Shift+S/O),行为与组件整体风格一致,可视需要后续补 toast 提示。
其他小项
enableBtnStyle/disableBtnStyle用了var(--ol-blue)等 CSS 变量,与现有 codebase 风格一致,无硬编码颜色问题 ✅#[serde(default = "default_switch_style_hotkey")]留在UserPreferences字段上(第 609 行)——该注解在自定义Deserializeimpl 下实际不生效,但保留作文档价值,不影响正确性 ✅
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
User description
问题
Closes #576
0602 版本默认注册的「切换风格 / 唤起 App」全局快捷键无法关闭,与其他软件冲突,且会拦截软件内快捷键。
方案
复用 QA 快捷键已有的
Option<ShortcutBinding> = None停用模式,给这两个 action 键加「可清空停用」。默认仍开启(Some(默认键)),对老用户零行为变化,仅新增可关。根因:原先 wire→inner 转换用
unwrap_or_else(default)把None塌缩成默认键,用户根本无法表达「已停用」。本 PR 改为直传Option。改动
后端
types.rs:innerswitch_style_hotkey/open_app_hotkey改Option;default_*返回Some;Wire::default携带Some(缺字段=启用);from-wire 直传Option。coordinator.rs:action_hotkey_binding返回Option;supervisor / update 加None反注册分支;reset-latch 对Option解包。commands.rs:两个 setter 接受Option(None=停用,跳过 validate/overlap);所有reject_*_overlap读取处对Option解包(停用项不参与冲突检测)。前端
types.ts/ipc.ts:字段与 setter 改 nullable。ShortcutsSection:有值→录制器 +「停用」按钮;停用→「启用」按钮恢复默认(仿 QA 行)。lib/hotkey.ts:defaultSwitchStyleShortcut/defaultOpenAppShortcut。settings.shortcuts.enable/disable。验证
cargo check/cargo test --lib通过(新增 4 项:默认启用、缺字段启用、None 往返为 null、显式绑定往返;既有 overlap 拒绝测试不变)tsc --noEmit通过兼容性
preferences.json带绑定 → 读回Some(启用)。Some(默认键)(启用)。null,读回保持None(停用)。@claude 请审核。
PR Type
Bug fix, Enhancement
Description
后端将全局快捷键字段改为可空
Option前端UI增加启用/停用按钮
冲突检测跳过已停用的快捷键
序列化保持
null,默认仍启用Diagram Walkthrough
File Walkthrough
6 files
将 action 热键字段改为 Option允许传入 null 表示停用,跳过验证将 switchStyleHotkey 和 openAppHotkey 改为可空setter 接受 null 参数添加启用/停用按钮切换状态添加默认快捷键辅助函数1 files
根据 Option 反注册或更新绑定5 files
添加 enable/disable 翻译添加 enable/disable 翻译添加 enable/disable 翻译添加「启用」/「停用」翻译添加「啟用」/「停用」翻譯