My personal-knowledge-system, with deeply integrated task tracking and long term goal planning capabilities.

feat/config: Keymap + kdl config

+446 -80
+5 -47
.config/config.kdl
··· 1 - scroll_offset 4 2 1 3 - keybindings { 4 - Explorer { 5 - "<q>" Quit // Quit the application 6 - "<Ctrl-d>" Quit // Another way to quit 7 - "<Ctrl-c>" Quit // Yet another way to quit 8 - "<Ctrl-z>" Suspend // Suspend the application 9 - "<2>" switch-to="TodoList" 10 - "<3>" switch-to="Inspector" 11 - f ToggleShowFinished 12 - x Delete 13 - t NewTask 14 - g NewSubGroup 15 - "<Shift-g>" NewGroup 16 - j MoveDown 17 - k MoveUp 18 - l MoveInto 19 - h MoveOutOf 20 - } 21 - 22 - TodoList { 23 - "<q>" Quit // Quit the application 24 - "<Ctrl-d>" Quit // Another way to quit 25 - "<Ctrl-c>" Quit // Yet another way to quit 26 - "<Ctrl-z>" Suspend // Suspend the application 27 - "<1>" switch-to="Explorer" 28 - "<3>" switch-to="Inspector" 29 - j MoveDown 30 - k MoveUp 31 - } 32 - 33 - Inspector { 34 - "<q>" Quit // Quit the application 35 - "<Ctrl-d>" Quit // Another way to quit 36 - "<Ctrl-c>" Quit // Yet another way to quit 37 - "<Ctrl-z>" Suspend // Suspend the application 38 - "<1>" switch-to="Explorer" 39 - "<2>" switch-to="TodoList" 40 - r RandomColor 41 - n EditName 42 - c EditColor 43 - p EditPriority 44 - u EditDue 45 - d EditDescription 46 - f ToggleFinishTask 47 - t NewTask 48 - g NewSubGroup 2 + keymap { 3 + Home { 4 + <q> Quit // Quit the application 5 + <Ctrl-c> Quit // Another way to quit 6 + <Ctrl-z> Suspend // Suspend the application 49 7 } 50 8 }
+50
.config/config_old.kdl
··· 1 + scroll_offset 4 2 + 3 + keybindings { 4 + Explorer { 5 + "<q>" Quit // Quit the application 6 + "<Ctrl-d>" Quit // Another way to quit 7 + "<Ctrl-c>" Quit // Yet another way to quit 8 + "<Ctrl-z>" Suspend // Suspend the application 9 + "<2>" switch-to="TodoList" 10 + "<3>" switch-to="Inspector" 11 + f ToggleShowFinished 12 + x Delete 13 + t NewTask 14 + g NewSubGroup 15 + "<Shift-g>" NewGroup 16 + j MoveDown 17 + k MoveUp 18 + l MoveInto 19 + h MoveOutOf 20 + } 21 + 22 + TodoList { 23 + "<q>" Quit // Quit the application 24 + "<Ctrl-d>" Quit // Another way to quit 25 + "<Ctrl-c>" Quit // Yet another way to quit 26 + "<Ctrl-z>" Suspend // Suspend the application 27 + "<1>" switch-to="Explorer" 28 + "<3>" switch-to="Inspector" 29 + j MoveDown 30 + k MoveUp 31 + } 32 + 33 + Inspector { 34 + "<q>" Quit // Quit the application 35 + "<Ctrl-d>" Quit // Another way to quit 36 + "<Ctrl-c>" Quit // Yet another way to quit 37 + "<Ctrl-z>" Suspend // Suspend the application 38 + "<1>" switch-to="Explorer" 39 + "<2>" switch-to="TodoList" 40 + r RandomColor 41 + n EditName 42 + c EditColor 43 + p EditPriority 44 + u EditDue 45 + d EditDescription 46 + f ToggleFinishTask 47 + t NewTask 48 + g NewSubGroup 49 + } 50 + }
+110 -9
Cargo.lock
··· 290 290 "strsim", 291 291 "terminal_size", 292 292 "unicase", 293 - "unicode-width", 293 + "unicode-width 0.2.2", 294 294 ] 295 295 296 296 [[package]] ··· 751 751 "directories", 752 752 "futures", 753 753 "human-panic", 754 + "kdl", 754 755 "ratatui", 755 756 "serde", 756 757 "signal-hook 0.4.3", ··· 1017 1018 "gix-utils", 1018 1019 "itoa", 1019 1020 "thiserror 2.0.18", 1020 - "winnow", 1021 + "winnow 0.7.15", 1021 1022 ] 1022 1023 1023 1024 [[package]] ··· 1098 1099 "smallvec", 1099 1100 "thiserror 2.0.18", 1100 1101 "unicode-bom", 1101 - "winnow", 1102 + "winnow 0.7.15", 1102 1103 ] 1103 1104 1104 1105 [[package]] ··· 1345 1346 "itoa", 1346 1347 "smallvec", 1347 1348 "thiserror 2.0.18", 1348 - "winnow", 1349 + "winnow 0.7.15", 1349 1350 ] 1350 1351 1351 1352 [[package]] ··· 1442 1443 "gix-utils", 1443 1444 "maybe-async", 1444 1445 "thiserror 2.0.18", 1445 - "winnow", 1446 + "winnow 0.7.15", 1446 1447 ] 1447 1448 1448 1449 [[package]] ··· 1474 1475 "gix-validate", 1475 1476 "memmap2", 1476 1477 "thiserror 2.0.18", 1477 - "winnow", 1478 + "winnow 0.7.15", 1478 1479 ] 1479 1480 1480 1481 [[package]] ··· 1912 1913 ] 1913 1914 1914 1915 [[package]] 1916 + name = "kdl" 1917 + version = "6.5.0" 1918 + source = "registry+https://github.com/rust-lang/crates.io-index" 1919 + checksum = "81a29e7b50079ff44549f68c0becb1c73d7f6de2a4ea952da77966daf3d4761e" 1920 + dependencies = [ 1921 + "miette", 1922 + "num", 1923 + "winnow 0.6.24", 1924 + ] 1925 + 1926 + [[package]] 1915 1927 name = "kstring" 1916 1928 version = "2.0.2" 1917 1929 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2062 2074 ] 2063 2075 2064 2076 [[package]] 2077 + name = "miette" 2078 + version = "7.6.0" 2079 + source = "registry+https://github.com/rust-lang/crates.io-index" 2080 + checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" 2081 + dependencies = [ 2082 + "cfg-if", 2083 + "unicode-width 0.1.14", 2084 + ] 2085 + 2086 + [[package]] 2065 2087 name = "minimal-lexical" 2066 2088 version = "0.2.1" 2067 2089 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2130 2152 ] 2131 2153 2132 2154 [[package]] 2155 + name = "num" 2156 + version = "0.4.3" 2157 + source = "registry+https://github.com/rust-lang/crates.io-index" 2158 + checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" 2159 + dependencies = [ 2160 + "num-bigint", 2161 + "num-complex", 2162 + "num-integer", 2163 + "num-iter", 2164 + "num-rational", 2165 + "num-traits", 2166 + ] 2167 + 2168 + [[package]] 2169 + name = "num-bigint" 2170 + version = "0.4.6" 2171 + source = "registry+https://github.com/rust-lang/crates.io-index" 2172 + checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 2173 + dependencies = [ 2174 + "num-integer", 2175 + "num-traits", 2176 + ] 2177 + 2178 + [[package]] 2179 + name = "num-complex" 2180 + version = "0.4.6" 2181 + source = "registry+https://github.com/rust-lang/crates.io-index" 2182 + checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 2183 + dependencies = [ 2184 + "num-traits", 2185 + ] 2186 + 2187 + [[package]] 2133 2188 name = "num-conv" 2134 2189 version = "0.2.0" 2135 2190 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2147 2202 ] 2148 2203 2149 2204 [[package]] 2205 + name = "num-integer" 2206 + version = "0.1.46" 2207 + source = "registry+https://github.com/rust-lang/crates.io-index" 2208 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 2209 + dependencies = [ 2210 + "num-traits", 2211 + ] 2212 + 2213 + [[package]] 2214 + name = "num-iter" 2215 + version = "0.1.45" 2216 + source = "registry+https://github.com/rust-lang/crates.io-index" 2217 + checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 2218 + dependencies = [ 2219 + "autocfg", 2220 + "num-integer", 2221 + "num-traits", 2222 + ] 2223 + 2224 + [[package]] 2225 + name = "num-rational" 2226 + version = "0.4.2" 2227 + source = "registry+https://github.com/rust-lang/crates.io-index" 2228 + checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 2229 + dependencies = [ 2230 + "num-bigint", 2231 + "num-integer", 2232 + "num-traits", 2233 + ] 2234 + 2235 + [[package]] 2150 2236 name = "num-traits" 2151 2237 version = "0.2.19" 2152 2238 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2477 2563 "thiserror 2.0.18", 2478 2564 "unicode-segmentation", 2479 2565 "unicode-truncate", 2480 - "unicode-width", 2566 + "unicode-width 0.2.2", 2481 2567 ] 2482 2568 2483 2569 [[package]] ··· 2528 2614 "strum 0.27.2", 2529 2615 "time", 2530 2616 "unicode-segmentation", 2531 - "unicode-width", 2617 + "unicode-width 0.2.2", 2532 2618 ] 2533 2619 2534 2620 [[package]] ··· 3293 3379 dependencies = [ 3294 3380 "itertools", 3295 3381 "unicode-segmentation", 3296 - "unicode-width", 3382 + "unicode-width 0.2.2", 3297 3383 ] 3298 3384 3299 3385 [[package]] 3300 3386 name = "unicode-width" 3387 + version = "0.1.14" 3388 + source = "registry+https://github.com/rust-lang/crates.io-index" 3389 + checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 3390 + 3391 + [[package]] 3392 + name = "unicode-width" 3301 3393 version = "0.2.2" 3302 3394 source = "registry+https://github.com/rust-lang/crates.io-index" 3303 3395 checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" ··· 3876 3968 version = "0.53.1" 3877 3969 source = "registry+https://github.com/rust-lang/crates.io-index" 3878 3970 checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 3971 + 3972 + [[package]] 3973 + name = "winnow" 3974 + version = "0.6.24" 3975 + source = "registry+https://github.com/rust-lang/crates.io-index" 3976 + checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" 3977 + dependencies = [ 3978 + "memchr", 3979 + ] 3879 3980 3880 3981 [[package]] 3881 3982 name = "winnow"
+1
Cargo.toml
··· 61 61 tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } 62 62 tracing-error = "0.2.1" 63 63 clap = { version = "4.5.60", features = ["derive", "cargo", "wrap_help", "unicode", "string", "unstable-styles"] } 64 + kdl = "6.5.0" 64 65 65 66 [build-dependencies] 66 67 anyhow = "1.0.102"
+27 -15
src/app.rs
··· 2 2 use crossterm::event::KeyEvent; 3 3 use ratatui::layout::Rect; 4 4 use serde::{Deserialize, Serialize}; 5 + use strum::{Display, EnumIter}; 5 6 use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; 6 - use tracing::{debug, info}; 7 + use tracing::debug; 7 8 8 9 use crate::{ 9 10 components::Component, ··· 29 30 /// The different regions of the application that the user can 30 31 /// be interacting with. Think of these kind of like the highest class of 31 32 /// components. 32 - #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 33 + #[derive( 34 + Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter, Display, 35 + )] 33 36 pub enum Region { 34 37 #[default] 35 38 Home, ··· 75 78 76 79 loop { 77 80 self.handle_events(&mut tui).await?; 78 - 79 - self.handle_signals(&mut tui).await?; 81 + self.handle_signals(&mut tui)?; 80 82 if self.should_suspend { 81 83 tui.suspend()?; 82 84 ··· 101 103 return Ok(()); 102 104 }; 103 105 106 + debug!("received event: {event:?}"); 107 + 104 108 let signal_tx = self.signal_tx.clone(); 105 109 106 110 match event { ··· 122 126 Ok(()) 123 127 } 124 128 125 - // We are okay with this because we know that this is the function signature, 126 - // we just haven't implemented the keyboard parsing logic just yet, revisit 127 - // this later. 128 - // 129 - // DO NOT LET THIS MERGE INTO MAIN WITH THIS CLIPPY IGNORES 130 - #[allow(clippy::needless_pass_by_ref_mut, clippy::unnecessary_wraps)] 131 129 fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> { 132 - let _signal_tx = self.signal_tx.clone(); 130 + debug!("key received: {key:#?}"); 133 131 134 - info!("key received: {key:#?}"); 132 + let signal_tx = self.signal_tx.clone(); 133 + 134 + let Some(region_keymap) = self.config.keymap.get(&self.region) else { 135 + return Ok(()); 136 + }; 137 + 138 + if let Some(signal) = region_keymap.get(&vec![key]) { 139 + signal_tx.send(signal.clone())?; 140 + } else { 141 + self.last_tick_key_events.push(key); 142 + if let Some(signal) = region_keymap.get(&self.last_tick_key_events) { 143 + debug!("Got signal: {signal:?}"); 144 + signal_tx.send(signal.clone())?; 145 + } 146 + } 135 147 136 148 Ok(()) 137 149 } 138 150 139 - async fn handle_signals(&mut self, tui: &mut Tui) -> Result<()> { 140 - while let Some(signal) = self.signal_rx.recv().await { 151 + fn handle_signals(&mut self, tui: &mut Tui) -> Result<()> { 152 + while let Ok(signal) = self.signal_rx.try_recv() { 141 153 if signal != Signal::Tick && signal != Signal::Render { 142 - debug!("App: handling signal: {signal:?}"); 154 + debug!("handling signal: {signal:?}"); 143 155 } 144 156 145 157 match signal {
+22 -6
src/config.rs
··· 1 1 use directories::ProjectDirs; 2 + use kdl::KdlDocument; 2 3 use serde::Deserialize; 3 4 use std::{env, path::PathBuf, sync::LazyLock}; 5 + 6 + use crate::keymap::KeyMap; 4 7 5 8 /// Project Name: Filaments 6 9 pub static PROJECT_NAME: LazyLock<String> = ··· 20 23 .map(PathBuf::from) 21 24 }); 22 25 26 + const DEFAULT_CONFIG: &str = include_str!("../.config/config.kdl"); 27 + 23 28 /// The App Config and Data locations. 24 29 #[derive(Clone, Debug, Deserialize, Default)] 25 30 #[expect(dead_code)] 26 - pub struct AppDirs { 31 + pub struct AppConfig { 27 32 #[serde(default)] 28 33 pub data_dir: PathBuf, 29 34 #[serde(default)] ··· 34 39 #[expect(dead_code)] 35 40 #[derive(Debug, Clone)] 36 41 pub struct Config { 37 - pub app_dirs: AppDirs, // pub data_dir: PathBuf, 38 - // pub keybindings: KeyBindings, 39 - 40 - // pub styles: Styles, 42 + pub app_config: AppConfig, 43 + pub keymap: KeyMap, 44 + // pub styles: Styles, 41 45 } 42 46 43 47 impl Config { 44 48 pub fn new() -> Self { 49 + let default_config: KdlDocument = DEFAULT_CONFIG 50 + .parse() 51 + .expect("Default config should always be a valid KDL document."); 52 + 53 + let keymap_node = default_config 54 + .get("keymap") 55 + .expect("Config::new Keymap must exist in default config."); 56 + 57 + let keymap = 58 + KeyMap::try_from(keymap_node).expect("default config should always be a valid keymap"); 59 + 45 60 Self { 46 - app_dirs: AppDirs { 61 + app_config: AppConfig { 47 62 data_dir: get_data_dir(), 48 63 config_dir: get_config_dir(), 49 64 }, 65 + keymap, 50 66 } 51 67 } 52 68 }
+209
src/keymap.rs
··· 1 + use std::{ 2 + collections::HashMap, 3 + ops::{Deref, DerefMut}, 4 + }; 5 + 6 + use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 7 + use kdl::KdlNode; 8 + use strum::IntoEnumIterator; 9 + 10 + use crate::{app::Region, signal::Signal}; 11 + 12 + #[derive(Debug, Clone)] 13 + pub struct KeyMap(pub HashMap<Region, HashMap<Vec<KeyEvent>, Signal>>); 14 + 15 + impl 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(&region.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 + 56 + impl 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 + 64 + impl DerefMut for KeyMap { 65 + fn deref_mut(&mut self) -> &mut Self::Target { 66 + &mut self.0 67 + } 68 + } 69 + 70 + pub 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 + 91 + fn 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 + 97 + fn 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 + 122 + fn 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)] 172 + mod 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 + }
+1 -1
src/main.rs
··· 3 3 //! 4 4 5 5 use clap::Parser; 6 - 7 6 use crate::{app::App, cli::Cli}; 8 7 9 8 mod app; ··· 11 10 mod components; 12 11 mod config; 13 12 mod errors; 13 + mod keymap; 14 14 mod logging; 15 15 mod signal; 16 16 mod tui;
+20
src/signal.rs
··· 1 + use std::str::FromStr; 2 + 3 + use color_eyre::eyre::eyre; 1 4 use strum::Display; 2 5 3 6 use serde::{Deserialize, Serialize}; ··· 15 18 Error(String), 16 19 Help, 17 20 } 21 + 22 + impl FromStr for Signal { 23 + type Err = color_eyre::Report; 24 + 25 + fn from_str(s: &str) -> Result<Self, Self::Err> { 26 + Ok(match s.to_lowercase().as_str() { 27 + "suspend" => Self::Suspend, 28 + "resume" => Self::Resume, 29 + "quit" => Self::Quit, 30 + _ => { 31 + return Err(eyre!(format!( 32 + "Attempt to construct a non-user Signal from str: {s}" 33 + ))); 34 + } 35 + }) 36 + } 37 + }
+1 -2
src/tui.rs
··· 165 165 // not doing anything related to up / down keypresses 166 166 CrosstermEvent::Key(key) if key.kind == KeyEventKind::Press => Event::Key(key), 167 167 CrosstermEvent::Key(_) => continue, 168 - 169 168 CrosstermEvent::Mouse(mouse) => Event::Mouse(mouse), 170 169 CrosstermEvent::Resize(x, y) => Event::Resize(x, y), 171 170 CrosstermEvent::FocusLost => {Event::FocusLost }, ··· 177 176 None => break, 178 177 } 179 178 }; 179 + 180 180 if event_tx.send(event).is_err() { 181 - // no more receiver 182 181 break; 183 182 } 184 183 }