use std::{
fs,
path::{Path, PathBuf},
};
use chrono::{DateTime, Utc};
use color_eyre::{Result, eyre::eyre};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::api::rest::{Gateway, TODOIST_API_URL};
#[derive(Serialize, Deserialize, Default)]
pub struct Config {
#[serde(default)]
pub token: Option<String>,
#[serde(default = "default_filter")]
pub default_filter: String,
#[serde(default = "default_url")]
pub url: Option<url::Url>,
#[serde(default)]
pub override_time: Option<DateTime<Utc>>,
#[serde(skip)]
pub prefix: Option<PathBuf>,
}
fn default_url() -> Option<url::Url> {
Some(TODOIST_API_URL.clone())
}
const DEFAULT_FILTER: &str = "(today | overdue)";
fn default_filter() -> String {
DEFAULT_FILTER.to_string()
}
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("unable to work with config file {file}")]
File {
file: PathBuf,
#[source]
io: Option<std::io::Error>,
},
#[error("unable to save config file")]
SaveFormat(#[from] toml::ser::Error),
}
const CONFIG_FILE: &str = "config.toml";
const XDG_PREFIX: &str = "doist";
impl Config {
#[cfg(windows)]
fn config_dir(prefix: Option<&Path>) -> Result<PathBuf, ConfigError> {
dirs::config_dir()
.map(|mut path| {
path.push(prefix.and_then(|p| p.to_str()).unwrap_or(XDG_PREFIX));
path
})
.ok_or_else(|| ConfigError::File {
file: PathBuf::from(XDG_PREFIX),
io: None,
})
}
#[cfg(not(windows))]
fn config_dir(prefix: Option<&Path>) -> Result<PathBuf, ConfigError> {
xdg::BaseDirectories::with_prefix(prefix.and_then(|p| p.to_str()).unwrap_or(XDG_PREFIX))
.get_config_home()
.ok_or_else(|| ConfigError::File {
file: PathBuf::from(XDG_PREFIX),
io: None,
})
}
fn config_file(prefix: Option<&Path>) -> Result<PathBuf, ConfigError> {
let mut path = Self::config_dir(prefix)?;
path.push(CONFIG_FILE);
Ok(path)
}
pub fn load() -> Result<Config, ConfigError> {
let file = Self::config_file(None)?;
Self::load_from(&file)
}
pub fn load_prefix(path: &Path) -> Result<Config, ConfigError> {
let file = Self::config_file(Some(path))?;
let mut cfg = Self::load_from(&file)?;
cfg.prefix = Some(path.to_owned());
Ok(cfg)
}
fn load_from(file: &PathBuf) -> Result<Config, ConfigError> {
let data = match fs::read_to_string(file) {
Ok(d) => d,
Err(io) => match io.kind() {
std::io::ErrorKind::NotFound => "".to_string(),
_ => {
return Err(ConfigError::File {
file: file.clone(),
io: Some(io),
})?;
}
},
};
let config = toml::from_str(&data).unwrap();
Ok(config)
}
pub fn save(&self) -> Result<(), ConfigError> {
let file = Self::config_file(self.prefix.as_deref())?;
file.parent()
.map(fs::create_dir_all)
.transpose()
.map_err(|io| ConfigError::File {
file: file.clone(),
io: Some(io),
})?;
let data = toml::to_string(self)?;
fs::write(&file, data).map_err(|io| ConfigError::File { file, io: Some(io) })?;
Ok(())
}
pub fn gateway(&self) -> Result<Gateway> {
let token = self.token.as_deref().ok_or_else(|| {
eyre!("No token in config specified. Use `doist auth` to register your token.")
})?;
Ok(Gateway::new(
token,
&self.url.clone().unwrap_or_else(|| default_url().unwrap()),
))
}
}