tsk_rs/
settings.rs

1use bat::{Input, PrettyPrinter};
2use color_eyre::eyre::{bail, Context, Result};
3use config::Config;
4use directories::ProjectDirs;
5use serde::{Deserialize, Serialize};
6use std::{fmt::Display, fs::create_dir_all, path::PathBuf};
7use thiserror::Error;
8
9/// Errors that can occur during settings handling
10#[derive(Error, Debug, PartialEq, Eq, Clone)]
11pub enum SettingsError {
12    /// Data directory where tasks and notes are stored does not exist
13    #[error("data directory does not exist, and createdir is set to false")]
14    DataDirectoryDoesNotExist,
15}
16
17/// Task spesific settings
18#[derive(Debug, Serialize, Deserialize, Clone)]
19#[serde(default)]
20pub struct TaskSettings {
21    /// If true special tag "hold" is removed from the task when time tracking is started
22    pub autorelease: bool,
23    /// If true and special tag "start" is present when creating a new task then time tracking for
24    /// the task is immediately started.
25    pub starttag: bool,
26    /// If true then special tags are listed along custom tags when listing tasks
27    pub specialvisible: bool,
28    /// If true then when ever task is marked done while running the timetracking (if running) is
29    /// automatically stopped.
30    pub stopondone: bool,
31    /// If true when marking task done the special tags that might be in effect for the task are
32    /// also removed.
33    pub clearpsecialtags: bool,
34}
35
36impl Default for TaskSettings {
37    fn default() -> Self {
38        Self {
39            autorelease: true,
40            starttag: true,
41            specialvisible: true,
42            stopondone: true,
43            clearpsecialtags: true,
44        }
45    }
46}
47
48/// Client binary output settings
49#[derive(Debug, Serialize, Deserialize, Clone)]
50#[serde(default)]
51pub struct OutputSettings {
52    /// Use colored output?
53    pub colors: bool,
54    /// Use grided output?
55    pub grid: bool,
56    /// Show line numbers?
57    pub numbers: bool,
58    /// Display namespace that is active
59    pub namespace: bool,
60    /// If the description of the task is longer than this then truncate the string for output
61    pub descriptionlength: usize,
62    /// Calculates totals and display them in task/note listings
63    pub totals: bool,
64    /// Score multiplier that can be used to adjust the weight given by score algorithm
65    pub scoremultiplier: f64,
66}
67
68impl Default for OutputSettings {
69    fn default() -> Self {
70        Self {
71            colors: true,
72            grid: true,
73            numbers: true,
74            namespace: true,
75            descriptionlength: 60,
76            totals: true,
77            scoremultiplier: 1.0
78        }
79    }
80}
81
82/// Note spesific settings
83#[cfg(feature = "note")]
84#[derive(Debug, Serialize, Deserialize, Clone)]
85#[serde(default)]
86pub struct NoteSettings {
87    /// If true then when note is created for an task the description of the Task is set as
88    /// Markdown title
89    pub description: bool,
90    /// If true then when note is created/edited for a task the current local timestamp is added as
91    /// subheader to the Markdown
92    pub timestamp: bool,
93}
94
95#[cfg(feature = "note")]
96impl Default for NoteSettings {
97    fn default() -> Self {
98        Self {
99            description: true,
100            timestamp: true,
101        }
102    }
103}
104
105/// Settings related to the the data storage path and handling
106#[derive(Debug, Serialize, Deserialize, Clone)]
107#[serde(default)]
108pub struct DataSettings {
109    /// Path under which the task and note files are created. If not spesified system default is
110    /// used.
111    pub path: String,
112    /// If true and the data directory does not exist then the folder is created. If False then
113    /// error is thrown if directory does not exist.
114    pub createdir: bool,
115    /// How many task and note data file backups should be rotated?
116    pub rotate: usize,
117}
118
119impl Default for DataSettings {
120    fn default() -> Self {
121        let proj_dirs = ProjectDirs::from("", "", "tsk-rs").unwrap();
122
123        Self {
124            path: String::from(proj_dirs.data_dir().to_str().unwrap()),
125            createdir: true,
126            rotate: 3,
127        }
128    }
129}
130
131/// Client tool settings
132#[derive(Default, Debug, Serialize, Deserialize, Clone)]
133#[serde(default)]
134pub struct Settings {
135    #[serde(skip_serializing)]
136    /// Namespace is read from environment or from command line. Default namespace is "default" and
137    /// cant be changed with configuration. The namespace is populated to the settings struct
138    /// during runtime only.
139    pub namespace: String,
140    /// Settings related to data storage
141    pub data: DataSettings,
142    #[cfg(feature = "note")]
143    /// Settings related to notes only
144    pub note: NoteSettings,
145    /// Settings related to tasks only
146    pub task: TaskSettings,
147    /// Display/output settings
148    pub output: OutputSettings,
149}
150
151impl AsRef<Settings> for Settings {
152    fn as_ref(&self) -> &Settings {
153        self
154    }
155}
156
157impl Display for Settings {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        write!(f, "{}", toml::to_string(&self).unwrap())
160    }
161}
162
163impl Settings {
164    /// Create new settings struct by creating defaults and overwriting them from either config
165    /// files or environment variables.
166    pub fn new(namespace: Option<String>, config_file: &str) -> Result<Self> {
167        let settings: Settings = Config::builder()
168            .set_override_option("namespace", namespace)?
169            .add_source(config::File::with_name(config_file).required(false))
170            .add_source(
171                config::Environment::with_prefix("TSK")
172                    .try_parsing(true)
173                    .separator("_"),
174            )
175            .build()
176            .with_context(|| "while reading configuration")?
177            .try_deserialize()
178            .with_context(|| "while applying defaults to configuration")?;
179
180        Ok(settings)
181    }
182
183    /// Returns the base database path where the task and notes files are stored in their own
184    /// subfolders.
185    pub fn db_pathbuf(&self) -> Result<PathBuf> {
186        let pathbuf = PathBuf::from(&self.data.path).join(&self.namespace);
187        if !pathbuf.is_dir() && self.data.createdir {
188            create_dir_all(&pathbuf).with_context(|| "while creating data directory")?;
189        } else if !pathbuf.is_dir() && !self.data.createdir {
190            bail!(SettingsError::DataDirectoryDoesNotExist);
191        }
192        Ok(pathbuf)
193    }
194
195    /// Return the subpath where task files are stored in under the dbpath
196    pub fn task_db_pathbuf(&self) -> Result<PathBuf> {
197        let pathbuf = &self.db_pathbuf()?.join("tasks");
198        if !pathbuf.is_dir() && self.data.createdir {
199            create_dir_all(&pathbuf).with_context(|| "while creating tasks data directory")?;
200        } else if !pathbuf.is_dir() && !self.data.createdir {
201            bail!(SettingsError::DataDirectoryDoesNotExist);
202        }
203        Ok(pathbuf.to_path_buf())
204    }
205
206    /// Return the subpath where note files are stored in under the dbpath
207    #[cfg(feature = "note")]
208    pub fn note_db_pathbuf(&self) -> Result<PathBuf> {
209        let pathbuf = &self.db_pathbuf()?.join("notes");
210        if !pathbuf.is_dir() && self.data.createdir {
211            create_dir_all(&pathbuf).with_context(|| "while creating notes data directory")?;
212        } else if !pathbuf.is_dir() && !self.data.createdir {
213            bail!(SettingsError::DataDirectoryDoesNotExist);
214        }
215        Ok(pathbuf.to_path_buf())
216    }
217}
218
219/// Show active configuration. Uses Bat.
220pub fn show_config(settings: &Settings) -> Result<()> {
221    let settings_toml = format!("{}", settings);
222    PrettyPrinter::new()
223        .language("toml")
224        .input(Input::from_bytes(settings_toml.as_bytes()))
225        .colored_output(settings.output.colors)
226        .grid(settings.output.grid)
227        .line_numbers(settings.output.numbers)
228        .print()
229        .with_context(|| "while trying to prettyprint yaml")?;
230
231    Ok(())
232}
233
234/// Returns default configuration path if none is configured at env or via the command line
235pub fn default_config() -> String {
236    let proj_dirs = ProjectDirs::from("", "", "tsk-rs").unwrap();
237    proj_dirs
238        .config_dir()
239        .join("tsk.toml")
240        .to_str()
241        .unwrap()
242        .to_owned()
243}
244
245// eof