use std::path::{Path, PathBuf};
use std::fs::{File, create_dir_all};
use std::io::{Read, Write};
use std::str::FromStr;
use std::collections::HashMap;
use directories::{UserDirs, ProjectDirs};
use serde::{Serialize, Deserialize};
use toml::to_string;
use chrono::Weekday;
use crate::{error::{Result, Error::{self, *}}, formatters::Formatter};
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
pub enum WeekDay {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
impl FromStr for WeekDay {
type Err = Error;
fn from_str(s: &str) -> Result<WeekDay> {
Ok(match s.to_lowercase().as_str() {
"monday" => WeekDay::Monday,
"tuesday" => WeekDay::Tuesday,
"wednesday" => WeekDay::Wednesday,
"thursday" => WeekDay::Thursday,
"friday" => WeekDay::Friday,
"saturday" => WeekDay::Saturday,
"sunday" => WeekDay::Sunday,
x => return Err(InvalidWeekDaySpec(x.to_owned())),
})
}
}
impl From<Weekday> for WeekDay {
fn from(wd: Weekday) -> WeekDay {
match wd {
Weekday::Mon => WeekDay::Monday,
Weekday::Tue => WeekDay::Tuesday,
Weekday::Wed => WeekDay::Wednesday,
Weekday::Thu => WeekDay::Thursday,
Weekday::Fri => WeekDay::Friday,
Weekday::Sat => WeekDay::Saturday,
Weekday::Sun => WeekDay::Sunday,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ChartFormatterSettings {
pub daily_goal_hours: u32,
pub weekly_goal_hours: u32,
pub character_equals_minutes: usize,
}
impl Default for ChartFormatterSettings {
fn default() -> Self {
Self {
daily_goal_hours: 0,
weekly_goal_hours: 0,
character_equals_minutes: 30,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct FormattersSettings {
pub chart: ChartFormatterSettings,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct BaseCommandSettings {
pub default_formatter: Option<Formatter>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct CommandsSettings {
pub display: BaseCommandSettings,
pub month: BaseCommandSettings,
pub today: BaseCommandSettings,
pub week: BaseCommandSettings,
pub yesterday: BaseCommandSettings,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
#[serde(skip)]
pub path: Option<PathBuf>,
pub database_file: PathBuf,
pub round_in_seconds: u32,
pub append_notes_delimiter: String,
pub formatter_search_paths: Vec<PathBuf>,
pub default_formatter: Formatter,
pub auto_sheet: String,
pub auto_sheet_search_paths: Vec<PathBuf>,
pub default_command: Option<String>,
pub auto_checkout: bool,
pub require_note: bool,
pub note_editor: Option<String>,
pub week_start: WeekDay,
pub interactive_entries: usize,
pub formatters: FormattersSettings,
pub commands: CommandsSettings,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
impl Config {
pub fn read(timetrap_config_file: Option<&str>) -> Result<Config> {
if let Some(value) = timetrap_config_file {
return if value.ends_with(".toml") {
let config_path = PathBuf::from(&value);
if config_path.is_file() {
Self::read_from_toml(value)
} else {
Self::create_and_return_config(config_path.parent().unwrap(), &config_path)
}
} else {
Self::read_from_yaml(value)
};
}
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)
}
}
pub fn write<P: AsRef<Path>>(&self, path: P) -> Result<String> {
let path = path.as_ref();
let ext = path.extension().and_then(|e| e.to_str());
let output = match ext {
Some("toml") => {
toml::to_string(self).unwrap()
},
Some("yaml" | "yml") => {
serde_yaml::to_string(self).unwrap()
},
Some(ext) => {
return Err(Error::GenericFailure(format!("\
Your config file has '{}' extension which I don't understand, so I'd rather not
mess with its contents. If its formatted as toml use '.toml' extension. If it is
yaml use '.yml' or '.yaml'", ext)));
},
None => {
return Err(Error::GenericFailure(format!("\
Your config file, located at {} has no extension so I'll not write to it. Please
set it an extension like '.toml' or '.yaml' (and ensure it matches the content's
format)", path.display())));
},
};
let mut f = File::create(path)?;
f.write_all(output.as_bytes())?;
Ok(output)
}
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,
})?;
let mut config: Config = serde_yaml::from_str(&contents).map_err(|error| YamlError {
path: path.clone(), error
})?;
config.path = Some(path);
Ok(config)
}
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,
})?;
let mut config: Config = toml::from_str(&contents).map_err(|error| TomlError {
path: path.clone(), error
})?;
config.path = Some(path);
Ok(config)
}
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 {
path: Some(config_filename.to_owned()),
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 {
path: None,
extra: HashMap::new(),
database_file: PathBuf::new(),
round_in_seconds: 900,
append_notes_delimiter: " ".into(),
formatter_search_paths: Vec::new(),
default_formatter: Formatter::Text,
auto_sheet: "dotfiles".into(),
auto_sheet_search_paths: Vec::new(),
default_command: None,
auto_checkout: false,
require_note: true,
note_editor: None,
week_start: WeekDay::Monday,
interactive_entries: 5,
formatters: Default::default(),
commands: Default::default(),
}
}
}