Skip to main content

sac/terminal/
keyparse.rs

1/// Parse Emacs-style key notation into raw bytes. Text outside `<KEY>` spans
2/// passes through as literal UTF-8; unrecognised keys are kept verbatim.
3/// Matching is case-insensitive.
4pub fn parse_keys(input: &str) -> Vec<u8> {
5    let chars: Vec<char> = input.chars().collect();
6    let mut result = Vec::with_capacity(input.len());
7    let mut i = 0;
8
9    while i < chars.len() {
10        if chars[i] == '<' {
11            if let Some(end) = chars[i + 1..].iter().position(|&c| c == '>') {
12                let j = i + 1 + end;
13                let key: String = chars[i + 1..j].iter().collect();
14                if let Some(expansion) = expand_key(&key) {
15                    result.extend_from_slice(&expansion);
16                    i = j + 1;
17                    continue;
18                }
19            }
20        }
21        let mut buf = [0u8; 4];
22        let encoded = chars[i].encode_utf8(&mut buf);
23        result.extend_from_slice(encoded.as_bytes());
24        i += 1;
25    }
26    result
27}
28
29fn expand_key(key: &str) -> Option<Vec<u8>> {
30    let u = key.to_uppercase();
31    match u.as_str() {
32        "RET" | "RETURN" | "ENTER" => Some(vec![b'\r']),
33        "TAB" => Some(vec![b'\t']),
34        "BSPC" | "BACKSPACE" => Some(vec![0x7f]),
35        "ESC" | "ESCAPE" => Some(vec![0x1b]),
36        "UP" => Some(vec![0x1b, b'[', b'A']),
37        "DOWN" => Some(vec![0x1b, b'[', b'B']),
38        "RIGHT" => Some(vec![0x1b, b'[', b'C']),
39        "LEFT" => Some(vec![0x1b, b'[', b'D']),
40        "C-C" | "CTRL-C" | "CTRL+C" => Some(vec![0x03]),
41        "C-D" | "CTRL-D" | "CTRL+D" => Some(vec![0x04]),
42        "C-Z" | "CTRL-Z" | "CTRL+Z" => Some(vec![0x1a]),
43        "C-/" | "CTRL-/" | "CTRL+SLASH" | "C-\\" | "CTRL-\\" | "CTRL+\\" => Some(vec![0x1c]),
44        _ => {
45            let letter = u
46                .strip_prefix("CTRL-")
47                .or_else(|| u.strip_prefix("CTRL+"))
48                .or_else(|| u.strip_prefix("C-"))
49                .and_then(|s| s.chars().next())?;
50            if letter.is_ascii_uppercase() {
51                Some(vec![letter as u8 - b'A' + 1])
52            } else if letter.is_ascii_lowercase() {
53                Some(vec![letter.to_ascii_uppercase() as u8 - b'A' + 1])
54            } else {
55                None
56            }
57        }
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn literal_text() {
67        assert_eq!(parse_keys("hello"), b"hello".to_vec());
68        assert_eq!(parse_keys("echo 'hi'\n"), b"echo 'hi'\n".to_vec());
69    }
70
71    #[test]
72    fn ret_and_enter() {
73        assert_eq!(parse_keys("ls<RET>"), b"ls\r".to_vec());
74        assert_eq!(parse_keys("pwd<ENTER>"), b"pwd\r".to_vec());
75    }
76
77    #[test]
78    fn ctrl_keys() {
79        assert_eq!(parse_keys("<C-c>"), vec![0x03]);
80        assert_eq!(parse_keys("<C-d>"), vec![0x04]);
81        assert_eq!(parse_keys("<C-a>"), vec![0x01]);
82        assert_eq!(parse_keys("<C-z>"), vec![0x1a]);
83        assert_eq!(parse_keys("<CTRL+C>"), vec![0x03]);
84    }
85
86    #[test]
87    fn mixed() {
88        assert_eq!(parse_keys("cargo build<RET>"), b"cargo build\r".to_vec());
89    }
90
91    #[test]
92    fn unknown_key_passthrough() {
93        assert_eq!(parse_keys("echo <F5> test"), b"echo <F5> test".to_vec());
94    }
95
96    #[test]
97    fn arrows() {
98        assert_eq!(parse_keys("<UP>"), vec![0x1b, b'[', b'A']);
99        assert_eq!(parse_keys("<DOWN>"), vec![0x1b, b'[', b'B']);
100        assert_eq!(parse_keys("<RIGHT>"), vec![0x1b, b'[', b'C']);
101        assert_eq!(parse_keys("<LEFT>"), vec![0x1b, b'[', b'D']);
102    }
103
104    #[test]
105    fn case_insensitive() {
106        assert_eq!(parse_keys("<ret>"), b"\r".to_vec());
107        assert_eq!(parse_keys("<Ret>"), b"\r".to_vec());
108    }
109}