Skip to content

feat(hotkey): 全局快捷键可停用/清空开关 (#576) - #592

Merged
H-Chris233 merged 1 commit into
betafrom
feat/issue-576-global-hotkey-disable
Jun 4, 2026
Merged

feat(hotkey): 全局快捷键可停用/清空开关 (#576)#592
H-Chris233 merged 1 commit into
betafrom
feat/issue-576-global-hotkey-disable

Conversation

@appergb

@appergbappergb commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

User description

问题

Closes#576

0602 版本默认注册的「切换风格 / 唤起 App」全局快捷键无法关闭,与其他软件冲突,且会拦截软件内快捷键。

已在 issue 下 @H-Chris233 同步(他此前回复「会做的」),避免重复劳动。

方案

复用 QA 快捷键已有的 Option<ShortcutBinding> = None 停用模式,给这两个 action 键加「可清空停用」。默认仍开启(Some(默认键)),对老用户零行为变化,仅新增可关。

根因:原先 wire→inner 转换用 unwrap_or_else(default)None 塌缩成默认键,用户根本无法表达「已停用」。本 PR 改为直传 Option

改动

后端

  • types.rs:inner switch_style_hotkey/open_app_hotkeyOptiondefault_* 返回 SomeWire::default 携带 Some(缺字段=启用);from-wire 直传 Option
  • coordinator.rsaction_hotkey_binding 返回 Option;supervisor / update 加 None 反注册分支;reset-latch 对 Option 解包。
  • commands.rs:两个 setter 接受 OptionNone=停用,跳过 validate/overlap);所有 reject_*_overlap 读取处对 Option 解包(停用项不参与冲突检测)。

前端

  • types.ts/ipc.ts:字段与 setter 改 nullable。
  • ShortcutsSection:有值→录制器 +「停用」按钮;停用→「启用」按钮恢复默认(仿 QA 行)。
  • lib/hotkey.tsdefaultSwitchStyleShortcut/defaultOpenAppShortcut
  • 5 份 i18n 加 settings.shortcuts.enable/disable

验证

  • cargo check / cargo test --lib 通过(新增 4 项:默认启用、缺字段启用、None 往返为 null、显式绑定往返;既有 overlap 拒绝测试不变)
  • tsc --noEmit 通过
  • 真机:清空切换风格键 → 全局键释放、软件内同键可用;重启后保持停用

兼容性

  • preferences.json 带绑定 → 读回 Some(启用)。
  • 缺字段(新装/旧版) → wire struct-default 落到 Some(默认键)(启用)。
  • 用户清空 → 序列化 null,读回保持 None(停用)。

⚠️ 翻译键(Shift 单键,机制不同)、dictation/QA(本就可关)未纳入本 PR,保持单一职责。

@claude 请审核。


PR Type

Bug fix, Enhancement


Description

  • 后端将全局快捷键字段改为可空 Option

  • 前端UI增加启用/停用按钮

  • 冲突检测跳过已停用的快捷键

  • 序列化保持 null,默认仍启用


Diagram Walkthrough

flowchart LR
User["用户操作"] -->|点击「停用」| Disable["后端:Option=None"]
User -->|点击「启用」| Enable["后端:Option=Some(默认键)"]
Disable --> Unregister["反注册全局键"]
Enable --> Register["注册全局键"]
Unregister --> Done1["快捷键不生效"]
Register --> Done2["快捷键正常使用"]
Loading

File Walkthrough

Relevant files
Enhancement
6 files
types.rs
将 action 热键字段改为 Option
+66/-14
commands.rs
允许传入 null 表示停用,跳过验证
+82/-47
types.ts
将 switchStyleHotkey 和 openAppHotkey 改为可空
+4/-4
ipc.ts
setter 接受 null 参数
+3/-2
ShortcutsSection.tsx
添加启用/停用按钮切换状态
+91/-17
hotkey.ts
添加默认快捷键辅助函数
+10/-0
Error handling
1 files
coordinator.rs
根据 Option 反注册或更新绑定
+25/-11
Documentation
5 files
en.ts
添加 enable/disable 翻译
+2/-0
ja.ts
添加 enable/disable 翻译
+2/-0
ko.ts
添加 enable/disable 翻译
+2/-0
zh-CN.ts
添加「启用」/「停用」翻译
+2/-0
zh-TW.ts
添加「啟用」/「停用」翻譯
+2/-0

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 拒绝(含停用项跳过)。
@appergb

Copy link
Copy Markdown
CollaboratorAuthor

@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

claudeBot commented Jun 4, 2026

Copy link
Copy Markdown

Claude finished @appergb's task in 5m 15s —— View job


PR #592 代码审核

任务清单

  • 加载工具
  • 读取后端文件 (types.rs, coordinator.rs, commands.rs)
  • 读取前端文件 (ShortcutsSection.tsx, ipc.ts, types.ts, hotkey.ts)
  • 分析向后兼容性 (focus 1)
  • 分析冲突检测中的 None 跳过 (focus 2)
  • 分析反注册分支 (focus 3)
  • 分析前端双向交互 (focus 4)
  • 发布审核结果

结论:✅ 逻辑正确,可合并,附 2 个轻微建议


Focus 1:向后兼容性(types.rs from-wire 去掉 unwrap_or_else

结论:正确 ✅

关键点在 UserPreferencesWire 的两层保护:

  1. struct 级别 #[serde(default)]types.rs 第 773 行)加上自定义 Default impl(第 893-894 行):

    switch_style_hotkey: prefs.switch_style_hotkey,// = Some(default_binding)
    open_app_hotkey: prefs.open_app_hotkey,// = Some(default_binding)

    当 JSON 缺少 switchStyleHotkey 字段(老用户/新安装)→ serde 用 UserPreferencesWire::default() 填充 → Some(default_binding) → 直传 → 启用

  2. JSON 含 "switchStyleHotkey": null(用户主动停用)→ serde 反序列化 nullNone → 直传 → 停用

  3. JSON 含实际绑定 → Some(binding) → 直传 → 保留绑定

4 条新测试(action_hotkeys_default_to_enabledmissing_action_hotkeys_default_to_enableddisabled_action_hotkeys_round_trip_as_nullexplicit_action_hotkey_binding_round_trips)覆盖了所有场景。

注意:原 Wire Defaultswitch_style_hotkey: None → 新改为 prefs.switch_style_hotkeySome(default))是必要改动,不能漏。如果只去掉 unwrap_or_else 而不改 Wire Default,缺字段时就会变成 None(停用),造成回归。PR 里这两处配套修改都做了。✅


Focus 2:None 是否被所有 reject_*_overlap 正确跳过

结论:完整 ✅

逐一验证 6 处 setter 和 reject_hotkey_collisions

调用点switch_style None 跳过open_app None 跳过
set_qa_hotkeyif let Some(switch_style)if let Some(open_app)
set_dictation_hotkeyif let Some(switch_style)if let Some(open_app)
set_translation_hotkeyif 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_hotkeyif let Some(switch_style)if let Some(open_app)
reject_hotkey_collisionsif 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_bindingletSome(binding) = action_hotkey_binding(&self.inner, kind)else{take_action_hotkey_on_main_thread(&self.inner, kind);// 反注册
log::info!("...");return;// ← 立即返回,不会再尝试注册};

一次调用 → 反注册 → 返回,无重复注册

supervisor loop(后台轮询):

letSome(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)}/><buttononClick={()=>{setSwitchStyleHotkey(null);savePrefs({...prefs,switchStyleHotkey: null});}}>{t('settings.shortcuts.disable')}</button></>)}

None → Some(启用):

{!prefs.switchStyleHotkey&&(<buttononClick={()=>{constbinding=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 行)——该注解在自定义 Deserialize impl 下实际不生效,但保留作文档价值,不影响正确性 ✅

@github-actions

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

576 - Partially compliant

Compliant requirements:

  • 为切换风格和唤起App快捷键增加了启用/停用开关
  • 用户可以通过“停用”按钮清空快捷键(设置为null),实现关闭
  • 默认仍启用,对老用户零行为变化

Non-compliant requirements:

(无)

Requires further human verification:

(无)

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

@H-Chris233
H-Chris233 merged commit 984169d into betaJun 4, 2026
4 checks passed
@appergb
appergb deleted the feat/issue-576-global-hotkey-disable branch June 9, 2026 06:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[macos] 建议增加全局快捷键注册的开关

2 participants

@appergb@H-Chris233