Skip to main content

taba_cli/tui/keys/
mod.rs

1//! The keys: decodes the bytes the raw-mode terminal delivers (UTF-8 text and ANSI sequences
2//! for arrows and the like).
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Key {
6    Up,
7    Down,
8    Left,
9    Right,
10    Home,
11    End,
12    Delete,
13    Backspace,
14    Enter,
15    Tab,
16    BackTab,
17    Esc,
18    Char(char),
19    /// Ctrl with a lowercase letter (`Ctrl('s')`).
20    Ctrl(char),
21}
22
23/// Decodes everything that arrived in one read. An unknown sequence is skipped whole.
24pub fn decode(bytes: &[u8]) -> Vec<Key> {
25    let mut keys = Vec::new();
26    let mut i = 0;
27    while i < bytes.len() {
28        let b = bytes[i];
29        match b {
30            0x1b => {
31                if i + 1 >= bytes.len() {
32                    keys.push(Key::Esc);
33                    i += 1;
34                    continue;
35                }
36                match bytes[i + 1] {
37                    b'[' | b'O' => {
38                        // CSI / SS3: parameters and then the final byte (0x40..=0x7e).
39                        let mut j = i + 2;
40                        while j < bytes.len() && !(0x40..=0x7e).contains(&bytes[j]) {
41                            j += 1;
42                        }
43                        let params = &bytes[i + 2..j.min(bytes.len())];
44                        if let Some(&final_byte) = bytes.get(j) {
45                            if let Some(key) = csi_key(params, final_byte) {
46                                keys.push(key);
47                            }
48                        }
49                        i = j + 1;
50                    }
51                    // Alt+key and others: ignores the ESC and processes the key.
52                    _ => i += 1,
53                }
54            }
55            0x0d | 0x0a => {
56                keys.push(Key::Enter);
57                i += 1;
58            }
59            0x09 => {
60                keys.push(Key::Tab);
61                i += 1;
62            }
63            0x7f | 0x08 => {
64                keys.push(Key::Backspace);
65                i += 1;
66            }
67            0x01..=0x1a => {
68                keys.push(Key::Ctrl((b - 1 + b'a') as char));
69                i += 1;
70            }
71            0x00 | 0x1c..=0x1f => i += 1,
72            _ => {
73                // A UTF-8 character (1 to 4 bytes).
74                let len = match b {
75                    0x00..=0x7f => 1,
76                    0xc0..=0xdf => 2,
77                    0xe0..=0xef => 3,
78                    0xf0..=0xf7 => 4,
79                    _ => 1,
80                };
81                let end = (i + len).min(bytes.len());
82                if let Some(c) = std::str::from_utf8(&bytes[i..end])
83                    .ok()
84                    .and_then(|s| s.chars().next())
85                {
86                    keys.push(Key::Char(c));
87                }
88                i = end;
89            }
90        }
91    }
92    keys
93}
94
95fn csi_key(params: &[u8], final_byte: u8) -> Option<Key> {
96    match final_byte {
97        b'A' => Some(Key::Up),
98        b'B' => Some(Key::Down),
99        b'C' => Some(Key::Right),
100        b'D' => Some(Key::Left),
101        b'H' => Some(Key::Home),
102        b'F' => Some(Key::End),
103        b'Z' => Some(Key::BackTab),
104        b'~' => match params {
105            b"1" | b"7" => Some(Key::Home),
106            b"4" | b"8" => Some(Key::End),
107            b"3" => Some(Key::Delete),
108            _ => None,
109        },
110        _ => None,
111    }
112}
113
114#[cfg(test)]
115mod tests;