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    if keys.is_empty() {
81        return Err(bad(expr)); // 纯修饰键不是可按键
82    }
83    // 归一修饰键顺序
84    let modifiers = MODIFIERS
85        .iter()
86        .filter(|m| mods.iter().any(|x| x == *m))
87        .map(|m| m.to_string())
88        .collect();
89    Ok(KeySpec { modifiers, keys })
90}
91
92fn bad(expr: &str) -> CtlError {
93    CtlError::new(
94        ErrorCode::Protocol,
95        format!("invalid key expression {expr:?}: use e.g. ctrl+s, ctrl+shift+t, enter, f5"),
96    )
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn parses_common_combos() {
105        let spec = parse_key_expr("ctrl+s").unwrap();
106        assert_eq!(spec.modifiers, vec!["ctrl"]);
107        assert_eq!(spec.keys, vec![Key::Char('s')]);
108
109        let spec = parse_key_expr("ctrl+shift+t").unwrap();
110        assert_eq!(spec.modifiers, vec!["ctrl", "shift"]);
111
112        let spec = parse_key_expr("alt+F4").unwrap();
113        assert_eq!(spec.keys, vec![Key::Named("f4".into())]);
114
115        assert_eq!(
116            parse_key_expr("pgdn").unwrap().keys,
117            vec![Key::Named("pgdn".into())]
118        );
119        assert_eq!(parse_key_expr("a").unwrap().keys, vec![Key::Char('a')]);
120    }
121
122    #[test]
123    fn modifier_order_is_normalized_and_deduped() {
124        let spec = parse_key_expr("shift+ctrl+a").unwrap();
125        assert_eq!(spec.modifiers, vec!["ctrl", "shift"]);
126        let spec = parse_key_expr("ctrl+ctrl+a").unwrap();
127        assert_eq!(spec.modifiers, vec!["ctrl"]);
128    }
129
130    #[test]
131    fn aliases_normalize() {
132        assert_eq!(
133            parse_key_expr("delete").unwrap().keys,
134            vec![Key::Named("del".into())]
135        );
136        assert_eq!(
137            parse_key_expr("back").unwrap().keys,
138            vec![Key::Named("backspace".into())]
139        );
140    }
141
142    #[test]
143    fn rejects_garbage() {
144        // 注:"f" 是合法单字符(Char('f')),"f01" 归一为 f1,均不该在此列
145        for bad in [
146            "", "ctrl", "foo+bar", "ctrl+", "+a", "notakey", "f0", "f13", "f99",
147        ] {
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}