use std::path::Path;
use std::rc::Rc;
use anyhow::anyhow;
use anyhow::Context as _;
use anyhow::Result;
use crate::cap::FileCap;
use crate::colors::Colors;
use crate::ser::backends::Json;
use crate::ser::state::UiConfig as SerUiConfig;
use crate::ser::ToSerde;
use crate::state::load_state_from_file;
use crate::state::save_state_to_file;
use crate::state::should_save_state;
use crate::state::TaskState;
use crate::tags::Tag;
use crate::view::View;
use crate::view::ViewBuilder;
#[derive(Debug)]
pub struct Config {
pub colors: Colors,
pub toggle_tag: Option<Tag>,
pub views: Vec<View>,
}
impl Config {
pub async fn load(config_path: &Path, task_state: &TaskState) -> Result<Self> {
let config = load_state_from_file::<Json, SerUiConfig>(config_path)
.await
.with_context(|| {
format!(
"failed to load UI configuration from {}",
config_path.display()
)
})?
.unwrap_or_default();
Self::with_serde(config, task_state)
}
pub fn with_serde(config: SerUiConfig, task_state: &TaskState) -> Result<Self> {
let SerUiConfig {
colors,
toggle_tag,
views,
} = config;
let templates = task_state.templates();
let tasks = task_state.tasks();
let mut views = views
.into_iter()
.map(|view| {
let name = view.name.clone();
let view = View::with_serde(view, templates, Rc::clone(tasks))
.with_context(|| format!("failed to instantiate view '{name}'"))?;
Ok(view)
})
.collect::<Result<Vec<_>>>()?;
if views.is_empty() {
views.push(ViewBuilder::new(Rc::clone(tasks)).build("all"))
}
let toggle_tag = if let Some(toggle_tag) = toggle_tag {
let toggle_tag = templates
.instantiate(toggle_tag.id)
.ok_or_else(|| anyhow!("encountered invalid toggle tag ID {}", toggle_tag.id))?;
Some(toggle_tag)
} else {
None
};
let slf = Self {
colors,
toggle_tag,
views,
};
Ok(slf)
}
pub async fn is_changed(&self, file: &Path) -> bool {
should_save_state::<Json, _>(file, &self.to_serde())
.await
.unwrap_or(true)
}
pub async fn save(&self, file_cap: &mut FileCap<'_>) -> Result<()> {
save_state_to_file::<Json, _>(file_cap, &self.to_serde()).await
}
}
impl ToSerde for Config {
type Output = SerUiConfig;
fn to_serde(&self) -> Self::Output {
let views = self.views.iter().map(View::to_serde).collect();
let config = SerUiConfig {
colors: self.colors,
toggle_tag: self.toggle_tag.as_ref().map(ToSerde::to_serde),
views,
};
config
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use std::ffi::OsStr;
use tempfile::TempDir;
use tokio::test;
use crate::cap::DirCap;
use crate::ser::state::TaskState as SerTaskState;
use crate::ser::tasks::Tasks as SerTasks;
use crate::test::make_tasks;
fn make_config(task_count: usize) -> (Config, TaskState) {
let tasks = make_tasks(task_count);
let task_state = SerTaskState {
tasks_meta: Default::default(),
tasks: SerTasks::from(tasks),
};
let task_state = TaskState::with_serde(task_state).unwrap();
let config = Default::default();
let config = Config::with_serde(config, &task_state).unwrap();
(config, task_state)
}
#[test]
async fn save_and_load_config() {
let (config, task_state) = make_config(3);
let ui_file_dir = TempDir::new().unwrap();
let ui_file_name = OsStr::new("config");
let ui_file = ui_file_dir.path().join(ui_file_name);
let mut ui_dir_cap = DirCap::for_dir(ui_file_dir.path().to_path_buf())
.await
.unwrap();
let ui_write_guard = ui_dir_cap.write().await.unwrap();
let mut ui_file_cap = ui_write_guard.file_cap(ui_file_name);
let () = config.save(&mut ui_file_cap).await.unwrap();
let _new_config = Config::load(&ui_file, &task_state).await.unwrap();
}
#[test]
async fn load_config_file_not_found() {
let (config, task_state) = {
let (config, task_state) = make_config(1);
let ui_file_dir = TempDir::new().unwrap();
let ui_file_name = OsStr::new("config");
let mut ui_dir_cap = DirCap::for_dir(ui_file_dir.path().to_path_buf())
.await
.unwrap();
let ui_write_guard = ui_dir_cap.write().await.unwrap();
let mut ui_file_cap = ui_write_guard.file_cap(ui_file_name);
let () = config.save(&mut ui_file_cap).await.unwrap();
(ui_file_dir.path().join(ui_file_name), task_state)
};
let _new_config = Config::load(&config, &task_state).await.unwrap();
}
}