use std::collections::BTreeSet;
use std::fs::File;
use std::fs::read_to_string;
use std::io::BufReader;
use std::io::Write;
use std::path::PathBuf;
use derive_more::derive::{AsRef, Deref, DerefMut, From, IntoIterator};
use directories::BaseDirs;
use directories::ProjectDirs;
use itertools::Either;
use itertools::Itertools;
use log::*;
use serde::{Deserialize, Serialize};
use serde_with::DisplayFromStr;
use serde_with::serde_as;
use crate::common::Arrow;
use crate::common::LabelledArrow;
use crate::common::Quiver;
use crate::common::Tag;
use crate::common::Vector;
use crate::dnf::DisjunctiveNormalForm;
use crate::parser::BooleanQuery;
use crate::state::EffectiveState;
use crate::state::EffectiveStateError;
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
pub track_lastmod: Option<bool>,
pub notmuch_config: Option<PathBuf>,
pub rules: Vec<ConfigRule>,
}
#[derive(From, Debug)]
pub enum ConfigError {
#[from]
ConfigFileError(std::io::Error),
#[from]
ConfigParseError(serde_yml::Error),
#[from]
NotmuchError(notmuch::Error),
#[from]
EffectiveStateError(EffectiveStateError),
WriteLastModError(Box<dyn std::error::Error>),
}
type ResultConfig = Result<Config, ConfigError>;
impl Config {
fn get_project_dirs() -> ProjectDirs {
ProjectDirs::from("dev", "xaltsc", "notmuch-tagrewriter")
.expect("Error expanding project dir")
}
fn get_defaut_config_path() -> PathBuf {
let project_dirs = Self::get_project_dirs();
project_dirs
.config_dir()
.join("config")
.with_extension("yaml")
}
fn find_config_path(path: Option<PathBuf>) -> PathBuf {
if let Some(cfg_path) = path {
cfg_path
} else {
Self::get_defaut_config_path()
}
}
pub fn get_rules(&self) -> Rules {
self.rules
.iter()
.flat_map(|r| r.clone().as_rules())
.collect_vec()
.into()
}
fn get_notmuch_config_path(&self) -> PathBuf {
self.notmuch_config.clone().unwrap_or_else(|| {
std::env::var("NOTMUCH_CONFIG")
.map(|x| x.into())
.unwrap_or_else(|_| {
let base_dirs = BaseDirs::new().expect("Couldn't get homedir.");
let notmuch_dir = base_dirs.config_dir().join("notmuch");
let profile_var = std::env::var("NOTMUCH_PROFILE");
let xdg_file = match profile_var.clone() {
Ok(profile) => notmuch_dir.join(profile).join("config"),
Err(_) => notmuch_dir.join("default").join("config"),
};
if xdg_file.exists() {
xdg_file
} else {
base_dirs.home_dir().join(match profile_var.clone() {
Ok(profile) => format!(".notmuch-config.{}", profile),
Err(_) => ".notmuch.config".to_string(),
})
}
})
})
}
pub fn open_database(&self, dry_run: bool) -> Result<notmuch::Database, notmuch::Error> {
let config_path = self.get_notmuch_config_path();
debug!("Configpath: {:?}", &config_path);
debug!("Opening database…");
notmuch::Database::open_with_config::<PathBuf, PathBuf>(
None,
if dry_run {
notmuch::DatabaseMode::ReadOnly
} else {
notmuch::DatabaseMode::ReadWrite
},
Some(config_path),
None,
)
}
fn get_lastmod(&self) -> Option<u64> {
if self.track_lastmod.is_some_and(|x| x) {
let project_dirs = Self::get_project_dirs();
let filepath = project_dirs.state_dir()?.join("last_revision");
if filepath.exists() {
let contents = read_to_string(filepath).ok()?;
let num = contents.parse().ok()?;
Some(num)
} else {
None
}
} else {
None
}
}
fn set_lastmod(db: ¬much::Database) -> Result<(), Box<dyn std::error::Error>> {
let revision = db.revision().revision;
let project_dirs = Self::get_project_dirs();
let statedir = project_dirs.state_dir().expect("Can't find state dir.");
std::fs::create_dir_all(statedir)?;
let filepath = statedir.join("last_revision");
let mut file = File::create(filepath)?;
file.write_all(revision.to_string().as_bytes())?;
Ok(())
}
pub fn execute(&self, dry_run: bool) -> Result<(), ConfigError> {
let db = self.open_database(dry_run)?;
let quiver: Quiver = self.get_rules().try_into()?;
quiver.execute(&db, dry_run, self.get_lastmod())?;
if self.track_lastmod.is_some_and(|x| x) {
Self::set_lastmod(&db).map_err(|x| ConfigError::WriteLastModError(x))?;
}
Ok(())
}
pub fn read(path: Option<PathBuf>) -> ResultConfig {
let config_path = Self::find_config_path(path);
let config_contents = File::open(config_path)?;
let reader = BufReader::new(config_contents);
let read: Config = serde_yml::from_reader(reader)?;
Ok(read)
}
}
#[derive(Debug, From, Clone, Deref, DerefMut, AsRef, IntoIterator)]
pub struct Rules(Vec<Rule>);
#[derive(Debug, Clone, Deserialize)]
#[serde(transparent)]
struct ConfigMultiQuery {
#[serde(with = "either::serde_untagged")]
query: Either<ConfigSingleQuery, Vec<ConfigSingleQuery>>,
}
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ConfigSingleQuery {
#[serde_as(as = "DisplayFromStr")]
query: BooleanQuery,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ConfigRule {
query: ConfigMultiQuery,
rewrite: Vector,
name: Option<String>,
}
impl ConfigRule {
fn as_rules(self) -> Vec<Rule> {
match self.query.query {
Either::Left(s) => vec![Rule {
query: s.query,
rewrite: self.rewrite,
name: self.name,
}],
Either::Right(ss) => ss
.iter()
.map(|s| Rule {
query: s.query.clone(),
rewrite: self.rewrite.clone(),
name: self.name.clone(),
})
.collect(),
}
}
}
#[derive(Debug, Clone)]
pub struct Rule {
pub query: BooleanQuery,
pub rewrite: Vector,
pub name: Option<String>,
}
impl TryFrom<Rule> for Vec<LabelledArrow> {
type Error = EffectiveStateError;
fn try_from(value: Rule) -> Result<Self, Self::Error> {
let label = value.name.unwrap_or_else(|| value.rewrite.to_string());
let dnf: DisjunctiveNormalForm<Tag> = value.query.into();
let states: Vec<EffectiveState> = dnf
.iter()
.map(|x| EffectiveState::try_from_conjunction(x.clone()))
.try_collect()?;
let arrows = states
.iter()
.map(|s| LabelledArrow {
name: label.clone(),
arrow: Arrow {
source: s.clone(),
vector: value.rewrite.clone(),
},
})
.collect();
Ok(arrows)
}
}
impl TryFrom<Rules> for Quiver {
type Error = EffectiveStateError;
fn try_from(value: Rules) -> Result<Self, Self::Error> {
let arrows: Vec<Vec<LabelledArrow>> = value
.iter()
.map(|x| <Vec<LabelledArrow>>::try_from(x.clone()))
.try_collect()?;
Ok(arrows
.iter()
.flatten()
.map(Clone::clone)
.collect::<BTreeSet<_>>()
.into())
}
}