Skip to main content

actl_core/
keys.rs

1//! 按键序列解析:actl 人文语法 `ctrl+s` → 结构化 KeySpec。
2//!
3//! 纯逻辑(core);执行(VK/UNICODE 注入)在 actl-uia 的 kbd 模块。
4//! 历史注记:最初实现翻译为 uiautomation crate 的键表达式,但实测其 VK 注入不带
5//! scan code(wScan=0),WinUI 应用直接丢弃 —— press 的执行已改为自建注入管线。
6
7use crate::{CtlError, ErrorCode};
8
9const MODIFIERS: &[&str] = &["ctrl", "shift", "alt", "win"];
10
11/// 命名键白名单(单字符键走 Key::Char)。
12pub const NAMED_KEYS: &[&str] = &[
13    "enter",
14    "esc",
15    "tab",
16    "space",
17    "del",
18    "delete",
19    "backspace",
20    "back",
21    "up",
22    "down",
23    "left",
24    "right",
25    "home",
26    "end",
27    "pgup",
28    "pgdn",
29];
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum Key {
33    Char(char),
34    Named(String),
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct KeySpec {
39    /// 修饰键,已按 ctrl→shift→alt→win 归一顺序去重
40    pub modifiers: Vec<String>,
41    pub keys: Vec<Key>,
42}
43
44/// 解析 `ctrl+shift+t` / `enter` / `a`;非法 → PROTOCOL。
45pub fn parse_key_expr(expr: &str) -> Result<KeySpec, CtlError> {
46    let mut mods: Vec<String> = Vec::new();
47    let mut keys: Vec<Key> = Vec::new();
48    for part in expr.split('+') {
49        let p = part.trim().to_lowercase();
50        if p.is_empty() {
51            return Err(bad(expr));
52        }
53        if MODIFIERS.contains(&p.as_str()) {
54            if !mods.iter().any(|m| m == &p) {
55                mods.push(p);
56            }
57            continue;
58        }
59        let mut chars = p.chars();
60        if let (Some(c), None) = (chars.next(), chars.next()) {
61            keys.push(Key::Char(c));
62            continue;
63        }
64        let named = match p.as_str() {
65            "delete" => "del",
66            "back" => "backspace",
67            other => other,
68        };
69        // f1..f12:数字段长度 1-2(曾因 p.len()==2 拒绝 f10-f12,回归见 tests)
70        let is_fkey = (2..=3).contains(&p.len())
71            && p.starts_with('f')
72            && p[1..].chars().all(|c| c.is_ascii_digit())
73            && (1..=12).contains(&p[1..].parse::<u8>().unwrap_or(0));
74        if NAMED_KEYS.contains(&named) || is_fkey {
75            keys.push(Key::Named(named.to_string()));
76            continue;
77        }
78        return Err(bad(expr)); // 未知命名键
79    }
80    // 裸修饰键(win/shift)= 按下并释放该键本身:Win 弹开始菜单/呼出隐藏任务栏的
81    // 入口(用户实测工作流);key-down shift 先例同语义。修饰键本身即按键。
82    if keys.is_empty() && mods.is_empty() {
83        return Err(bad(expr));
84    }
85    // 归一修饰键顺序
86    let modifiers = MODIFIERS
87        .iter()
88        .filter(|m| mods.iter().any(|x| x == *m))
89        .map(|m| m.to_string())
90        .collect();
91    Ok(KeySpec { modifiers, keys })
92}
93
94fn bad(expr: &str) -> CtlError {
95    CtlError::new(
96        ErrorCode::Protocol,
97        format!("invalid key expression {expr:?}: use e.g. ctrl+s, ctrl+shift+t, enter, f5"),
98    )
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn parses_common_combos() {
107        let spec = parse_key_expr("ctrl+s").unwrap();
108        assert_eq!(spec.modifiers, vec!["ctrl"]);
109        assert_eq!(spec.keys, vec![Key::Char('s')]);
110
111        let spec = parse_key_expr("ctrl+shift+t").unwrap();
112        assert_eq!(spec.modifiers, vec!["ctrl", "shift"]);
113
114        let spec = parse_key_expr("alt+F4").unwrap();
115        assert_eq!(spec.keys, vec![Key::Named("f4".into())]);
116
117        assert_eq!(
118            parse_key_expr("pgdn").unwrap().keys,
119            vec![Key::Named("pgdn".into())]
120        );
121        assert_eq!(parse_key_expr("a").unwrap().keys, vec![Key::Char('a')]);
122    }
123
124    #[test]
125    fn modifier_order_is_normalized_and_deduped() {
126        let spec = parse_key_expr("shift+ctrl+a").unwrap();
127        assert_eq!(spec.modifiers, vec!["ctrl", "shift"]);
128        let spec = parse_key_expr("ctrl+ctrl+a").unwrap();
129        assert_eq!(spec.modifiers, vec!["ctrl"]);
130    }
131
132    #[test]
133    fn aliases_normalize() {
134        assert_eq!(
135            parse_key_expr("delete").unwrap().keys,
136            vec![Key::Named("del".into())]
137        );
138        assert_eq!(
139            parse_key_expr("back").unwrap().keys,
140            vec![Key::Named("backspace".into())]
141        );
142    }
143
144    #[test]
145    fn rejects_garbage() {
146        // 注:"f" 是合法单字符(Char('f')),"f01" 归一为 f1,均不该在此列
147        for bad in ["", "foo+bar", "ctrl+", "+a", "notakey", "f0", "f13", "f99"] {
148            assert_eq!(
149                parse_key_expr(bad).unwrap_err().code,
150                ErrorCode::Protocol,
151                "{bad:?} should be PROTOCOL"
152            );
153        }
154    }
155
156    #[test]
157    fn fkeys_f1_through_f12_all_parse() {
158        // 回归:旧条件 p.len()==2 把 f10/f11/f12 挡在解析层,后端却支持 1..=12
159        for name in [
160            "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12",
161        ] {
162            let spec = parse_key_expr(name).unwrap_or_else(|e| panic!("{name}: {e:?}"));
163            assert_eq!(spec.keys, vec![Key::Named(name.into())], "{name}");
164            assert!(spec.modifiers.is_empty());
165        }
166    }
167}
168
169#[cfg(test)]
170mod bare_modifier_tests {
171    use super::*;
172
173    #[test]
174    fn bare_modifier_is_a_keyable_press() {
175        // press win = 弹开始菜单(呼出自动隐藏任务栏的工作流);key-down shift 同语义
176        let spec = parse_key_expr("win").unwrap();
177        assert_eq!(spec.modifiers, vec!["win"]);
178        assert!(spec.keys.is_empty());
179        assert!(parse_key_expr("shift").is_ok());
180        assert!(parse_key_expr("").is_err());
181    }
182}