tiempo-rs/src/config.rs

212 lines
6.8 KiB
Rust

use std::env;
use std::path::{Path, PathBuf};
use std::fs::{File, create_dir_all};
use std::io::{Read, Write};
use directories::{UserDirs, ProjectDirs};
use serde::{Serialize, Deserialize};
use toml::to_string;
use crate::{error::{Result, Error::*}, formatters::Formatter};
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub enum WeekDay {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
/// Absolute path to the sqlite database
pub database_file: PathBuf, // "/home/user/.timetrap.db"
/// The duration of time to use for rounding with the -r flag
pub round_in_seconds: u32, // 900
/// delimiter used when appending notes via t edit --append
pub append_notes_delimiter: String, //" "
/// an array of directories to search for user defined fomatter classes
pub formatter_search_paths: Vec<PathBuf>, //- "/home/user/.timetrap/formatters"
/// The format to use when display is invoked without a --format option
pub default_formatter: Formatter, //text
/// Which auto sheet module to use.
pub auto_sheet: String, //dotfiles
/// an array of directories to search for user defined auto_sheet classes
pub auto_sheet_search_paths: Vec<PathBuf>, // - "/home/user/.timetrap/auto_sheets"
/// The default command to invoke when you call t
pub default_command: Option<String>,
/// Automatically check out of running entries when you check in or out
pub auto_checkout: bool, // false
/// Prompt for a note if one isn't provided when checking in
pub require_note: bool, // true
/// The command to start editing notes. Defaults to false which means no
/// external editor is used. Please see the section below on Notes Editing
/// for tips on using non-terminal based editors. Example: note_editor:
/// "vim"
pub note_editor: Option<String>, // nvim
/// The day of the week to use as the start of the week for t week.
pub week_start: WeekDay, // Monday
}
impl Config {
/// Tries as hard as possible to read the current configuration. Retrieving
/// the path to it from the environment or common locations.
pub fn read() -> Result<Config> {
// first try from env variable TIMETRAP_CONFIG_FILE
if let Ok(value) = env::var("TIMETRAP_CONFIG_FILE") {
if value.ends_with(".toml") {
return Self::read_from_toml(value);
} else {
return Self::read_from_yaml(value);
}
}
// Next try from some known directories
if let Some(user_dirs) = UserDirs::new() {
let old_location = {
let mut p = user_dirs.home_dir().to_owned();
p.push(".timetrap.yml");
p
};
if old_location.is_file() {
return Self::read_from_yaml(old_location);
}
if let Some(project_dirs) = ProjectDirs::from("tk", "categulario", "tiempo") {
let config_filename = {
let mut conf = project_dirs.config_dir().to_owned();
conf.push("config.toml");
conf
};
if config_filename.is_file() {
Self::read_from_toml(config_filename)
} else {
let config_dir = project_dirs.config_dir();
create_dir_all(config_dir).map_err(|e| CouldntCreateConfigDir {
path: config_dir.to_owned(),
error: e.to_string(),
})?;
Self::create_and_return_config(project_dirs.config_dir(), &config_filename)
}
} else {
Err(NoHomeDir)
}
} else {
Err(NoHomeDir)
}
}
fn read_from_yaml<P: AsRef<Path>>(path: P) -> Result<Config> {
let path: PathBuf = path.as_ref().into();
let mut contents = String::new();
let mut file = File::open(&path).map_err(|e| CouldntReadConfigFile {
path: path.clone(),
error: e,
})?;
file.read_to_string(&mut contents).map_err(|e| CouldntReadConfigFile {
path: path.clone(),
error: e,
})?;
serde_yaml::from_str(&contents).map_err(|error| YamlError {
path, error
})
}
fn read_from_toml<P: AsRef<Path>>(path: P) -> Result<Config> {
let path: PathBuf = path.as_ref().into();
let mut contents = String::new();
let mut file = File::open(&path).map_err(|e| CouldntReadConfigFile {
path: path.clone(),
error: e,
})?;
file.read_to_string(&mut contents).map_err(|e| CouldntReadConfigFile {
path: path.clone(),
error: e,
})?;
toml::from_str(&contents).map_err(|error| TomlError {
path, error
})
}
/// Assume the configuration file does not exist, create a default one and
/// return it.
fn create_and_return_config(project_dir: &Path, config_filename: &Path) -> Result<Config> {
let database_filename = {
let mut p = project_dir.to_owned();
p.push("database.sqlite3");
p
};
let formatter_search_paths = {
let mut p = project_dir.to_owned();
p.push("formatters");
p
};
let auto_sheet_search_paths = {
let mut p = project_dir.to_owned();
p.push("auto_sheets");
p
};
let config = Config {
database_file: database_filename,
formatter_search_paths: vec![formatter_search_paths],
auto_sheet_search_paths: vec![auto_sheet_search_paths],
..Default::default()
};
let mut config_file = File::create(config_filename).map_err(|e| CouldntEditConfigFile {
path: config_filename.to_owned(),
error: e.to_string(),
})?;
config_file.write_all(to_string(&config).unwrap().as_bytes()).map_err(|e| CouldntEditConfigFile {
path: config_filename.to_owned(),
error: e.to_string(),
})?;
Ok(config)
}
}
impl Default for Config {
fn default() -> Config {
Config {
database_file: PathBuf::new(), // see above for definition
round_in_seconds: 900,
append_notes_delimiter: " ".into(),
formatter_search_paths: Vec::new(), // see above for definition
default_formatter: Formatter::Text,
auto_sheet: "dotfiles".into(),
auto_sheet_search_paths: Vec::new(), // see above for definition
default_command: None,
auto_checkout: false,
require_note: true,
note_editor: None,
week_start: WeekDay::Monday, // Monday
}
}
}