1use std::{
2 collections::{BTreeMap, HashMap},
3 fs,
4 path::PathBuf,
5};
6
7use color_eyre::{
8 Result,
9 eyre::{Context, ContextCompat, eyre},
10};
11use crossterm::{
12 event::{KeyCode, KeyEvent, KeyModifiers},
13 style::{Attribute, Attributes, Color, ContentStyle},
14};
15use directories::ProjectDirs;
16use itertools::Itertools;
17use serde::{
18 Deserialize,
19 de::{Deserializer, Error},
20};
21
22use crate::model::SearchMode;
23
24#[derive(Clone, Deserialize)]
26#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
27#[cfg_attr(not(test), serde(default))]
28pub struct Config {
29 pub data_dir: PathBuf,
31 pub check_updates: bool,
33 pub inline: bool,
35 pub search: SearchConfig,
37 pub logs: LogsConfig,
39 pub keybindings: KeyBindingsConfig,
41 pub theme: Theme,
43 pub gist: GistConfig,
45 pub tuning: SearchTuning,
47}
48
49#[derive(Clone, Copy, Deserialize)]
51#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
52#[cfg_attr(not(test), serde(default))]
53pub struct SearchConfig {
54 pub delay: u64,
56 pub mode: SearchMode,
58 pub user_only: bool,
60}
61
62#[derive(Clone, Deserialize)]
64#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
65#[cfg_attr(not(test), serde(default))]
66pub struct LogsConfig {
67 pub enabled: bool,
69 pub filter: String,
73}
74
75#[derive(Clone, Deserialize)]
80#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
81#[cfg_attr(not(test), serde(default))]
82pub struct KeyBindingsConfig(
83 #[serde(deserialize_with = "deserialize_bindings_with_defaults")] BTreeMap<KeyBindingAction, KeyBinding>,
84);
85
86#[derive(Copy, Clone, Deserialize, PartialOrd, PartialEq, Eq, Ord, Debug)]
88#[cfg_attr(test, derive(strum::EnumIter))]
89#[serde(rename_all = "snake_case")]
90pub enum KeyBindingAction {
91 Quit,
93 Update,
95 Delete,
97 Confirm,
99 Execute,
101 SearchMode,
103 SearchUserOnly,
105}
106
107#[derive(Clone, Deserialize)]
112#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
113pub struct KeyBinding(#[serde(deserialize_with = "deserialize_key_events")] Vec<KeyEvent>);
114
115#[derive(Clone, Deserialize)]
119#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
120#[cfg_attr(not(test), serde(default))]
121pub struct Theme {
122 #[serde(deserialize_with = "deserialize_style")]
124 pub primary: ContentStyle,
125 #[serde(deserialize_with = "deserialize_style")]
127 pub secondary: ContentStyle,
128 #[serde(deserialize_with = "deserialize_style")]
130 pub accent: ContentStyle,
131 #[serde(deserialize_with = "deserialize_style")]
133 pub comment: ContentStyle,
134 #[serde(deserialize_with = "deserialize_style")]
136 pub error: ContentStyle,
137 #[serde(deserialize_with = "deserialize_color")]
139 pub highlight: Option<Color>,
140 pub highlight_symbol: String,
142 #[serde(deserialize_with = "deserialize_style")]
144 pub highlight_primary: ContentStyle,
145 #[serde(deserialize_with = "deserialize_style")]
147 pub highlight_secondary: ContentStyle,
148 #[serde(deserialize_with = "deserialize_style")]
150 pub highlight_accent: ContentStyle,
151 #[serde(deserialize_with = "deserialize_style")]
153 pub highlight_comment: ContentStyle,
154}
155
156#[derive(Clone, Default, Deserialize)]
158#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
159pub struct GistConfig {
160 pub id: String,
162 pub token: String,
164}
165
166#[derive(Clone, Copy, Default, Deserialize)]
168#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
169#[cfg_attr(not(test), serde(default))]
170pub struct SearchTuning {
171 pub commands: SearchCommandTuning,
173 pub variables: SearchVariableTuning,
175}
176
177#[derive(Clone, Copy, Default, Deserialize)]
179#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
180#[cfg_attr(not(test), serde(default))]
181pub struct SearchCommandTuning {
182 pub text: SearchCommandsTextTuning,
184 pub path: SearchPathTuning,
186 pub usage: SearchUsageTuning,
188}
189
190#[derive(Clone, Copy, Deserialize)]
192#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
193#[cfg_attr(not(test), serde(default))]
194pub struct SearchCommandsTextTuning {
195 pub points: u32,
197 pub command: f64,
199 pub description: f64,
201 pub auto: SearchCommandsTextAutoTuning,
203}
204
205#[derive(Clone, Copy, Deserialize)]
207#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
208#[cfg_attr(not(test), serde(default))]
209pub struct SearchCommandsTextAutoTuning {
210 pub prefix: f64,
212 pub fuzzy: f64,
214 pub relaxed: f64,
216 pub root: f64,
218}
219
220#[derive(Clone, Copy, Deserialize)]
222#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
223#[cfg_attr(not(test), serde(default))]
224pub struct SearchPathTuning {
225 pub points: u32,
227 pub exact: f64,
229 pub ancestor: f64,
231 pub descendant: f64,
233 pub unrelated: f64,
235}
236
237#[derive(Clone, Copy, Deserialize)]
239#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
240#[cfg_attr(not(test), serde(default))]
241pub struct SearchUsageTuning {
242 pub points: u32,
244}
245
246#[derive(Clone, Copy, Default, Deserialize)]
248#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
249#[cfg_attr(not(test), serde(default))]
250pub struct SearchVariableTuning {
251 pub context: SearchVariableContextTuning,
253 pub path: SearchPathTuning,
255}
256
257#[derive(Clone, Copy, Deserialize)]
259#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
260#[cfg_attr(not(test), serde(default))]
261pub struct SearchVariableContextTuning {
262 pub points: u32,
264}
265
266impl Config {
267 pub fn init(config_file: Option<PathBuf>) -> Result<Self> {
272 let proj_dirs = ProjectDirs::from("org", "IntelliShell", "Intelli-Shell")
274 .wrap_err("Couldn't initialize project directory")?;
275 let config_dir = proj_dirs.config_dir().to_path_buf();
276
277 let config_path = config_file.unwrap_or_else(|| config_dir.join("config.toml"));
279 let mut config = if config_path.exists() {
280 let config_str = fs::read_to_string(&config_path)
282 .wrap_err_with(|| format!("Couldn't read config file {}", config_path.display()))?;
283 toml::from_str(&config_str)
284 .wrap_err_with(|| format!("Couldn't parse config file {}", config_path.display()))?
285 } else {
286 Config::default()
288 };
289 if config.data_dir.as_os_str().is_empty() {
291 config.data_dir = proj_dirs.data_dir().to_path_buf();
292 }
293
294 let conflicts = config.keybindings.find_conflicts();
296 if !conflicts.is_empty() {
297 return Err(eyre!(
298 "Invalid config, there are some key binding conflicts:\n{}",
299 conflicts
300 .into_iter()
301 .map(|(_, a)| format!("- {}", a.into_iter().map(|a| format!("{a:?}")).join(", ")))
302 .join("\n")
303 ));
304 }
305
306 fs::create_dir_all(&config.data_dir)
308 .wrap_err_with(|| format!("Could't create data dir {}", config.data_dir.display()))?;
309
310 Ok(config)
311 }
312}
313
314impl KeyBindingsConfig {
315 pub fn get(&self, action: &KeyBindingAction) -> &KeyBinding {
317 self.0.get(action).unwrap()
318 }
319
320 pub fn get_action_matching(&self, event: &KeyEvent) -> Option<KeyBindingAction> {
322 self.0.iter().find_map(
323 |(action, binding)| {
324 if binding.matches(event) { Some(*action) } else { None }
325 },
326 )
327 }
328
329 pub fn find_conflicts(&self) -> Vec<(KeyEvent, Vec<KeyBindingAction>)> {
331 let mut event_to_actions_map: HashMap<KeyEvent, Vec<KeyBindingAction>> = HashMap::new();
333
334 for (action, key_binding) in self.0.iter() {
336 for event_in_binding in key_binding.0.iter() {
338 event_to_actions_map.entry(*event_in_binding).or_default().push(*action);
340 }
341 }
342
343 event_to_actions_map
345 .into_iter()
346 .filter_map(|(key_event, actions)| {
347 if actions.len() > 1 {
348 Some((key_event, actions))
349 } else {
350 None
351 }
352 })
353 .collect()
354 }
355}
356
357impl KeyBinding {
358 pub fn matches(&self, event: &KeyEvent) -> bool {
361 self.0
362 .iter()
363 .any(|e| e.code == event.code && e.modifiers == event.modifiers)
364 }
365}
366
367impl Theme {
368 pub fn highlight_primary_full(&self) -> ContentStyle {
370 if let Some(color) = self.highlight {
371 let mut ret = self.highlight_primary;
372 ret.background_color = Some(color);
373 ret
374 } else {
375 self.highlight_primary
376 }
377 }
378
379 pub fn highlight_secondary_full(&self) -> ContentStyle {
381 if let Some(color) = self.highlight {
382 let mut ret = self.highlight_secondary;
383 ret.background_color = Some(color);
384 ret
385 } else {
386 self.highlight_secondary
387 }
388 }
389
390 pub fn highlight_accent_full(&self) -> ContentStyle {
392 if let Some(color) = self.highlight {
393 let mut ret = self.highlight_accent;
394 ret.background_color = Some(color);
395 ret
396 } else {
397 self.highlight_accent
398 }
399 }
400
401 pub fn highlight_comment_full(&self) -> ContentStyle {
403 if let Some(color) = self.highlight {
404 let mut ret = self.highlight_comment;
405 ret.background_color = Some(color);
406 ret
407 } else {
408 self.highlight_comment
409 }
410 }
411}
412
413impl Default for Config {
414 fn default() -> Self {
415 Self {
416 data_dir: PathBuf::new(),
417 check_updates: true,
418 inline: true,
419 search: SearchConfig::default(),
420 logs: LogsConfig::default(),
421 keybindings: KeyBindingsConfig::default(),
422 theme: Theme::default(),
423 gist: GistConfig::default(),
424 tuning: SearchTuning::default(),
425 }
426 }
427}
428impl Default for SearchConfig {
429 fn default() -> Self {
430 Self {
431 delay: 250,
432 mode: SearchMode::Auto,
433 user_only: false,
434 }
435 }
436}
437impl Default for LogsConfig {
438 fn default() -> Self {
439 Self {
440 enabled: false,
441 filter: String::from("info"),
442 }
443 }
444}
445impl Default for KeyBindingsConfig {
446 fn default() -> Self {
447 Self(BTreeMap::from([
448 (KeyBindingAction::Quit, KeyBinding(vec![KeyEvent::from(KeyCode::Esc)])),
449 (
450 KeyBindingAction::Update,
451 KeyBinding(vec![
452 KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL),
453 KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL),
454 KeyEvent::from(KeyCode::F(2)),
455 ]),
456 ),
457 (
458 KeyBindingAction::Delete,
459 KeyBinding(vec![KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL)]),
460 ),
461 (
462 KeyBindingAction::Confirm,
463 KeyBinding(vec![KeyEvent::from(KeyCode::Tab), KeyEvent::from(KeyCode::Enter)]),
464 ),
465 (
466 KeyBindingAction::Execute,
467 KeyBinding(vec![
468 KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL),
469 KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL),
470 ]),
471 ),
472 (
473 KeyBindingAction::SearchMode,
474 KeyBinding(vec![KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL)]),
475 ),
476 (
477 KeyBindingAction::SearchUserOnly,
478 KeyBinding(vec![KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL)]),
479 ),
480 ]))
481 }
482}
483impl Default for Theme {
484 fn default() -> Self {
485 let primary = ContentStyle::new();
486 let highlight_primary = primary;
487
488 let mut secondary = ContentStyle::new();
489 secondary.attributes.set(Attribute::Dim);
490 let highlight_secondary = secondary;
491
492 let mut accent = ContentStyle::new();
493 accent.foreground_color = Some(Color::Yellow);
494 let highlight_accent = accent;
495
496 let mut comment = ContentStyle::new();
497 comment.foreground_color = Some(Color::Green);
498 comment.attributes.set(Attribute::Italic);
499 let highlight_comment = comment;
500
501 let mut error = ContentStyle::new();
502 error.foreground_color = Some(Color::DarkRed);
503
504 Self {
505 primary,
506 secondary,
507 accent,
508 comment,
509 error,
510 highlight: Some(Color::DarkGrey),
511 highlight_symbol: String::from("ยป "),
512 highlight_primary,
513 highlight_secondary,
514 highlight_accent,
515 highlight_comment,
516 }
517 }
518}
519impl Default for SearchCommandsTextTuning {
520 fn default() -> Self {
521 Self {
522 points: 600,
523 command: 2.0,
524 description: 1.0,
525 auto: SearchCommandsTextAutoTuning::default(),
526 }
527 }
528}
529impl Default for SearchCommandsTextAutoTuning {
530 fn default() -> Self {
531 Self {
532 prefix: 1.5,
533 fuzzy: 1.0,
534 relaxed: 0.5,
535 root: 2.0,
536 }
537 }
538}
539impl Default for SearchUsageTuning {
540 fn default() -> Self {
541 Self { points: 100 }
542 }
543}
544impl Default for SearchPathTuning {
545 fn default() -> Self {
546 Self {
547 points: 300,
548 exact: 1.0,
549 ancestor: 0.5,
550 descendant: 0.25,
551 unrelated: 0.1,
552 }
553 }
554}
555impl Default for SearchVariableContextTuning {
556 fn default() -> Self {
557 Self { points: 700 }
558 }
559}
560
561fn deserialize_bindings_with_defaults<'de, D>(
567 deserializer: D,
568) -> Result<BTreeMap<KeyBindingAction, KeyBinding>, D::Error>
569where
570 D: Deserializer<'de>,
571{
572 let user_provided_bindings = BTreeMap::<KeyBindingAction, KeyBinding>::deserialize(deserializer)?;
574
575 #[cfg(test)]
576 {
577 use strum::IntoEnumIterator;
578 for action_variant in KeyBindingAction::iter() {
580 if !user_provided_bindings.contains_key(&action_variant) {
581 return Err(D::Error::custom(format!(
582 "Missing key binding for action '{action_variant:?}'."
583 )));
584 }
585 }
586 Ok(user_provided_bindings)
587 }
588 #[cfg(not(test))]
589 {
590 let mut final_bindings = user_provided_bindings;
593 let default_bindings = KeyBindingsConfig::default();
594
595 for (action, default_binding) in default_bindings.0 {
596 final_bindings.entry(action).or_insert(default_binding);
597 }
598 Ok(final_bindings)
599 }
600}
601
602fn deserialize_key_events<'de, D>(deserializer: D) -> Result<Vec<KeyEvent>, D::Error>
606where
607 D: Deserializer<'de>,
608{
609 #[derive(Deserialize)]
610 #[serde(untagged)]
611 enum StringOrVec {
612 Single(String),
613 Multiple(Vec<String>),
614 }
615
616 let strings = match StringOrVec::deserialize(deserializer)? {
617 StringOrVec::Single(s) => vec![s],
618 StringOrVec::Multiple(v) => v,
619 };
620
621 strings
622 .iter()
623 .map(String::as_str)
624 .map(parse_key_event)
625 .map(|r| r.map_err(D::Error::custom))
626 .collect()
627}
628
629fn deserialize_color<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
634where
635 D: Deserializer<'de>,
636{
637 parse_color(&String::deserialize(deserializer)?).map_err(D::Error::custom)
638}
639
640fn deserialize_style<'de, D>(deserializer: D) -> Result<ContentStyle, D::Error>
644where
645 D: Deserializer<'de>,
646{
647 parse_style(&String::deserialize(deserializer)?).map_err(D::Error::custom)
648}
649
650fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
654 let raw_lower = raw.to_ascii_lowercase();
655 let (remaining, modifiers) = extract_key_modifiers(&raw_lower);
656 parse_key_code_with_modifiers(remaining, modifiers)
657}
658
659fn extract_key_modifiers(raw: &str) -> (&str, KeyModifiers) {
663 let mut modifiers = KeyModifiers::empty();
664 let mut current = raw;
665
666 loop {
667 match current {
668 rest if rest.starts_with("ctrl-") || rest.starts_with("ctrl+") => {
669 modifiers.insert(KeyModifiers::CONTROL);
670 current = &rest[5..];
671 }
672 rest if rest.starts_with("shift-") || rest.starts_with("shift+") => {
673 modifiers.insert(KeyModifiers::SHIFT);
674 current = &rest[6..];
675 }
676 rest if rest.starts_with("alt-") || rest.starts_with("alt+") => {
677 modifiers.insert(KeyModifiers::ALT);
678 current = &rest[4..];
679 }
680 _ => break,
681 };
682 }
683
684 (current, modifiers)
685}
686
687fn parse_key_code_with_modifiers(raw: &str, mut modifiers: KeyModifiers) -> Result<KeyEvent, String> {
689 let code = match raw {
690 "esc" => KeyCode::Esc,
691 "enter" => KeyCode::Enter,
692 "left" => KeyCode::Left,
693 "right" => KeyCode::Right,
694 "up" => KeyCode::Up,
695 "down" => KeyCode::Down,
696 "home" => KeyCode::Home,
697 "end" => KeyCode::End,
698 "pageup" => KeyCode::PageUp,
699 "pagedown" => KeyCode::PageDown,
700 "backtab" => {
701 modifiers.insert(KeyModifiers::SHIFT);
702 KeyCode::BackTab
703 }
704 "backspace" => KeyCode::Backspace,
705 "delete" => KeyCode::Delete,
706 "insert" => KeyCode::Insert,
707 "f1" => KeyCode::F(1),
708 "f2" => KeyCode::F(2),
709 "f3" => KeyCode::F(3),
710 "f4" => KeyCode::F(4),
711 "f5" => KeyCode::F(5),
712 "f6" => KeyCode::F(6),
713 "f7" => KeyCode::F(7),
714 "f8" => KeyCode::F(8),
715 "f9" => KeyCode::F(9),
716 "f10" => KeyCode::F(10),
717 "f11" => KeyCode::F(11),
718 "f12" => KeyCode::F(12),
719 "space" | "spacebar" => KeyCode::Char(' '),
720 "hyphen" => KeyCode::Char('-'),
721 "minus" => KeyCode::Char('-'),
722 "tab" => KeyCode::Tab,
723 c if c.len() == 1 => {
724 let mut c = c.chars().next().expect("just checked");
725 if modifiers.contains(KeyModifiers::SHIFT) {
726 c = c.to_ascii_uppercase();
727 }
728 KeyCode::Char(c)
729 }
730 _ => return Err(format!("Unable to parse key binding: {raw}")),
731 };
732 Ok(KeyEvent::new(code, modifiers))
733}
734
735fn parse_color(raw: &str) -> Result<Option<Color>, String> {
739 let raw_lower = raw.to_ascii_lowercase();
740 if raw.is_empty() || raw == "none" {
741 Ok(None)
742 } else {
743 Ok(Some(parse_color_inner(&raw_lower)?))
744 }
745}
746
747fn parse_style(raw: &str) -> Result<ContentStyle, String> {
751 let raw_lower = raw.to_ascii_lowercase();
752 let (remaining, attributes) = extract_style_attributes(&raw_lower);
753 let mut style = ContentStyle::new();
754 style.attributes = attributes;
755 if !remaining.is_empty() && remaining != "default" {
756 style.foreground_color = Some(parse_color_inner(remaining)?);
757 }
758 Ok(style)
759}
760
761fn extract_style_attributes(raw: &str) -> (&str, Attributes) {
765 let mut attributes = Attributes::none();
766 let mut current = raw;
767
768 loop {
769 match current {
770 rest if rest.starts_with("bold") => {
771 attributes.set(Attribute::Bold);
772 current = &rest[4..];
773 if current.starts_with(' ') {
774 current = ¤t[1..];
775 }
776 }
777 rest if rest.starts_with("dim") => {
778 attributes.set(Attribute::Dim);
779 current = &rest[3..];
780 if current.starts_with(' ') {
781 current = ¤t[1..];
782 }
783 }
784 rest if rest.starts_with("italic") => {
785 attributes.set(Attribute::Italic);
786 current = &rest[6..];
787 if current.starts_with(' ') {
788 current = ¤t[1..];
789 }
790 }
791 rest if rest.starts_with("underline") => {
792 attributes.set(Attribute::Underlined);
793 current = &rest[9..];
794 if current.starts_with(' ') {
795 current = ¤t[1..];
796 }
797 }
798 rest if rest.starts_with("underlined") => {
799 attributes.set(Attribute::Underlined);
800 current = &rest[10..];
801 if current.starts_with(' ') {
802 current = ¤t[1..];
803 }
804 }
805 _ => break,
806 };
807 }
808
809 (current.trim(), attributes)
810}
811
812fn parse_color_inner(raw: &str) -> Result<Color, String> {
816 Ok(match raw {
817 "black" => Color::Black,
818 "red" => Color::Red,
819 "green" => Color::Green,
820 "yellow" => Color::Yellow,
821 "blue" => Color::Blue,
822 "magenta" => Color::Magenta,
823 "cyan" => Color::Cyan,
824 "gray" | "grey" => Color::Grey,
825 "dark gray" | "darkgray" | "dark grey" | "darkgrey" => Color::DarkGrey,
826 "dark red" | "darkred" => Color::DarkRed,
827 "dark green" | "darkgreen" => Color::DarkGreen,
828 "dark yellow" | "darkyellow" => Color::DarkYellow,
829 "dark blue" | "darkblue" => Color::DarkBlue,
830 "dark magenta" | "darkmagenta" => Color::DarkMagenta,
831 "dark cyan" | "darkcyan" => Color::DarkCyan,
832 "white" => Color::White,
833 rgb if rgb.starts_with("rgb(") => {
834 let rgb = rgb.trim_start_matches("rgb(").trim_end_matches(")").split(',');
835 let rgb = rgb
836 .map(|c| c.trim().parse::<u8>())
837 .collect::<Result<Vec<u8>, _>>()
838 .map_err(|_| format!("Unable to parse color: {raw}"))?;
839 if rgb.len() != 3 {
840 return Err(format!("Unable to parse color: {raw}"));
841 }
842 Color::Rgb {
843 r: rgb[0],
844 g: rgb[1],
845 b: rgb[2],
846 }
847 }
848 hex if hex.starts_with("#") => {
849 let hex = hex.trim_start_matches("#");
850 if hex.len() != 6 {
851 return Err(format!("Unable to parse color: {raw}"));
852 }
853 let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| format!("Unable to parse color: {raw}"))?;
854 let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| format!("Unable to parse color: {raw}"))?;
855 let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| format!("Unable to parse color: {raw}"))?;
856 Color::Rgb { r, g, b }
857 }
858 c => {
859 if let Ok(c) = c.parse::<u8>() {
860 Color::AnsiValue(c)
861 } else {
862 return Err(format!("Unable to parse color: {raw}"));
863 }
864 }
865 })
866}
867
868#[cfg(test)]
869mod tests {
870 use pretty_assertions::assert_eq;
871 use strum::IntoEnumIterator;
872
873 use super::*;
874
875 #[test]
876 fn test_default_config() -> Result<()> {
877 let config_str = fs::read_to_string("default_config.toml").wrap_err("Couldn't read default config file")?;
878 let config: Config = toml::from_str(&config_str).wrap_err("Couldn't parse default config file")?;
879
880 assert_eq!(Config::default(), config);
881
882 Ok(())
883 }
884
885 #[test]
886 fn test_default_keybindings_complete() {
887 let config = KeyBindingsConfig::default();
888
889 for action in KeyBindingAction::iter() {
890 assert!(
891 config.0.contains_key(&action),
892 "Missing default binding for action: {action:?}"
893 );
894 }
895 }
896
897 #[test]
898 fn test_default_keybindings_no_conflicts() {
899 let config = KeyBindingsConfig::default();
900
901 let conflicts = config.find_conflicts();
902 assert_eq!(conflicts.len(), 0, "Key binding conflicts: {conflicts:?}");
903 }
904
905 #[test]
906 fn test_keybinding_matches() {
907 let binding = KeyBinding(vec![
908 KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
909 KeyEvent::from(KeyCode::Enter),
910 ]);
911
912 assert!(binding.matches(&KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)));
914 assert!(binding.matches(&KeyEvent::from(KeyCode::Enter)));
915
916 assert!(!binding.matches(&KeyEvent::new(
918 KeyCode::Char('a'),
919 KeyModifiers::CONTROL | KeyModifiers::ALT
920 )));
921
922 assert!(!binding.matches(&KeyEvent::from(KeyCode::Esc)));
924 }
925
926 #[test]
927 fn test_simple_keys() {
928 assert_eq!(
929 parse_key_event("a").unwrap(),
930 KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())
931 );
932
933 assert_eq!(
934 parse_key_event("enter").unwrap(),
935 KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())
936 );
937
938 assert_eq!(
939 parse_key_event("esc").unwrap(),
940 KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())
941 );
942 }
943
944 #[test]
945 fn test_with_modifiers() {
946 assert_eq!(
947 parse_key_event("ctrl-a").unwrap(),
948 KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
949 );
950
951 assert_eq!(
952 parse_key_event("alt-enter").unwrap(),
953 KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
954 );
955
956 assert_eq!(
957 parse_key_event("shift-esc").unwrap(),
958 KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT)
959 );
960 }
961
962 #[test]
963 fn test_multiple_modifiers() {
964 assert_eq!(
965 parse_key_event("ctrl-alt-a").unwrap(),
966 KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::ALT)
967 );
968
969 assert_eq!(
970 parse_key_event("ctrl-shift-enter").unwrap(),
971 KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT)
972 );
973 }
974
975 #[test]
976 fn test_invalid_keys() {
977 let res = parse_key_event("invalid-key");
978 assert_eq!(res, Err(String::from("Unable to parse key binding: invalid-key")));
979 }
980
981 #[test]
982 fn test_parse_color_none() {
983 let color = parse_color("none").unwrap();
984 assert_eq!(color, None);
985 }
986
987 #[test]
988 fn test_parse_color_simple() {
989 let color = parse_color("red").unwrap();
990 assert_eq!(color, Some(Color::Red));
991 }
992
993 #[test]
994 fn test_parse_color_rgb() {
995 let color = parse_color("rgb(50, 25, 15)").unwrap();
996 assert_eq!(color, Some(Color::Rgb { r: 50, g: 25, b: 15 }));
997 }
998
999 #[test]
1000 fn test_parse_color_rgb_out_of_range() {
1001 let res = parse_color("rgb(500, 25, 15)");
1002 assert_eq!(res, Err(String::from("Unable to parse color: rgb(500, 25, 15)")));
1003 }
1004
1005 #[test]
1006 fn test_parse_color_rgb_invalid() {
1007 let res = parse_color("rgb(50, 25, 15, 5)");
1008 assert_eq!(res, Err(String::from("Unable to parse color: rgb(50, 25, 15, 5)")));
1009 }
1010
1011 #[test]
1012 fn test_parse_color_hex() {
1013 let color = parse_color("#4287f5").unwrap();
1014 assert_eq!(color, Some(Color::Rgb { r: 66, g: 135, b: 245 }));
1015 }
1016
1017 #[test]
1018 fn test_parse_color_hex_out_of_range() {
1019 let res = parse_color("#4287fg");
1020 assert_eq!(res, Err(String::from("Unable to parse color: #4287fg")));
1021 }
1022
1023 #[test]
1024 fn test_parse_color_hex_invalid() {
1025 let res = parse_color("#4287f50");
1026 assert_eq!(res, Err(String::from("Unable to parse color: #4287f50")));
1027 }
1028
1029 #[test]
1030 fn test_parse_color_index() {
1031 let color = parse_color("6").unwrap();
1032 assert_eq!(color, Some(Color::AnsiValue(6)));
1033 }
1034
1035 #[test]
1036 fn test_parse_color_fail() {
1037 let res = parse_color("1234");
1038 assert_eq!(res, Err(String::from("Unable to parse color: 1234")));
1039 }
1040
1041 #[test]
1042 fn test_parse_style_empty() {
1043 let style = parse_style("").unwrap();
1044 assert_eq!(style, ContentStyle::new());
1045 }
1046
1047 #[test]
1048 fn test_parse_style_default() {
1049 let style = parse_style("default").unwrap();
1050 assert_eq!(style, ContentStyle::new());
1051 }
1052
1053 #[test]
1054 fn test_parse_style_simple() {
1055 let style = parse_style("red").unwrap();
1056 assert_eq!(style.foreground_color, Some(Color::Red));
1057 assert_eq!(style.attributes, Attributes::none());
1058 }
1059
1060 #[test]
1061 fn test_parse_style_only_modifier() {
1062 let style = parse_style("bold").unwrap();
1063 assert_eq!(style.foreground_color, None);
1064 let mut expected_attributes = Attributes::none();
1065 expected_attributes.set(Attribute::Bold);
1066 assert_eq!(style.attributes, expected_attributes);
1067 }
1068
1069 #[test]
1070 fn test_parse_style_with_modifier() {
1071 let style = parse_style("italic red").unwrap();
1072 assert_eq!(style.foreground_color, Some(Color::Red));
1073 let mut expected_attributes = Attributes::none();
1074 expected_attributes.set(Attribute::Italic);
1075 assert_eq!(style.attributes, expected_attributes);
1076 }
1077
1078 #[test]
1079 fn test_parse_style_multiple_modifier() {
1080 let style = parse_style("underline dim dark red").unwrap();
1081 assert_eq!(style.foreground_color, Some(Color::DarkRed));
1082 let mut expected_attributes = Attributes::none();
1083 expected_attributes.set(Attribute::Underlined);
1084 expected_attributes.set(Attribute::Dim);
1085 assert_eq!(style.attributes, expected_attributes);
1086 }
1087}