#![forbid(unsafe_code)]
use std::fs::{self, File};
use std::io;
use std::path::{Path, PathBuf};
use ratatui::{text::Text, widgets::Row};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::modes::Mode;
pub static DEFAULT_INCLUDE_COMPLETED: bool = false;
pub static DEFAULT_DAYS_TO_STALE: u64 = 365;
pub static DEFAULT_OVERDUE_BLINK: bool = true;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Cannot locate user's configuration directory.")]
LocateConfigDir,
#[error("Cannot locate user's data directory.")]
LocateDataDir,
#[error("Cannot locate user's home directory.")]
LocateHomeDir,
#[error("Cannot create directory to contain log file.")]
CreateDataDir { error: io::Error },
#[error("Cannot create log file: {error}")]
CreateLogFile { error: io::Error },
#[error("Cannot create directory to contain files with tasks.")]
CreateTaskFilesDir { error: io::Error },
#[error("Cannot read/open configuration file: {error}")]
ReadConfigFile { error: Box<dyn std::error::Error> },
#[error("Cannot write to configuration file: {error}")]
WriteConfigFile { error: Box<dyn std::error::Error> },
#[error("Cannot create configuration directory: {error}")]
CreateConfigDir { error: io::Error },
#[error("Cannot create configuration file: {error}")]
CreateConfigFile { error: io::Error },
#[error("There must be 5 priority markers in the configuration file.")]
IncorrectNumberOfPriorityMarkers,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct Config {
pub path: PathBuf,
pub num_tasks_log: PathBuf,
pub file_extensions: Vec<String>,
pub days_to_stale: u64,
pub include_completed: bool,
pub priority_markers: Vec<String>,
pub evergreen_file: PathBuf,
pub start_mode: Mode,
pub overdue_blink: bool,
}
impl Config {
pub fn from_file() -> Result<Self, ConfigError> {
let mut config = match fs::read_to_string(Config::path()?) {
Ok(v) => v,
Err(e) => return Err(ConfigError::ReadConfigFile { error: Box::new(e) }),
};
let mut save = false;
if !config.contains("start_mode") {
config.push_str("\nstart_mode = \"Files\"");
save = true;
}
if !config.contains("overdue_blink") {
config.push_str("\noverdue_blink = true");
save = true;
}
let config: Config = match toml::from_str(&config) {
Ok(v) => v,
Err(e) => return Err(ConfigError::ReadConfigFile { error: Box::new(e) }),
};
if save {
config.save()?;
}
Ok(config)
}
pub fn save(&self) -> Result<(), ConfigError> {
let contents = match toml::to_string(&self) {
Ok(v) => v,
Err(e) => return Err(ConfigError::WriteConfigFile { error: Box::new(e) }),
};
if let Err(e) = fs::write(Config::path()?, contents) {
return Err(ConfigError::WriteConfigFile { error: Box::new(e) });
}
Ok(())
}
fn path() -> Result<PathBuf, ConfigError> {
let mut config_dir = match dirs::config_dir() {
Some(v) => v,
None => return Err(ConfigError::LocateConfigDir),
};
config_dir.push("taskfinder");
if !config_dir.exists()
&& let Err(e) = fs::create_dir_all(&config_dir)
{
return Err(ConfigError::CreateConfigDir { error: e });
}
let mut config_path = config_dir;
config_path.push("config.toml");
Ok(config_path)
}
pub fn default_files_path() -> Result<PathBuf, ConfigError> {
let mut home_dir = match dirs::home_dir() {
Some(v) => v,
None => return Err(ConfigError::LocateHomeDir),
};
home_dir.push("taskfinder");
Ok(home_dir.to_path_buf())
}
pub fn default_num_tasks_log() -> Result<PathBuf, ConfigError> {
let mut data_dir = match dirs::data_dir() {
Some(v) => v,
None => return Err(ConfigError::LocateDataDir),
};
data_dir.push("taskfinder");
if !data_dir.exists()
&& let Err(e) = fs::create_dir_all(&data_dir)
{
return Err(ConfigError::CreateDataDir { error: e });
}
let mut num_tasks_log = data_dir;
num_tasks_log.push("num_tasks_log");
if !num_tasks_log.exists()
&& let Err(e) = File::create(&num_tasks_log)
{
return Err(ConfigError::CreateLogFile { error: e });
}
Ok(num_tasks_log)
}
pub fn default_file_extensions() -> Vec<String> {
vec!["txt".to_string(), "md".to_string()]
}
pub fn default_priority_markers() -> Vec<String> {
vec![
"pri@1".to_string(),
"pri@2".to_string(),
"pri@3".to_string(),
"pri@4".to_string(),
"pri@5".to_string(),
]
}
pub fn default_evergreen_file() -> PathBuf {
Path::new("").to_path_buf()
}
pub fn create_or_get() -> Result<Self, ConfigError> {
let config_path = Config::path()?;
let home_dir = Config::default_files_path()?;
let config = if !config_path.exists() {
if let Err(e) = File::create(&config_path) {
return Err(ConfigError::CreateConfigFile { error: e });
}
let config = Config {
path: home_dir.clone(),
num_tasks_log: Config::default_num_tasks_log()?,
file_extensions: Config::default_file_extensions(),
days_to_stale: DEFAULT_DAYS_TO_STALE,
include_completed: DEFAULT_INCLUDE_COMPLETED,
priority_markers: Config::default_priority_markers(),
evergreen_file: Config::default_evergreen_file(),
start_mode: Mode::Files,
overdue_blink: DEFAULT_OVERDUE_BLINK,
};
if let Err(e) = File::options()
.write(true)
.truncate(true)
.open(&config_path)
{
return Err(ConfigError::ReadConfigFile { error: Box::new(e) });
}
config.save()?;
config
} else {
Config::from_file()?
};
if !home_dir.exists()
&& config.path == home_dir.to_path_buf()
&& let Err(e) = fs::create_dir_all(&home_dir)
{
return Err(ConfigError::CreateTaskFilesDir { error: e });
}
Ok(config)
}
pub fn table_rows(&self) -> Vec<Row<'static>> {
vec![
Row::new(vec![
Text::from("path"),
Text::from(format!("{}", self.path.to_string_lossy())),
]),
Row::new(vec![
Text::from("file extensions"),
Text::from(self.file_extensions.join(",")),
]),
Row::new(vec![
Text::from("days to stale"),
Text::from(format!("{:?}", self.days_to_stale)),
]),
Row::new(vec![
Text::from("include completed?"),
Text::from(format!("{:?}", self.include_completed)),
]),
Row::new(vec![
Text::from("priority markers"),
Text::from(self.priority_markers.join(",")),
]),
Row::new(vec![
Text::from("evergreen file"),
Text::from(format!("{}", self.evergreen_file.to_string_lossy())),
]),
Row::new(vec![
Text::from("starting mode"),
Text::from(format!("{:?}", self.start_mode)),
]),
Row::new(vec![
Text::from("overdue blink"),
Text::from(format!("{:?}", self.overdue_blink)),
]),
]
}
}