Skip to main content

todotxt_tui/
config.rs

1mod colors;
2mod export;
3mod options;
4mod parsers;
5mod styles;
6mod text_style;
7mod traits;
8
9pub use self::{
10    colors::Color,
11    options::{
12        PasteBehavior, SavePolicy, SetFinalDateType, TaskSort, TextModifier, WidgetBorderType,
13    },
14    styles::CustomCategoryStyle,
15    text_style::{TextStyle, TextStyleList},
16    traits::{Conf, ConfMerge, ConfigDefaults},
17};
18
19use crate::{
20    layout::widget::WidgetType,
21    todo::{ToDoCategory, ToDoData},
22    ui::{EventHandlerUI, KeyShortcut, UIEvent},
23};
24use clap::{builder::styling::AnsiColor, FromArgMatches};
25use crossterm::event::{KeyCode, KeyModifiers};
26use export::Export;
27use std::{
28    env::var,
29    path::{Path, PathBuf},
30    time::Duration,
31};
32use todotxt_tui_macros::{Conf, ConfMerge};
33use traits::ExportConf;
34use tui::style::Color as tuiColor;
35
36#[derive(Conf, Clone, Debug, PartialEq, Eq)]
37pub struct FileWorkerConfig {
38    /// Path to the task storage. The format is selected automatically:
39    /// a directory or a file with extension `.ics`/`.ical` uses the iCalendar format
40    /// (vdirsyncer-style, one `.ics` file per task); any other file uses todo.txt format.
41    pub todo_path: PathBuf,
42    /// Path to the archive file where completed tasks are stored separately.
43    /// If not provided, completed tasks are kept in the same storage as pending tasks.
44    /// Ignored when using the iCalendar format.
45    pub archive_path: Option<PathBuf>,
46    /// The duration (in seconds) between automatic saves of the `todo.txt` file.
47    #[arg(short = 'd')]
48    pub autosave_duration: Duration,
49    /// Enable or disable the file watcher, which automatically reloads the `todo.txt` file
50    /// when changes are detected.
51    #[arg(short = 'f')]
52    pub file_watcher: bool,
53    /// The save policy for how and when the `todo.txt` and optionally `archive.txt` files
54    /// should be saved.
55    pub save_policy: SavePolicy,
56}
57
58impl Default for FileWorkerConfig {
59    fn default() -> Self {
60        Self {
61            todo_path: PathBuf::from(var("HOME").unwrap_or(String::from("~")) + "/todo.txt"),
62            archive_path: None,
63            autosave_duration: Duration::from_secs(900),
64            file_watcher: true,
65            save_policy: SavePolicy::default(),
66        }
67    }
68}
69
70#[derive(Conf, Clone, Debug, PartialEq, Eq)]
71pub struct ActiveColorConfig {
72    /// The text style used to highlight the active item in a list.
73    #[arg(short = 'A')]
74    list_active_color: TextStyle,
75    /// The text style used to highlight an active task that is in the pending list.
76    /// This option overrides the `list_active_color`.
77    #[arg(short = 'P')]
78    pending_active_color: TextStyle,
79    /// The text style used to highlight an active task that is in the completed list.
80    /// This option overrides the `list_active_color`.
81    #[arg(short = 'D')]
82    done_active_color: TextStyle,
83    /// The text style used to highlight an active category.
84    /// This option overrides the `list_active_color`.
85    category_active_color: TextStyle,
86    /// The text style used to highlight an active project.
87    /// This option overrides the `category_active_color`.
88    projects_active_color: TextStyle,
89    /// The text style used to highlight an active context.
90    /// This option overrides the `category_active_color`.
91    contexts_active_color: TextStyle,
92    /// The text style used to highlight an active tag.
93    /// This option overrides the `category_active_color`.
94    tags_active_color: TextStyle,
95}
96
97impl ActiveColorConfig {
98    /// Retrieves the active style for a given `ToDoData` type, combining it with
99    /// the list's active color.
100    pub fn get_active_style(&self, data_type: &ToDoData) -> TextStyle {
101        self.list_active_color.combine(&match data_type {
102            ToDoData::Done => self.done_active_color,
103            ToDoData::Pending => self.pending_active_color,
104        })
105    }
106
107    /// Returns the active configuration style for a given category.
108    /// This function combines three color settings based on the specified `ToDoCategory`:
109    /// - The list active color.
110    /// - The category specific active color (projects, contexts, or hashtags).
111    pub fn get_active_config_style(&self, category: &ToDoCategory) -> TextStyle {
112        self.list_active_color
113            .combine(&self.category_active_color)
114            .combine(match category {
115                ToDoCategory::Projects => &self.projects_active_color,
116                ToDoCategory::Contexts => &self.contexts_active_color,
117                ToDoCategory::Hashtags => &self.tags_active_color,
118            })
119    }
120}
121
122impl Default for ActiveColorConfig {
123    fn default() -> Self {
124        Self {
125            list_active_color: TextStyle::default().bg(Color::lightred()),
126            pending_active_color: TextStyle::default(),
127            done_active_color: TextStyle::default(),
128            category_active_color: TextStyle::default(),
129            projects_active_color: TextStyle::default(),
130            contexts_active_color: TextStyle::default(),
131            tags_active_color: TextStyle::default(),
132        }
133    }
134}
135
136#[derive(Conf, Clone, Debug, PartialEq, Eq)]
137pub struct ListConfig {
138    /// The number of lines displayed above and below the currently active
139    /// item in a list when the list is moving.
140    #[arg(short = 's')]
141    pub list_shift: usize,
142    /// Keybindings configured for interacting with lists.
143    #[arg(short = 'L')]
144    pub list_keybind: EventHandlerUI,
145    /// The format string used to render pending tasks in the list.
146    pub pending_format: String,
147    /// The format string used to render completed tasks in the list.
148    pub done_format: String,
149}
150
151impl Default for ListConfig {
152    fn default() -> Self {
153        Self {
154            list_shift: 4,
155            list_keybind: EventHandlerUI::from([
156                (KeyShortcut::from(KeyCode::Char('j')), UIEvent::ListDown),
157                (KeyShortcut::from(KeyCode::Char('k')), UIEvent::ListUp),
158                (KeyShortcut::from(KeyCode::Char('g')), UIEvent::ListFirst),
159                (
160                    KeyShortcut::new(KeyCode::Char('g'), KeyModifiers::SHIFT),
161                    UIEvent::ListLast,
162                ),
163                (KeyShortcut::from(KeyCode::Char('h')), UIEvent::CleanSearch),
164            ]),
165            pending_format: String::from("[$subject](! priority)"),
166            done_format: String::from("[$subject](! priority)"),
167        }
168    }
169}
170
171#[derive(Conf, Clone, Debug, PartialEq, Eq)]
172pub struct PreviewConfig {
173    /// The format string used to generate the preview, supporting placeholders
174    /// for dynamic content.
175    #[arg(short = 'p')]
176    pub preview_format: String,
177    /// Determines whether long lines in the preview should be wrapped to fit
178    /// within the available width.
179    #[arg(short = 'w')]
180    pub wrap_preview: bool,
181}
182
183impl Default for PreviewConfig {
184    fn default() -> Self {
185        Self {
186            preview_format: String::from(
187                "Pending: $pending Done: $done
188Subject: $subject
189Priority: $priority
190Create date: $create_date
191Link: $link",
192            ),
193            wrap_preview: true,
194        }
195    }
196}
197
198#[derive(Conf, Clone, Debug, PartialEq, Eq)]
199pub struct ToDoConfig {
200    /// Determines whether projects, contexts, and tags from completed tasks
201    /// should be included in the lists of available projects, contexts, and tags.
202    pub use_done: bool,
203    /// Sorting options to apply to pending tasks. Priority of sorting options is
204    /// decreasing from left to right. If empty, tasks are not sorted.
205    #[arg(num_args = 0..)]
206    pub pending_sort: Vec<TaskSort>,
207    /// Sorting options to apply to completed tasks. Priority of sorting options is
208    /// decreasing from left to right. If empty, tasks are not sorted.
209    #[arg(num_args = 0..)]
210    pub done_sort: Vec<TaskSort>,
211    /// Specifies whether to delete the final date (if it exists) when a task is moved from completed back to pending.
212    pub delete_final_date: bool,
213    /// Configures how the final date is handled when a task is marked as completed.
214    /// Options include overriding the date, only adding it if missing, or never setting it.
215    pub set_final_date: SetFinalDateType,
216    /// Specifies whether to set the creation date when a new task is added.
217    /// If the user provides their own creation date, it will still be added
218    /// regardless of this setting.
219    pub set_created_date: bool,
220}
221
222impl Default for ToDoConfig {
223    fn default() -> Self {
224        Self {
225            use_done: false,
226            pending_sort: vec![],
227            done_sort: vec![],
228            delete_final_date: true,
229            set_final_date: SetFinalDateType::default(),
230            set_created_date: true,
231        }
232    }
233}
234
235#[derive(Conf, Clone, Debug, PartialEq, Eq)]
236pub struct UiConfig {
237    /// The widget that will be active when the application starts.
238    #[arg(short = 'i')]
239    pub init_widget: WidgetType,
240    /// The title of the window when `todotxt-tui` is opened.
241    #[arg(short = 't')]
242    pub window_title: String,
243    /// Keybindings configured for interacting with the application window.
244    #[arg(short = 'W')]
245    pub window_keybinds: EventHandlerUI,
246    /// The refresh rate for the list display, in seconds.
247    #[arg(short = 'R')]
248    pub list_refresh_rate: Duration,
249    /// Path to save the application's state (currently unused).
250    #[arg(short = 'S')]
251    pub save_state_path: Option<PathBuf>,
252    /// The layout setting allows you to define a custom layout for the application using blocks `[]`. You can specify the orientation of the blocks as either `Direction: Vertical` or `Direction: Horizontal`, along with the size of each block as a percentage or value. Within these blocks, you can include various widgets, such as:
253    ///
254    /// - `List`: The main list of tasks.
255    /// - `Preview`: The task preview section.
256    /// - `Done`: The list of completed tasks.
257    /// - `Projects`: The list of projects.
258    /// - `Contexts`: The list of contexts.
259    /// - `Hashtags`: The list of hashtags.
260    #[arg(short = 'l', verbatim_doc_comment)]
261    pub layout: String,
262    /// Determines how pasted content is processed.
263    ///
264    /// Option as-keys simulates typing the pasted content as if entered via the keyboard.
265    /// Option insert directly inserts the pasted content at the cursor position.
266    /// Option none disables pasting altogether.
267    pub paste_behavior: PasteBehavior,
268    /// Enables or disables mouse interaction support.
269    pub enable_mouse: bool,
270}
271
272impl Default for UiConfig {
273    fn default() -> Self {
274        Self {
275            init_widget: WidgetType::List,
276            window_title: String::from("ToDo tui"),
277            window_keybinds: EventHandlerUI::from([
278                (KeyShortcut::from(KeyCode::Char('q')), UIEvent::Quit),
279                (
280                    KeyShortcut::new(KeyCode::Char('s'), KeyModifiers::SHIFT),
281                    UIEvent::Save,
282                ),
283                (KeyShortcut::from(KeyCode::Char('u')), UIEvent::Load),
284                (
285                    KeyShortcut::new(KeyCode::Char('h'), KeyModifiers::SHIFT),
286                    UIEvent::MoveLeft,
287                ),
288                (
289                    KeyShortcut::new(KeyCode::Char('l'), KeyModifiers::SHIFT),
290                    UIEvent::MoveRight,
291                ),
292                (
293                    KeyShortcut::new(KeyCode::Char('k'), KeyModifiers::SHIFT),
294                    UIEvent::MoveUp,
295                ),
296                (
297                    KeyShortcut::new(KeyCode::Char('j'), KeyModifiers::SHIFT),
298                    UIEvent::MoveDown,
299                ),
300                (
301                    KeyShortcut::new(KeyCode::Char('i'), KeyModifiers::SHIFT),
302                    UIEvent::InsertMode,
303                ),
304                (
305                    KeyShortcut::new(KeyCode::Char('e'), KeyModifiers::SHIFT),
306                    UIEvent::EditMode,
307                ),
308                (KeyShortcut::from(KeyCode::Char('/')), UIEvent::SearchMode),
309                (KeyShortcut::from(KeyCode::Char('?')), UIEvent::ShowHelp),
310            ]),
311            list_refresh_rate: Duration::from_secs(5),
312            save_state_path: None,
313            layout: String::from(concat!(
314                "[",
315                "  Direction: Horizontal,",
316                "  Size: 50%,",
317                "  [",
318                "    List: 80%, Preview: 20%,",
319                "  ],",
320                "  [",
321                "    Direction: Vertical,",
322                "    Done: 60%,",
323                "    [",
324                "      Contexts: 50%,",
325                "      Projects: 50%,",
326                "    ],",
327                "  ],",
328                "]",
329            )),
330            paste_behavior: Default::default(),
331            enable_mouse: true,
332        }
333    }
334}
335
336#[derive(Conf, Clone, Debug, PartialEq, Eq)]
337pub struct WidgetBaseConfig {
338    /// Keybindings configured for interacting with tasks.
339    #[arg(short = 'T')]
340    pub tasks_keybind: EventHandlerUI,
341    /// Keybindings configured for interacting with categories.
342    #[arg(short = 'C')]
343    pub category_keybind: EventHandlerUI,
344    /// The type of border style to use for the UI widgets.
345    pub border_type: WidgetBorderType,
346    /// The title label displayed on the pending tasks widget border.
347    pub pending_widget_name: String,
348    /// The title label displayed on the done tasks widget border.
349    pub done_widget_name: String,
350    /// The title label displayed on the projects category widget border.
351    pub project_widget_name: String,
352    /// The title label displayed on the contexts category widget border.
353    pub context_widget_name: String,
354    /// The title label displayed on the hashtags category widget border.
355    pub hashtag_widget_name: String,
356    /// The title label displayed on the preview widget border.
357    pub preview_widget_name: String,
358    /// The title label displayed on the pending live preview widget border.
359    pub pending_live_preview_widget_name: String,
360    /// The title label displayed on the done live preview widget border.
361    pub done_live_preview_widget_name: String,
362}
363
364impl Default for WidgetBaseConfig {
365    fn default() -> Self {
366        Self {
367            tasks_keybind: EventHandlerUI::from([
368                (
369                    KeyShortcut::new(KeyCode::Char('u'), KeyModifiers::SHIFT),
370                    UIEvent::SwapUpItem,
371                ),
372                (
373                    KeyShortcut::new(KeyCode::Char('d'), KeyModifiers::SHIFT),
374                    UIEvent::SwapDownItem,
375                ),
376                (KeyShortcut::from(KeyCode::Char('x')), UIEvent::RemoveItem),
377                (KeyShortcut::from(KeyCode::Char('d')), UIEvent::MoveItem),
378                (KeyShortcut::from(KeyCode::Enter), UIEvent::Select),
379                (KeyShortcut::from(KeyCode::Char('n')), UIEvent::NextSearch),
380                (
381                    KeyShortcut::new(KeyCode::Char('n'), KeyModifiers::SHIFT),
382                    UIEvent::PrevSearch,
383                ),
384            ]),
385            category_keybind: EventHandlerUI::from([
386                (KeyShortcut::from(KeyCode::Enter), UIEvent::Select),
387                (KeyShortcut::from(KeyCode::Backspace), UIEvent::Remove),
388                (KeyShortcut::from(KeyCode::Char('n')), UIEvent::NextSearch),
389                (
390                    KeyShortcut::new(KeyCode::Char('n'), KeyModifiers::SHIFT),
391                    UIEvent::PrevSearch,
392                ),
393            ]),
394            border_type: WidgetBorderType::default(),
395            pending_widget_name: String::from("list"),
396            done_widget_name: String::from("done"),
397            project_widget_name: String::from("project"),
398            context_widget_name: String::from("context"),
399            hashtag_widget_name: String::from("hashtag"),
400            preview_widget_name: String::from("preview"),
401            pending_live_preview_widget_name: String::from("pending live preview"),
402            done_live_preview_widget_name: String::from("done live preview"),
403        }
404    }
405}
406
407#[derive(Conf, Clone, Debug, PartialEq, Eq)]
408pub struct Styles {
409    /// Defines the color used to highlight the active window.
410    pub active_color: Color,
411    /// A list of text styles applied to tasks based on their priority levels.
412    pub priority_style: TextStyleList,
413    /// Specifies the text style used for displaying projects within task lists.
414    pub projects_style: TextStyle,
415    /// Specifies the text style used for displaying contexts (e.g., @home, @work)
416    /// within task lists.
417    pub contexts_style: TextStyle,
418    /// Specifies the text style used for displaying hashtags within task lists.
419    /// Note: This style is overridden by custom styles defined for specific categories.
420    pub hashtags_style: TextStyle,
421    /// Defines the default text style for displaying projects, contexts,
422    /// and hashtags within task lists.
423    /// Note: This style is overridden by specific styles for individual categories.
424    pub category_style: TextStyle,
425    /// Specifies the text style applied to categories when they are selected for filtering.
426    pub category_select_style: TextStyle,
427    /// Specifies the text style applied to categories that are filtered out from the view.
428    pub category_remove_style: TextStyle,
429    /// Allows custom text styles to be applied to specific categories by name.
430    /// Note: Custom styles defined here will override all other category-specific styles,
431    /// including `category_style`, `category_select_style`, and `category_remove_style`.
432    pub custom_category_style: CustomCategoryStyle,
433    /// Specifies the text style used to highlight elements that match a search
434    /// within lists.
435    pub highlight: TextStyle,
436}
437
438impl Styles {
439    /// Retrieves the text style for a specified category. If a custom style
440    /// has been defined for the category, it will be used; otherwise,
441    /// the base style for that category is employed.
442    pub fn get_category_style(&self, category: &str) -> TextStyle {
443        match self.custom_category_style.get(category) {
444            Some(style) => *style,
445            None => self.get_category_base_style(category),
446        }
447    }
448
449    /// Retrieves the base style for a specified category based on its initial
450    /// character: '+' for projects, '@' for contexts, and '#' for hashtags.
451    /// If the category does not match any of these prefixes, it defaults
452    /// to the general `category_style`.
453    fn get_category_base_style(&self, category: &str) -> TextStyle {
454        match category.chars().next().unwrap() {
455            '+' => self.category_style.combine(&self.projects_style),
456            '@' => self.category_style.combine(&self.contexts_style),
457            '#' => self.category_style.combine(&self.hashtags_style),
458            _ => self.category_style,
459        }
460    }
461}
462
463impl Default for Styles {
464    fn default() -> Self {
465        let mut custom_category_style = CustomCategoryStyle::default();
466        custom_category_style.insert(
467            String::from("+todo-tui"),
468            TextStyle::default().fg(Color::lightblue()),
469        );
470        Self {
471            active_color: Color(tuiColor::Red),
472            priority_style: TextStyleList::default(),
473            projects_style: TextStyle::default(),
474            contexts_style: TextStyle::default(),
475            hashtags_style: TextStyle::default(),
476            category_style: TextStyle::default(),
477            category_select_style: TextStyle::default().fg(Color::green()),
478            category_remove_style: TextStyle::default().fg(Color::red()),
479            custom_category_style,
480            highlight: TextStyle::default().bg(Color::yellow()),
481        }
482    }
483}
484
485#[derive(Conf, Clone, Debug, PartialEq, Eq, Default)]
486pub struct HookPaths {
487    /// Path to the script executed before creating a new task.
488    /// If none, no action is taken before a new task is created.
489    pub pre_new_task: Option<PathBuf>,
490    /// Path to the script executed after creating a new task.
491    /// If none, no action is taken after a new task is created.
492    pub post_new_task: Option<PathBuf>,
493    /// Path to the script executed before removing a task.
494    /// If none, no action is taken before a task is removed.
495    pub pre_remove_task: Option<PathBuf>,
496    /// Path to the script executed after removing a task.
497    /// If none, no action is taken after a task is removed.
498    pub post_remove_task: Option<PathBuf>,
499    /// Path to the script executed before moving a task.
500    /// If none, no action is taken before a task is moved.
501    pub pre_move_task: Option<PathBuf>,
502    /// Path to the script executed after moving a task.
503    /// If none, no action is taken after a task is moved.
504    pub post_move_task: Option<PathBuf>,
505    /// Path to the script executed before updating a task.
506    /// If none, no action is taken before a task is updated.
507    pub pre_update_task: Option<PathBuf>,
508    /// Path to the script executed after updating a task.
509    /// If none, no action is taken after a task is updated.
510    pub post_update_task: Option<PathBuf>,
511}
512
513#[derive(ConfMerge, Default, Debug, PartialEq, Eq)]
514#[command(author, version, about, long_about = None)]
515#[export_option(Export)]
516pub struct Config {
517    pub ui_config: UiConfig,
518    pub todo_config: ToDoConfig,
519    pub file_worker_config: FileWorkerConfig,
520    pub widget_base_config: WidgetBaseConfig,
521    pub list_config: ListConfig,
522    pub preview_config: PreviewConfig,
523    pub active_color_config: ActiveColorConfig,
524    pub styles: Styles,
525    pub hook_paths: HookPaths,
526}
527
528impl Config {
529    pub fn config_folder() -> PathBuf {
530        match var("XDG_CONFIG_HOME") {
531            Ok(config_path) => PathBuf::from(config_path),
532            Err(_) => PathBuf::from(var("HOME").unwrap_or(String::from("~"))).join(".config"),
533        }
534        .join(env!("CARGO_PKG_NAME"))
535    }
536}
537
538impl ConfigDefaults for Config {
539    fn config_path() -> PathBuf {
540        Self::config_folder().join(concat!(env!("CARGO_PKG_NAME"), ".toml"))
541    }
542
543    fn help_colors() -> clap::builder::Styles {
544        clap::builder::Styles::styled()
545            .usage(AnsiColor::Green.on_default().bold())
546            .literal(AnsiColor::Cyan.on_default().bold())
547            .header(AnsiColor::Green.on_default().bold())
548            .invalid(AnsiColor::Yellow.on_default())
549            .error(AnsiColor::Red.on_default().bold())
550            .valid(AnsiColor::Green.on_default())
551            .placeholder(AnsiColor::Cyan.on_default())
552    }
553}
554
555#[cfg(test)]
556mod tests {
557    use self::parsers::*;
558    use super::*;
559    use anyhow::Result;
560    use pretty_assertions::assert_eq;
561    use std::{path::PathBuf, time::Duration};
562    use test_log::test;
563
564    pub fn get_test_dir() -> String {
565        var("TODO_TUI_TEST_DIR").unwrap()
566    }
567
568    pub fn get_test_file(name: &str) -> PathBuf {
569        let path = PathBuf::from(get_test_dir()).join(name);
570        log::trace!("Get test file {path:?}");
571        path
572    }
573
574    #[test]
575    fn test_deserialization() {
576        let deserialized = Config::from_reader(
577            r#"
578            active_color = "Green"
579            init_widget = "Done"
580        "#
581            .as_bytes(),
582        )
583        .unwrap();
584
585        assert_eq!(*deserialized.styles.active_color, tuiColor::Green);
586        assert_eq!(deserialized.ui_config.init_widget, WidgetType::Done);
587        assert_eq!(
588            deserialized.ui_config.window_title,
589            UiConfig::default().window_title
590        );
591    }
592
593    #[test]
594    fn get_active_style() {
595        {
596            let color = ActiveColorConfig {
597                list_active_color: TextStyle::default().bg(Color::red()),
598                pending_active_color: TextStyle::default().bg(Color::yellow()),
599                ..Default::default()
600            };
601            assert_eq!(
602                color.get_active_style(&ToDoData::Pending),
603                TextStyle::default().bg(Color::yellow())
604            );
605        }
606
607        {
608            let color = ActiveColorConfig {
609                list_active_color: TextStyle::default().bg(Color::red()),
610                ..Default::default()
611            };
612            assert_eq!(
613                color.get_active_style(&ToDoData::Pending),
614                TextStyle::default().bg(Color::red())
615            );
616        }
617
618        {
619            let color = ActiveColorConfig {
620                list_active_color: TextStyle::default().bg(Color::green()).fg(Color::blue()),
621                done_active_color: TextStyle::default()
622                    .fg(Color::black())
623                    .modifier(TextModifier::Bold),
624                ..Default::default()
625            };
626            assert_eq!(
627                color.get_active_style(&ToDoData::Done),
628                TextStyle::default()
629                    .bg(Color::green())
630                    .fg(Color::black())
631                    .modifier(TextModifier::Bold)
632            );
633        }
634    }
635
636    #[test]
637    fn get_active_config_style() {
638        let color = ActiveColorConfig {
639            list_active_color: TextStyle::default().bg(Color::red()),
640            category_active_color: TextStyle::default().fg(Color::white()),
641            ..Default::default()
642        };
643        assert_eq!(
644            color.get_active_config_style(&ToDoCategory::Projects),
645            TextStyle::default().bg(Color::red()).fg(Color::white())
646        );
647    }
648
649    #[test]
650    fn test_load() -> Result<()> {
651        let s = r#"
652        active_color = "Blue"
653        window_title = "Title"
654        todo_path = "path to todo file"
655        "#;
656
657        let default = Config::default();
658        let c = Config::from_reader(s.as_bytes())?;
659        assert_eq!(*c.styles.active_color, tuiColor::Blue);
660        assert_eq!(c.ui_config.init_widget, default.ui_config.init_widget);
661        assert_eq!(c.ui_config.window_title, String::from("Title"));
662        assert_eq!(
663            c.file_worker_config.todo_path,
664            PathBuf::from("path to todo file")
665        );
666        assert_eq!(c.file_worker_config.archive_path, None);
667
668        Ok(())
669    }
670
671    #[test]
672    fn help_can_be_generated() -> Result<()> {
673        Config::from_args(Vec::<&str>::new())?;
674        Ok(())
675    }
676
677    #[test]
678    fn test_parse_duration() {
679        assert_eq!(parse_duration("1000"), Ok(Duration::from_secs(1000)));
680        assert!(parse_duration("-1000").is_err());
681    }
682
683    #[test]
684    fn empty_config() -> Result<()> {
685        let empty_config = get_test_file("empty_config.toml");
686        let default = Config::from_file(empty_config)?;
687        assert_eq!(default, Config::default());
688
689        Ok(())
690    }
691
692    #[test]
693    fn changed_config() -> Result<()> {
694        let testing_config = get_test_file("testing_config.toml");
695        let config = Config::from_file(testing_config)?;
696        let mut expected = Config::default();
697        expected.styles.active_color = Color::blue();
698        expected.ui_config.init_widget = WidgetType::Project;
699        expected.ui_config.window_title = String::from("Window title");
700        expected.ui_config.layout = String::from("Short invalid layout");
701        expected.file_worker_config.todo_path = PathBuf::from("invalid/path/to/todo.txt");
702        expected.file_worker_config.archive_path =
703            Some(PathBuf::from("invalid/path/to/archive.txt"));
704        expected.file_worker_config.file_watcher = false;
705        expected.list_config.list_shift = 0;
706        expected.todo_config.use_done = true;
707        expected.todo_config.pending_sort = vec![TaskSort::Priority];
708        expected.todo_config.done_sort = vec![TaskSort::Reverse];
709        expected.todo_config.delete_final_date = false;
710        expected.todo_config.set_final_date = SetFinalDateType::Never;
711        expected.preview_config.preview_format = String::from("unimportant preview");
712        expected.preview_config.wrap_preview = false;
713        expected.ui_config.window_keybinds = EventHandlerUI::from([
714            (KeyShortcut::from(KeyCode::Char('e')), UIEvent::EditMode),
715            (KeyShortcut::from(KeyCode::Char('q')), UIEvent::Quit),
716            (
717                KeyShortcut::new(KeyCode::Char('s'), KeyModifiers::SHIFT),
718                UIEvent::Save,
719            ),
720            (KeyShortcut::from(KeyCode::Char('u')), UIEvent::Load),
721            (
722                KeyShortcut::new(KeyCode::Char('h'), KeyModifiers::SHIFT),
723                UIEvent::MoveLeft,
724            ),
725            (
726                KeyShortcut::new(KeyCode::Char('l'), KeyModifiers::SHIFT),
727                UIEvent::MoveRight,
728            ),
729            (
730                KeyShortcut::new(KeyCode::Char('k'), KeyModifiers::SHIFT),
731                UIEvent::MoveUp,
732            ),
733            (
734                KeyShortcut::new(KeyCode::Char('j'), KeyModifiers::SHIFT),
735                UIEvent::MoveDown,
736            ),
737            (
738                KeyShortcut::new(KeyCode::Char('i'), KeyModifiers::SHIFT),
739                UIEvent::InsertMode,
740            ),
741            (
742                KeyShortcut::new(KeyCode::Char('e'), KeyModifiers::SHIFT),
743                UIEvent::EditMode,
744            ),
745            (KeyShortcut::from(KeyCode::Char('/')), UIEvent::SearchMode),
746            (KeyShortcut::from(KeyCode::Char('?')), UIEvent::ShowHelp),
747        ]);
748        expected.ui_config.list_refresh_rate = Duration::from_secs(10);
749        expected.active_color_config.list_active_color = TextStyle::default().bg(Color::green());
750        expected.file_worker_config.autosave_duration = Duration::from_secs(100);
751        expected.list_config.list_keybind = EventHandlerUI::from([
752            (KeyShortcut::from(KeyCode::Char('g')), UIEvent::ListLast),
753            (KeyShortcut::from(KeyCode::Char('j')), UIEvent::ListDown),
754            (KeyShortcut::from(KeyCode::Char('k')), UIEvent::ListUp),
755            (
756                KeyShortcut::new(KeyCode::Char('g'), KeyModifiers::SHIFT),
757                UIEvent::ListLast,
758            ),
759            (KeyShortcut::from(KeyCode::Char('h')), UIEvent::CleanSearch),
760        ]);
761        expected.widget_base_config.tasks_keybind = EventHandlerUI::from([
762            (KeyShortcut::from(KeyCode::Char('s')), UIEvent::Select),
763            (
764                KeyShortcut::new(KeyCode::Char('u'), KeyModifiers::SHIFT),
765                UIEvent::SwapUpItem,
766            ),
767            (
768                KeyShortcut::new(KeyCode::Char('d'), KeyModifiers::SHIFT),
769                UIEvent::SwapDownItem,
770            ),
771            (KeyShortcut::from(KeyCode::Char('x')), UIEvent::RemoveItem),
772            (KeyShortcut::from(KeyCode::Char('d')), UIEvent::MoveItem),
773            (KeyShortcut::from(KeyCode::Enter), UIEvent::Select),
774            (KeyShortcut::from(KeyCode::Char('n')), UIEvent::NextSearch),
775            (
776                KeyShortcut::new(KeyCode::Char('n'), KeyModifiers::SHIFT),
777                UIEvent::PrevSearch,
778            ),
779        ]);
780        expected.widget_base_config.category_keybind = EventHandlerUI::from([
781            (KeyShortcut::from(KeyCode::Char('r')), UIEvent::Remove),
782            (KeyShortcut::from(KeyCode::Enter), UIEvent::Select),
783            (KeyShortcut::from(KeyCode::Backspace), UIEvent::Remove),
784            (KeyShortcut::from(KeyCode::Char('n')), UIEvent::NextSearch),
785            (
786                KeyShortcut::new(KeyCode::Char('n'), KeyModifiers::SHIFT),
787                UIEvent::PrevSearch,
788            ),
789        ]);
790        expected.styles.category_select_style = TextStyle::default().fg(Color::red());
791        expected.styles.category_remove_style = TextStyle::default().fg(Color::green());
792        expected.styles.custom_category_style = CustomCategoryStyle::default();
793        expected.styles.custom_category_style.insert(
794            String::from("+project"),
795            TextStyle::default().fg(Color::green()),
796        );
797
798        assert_eq!(config.ui_config, expected.ui_config);
799        assert_eq!(config.todo_config, expected.todo_config);
800        assert_eq!(config.file_worker_config, expected.file_worker_config);
801        assert_eq!(config.widget_base_config, expected.widget_base_config);
802        assert_eq!(config.list_config, expected.list_config);
803        assert_eq!(config.preview_config, expected.preview_config);
804        assert_eq!(config.active_color_config, expected.active_color_config);
805        assert_eq!(config.styles, expected.styles);
806
807        Ok(())
808    }
809
810    #[test]
811    fn default_values_clap() -> Result<()> {
812        let empty_config = get_test_file("empty_config.toml");
813        let default = Config::from_args(vec![
814            "NAME",
815            "--config-path",
816            empty_config.to_str().unwrap(),
817        ])?;
818        assert_eq!(default, Config::default());
819        Ok(())
820    }
821
822    #[test]
823    fn custom_clap_arguments() -> Result<()> {
824        let testing_config = get_test_file("testing_config.toml");
825        let config = Config::from_args(vec![
826            "NAME",
827            "--config-path",
828            testing_config.to_str().unwrap(),
829            "--active-color",
830            "Green",
831            "--window-title",
832            "New window title",
833            "--layout",
834            "Shorter layout",
835            "--todo-path",
836            "todo.txt",
837            "--archive-path",
838            "archive.txt",
839            "--file-watcher",
840            "true",
841            "--list-shift",
842            "10",
843            "--pending-sort",
844            "reverse",
845            "alphanumeric",
846            "--done-sort",
847            "priority",
848            "--delete-final-date",
849            "true",
850            "--set-final-date",
851            "override",
852            "--preview-format",
853            "extra important preview",
854            "--wrap-preview",
855            "true",
856            "--list-refresh-rate",
857            "15",
858            "--list-active-color",
859            "yellow ^blue",
860            "--autosave-duration",
861            "150",
862            "--category-select-style",
863            "blue",
864            "--category-remove-style",
865            "yellow",
866        ])?;
867        let mut expected = Config::default();
868        expected.styles.active_color = Color::green();
869        expected.ui_config.init_widget = WidgetType::Project;
870        expected.ui_config.window_title = String::from("New window title");
871        expected.ui_config.layout = String::from("Shorter layout");
872        expected.file_worker_config.todo_path = PathBuf::from("todo.txt");
873        expected.file_worker_config.archive_path = Some(PathBuf::from("archive.txt"));
874        expected.file_worker_config.file_watcher = true;
875        expected.list_config.list_shift = 10;
876        expected.todo_config.use_done = true;
877        expected.todo_config.pending_sort = vec![TaskSort::Reverse, TaskSort::Alphanumeric];
878        expected.todo_config.done_sort = vec![TaskSort::Priority];
879        expected.todo_config.delete_final_date = true;
880        expected.todo_config.set_final_date = SetFinalDateType::Override;
881        expected.preview_config.preview_format = String::from("extra important preview");
882        expected.preview_config.wrap_preview = true;
883        expected.ui_config.window_keybinds = EventHandlerUI::from([
884            (KeyShortcut::from(KeyCode::Char('e')), UIEvent::EditMode),
885            (KeyShortcut::from(KeyCode::Char('q')), UIEvent::Quit),
886            (
887                KeyShortcut::new(KeyCode::Char('s'), KeyModifiers::SHIFT),
888                UIEvent::Save,
889            ),
890            (KeyShortcut::from(KeyCode::Char('u')), UIEvent::Load),
891            (
892                KeyShortcut::new(KeyCode::Char('h'), KeyModifiers::SHIFT),
893                UIEvent::MoveLeft,
894            ),
895            (
896                KeyShortcut::new(KeyCode::Char('l'), KeyModifiers::SHIFT),
897                UIEvent::MoveRight,
898            ),
899            (
900                KeyShortcut::new(KeyCode::Char('k'), KeyModifiers::SHIFT),
901                UIEvent::MoveUp,
902            ),
903            (
904                KeyShortcut::new(KeyCode::Char('j'), KeyModifiers::SHIFT),
905                UIEvent::MoveDown,
906            ),
907            (
908                KeyShortcut::new(KeyCode::Char('i'), KeyModifiers::SHIFT),
909                UIEvent::InsertMode,
910            ),
911            (
912                KeyShortcut::new(KeyCode::Char('e'), KeyModifiers::SHIFT),
913                UIEvent::EditMode,
914            ),
915            (KeyShortcut::from(KeyCode::Char('/')), UIEvent::SearchMode),
916            (KeyShortcut::from(KeyCode::Char('?')), UIEvent::ShowHelp),
917        ]);
918        expected.ui_config.list_refresh_rate = Duration::from_secs(15);
919        expected.active_color_config.list_active_color =
920            TextStyle::default().bg(Color::blue()).fg(Color::yellow());
921        expected.file_worker_config.autosave_duration = Duration::from_secs(150);
922        expected.list_config.list_keybind = EventHandlerUI::from([
923            (KeyShortcut::from(KeyCode::Char('g')), UIEvent::ListLast),
924            (KeyShortcut::from(KeyCode::Char('j')), UIEvent::ListDown),
925            (KeyShortcut::from(KeyCode::Char('k')), UIEvent::ListUp),
926            (
927                KeyShortcut::new(KeyCode::Char('g'), KeyModifiers::SHIFT),
928                UIEvent::ListLast,
929            ),
930            (KeyShortcut::from(KeyCode::Char('h')), UIEvent::CleanSearch),
931        ]);
932        expected.widget_base_config.tasks_keybind = EventHandlerUI::from([
933            (KeyShortcut::from(KeyCode::Char('s')), UIEvent::Select),
934            (
935                KeyShortcut::new(KeyCode::Char('u'), KeyModifiers::SHIFT),
936                UIEvent::SwapUpItem,
937            ),
938            (
939                KeyShortcut::new(KeyCode::Char('d'), KeyModifiers::SHIFT),
940                UIEvent::SwapDownItem,
941            ),
942            (KeyShortcut::from(KeyCode::Char('x')), UIEvent::RemoveItem),
943            (KeyShortcut::from(KeyCode::Char('d')), UIEvent::MoveItem),
944            (KeyShortcut::from(KeyCode::Enter), UIEvent::Select),
945            (KeyShortcut::from(KeyCode::Char('n')), UIEvent::NextSearch),
946            (
947                KeyShortcut::new(KeyCode::Char('n'), KeyModifiers::SHIFT),
948                UIEvent::PrevSearch,
949            ),
950        ]);
951        expected.widget_base_config.category_keybind = EventHandlerUI::from([
952            (KeyShortcut::from(KeyCode::Char('r')), UIEvent::Remove),
953            (KeyShortcut::from(KeyCode::Enter), UIEvent::Select),
954            (KeyShortcut::from(KeyCode::Backspace), UIEvent::Remove),
955            (KeyShortcut::from(KeyCode::Char('n')), UIEvent::NextSearch),
956            (
957                KeyShortcut::new(KeyCode::Char('n'), KeyModifiers::SHIFT),
958                UIEvent::PrevSearch,
959            ),
960        ]);
961        expected.styles.category_select_style = TextStyle::default().fg(Color::blue());
962        expected.styles.category_remove_style = TextStyle::default().fg(Color::yellow());
963        let mut custom_styles = CustomCategoryStyle::default();
964        custom_styles.insert(
965            String::from("+project"),
966            TextStyle::default().fg(Color::green()),
967        );
968        expected.styles.custom_category_style = custom_styles;
969
970        assert_eq!(config, expected);
971
972        Ok(())
973    }
974
975    #[test]
976    #[cfg(unix)]
977    fn export_default_is_possible() -> Result<()> {
978        Config::export_default(PathBuf::from("/dev/null"))?;
979        Ok(())
980    }
981}