Fix custom dictation hotkey fallbacks

This commit is contained in:
Leslie Leung committed 2026-05-04 23:56:37 +08:00
1 parent 61214aa8fb
commit 913039e435
3 files changed
+82 -15

No files matched your search

+26 -6
View File
@@ -642,9 +642,7 @@ pub fn set_dictation_hotkey(
binding: ShortcutBinding,
) -> Result<(), String> {
crate::shortcut_binding::validate_binding(&binding).map_err(|e| e.to_string())?;
if binding.modifiers.is_empty() && binding.primary.eq_ignore_ascii_case("shift") {
return Err("Shift 单键目前只能用于翻译快捷键".into());
}
reject_bare_shift_dictation_shortcut(&binding)?;
let mut prefs = coord.prefs().get();
prefs.dictation_hotkey = binding;
coord.prefs().set(prefs).map_err(|e| e.to_string())?;
@@ -706,11 +704,12 @@ fn reject_modifier_only_action_shortcut(binding: &ShortcutBinding) -> Result<(),
#[tauri::command]
pub fn validate_combo_hotkey(binding: ComboBinding) -> Result<(), String> {
crate::combo_hotkey::validate_binding(&ShortcutBinding {
let shortcut = ShortcutBinding {
primary: binding.primary,
modifiers: binding.modifiers,
})
.map_err(|e| e.to_string())
};
reject_bare_shift_dictation_shortcut(&shortcut)?;
crate::combo_hotkey::validate_binding(&shortcut).map_err(|e| e.to_string())
}
/// 设置自定义录音组合键并热更新 monitor。
@@ -721,6 +720,7 @@ pub fn set_combo_hotkey(coord: CoordinatorState<'_>, binding: ComboBinding) -> R
primary: binding.primary.clone(),
modifiers: binding.modifiers.clone(),
};
reject_bare_shift_dictation_shortcut(&shortcut)?;
crate::combo_hotkey::validate_binding(&shortcut).map_err(|e| e.to_string())?;
prefs.custom_combo_hotkey = Some(binding);
prefs.dictation_hotkey = shortcut;
@@ -731,6 +731,13 @@ pub fn set_combo_hotkey(coord: CoordinatorState<'_>, binding: ComboBinding) -> R
Ok(())
}
fn reject_bare_shift_dictation_shortcut(binding: &ShortcutBinding) -> Result<(), String> {
if binding.modifiers.is_empty() && binding.primary.eq_ignore_ascii_case("shift") {
return Err("Shift 单键目前只能用于翻译快捷键".into());
}
Ok(())
}
// ─────────────────────────── unused but exported (silences dead_code) ───────────────────────────
#[allow(dead_code)]
@@ -845,6 +852,19 @@ mod tests {
assert!(result.is_err());
}
#[test]
fn combo_hotkey_bare_shift_rejection_matches_dictation_setter() {
let binding = ShortcutBinding {
primary: "Shift".into(),
modifiers: vec![],
};
assert_eq!(
super::reject_bare_shift_dictation_shortcut(&binding),
Err("Shift 单键目前只能用于翻译快捷键".into())
);
}
#[tokio::test]
async fn fetch_provider_models_omits_authorization_when_api_key_is_empty() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
@@ -441,7 +441,7 @@ impl PreferencesStore {
ensure_dir(&dir)?;
let path = dir.join(PREFERENCES_FILE);
let prefs = if path.exists() {
read_or_default::<UserPreferences>(&path).unwrap_or_default()
read_or_default::<UserPreferences>(&path)?
} else {
UserPreferences::default()
};
+55 -8
View File
@@ -192,9 +192,11 @@ impl<'de> Deserialize<'de> for UserPreferences {
D: serde::Deserializer<'de>,
{
let wire = UserPreferencesWire::deserialize(deserializer)?;
let dictation_hotkey = wire.dictation_hotkey.unwrap_or_else(|| {
default_dictation_hotkey_from_legacy(&wire.hotkey, &wire.custom_combo_hotkey)
});
let dictation_hotkey = match wire.dictation_hotkey {
Some(binding) => binding,
None => default_dictation_hotkey_from_legacy(&wire.hotkey, &wire.custom_combo_hotkey)
.map_err(serde::de::Error::custom)?,
};
Ok(Self {
hotkey: wire.hotkey,
dictation_hotkey,
@@ -261,16 +263,21 @@ fn default_app_shortcut_modifiers() -> Vec<String> {
fn default_dictation_hotkey_from_legacy(
hotkey: &HotkeyBinding,
custom_combo_hotkey: &Option<ComboBinding>,
) -> ShortcutBinding {
) -> Result<ShortcutBinding, String> {
if hotkey.trigger == HotkeyTrigger::Custom {
if let Some(combo) = custom_combo_hotkey {
return ShortcutBinding {
return Ok(ShortcutBinding {
primary: combo.primary.clone(),
modifiers: combo.modifiers.clone(),
};
});
}
return Err(
"hotkey.trigger is custom but dictationHotkey/customComboHotkey is missing".into(),
);
}
crate::shortcut_binding::binding_from_legacy_trigger(hotkey.trigger)
Ok(crate::shortcut_binding::binding_from_legacy_trigger(
hotkey.trigger,
))
}
fn default_working_languages() -> Vec<String> {
@@ -284,7 +291,8 @@ impl Default for UserPreferences {
dictation_hotkey: default_dictation_hotkey_from_legacy(
&HotkeyBinding::default(),
&None,
),
)
.expect("default legacy hotkey is not custom"),
default_mode: PolishMode::Light,
enabled_modes: vec![
PolishMode::Raw,
@@ -759,4 +767,43 @@ mod tests {
assert!(prefs.allow_non_tsf_insertion_fallback);
}
#[test]
fn legacy_custom_hotkey_without_custom_binding_is_rejected() {
let result = serde_json::from_str::<UserPreferences>(
r#"{
"hotkey": { "trigger": "custom", "mode": "toggle" }
}"#,
);
assert!(result.is_err());
}
#[test]
fn legacy_custom_hotkey_uses_custom_combo_binding() {
let prefs: UserPreferences = serde_json::from_str(
r#"{
"hotkey": { "trigger": "custom", "mode": "toggle" },
"customComboHotkey": { "primary": "D", "modifiers": ["cmd", "shift"] }
}"#,
)
.unwrap();
assert_eq!(prefs.dictation_hotkey.primary, "D");
assert_eq!(prefs.dictation_hotkey.modifiers, vec!["cmd", "shift"]);
}
#[test]
fn custom_hotkey_with_dictation_hotkey_preserves_dictation_binding() {
let prefs: UserPreferences = serde_json::from_str(
r#"{
"hotkey": { "trigger": "custom", "mode": "toggle" },
"dictationHotkey": { "primary": "Space", "modifiers": ["ctrl"] }
}"#,
)
.unwrap();
assert_eq!(prefs.dictation_hotkey.primary, "Space");
assert_eq!(prefs.dictation_hotkey.modifiers, vec!["ctrl"]);
}
}