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#[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
40impl Default for TaskMarkerConfig {
42 fn default() -> Self {
43 Self {
44 done: 'x',
45 todo: ' ',
46 incomplete: '/',
47 canceled: '-',
48 }
49 }
50}
51
52#[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#[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 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 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) = ¶ms.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
318fn project_directory() -> Option<ProjectDirs> {
320 ProjectDirs::from("com", "kdheepak", APP_NAME)
321}