Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions openless-all/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"copy:android-scaffolding": "node scripts/copy-android-scaffolding.mjs",
"check:macos-capsule-spaces": "node scripts/macos-capsule-spaces-contract.test.mjs",
"check:hotkey-injection": "node scripts/check-hotkey-injection.mjs",
"check:hotkey-side-modifiers": "npx tsx src/lib/hotkeySideModifiers.test.ts",
"check:windows-startup-lifecycle": "node scripts/windows-startup-lifecycle-contract.test.mjs",
"check:android-updater-pubkey": "node scripts/check-android-updater-pubkey.mjs"
},
Expand Down
83 changes: 79 additions & 4 deletions openless-all/app/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions openless-all/app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ features = ["windows-native"]

[target.'cfg(target_os = "linux")'.dependencies]
dbus = "0.9"
evdev = "0.12"
[target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))'.dependencies.keyring]
version = "3.6.3"
default-features = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ mod asr {

#[path = "../../src/coordinator_state.rs"]
mod coordinator_state;
#[path = "../../src/global_hotkey_runtime.rs"]
mod global_hotkey_runtime;
#[path = "../../src/combo_hotkey.rs"]
mod combo_hotkey;
#[path = "../../src/side_aware_combo.rs"]
mod side_aware_combo;
#[path = "../../src/hotkey.rs"]
mod hotkey;
#[cfg(not(target_os = "macos"))]
Expand Down
2 changes: 1 addition & 1 deletion openless-all/app/src-tauri/src/combo_hotkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::global_hotkey_runtime::{GlobalHotkeyRuntime, RegisteredHotkey};
use crate::shortcut_binding::{parse_global_hotkey, ShortcutBindingError};
use crate::types::ShortcutBinding;

#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComboHotkeyEvent {
/// 用户按下了配置的组合键。
Pressed,
Expand Down
99 changes: 81 additions & 18 deletions openless-all/app/src-tauri/src/commands/hotkeys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub fn set_translation_hotkey(
binding: ShortcutBinding,
) -> Result<(), String> {
crate::shortcut_binding::validate_binding(&binding).map_err(|e| e.to_string())?;
crate::shortcut_binding::reject_side_specific_non_dictation(&binding)?;
let previous = coord.prefs().get();
reject_dictation_translation_hotkey_overlap(&previous.dictation_hotkey, &binding)?;
if let Some(qa_hotkey) = previous.qa_hotkey.as_ref() {
Expand Down Expand Up @@ -76,6 +77,7 @@ pub fn set_switch_style_hotkey(
) -> Result<(), String> {
if let Some(binding) = binding.as_ref() {
crate::shortcut_binding::validate_binding(binding).map_err(|e| e.to_string())?;
crate::shortcut_binding::reject_side_specific_non_dictation(binding)?;
reject_modifier_only_action_shortcut(binding)?;
}
let mut prefs = coord.prefs().get();
Expand Down Expand Up @@ -106,6 +108,7 @@ pub fn set_open_app_hotkey(
) -> Result<(), String> {
if let Some(binding) = binding.as_ref() {
crate::shortcut_binding::validate_binding(binding).map_err(|e| e.to_string())?;
crate::shortcut_binding::reject_side_specific_non_dictation(binding)?;
reject_modifier_only_action_shortcut(binding)?;
}
let mut prefs = coord.prefs().get();
Expand Down Expand Up @@ -229,6 +232,7 @@ fn reject_hotkey_overlap(
}

pub(crate) fn reject_hotkey_collisions(prefs: &UserPreferences) -> Result<(), String> {
reject_non_dictation_side_specific_shortcuts(prefs)?;
// 停用(None)的 action 快捷键不参与任何冲突检测。
let switch_style = prefs.switch_style_hotkey.as_ref();
let open_app = prefs.open_app_hotkey.as_ref();
Expand Down Expand Up @@ -274,6 +278,25 @@ pub(crate) fn reject_hotkey_collisions(prefs: &UserPreferences) -> Result<(), St
Ok(())
}

pub(crate) fn reject_non_dictation_side_specific_shortcuts(
prefs: &UserPreferences,
) -> Result<(), String> {
crate::shortcut_binding::reject_side_specific_non_dictation(&prefs.translation_hotkey)?;
if let Some(binding) = prefs.qa_hotkey.as_ref() {
crate::shortcut_binding::reject_side_specific_non_dictation(binding)?;
}
if let Some(binding) = prefs.switch_style_hotkey.as_ref() {
crate::shortcut_binding::reject_side_specific_non_dictation(binding)?;
}
if let Some(binding) = prefs.open_app_hotkey.as_ref() {
crate::shortcut_binding::reject_side_specific_non_dictation(binding)?;
}
if let Some(binding) = prefs.coding_agent_voice_hotkey.as_ref() {
crate::shortcut_binding::reject_side_specific_non_dictation(binding)?;
}
Ok(())
}

pub(crate) fn reject_dictation_translation_hotkey_overlap(
dictation: &ShortcutBinding,
translation: &ShortcutBinding,
Expand Down Expand Up @@ -405,21 +428,7 @@ fn reject_less_computer_open_app_hotkey_overlap(
}

fn shortcut_bindings_overlap(left: &ShortcutBinding, right: &ShortcutBinding) -> bool {
let left_legacy = crate::shortcut_binding::legacy_modifier_trigger(left);
let right_legacy = crate::shortcut_binding::legacy_modifier_trigger(right);
match (left_legacy, right_legacy) {
(Some(left), Some(right)) => left == right,
(Some(_), None) | (None, Some(_)) => false,
(None, None) => {
let Ok(left) = crate::shortcut_binding::parse_global_hotkey(left) else {
return false;
};
let Ok(right) = crate::shortcut_binding::parse_global_hotkey(right) else {
return false;
};
left == right
}
}
crate::shortcut_binding::bindings_overlap(left, right)
}

#[cfg(test)]
Expand All @@ -433,9 +442,6 @@ mod tests {
}
}

/// 锁定碰撞矩阵:每个动作键与 Less Computer 键相同都必须被 reject_hotkey_collisions
/// 拒绝。5 个快捷 setter(set_dictation/translation/switch_style/open_app/qa_hotkey)
/// 此前漏校验 coding_agent_voice_hotkey,已接入对应的 less_computer 校验。
#[test]
fn each_action_hotkey_collides_with_less_computer() {
let lc = key("LeftControl");
Expand Down Expand Up @@ -474,4 +480,61 @@ mod tests {
// 复位后再次全不同 → 通过。
assert!(reject_hotkey_collisions(&prefs).is_ok());
}

#[test]
fn side_specific_dictation_overlaps_generic_qa_hotkey() {
let mut prefs = UserPreferences {
dictation_hotkey: ShortcutBinding {
primary: "D".into(),
modifiers: vec!["cmd-left".into()],
},
qa_hotkey: Some(ShortcutBinding {
primary: "D".into(),
modifiers: vec!["cmd".into()],
}),
..Default::default()
};
#[cfg(target_os = "windows")]
{
assert!(reject_hotkey_collisions(&prefs).is_ok());
prefs.qa_hotkey = Some(ShortcutBinding {
primary: "D".into(),
modifiers: vec!["super".into()],
});
assert!(reject_hotkey_collisions(&prefs).is_err());
}
#[cfg(not(target_os = "windows"))]
{
assert!(reject_hotkey_collisions(&prefs).is_err());
}
prefs.qa_hotkey = Some(ShortcutBinding {
primary: "D".into(),
modifiers: vec!["cmd".into(), "shift".into()],
});
assert!(reject_hotkey_collisions(&prefs).is_ok());
}

#[test]
fn rejects_side_specific_qa_hotkey_on_save() {
let prefs = UserPreferences {
qa_hotkey: Some(ShortcutBinding {
primary: "D".into(),
modifiers: vec!["cmd-left".into()],
}),
..Default::default()
};
assert!(reject_non_dictation_side_specific_shortcuts(&prefs).is_err());
}

#[test]
fn accepts_side_specific_dictation_hotkey_on_save() {
let prefs = UserPreferences {
dictation_hotkey: ShortcutBinding {
primary: "D".into(),
modifiers: vec!["cmd-left".into()],
},
..Default::default()
};
assert!(reject_non_dictation_side_specific_shortcuts(&prefs).is_ok());
}
}
5 changes: 5 additions & 0 deletions openless-all/app/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ mod tests {
switch_style_refreshes: Mutex<u32>,
open_app_refreshes: Mutex<u32>,
coding_agent_refreshes: Mutex<u32>,
mouse_dictation_refreshes: Mutex<u32>,
}

fn snapshot() -> CredentialsSnapshot {
Expand Down Expand Up @@ -636,6 +637,10 @@ mod tests {
fn refresh_coding_agent_hotkey(&self) {
*self.coding_agent_refreshes.lock().unwrap() += 1;
}

fn refresh_mouse_dictation(&self) {
*self.mouse_dictation_refreshes.lock().unwrap() += 1;
}
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src-tauri/src/commands/qa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub fn set_qa_hotkey(
) -> Result<(), String> {
if let Some(binding) = binding.as_ref() {
crate::shortcut_binding::validate_binding(binding).map_err(|e| e.to_string())?;
crate::shortcut_binding::reject_side_specific_non_dictation(binding)?;
if binding.modifiers.is_empty() && binding.primary.eq_ignore_ascii_case("shift") {
return Err("Shift 单键目前只能用于翻译快捷键".into());
}
Expand Down
Loading
Loading