My personal-knowledge-system, with deeply integrated task tracking and long term goal planning capabilities.
1use std::{
2 collections::HashMap,
3 ops::{Deref, DerefMut},
4};
5
6use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
7use kdl::KdlNode;
8use strum::IntoEnumIterator;
9
10use crate::{app::Region, signal::Signal};
11
12#[derive(Debug, Clone)]
13pub struct KeyMap(pub HashMap<Region, HashMap<Vec<KeyEvent>, Signal>>);
14
15impl TryFrom<&KdlNode> for KeyMap {
16 type Error = color_eyre::Report;
17
18 fn try_from(value: &KdlNode) -> std::result::Result<Self, Self::Error> {
19 let mut all_binds = HashMap::new();
20
21 for region in Region::iter() {
22 let mut region_binds = HashMap::new();
23 let Some(binds) = value
24 .children()
25 .expect("Keymap must have children.")
26 .get(®ion.to_string())
27 else {
28 continue;
29 };
30
31 // now we iter through the things children
32 for child in binds.iter_children() {
33 let key_combo_str = child.name().to_string();
34 let key_combo_str = key_combo_str.trim();
35
36 let signal_str = child
37 .entries()
38 .first()
39 .expect("A bind must map to an entry")
40 .to_string();
41 let signal_str = signal_str.trim();
42
43 let signal: Signal = signal_str.parse().expect("Must be a \"bindable\" Signal");
44 let key_combo = parse_key_sequence(key_combo_str).unwrap();
45
46 let _ = region_binds.insert(key_combo, signal);
47 }
48
49 let _ = all_binds.insert(region, region_binds);
50 }
51
52 Ok(Self(all_binds))
53 }
54}
55
56impl Deref for KeyMap {
57 type Target = HashMap<Region, HashMap<Vec<KeyEvent>, Signal>>;
58
59 fn deref(&self) -> &Self::Target {
60 &self.0
61 }
62}
63
64impl DerefMut for KeyMap {
65 fn deref_mut(&mut self) -> &mut Self::Target {
66 &mut self.0
67 }
68}
69
70pub fn parse_key_sequence(raw: &str) -> color_eyre::Result<Vec<KeyEvent>, String> {
71 if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() {
72 return Err(format!("Unable to parse `{raw}`"));
73 }
74 let raw = if raw.contains("><") {
75 raw
76 } else {
77 let raw = raw.strip_prefix('<').unwrap_or(raw);
78
79 raw.strip_prefix('>').unwrap_or(raw)
80 };
81
82 raw.split("><")
83 .map(|seq| {
84 seq.strip_prefix('<')
85 .unwrap_or_else(|| seq.strip_suffix('>').map_or(seq, |s| s))
86 })
87 .map(parse_key_event)
88 .collect()
89}
90
91fn parse_key_event(raw: &str) -> color_eyre::Result<KeyEvent, String> {
92 let raw_lower = raw.to_ascii_lowercase();
93 let (remaining, modifiers) = extract_modifiers(&raw_lower);
94 parse_key_code_with_modifiers(remaining, modifiers)
95}
96
97fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
98 let mut modifiers = KeyModifiers::empty();
99 let mut current = raw;
100
101 loop {
102 match current {
103 rest if rest.starts_with("ctrl-") => {
104 modifiers.insert(KeyModifiers::CONTROL);
105 current = &rest[5..];
106 }
107 rest if rest.starts_with("alt-") => {
108 modifiers.insert(KeyModifiers::ALT);
109 current = &rest[4..];
110 }
111 rest if rest.starts_with("shift-") => {
112 modifiers.insert(KeyModifiers::SHIFT);
113 current = &rest[6..];
114 }
115 _ => break, // break out of the loop if no known prefix is detected
116 }
117 }
118
119 (current, modifiers)
120}
121
122fn parse_key_code_with_modifiers(
123 raw: &str,
124 mut modifiers: KeyModifiers,
125) -> color_eyre::Result<KeyEvent, String> {
126 let c = match raw {
127 "esc" => KeyCode::Esc,
128 "enter" => KeyCode::Enter,
129 "left" => KeyCode::Left,
130 "right" => KeyCode::Right,
131 "up" => KeyCode::Up,
132 "down" => KeyCode::Down,
133 "home" => KeyCode::Home,
134 "end" => KeyCode::End,
135 "pageup" => KeyCode::PageUp,
136 "pagedown" => KeyCode::PageDown,
137 "backtab" => {
138 modifiers.insert(KeyModifiers::SHIFT);
139 KeyCode::BackTab
140 }
141 "backspace" => KeyCode::Backspace,
142 "delete" => KeyCode::Delete,
143 "insert" => KeyCode::Insert,
144 "f1" => KeyCode::F(1),
145 "f2" => KeyCode::F(2),
146 "f3" => KeyCode::F(3),
147 "f4" => KeyCode::F(4),
148 "f5" => KeyCode::F(5),
149 "f6" => KeyCode::F(6),
150 "f7" => KeyCode::F(7),
151 "f8" => KeyCode::F(8),
152 "f9" => KeyCode::F(9),
153 "f10" => KeyCode::F(10),
154 "f11" => KeyCode::F(11),
155 "f12" => KeyCode::F(12),
156 "space" => KeyCode::Char(' '),
157 "hyphen" | "minuc" => KeyCode::Char('-'),
158 "tab" => KeyCode::Tab,
159 c if c.len() == 1 => {
160 let mut c = c.chars().next().unwrap();
161 if modifiers.contains(KeyModifiers::SHIFT) {
162 c = c.to_ascii_uppercase();
163 }
164 KeyCode::Char(c)
165 }
166 _ => return Err(format!("Unable to parse {raw}")),
167 };
168 Ok(KeyEvent::new(c, modifiers))
169}
170
171#[cfg(test)]
172mod test {
173 use crossterm::event::{KeyEvent, KeyModifiers};
174 use kdl::KdlNode;
175
176 use crate::{keymap::KeyMap, signal::Signal};
177
178 #[test]
179 fn test_quit_in_home_region() {
180 let keymap_str = "
181 keymap {
182 Home {
183 q Quit
184 <Ctrl-C> Quit
185 }
186 }
187 ";
188
189 let kdl: &KdlNode = &keymap_str
190 .parse()
191 .expect("Keymap_str should be a valid KDL document");
192
193 let keymap: KeyMap = kdl.try_into().expect("Must be a valid keymap");
194
195 let map = keymap
196 .get(&crate::app::Region::Home)
197 .expect("Home region must exist in keymap");
198
199 let signal = map
200 .get(&vec![KeyEvent::new_with_kind(
201 crossterm::event::KeyCode::Char('q'),
202 KeyModifiers::empty(),
203 crossterm::event::KeyEventKind::Press,
204 )])
205 .expect("Must resolve to a signal");
206
207 assert_eq!(*signal, Signal::Quit);
208 }
209}