Skip to main content

lasr/
config.rs

1use std::{collections::HashMap, fmt::Display, str::FromStr};
2
3use anyhow::{Context, bail};
4use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5use ratatui::style::{Color, Modifier, Style};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "snake_case")]
10pub enum Action {
11    Noop,
12    Exit,
13    Confirm,
14    ToggleSearchReplace,
15    ToggleIgnoreCase,
16    ToggleMultiLine,
17    CursorLeft,
18    CursorRight,
19    CursorHome,
20    CursorEnd,
21    DeleteChar,
22    DeleteCharBackward,
23    DeleteWord,
24    DeleteToEndOfLine,
25    DeleteLine,
26    ScrollDown,
27    ScrollUp,
28    ScrollTop,
29}
30
31#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash, Serialize, Deserialize)]
32#[serde(try_from = "String")]
33#[serde(into = "String")]
34pub struct Key {
35    code: KeyCode,
36    modifiers: KeyModifiers,
37}
38
39#[cfg(test)]
40impl Key {
41    fn new(code: KeyCode, modifiers: KeyModifiers) -> Key {
42        Key { code, modifiers }
43    }
44
45    fn char(c: char, modifiers: KeyModifiers) -> Key {
46        Key::new(KeyCode::Char(c), modifiers)
47    }
48}
49
50impl From<KeyEvent> for Key {
51    fn from(value: KeyEvent) -> Self {
52        Self {
53            code: value.code,
54            modifiers: value.modifiers,
55        }
56    }
57}
58
59impl TryFrom<String> for Key {
60    type Error = anyhow::Error;
61
62    fn try_from(s: String) -> Result<Self, Self::Error> {
63        let s = s.to_lowercase();
64        let (modifiers, code) = match s.rsplit_once("-") {
65            Some((mod_str, code)) => {
66                let mut modifiers = KeyModifiers::empty();
67                for m in mod_str.split('-') {
68                    modifiers |= match m {
69                        "c" => KeyModifiers::CONTROL,
70                        "a" => KeyModifiers::ALT,
71                        _ => {
72                            bail!("Unknown modifier '{m}'");
73                        }
74                    }
75                }
76                (modifiers, code)
77            }
78            None => (KeyModifiers::empty(), s.as_str()),
79        };
80        let code = match code {
81            "backspace" => KeyCode::Backspace,
82            "enter" => KeyCode::Enter,
83            "left" => KeyCode::Left,
84            "right" => KeyCode::Right,
85            "up" => KeyCode::Up,
86            "down" => KeyCode::Down,
87            "home" => KeyCode::Home,
88            "end" => KeyCode::End,
89            "pageup" => KeyCode::PageUp,
90            "pagedown" => KeyCode::PageDown,
91            "tab" => KeyCode::Tab,
92            "backtab" => KeyCode::BackTab,
93            "delete" => KeyCode::Delete,
94            "insert" => KeyCode::Insert,
95            "esc" => KeyCode::Esc,
96            _ => {
97                if code.len() == 1 && code.is_ascii() {
98                    KeyCode::Char(code.chars().next().unwrap())
99                } else if let Some(f) = s.strip_prefix("f") {
100                    // Fn-key
101                    let f: u8 = f
102                        .parse()
103                        .with_context(|| format!("'{code}' is not a valid F-key"))?;
104                    KeyCode::F(f)
105                } else {
106                    bail!("Unknown key code '{code}'");
107                }
108            }
109        };
110
111        Ok(Key { code, modifiers })
112    }
113}
114
115#[test]
116fn test_key_from_string() {
117    assert_eq!(
118        Key::char('x', KeyModifiers::empty()),
119        "x".to_string().try_into().unwrap(),
120    );
121
122    assert_eq!(
123        Key::char('y', KeyModifiers::CONTROL),
124        "c-y".to_string().try_into().unwrap(),
125    );
126
127    assert_eq!(
128        Key::char('z', KeyModifiers::ALT),
129        "a-z".to_string().try_into().unwrap(),
130    );
131
132    assert_eq!(
133        Key::char('a', KeyModifiers::CONTROL | KeyModifiers::ALT),
134        "c-a-a".to_string().try_into().unwrap(),
135    );
136
137    assert!(Key::try_from("x-a-a".to_string()).is_err());
138}
139
140impl From<Key> for String {
141    fn from(val: Key) -> Self {
142        let mut s = String::new();
143        if val.modifiers.contains(KeyModifiers::CONTROL) {
144            s += "c-";
145        }
146        if val.modifiers.contains(KeyModifiers::ALT) {
147            s += "a-";
148        }
149        match val.code {
150            KeyCode::Backspace => s += "backspace",
151            KeyCode::Enter => s += "enter",
152            KeyCode::Left => s += "left",
153            KeyCode::Right => s += "right",
154            KeyCode::Up => s += "up",
155            KeyCode::Down => s += "down",
156            KeyCode::Home => s += "home",
157            KeyCode::End => s += "end",
158            KeyCode::PageUp => s += "pageup",
159            KeyCode::PageDown => s += "pagedown",
160            KeyCode::Tab => s += "tab",
161            KeyCode::BackTab => s += "backtab",
162            KeyCode::Delete => s += "delete",
163            KeyCode::Insert => s += "insert",
164            KeyCode::Esc => s += "esc",
165            KeyCode::F(f) => s += format!("f{f}").as_str(),
166            KeyCode::Char(c) => s.push(c.to_ascii_lowercase()),
167            _ => unimplemented!(),
168        };
169
170        s
171    }
172}
173
174impl Display for Key {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        f.write_str(&String::from(*self))
177    }
178}
179
180#[derive(Debug, Serialize, Deserialize, PartialEq)]
181pub struct KeyMap(HashMap<Key, Action>);
182
183#[derive(Debug, Serialize, Deserialize, PartialEq)]
184#[serde(default)]
185pub struct Theme {
186    pub base: Style,
187    pub find: Style,
188    pub replace: Style,
189}
190
191impl Default for Theme {
192    fn default() -> Self {
193        Self {
194            base: Style {
195                fg: Some(Color::Reset),
196                ..Default::default()
197            },
198            find: Style {
199                fg: Some(Color::Red),
200                add_modifier: Modifier::CROSSED_OUT,
201                ..Default::default()
202            },
203            replace: Style {
204                fg: Some(Color::Green),
205                add_modifier: Modifier::BOLD,
206                ..Default::default()
207            },
208        }
209    }
210}
211
212#[derive(Debug, Serialize, Deserialize, PartialEq)]
213#[serde(default)]
214pub struct Config {
215    pub theme: Theme,
216    pub keys: HashMap<Key, Action>,
217    pub auto_pairs: bool,
218    pub threads: usize,
219}
220
221impl Default for Config {
222    fn default() -> Self {
223        Self {
224            theme: Theme::default(),
225            keys: [
226                ("enter", Action::Confirm),
227                ("esc", Action::Exit),
228                ("c-c", Action::Exit),
229                ("tab", Action::ToggleSearchReplace),
230                ("c-s", Action::ToggleIgnoreCase),
231                ("c-l", Action::ToggleMultiLine),
232                ("left", Action::CursorLeft),
233                ("c-b", Action::CursorLeft),
234                ("right", Action::CursorRight),
235                ("c-f", Action::CursorRight),
236                ("home", Action::CursorHome),
237                ("c-a", Action::CursorHome),
238                ("end", Action::CursorEnd),
239                ("c-e", Action::CursorEnd),
240                ("backspace", Action::DeleteCharBackward),
241                ("c-h", Action::DeleteCharBackward),
242                ("c-d", Action::DeleteChar),
243                ("c-w", Action::DeleteWord),
244                ("c-k", Action::DeleteToEndOfLine),
245                ("c-u", Action::DeleteLine),
246                ("c-n", Action::ScrollDown),
247                ("c-p", Action::ScrollUp),
248                ("c-g", Action::ScrollTop),
249            ]
250            .map(|(k, v)| (k.to_string().try_into().unwrap(), v))
251            .into(),
252            auto_pairs: true,
253            threads: 0,
254        }
255    }
256}
257
258impl FromStr for Config {
259    type Err = anyhow::Error;
260
261    fn from_str(s: &str) -> Result<Self, Self::Err> {
262        let mut c: Config = toml::from_str(s)?;
263        let base = Self::default();
264        // merge in any keys that the user didn't override
265        for (k, v) in base.keys {
266            c.keys.entry(k).or_insert(v);
267        }
268        Ok(c)
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use pretty_assertions::assert_eq;
276    use ratatui::style::Modifier;
277
278    #[test]
279    fn test_config_valid() {
280        let t = toml::toml! {
281            auto_pairs = false
282
283            [theme]
284            base.fg = "6"
285            find.fg = "#00FF00"
286            find.add_modifier = "BOLD"
287
288            [keys]
289            c-x = "exit"
290        }
291        .to_string();
292
293        let c: Config = t.parse().unwrap();
294        let mut keys = Config::default().keys;
295        keys.insert(
296            Key {
297                code: KeyCode::Char('x'),
298                modifiers: KeyModifiers::CONTROL,
299            },
300            Action::Exit,
301        );
302
303        assert_eq!(
304            c,
305            Config {
306                keys,
307                theme: Theme {
308                    base: Style {
309                        fg: Some(Color::Indexed(6)),
310                        ..Default::default()
311                    },
312                    find: Style {
313                        fg: Some(Color::Rgb(0, 255, 0)),
314                        add_modifier: Modifier::BOLD,
315                        ..Default::default()
316                    },
317                    replace: Style {
318                        fg: Some(Color::Green),
319                        add_modifier: Modifier::BOLD,
320                        ..Default::default()
321                    },
322                },
323                auto_pairs: false,
324                threads: 0,
325            }
326        )
327    }
328}