Skip to main content

vault_tasks_core/
config.rs

1#![allow(dead_code)]
2use std::{
3    env,
4    fs::{File, create_dir_all},
5    io::Write,
6    path::PathBuf,
7};
8
9use color_eyre::{Result, eyre::bail};
10use directories::ProjectDirs;
11use lazy_static::lazy_static;
12use serde::Deserialize;
13use tracing::{debug, info};
14
15const CONFIG: &str = include_str!("../.config/core.toml");
16const CONFIG_FILE_NAME: &str = "core";
17const APP_NAME: &str = "vault-tasks";
18
19lazy_static! {
20    pub(crate) static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase();
21    pub(crate) static ref DATA_FOLDER: Option<PathBuf> =
22        env::var(format!("{}_DATA", PROJECT_NAME.clone()))
23            .ok()
24            .map(PathBuf::from);
25    pub(crate) static ref CONFIG_FOLDER: Option<PathBuf> =
26        env::var(format!("{}_CONFIG", PROJECT_NAME.clone()))
27            .ok()
28            .map(PathBuf::from);
29}
30
31/// Characters used to mark the state of a task in Markdown.
32#[derive(Clone, Debug, Deserialize)]
33pub(crate) struct TaskMarkerConfig {
34    pub done: char,
35    pub todo: char,
36    pub incomplete: char,
37    pub canceled: char,
38}
39
40// Mostly for tests
41impl Default for TaskMarkerConfig {
42    fn default() -> Self {
43        Self {
44            done: 'x',
45            todo: ' ',
46            incomplete: '/',
47            canceled: '-',
48        }
49    }
50}
51
52/// Configuration for pretty symbols in the UI.
53#[derive(Clone, Debug, Deserialize, Default)]
54pub struct PrettySymbolsConfig {
55    #[serde(default)]
56    pub(crate) task_done: String,
57    #[serde(default)]
58    pub(crate) task_todo: String,
59    #[serde(default)]
60    pub(crate) task_incomplete: String,
61    #[serde(default)]
62    pub(crate) task_canceled: String,
63    #[serde(default)]
64    pub(crate) due_date: String,
65    #[serde(default)]
66    pub(crate) priority: String,
67    #[serde(default)]
68    pub(crate) today_tag: String,
69    #[serde(default)]
70    pub(crate) progress_bar_true: String,
71    #[serde(default)]
72    pub(crate) progress_bar_false: String,
73}
74/// Configuration for the core features of the application.
75#[derive(Clone, Debug, Deserialize, Default)]
76pub struct CoreConfig {
77    #[serde(default)]
78    pub vault_path: PathBuf,
79    #[serde(default)]
80    pub use_american_format: bool,
81    #[serde(default)]
82    pub(crate) parse_dot_files: bool,
83    #[serde(default)]
84    pub(crate) file_tags_propagation: bool,
85    #[serde(default)]
86    pub(crate) ignored: Vec<PathBuf>,
87    #[serde(default)]
88    pub(crate) indent_length: usize,
89    #[serde(default)]
90    pub(crate) tasks_drop_file: Option<PathBuf>,
91}
92#[derive(Clone, Debug, Deserialize, Default)]
93pub struct DisplayConfig {
94    #[serde(default)]
95    pub show_relative_due_dates: bool,
96}
97#[derive(Clone, Debug, Deserialize)]
98pub struct TasksConfig {
99    #[serde(default)]
100    pub core: CoreConfig,
101    #[serde(default)]
102    pub display: DisplayConfig,
103    #[serde(default)]
104    pub pretty_symbols: PrettySymbolsConfig,
105    #[serde(default)]
106    pub(crate) task_state_markers: TaskMarkerConfig,
107}
108
109impl Default for TasksConfig {
110    fn default() -> Self {
111        let mut config: Self = toml::from_str(CONFIG).unwrap();
112        if cfg!(test) {
113            config.core.vault_path = PathBuf::from("./test-vault");
114        }
115        config
116    }
117}
118pub struct ProtoConfig {
119    pub vault_path: Option<PathBuf>,
120    pub config_path: Option<PathBuf>,
121}
122impl TasksConfig {
123    pub fn new(params: &ProtoConfig) -> Result<Self> {
124        let default_config: Self = Self::default();
125        let data_dir = get_data_dir();
126        let config_path = params.config_path.clone().unwrap_or_else(get_config_dir);
127        debug!(
128            "Using data directory at {} and config directory at {}",
129            data_dir.display(),
130            config_path.display()
131        );
132
133        // A config file was provided
134        let builder = if config_path.is_file() {
135            config::Config::builder()
136                .set_default("data_dir", data_dir.to_str().unwrap())?
137                .add_source(config::File::from(config_path))
138        } else {
139            let mut builder = config::Config::builder()
140                .set_default("data_dir", data_dir.to_str().unwrap())?
141                .set_default("config_dir", config_path.to_str().unwrap())?;
142
143            let config_files = [
144                (
145                    format!("{CONFIG_FILE_NAME}.json5"),
146                    config::FileFormat::Json5,
147                ),
148                (format!("{CONFIG_FILE_NAME}.json"), config::FileFormat::Json),
149                (format!("{CONFIG_FILE_NAME}.yaml"), config::FileFormat::Yaml),
150                (format!("{CONFIG_FILE_NAME}.toml"), config::FileFormat::Toml),
151                (format!("{CONFIG_FILE_NAME}.ini"), config::FileFormat::Ini),
152            ];
153            let mut found_config = false;
154            for (file, format) in &config_files {
155                let source = config::File::from(config_path.join(file))
156                    .format(*format)
157                    .required(false);
158                builder = builder.add_source(source);
159                if config_path.join(file).exists() {
160                    found_config = true;
161                }
162            }
163            // TODO: clean up this error
164            if !found_config && !cfg!(test) {
165                info!(
166                    "No configuration file found.\nCreate one at {config_path:?} or generate one using `vault-tasks generate-config`"
167                );
168            }
169            builder
170        };
171
172        let mut cfg: Self = builder.build()?.try_deserialize()?;
173
174        cfg = Self::merge_tasks_config(cfg, default_config);
175
176        if let Some(path) = &params.vault_path {
177            cfg.core.vault_path.clone_from(path);
178        }
179
180        Ok(cfg)
181    }
182
183    fn merge_tasks_config(user_config: TasksConfig, default_config: TasksConfig) -> TasksConfig {
184        TasksConfig {
185            core: CoreConfig {
186                parse_dot_files: user_config.core.parse_dot_files,
187                file_tags_propagation: user_config.core.file_tags_propagation,
188                ignored: if user_config.core.ignored.is_empty() {
189                    default_config.core.ignored
190                } else {
191                    user_config.core.ignored
192                },
193                indent_length: if user_config.core.indent_length == 0 {
194                    default_config.core.indent_length
195                } else {
196                    user_config.core.indent_length
197                },
198                use_american_format: user_config.core.use_american_format,
199
200                vault_path: if user_config.core.vault_path == PathBuf::new() {
201                    default_config.core.vault_path
202                } else {
203                    user_config.core.vault_path
204                },
205                tasks_drop_file: if user_config.core.tasks_drop_file.is_none() {
206                    default_config.core.tasks_drop_file
207                } else {
208                    user_config.core.tasks_drop_file
209                },
210            },
211
212            display: DisplayConfig {
213                show_relative_due_dates: user_config.display.show_relative_due_dates,
214            },
215            task_state_markers: user_config.task_state_markers,
216            pretty_symbols: Self::merge_pretty_symbols_config(
217                user_config.pretty_symbols,
218                default_config.pretty_symbols,
219            ),
220        }
221    }
222
223    fn merge_pretty_symbols_config(
224        user_config: PrettySymbolsConfig,
225        default_config: PrettySymbolsConfig,
226    ) -> PrettySymbolsConfig {
227        PrettySymbolsConfig {
228            task_done: if user_config.task_done.is_empty() {
229                default_config.task_done
230            } else {
231                user_config.task_done
232            },
233            task_todo: if user_config.task_todo.is_empty() {
234                default_config.task_todo
235            } else {
236                user_config.task_todo
237            },
238            task_incomplete: if user_config.task_incomplete.is_empty() {
239                default_config.task_incomplete
240            } else {
241                user_config.task_incomplete
242            },
243            task_canceled: if user_config.task_canceled.is_empty() {
244                default_config.task_canceled
245            } else {
246                user_config.task_canceled
247            },
248            due_date: if user_config.due_date.is_empty() {
249                default_config.due_date
250            } else {
251                user_config.due_date
252            },
253            priority: if user_config.priority.is_empty() {
254                default_config.priority
255            } else {
256                user_config.priority
257            },
258            today_tag: if user_config.today_tag.is_empty() {
259                default_config.today_tag
260            } else {
261                user_config.today_tag
262            },
263            progress_bar_true: if user_config.progress_bar_true.is_empty() {
264                default_config.progress_bar_true
265            } else {
266                user_config.progress_bar_true
267            },
268            progress_bar_false: if user_config.progress_bar_false.is_empty() {
269                default_config.progress_bar_false
270            } else {
271                user_config.progress_bar_false
272            },
273        }
274    }
275
276    pub fn generate_config(path: Option<PathBuf>) -> Result<()> {
277        let config_dir = path.unwrap_or_else(get_config_dir);
278        let dest = config_dir.join(format!("{CONFIG_FILE_NAME}.toml"));
279        if create_dir_all(config_dir).is_err() {
280            bail!("Failed to create config directory at {dest:?}".to_owned());
281        }
282        if let Ok(mut file) = File::create(dest.clone()) {
283            if file.write_all(CONFIG.as_bytes()).is_err() {
284                bail!("Failed to write default config at {dest:?}".to_owned());
285            }
286        } else {
287            bail!("Failed to create default config at {dest:?}".to_owned());
288        }
289        println!(
290            "Configuration has been created at {}. You can fill the `vault-path` value to set a default vault.",
291            dest.display()
292        );
293        Ok(())
294    }
295}
296
297pub(crate) fn get_data_dir() -> PathBuf {
298    DATA_FOLDER.clone().map_or(
299        {
300            project_directory().map_or_else(
301                || PathBuf::from(".").join(".data"),
302                |proj_dirs| proj_dirs.data_local_dir().to_path_buf(),
303            )
304        },
305        |s| s,
306    )
307}
308
309pub(crate) fn get_config_dir() -> PathBuf {
310    CONFIG_FOLDER.clone().unwrap_or_else(|| {
311        project_directory().map_or_else(
312            || PathBuf::from(".").join(".config"),
313            |proj_dirs| proj_dirs.config_local_dir().to_path_buf(),
314        )
315    })
316}
317
318// TODO: change this
319fn project_directory() -> Option<ProjectDirs> {
320    ProjectDirs::from("com", "kdheepak", APP_NAME)
321}