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