#![allow(dead_code)]
use std::{
env,
fs::{File, create_dir_all},
io::Write,
path::PathBuf,
};
use color_eyre::{Result, eyre::bail};
use directories::ProjectDirs;
use lazy_static::lazy_static;
use serde::Deserialize;
use tracing::{debug, info};
const CONFIG: &str = include_str!("../.config/core.toml");
const CONFIG_FILE_NAME: &str = "core";
const APP_NAME: &str = "vault-tasks";
lazy_static! {
pub(crate) static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase();
pub(crate) static ref DATA_FOLDER: Option<PathBuf> =
env::var(format!("{}_DATA", PROJECT_NAME.clone()))
.ok()
.map(PathBuf::from);
pub(crate) static ref CONFIG_FOLDER: Option<PathBuf> =
env::var(format!("{}_CONFIG", PROJECT_NAME.clone()))
.ok()
.map(PathBuf::from);
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct TaskMarkerConfig {
pub done: char,
pub todo: char,
pub incomplete: char,
pub canceled: char,
}
impl Default for TaskMarkerConfig {
fn default() -> Self {
Self {
done: 'x',
todo: ' ',
incomplete: '/',
canceled: '-',
}
}
}
#[derive(Clone, Debug, Deserialize, Default)]
pub struct PrettySymbolsConfig {
#[serde(default)]
pub(crate) task_done: String,
#[serde(default)]
pub(crate) task_todo: String,
#[serde(default)]
pub(crate) task_incomplete: String,
#[serde(default)]
pub(crate) task_canceled: String,
#[serde(default)]
pub(crate) due_date: String,
#[serde(default)]
pub(crate) priority: String,
#[serde(default)]
pub(crate) today_tag: String,
#[serde(default)]
pub(crate) progress_bar_true: String,
#[serde(default)]
pub(crate) progress_bar_false: String,
}
#[derive(Clone, Debug, Deserialize, Default)]
pub struct CoreConfig {
#[serde(default)]
pub vault_path: PathBuf,
#[serde(default)]
pub use_american_format: bool,
#[serde(default)]
pub(crate) parse_dot_files: bool,
#[serde(default)]
pub(crate) file_tags_propagation: bool,
#[serde(default)]
pub(crate) ignored: Vec<PathBuf>,
#[serde(default)]
pub(crate) indent_length: usize,
#[serde(default)]
pub(crate) tasks_drop_file: Option<PathBuf>,
}
#[derive(Clone, Debug, Deserialize, Default)]
pub struct DisplayConfig {
#[serde(default)]
pub show_relative_due_dates: bool,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TasksConfig {
#[serde(default)]
pub core: CoreConfig,
#[serde(default)]
pub display: DisplayConfig,
#[serde(default)]
pub pretty_symbols: PrettySymbolsConfig,
#[serde(default)]
pub(crate) task_state_markers: TaskMarkerConfig,
}
impl Default for TasksConfig {
fn default() -> Self {
let mut config: Self = toml::from_str(CONFIG).unwrap();
if cfg!(test) {
config.core.vault_path = PathBuf::from("./test-vault");
}
config
}
}
pub struct ProtoConfig {
pub vault_path: Option<PathBuf>,
pub config_path: Option<PathBuf>,
}
impl TasksConfig {
pub fn new(params: &ProtoConfig) -> Result<Self> {
let default_config: Self = Self::default();
let data_dir = get_data_dir();
let config_path = params.config_path.clone().unwrap_or_else(get_config_dir);
debug!(
"Using data directory at {} and config directory at {}",
data_dir.display(),
config_path.display()
);
let builder = if config_path.is_file() {
config::Config::builder()
.set_default("data_dir", data_dir.to_str().unwrap())?
.add_source(config::File::from(config_path))
} else {
let mut builder = config::Config::builder()
.set_default("data_dir", data_dir.to_str().unwrap())?
.set_default("config_dir", config_path.to_str().unwrap())?;
let config_files = [
(
format!("{CONFIG_FILE_NAME}.json5"),
config::FileFormat::Json5,
),
(format!("{CONFIG_FILE_NAME}.json"), config::FileFormat::Json),
(format!("{CONFIG_FILE_NAME}.yaml"), config::FileFormat::Yaml),
(format!("{CONFIG_FILE_NAME}.toml"), config::FileFormat::Toml),
(format!("{CONFIG_FILE_NAME}.ini"), config::FileFormat::Ini),
];
let mut found_config = false;
for (file, format) in &config_files {
let source = config::File::from(config_path.join(file))
.format(*format)
.required(false);
builder = builder.add_source(source);
if config_path.join(file).exists() {
found_config = true;
}
}
if !found_config && !cfg!(test) {
info!(
"No configuration file found.\nCreate one at {config_path:?} or generate one using `vault-tasks generate-config`"
);
}
builder
};
let mut cfg: Self = builder.build()?.try_deserialize()?;
cfg = Self::merge_tasks_config(cfg, default_config);
if let Some(path) = ¶ms.vault_path {
cfg.core.vault_path.clone_from(path);
}
Ok(cfg)
}
fn merge_tasks_config(user_config: TasksConfig, default_config: TasksConfig) -> TasksConfig {
TasksConfig {
core: CoreConfig {
parse_dot_files: user_config.core.parse_dot_files,
file_tags_propagation: user_config.core.file_tags_propagation,
ignored: if user_config.core.ignored.is_empty() {
default_config.core.ignored
} else {
user_config.core.ignored
},
indent_length: if user_config.core.indent_length == 0 {
default_config.core.indent_length
} else {
user_config.core.indent_length
},
use_american_format: user_config.core.use_american_format,
vault_path: if user_config.core.vault_path == PathBuf::new() {
default_config.core.vault_path
} else {
user_config.core.vault_path
},
tasks_drop_file: if user_config.core.tasks_drop_file.is_none() {
default_config.core.tasks_drop_file
} else {
user_config.core.tasks_drop_file
},
},
display: DisplayConfig {
show_relative_due_dates: user_config.display.show_relative_due_dates,
},
task_state_markers: user_config.task_state_markers,
pretty_symbols: Self::merge_pretty_symbols_config(
user_config.pretty_symbols,
default_config.pretty_symbols,
),
}
}
fn merge_pretty_symbols_config(
user_config: PrettySymbolsConfig,
default_config: PrettySymbolsConfig,
) -> PrettySymbolsConfig {
PrettySymbolsConfig {
task_done: if user_config.task_done.is_empty() {
default_config.task_done
} else {
user_config.task_done
},
task_todo: if user_config.task_todo.is_empty() {
default_config.task_todo
} else {
user_config.task_todo
},
task_incomplete: if user_config.task_incomplete.is_empty() {
default_config.task_incomplete
} else {
user_config.task_incomplete
},
task_canceled: if user_config.task_canceled.is_empty() {
default_config.task_canceled
} else {
user_config.task_canceled
},
due_date: if user_config.due_date.is_empty() {
default_config.due_date
} else {
user_config.due_date
},
priority: if user_config.priority.is_empty() {
default_config.priority
} else {
user_config.priority
},
today_tag: if user_config.today_tag.is_empty() {
default_config.today_tag
} else {
user_config.today_tag
},
progress_bar_true: if user_config.progress_bar_true.is_empty() {
default_config.progress_bar_true
} else {
user_config.progress_bar_true
},
progress_bar_false: if user_config.progress_bar_false.is_empty() {
default_config.progress_bar_false
} else {
user_config.progress_bar_false
},
}
}
pub fn generate_config(path: Option<PathBuf>) -> Result<()> {
let config_dir = path.unwrap_or_else(get_config_dir);
let dest = config_dir.join(format!("{CONFIG_FILE_NAME}.toml"));
if create_dir_all(config_dir).is_err() {
bail!("Failed to create config directory at {dest:?}".to_owned());
}
if let Ok(mut file) = File::create(dest.clone()) {
if file.write_all(CONFIG.as_bytes()).is_err() {
bail!("Failed to write default config at {dest:?}".to_owned());
}
} else {
bail!("Failed to create default config at {dest:?}".to_owned());
}
println!(
"Configuration has been created at {}. You can fill the `vault-path` value to set a default vault.",
dest.display()
);
Ok(())
}
}
pub(crate) fn get_data_dir() -> PathBuf {
DATA_FOLDER.clone().map_or(
{
project_directory().map_or_else(
|| PathBuf::from(".").join(".data"),
|proj_dirs| proj_dirs.data_local_dir().to_path_buf(),
)
},
|s| s,
)
}
pub(crate) fn get_config_dir() -> PathBuf {
CONFIG_FOLDER.clone().unwrap_or_else(|| {
project_directory().map_or_else(
|| PathBuf::from(".").join(".config"),
|proj_dirs| proj_dirs.config_local_dir().to_path_buf(),
)
})
}
fn project_directory() -> Option<ProjectDirs> {
ProjectDirs::from("com", "kdheepak", APP_NAME)
}