1use std::collections::BTreeSet;
2use std::fs::File;
3use std::fs::read_to_string;
4use std::io::BufReader;
5use std::io::Write;
6use std::path::PathBuf;
7
8use derive_more::derive::{AsRef, Deref, DerefMut, From, IntoIterator};
9use directories::BaseDirs;
10use directories::ProjectDirs;
11use itertools::Either;
12use itertools::Itertools;
13use log::*;
14use serde::{Deserialize, Serialize};
15use serde_with::DisplayFromStr;
16use serde_with::serde_as;
17
18use crate::common::Arrow;
19use crate::common::LabelledArrow;
20use crate::common::Quiver;
21use crate::common::Tag;
22use crate::common::Vector;
23use crate::dnf::DisjunctiveNormalForm;
24use crate::parser::BooleanQuery;
25use crate::state::EffectiveState;
26use crate::state::EffectiveStateError;
27
28#[derive(Debug, Clone, Deserialize)]
29pub struct Config {
30 pub track_lastmod: Option<bool>,
33 pub notmuch_config: Option<PathBuf>,
34 pub rules: Vec<ConfigRule>,
35}
36
37#[derive(From, Debug)]
38pub enum ConfigError {
39 #[from]
40 ConfigFileError(std::io::Error),
41 #[from]
42 ConfigParseError(serde_yml::Error),
43 #[from]
44 NotmuchError(notmuch::Error),
45 #[from]
46 EffectiveStateError(EffectiveStateError),
47 WriteLastModError(Box<dyn std::error::Error>),
48}
49
50type ResultConfig = Result<Config, ConfigError>;
51
52impl Config {
53 fn get_project_dirs() -> ProjectDirs {
54 ProjectDirs::from("dev", "xaltsc", "notmuch-tagrewriter")
55 .expect("Error expanding project dir")
56 }
57 fn get_defaut_config_path() -> PathBuf {
59 let project_dirs = Self::get_project_dirs();
60 project_dirs
61 .config_dir()
62 .join("config")
63 .with_extension("yaml")
64 }
65
66 fn find_config_path(path: Option<PathBuf>) -> PathBuf {
68 if let Some(cfg_path) = path {
69 cfg_path
70 } else {
71 Self::get_defaut_config_path()
72 }
73 }
74
75 pub fn get_rules(&self) -> Rules {
76 self.rules
77 .iter()
78 .flat_map(|r| r.clone().as_rules())
79 .collect_vec()
80 .into()
81 }
82
83 fn get_notmuch_config_path(&self) -> PathBuf {
85 self.notmuch_config.clone().unwrap_or_else(|| {
86 std::env::var("NOTMUCH_CONFIG")
87 .map(|x| x.into())
88 .unwrap_or_else(|_| {
89 let base_dirs = BaseDirs::new().expect("Couldn't get homedir.");
90 let notmuch_dir = base_dirs.config_dir().join("notmuch");
91 let profile_var = std::env::var("NOTMUCH_PROFILE");
92 let xdg_file = match profile_var.clone() {
93 Ok(profile) => notmuch_dir.join(profile).join("config"),
94 Err(_) => notmuch_dir.join("default").join("config"),
95 };
96 if xdg_file.exists() {
97 xdg_file
98 } else {
99 base_dirs.home_dir().join(match profile_var.clone() {
100 Ok(profile) => format!(".notmuch-config.{}", profile),
101 Err(_) => ".notmuch.config".to_string(),
102 })
103 }
104 })
105 })
106 }
107
108 pub fn open_database(&self, dry_run: bool) -> Result<notmuch::Database, notmuch::Error> {
110 let config_path = self.get_notmuch_config_path();
111 debug!("Configpath: {:?}", &config_path);
112 debug!("Opening database…");
113 notmuch::Database::open_with_config::<PathBuf, PathBuf>(
114 None,
115 if dry_run {
116 notmuch::DatabaseMode::ReadOnly
117 } else {
118 notmuch::DatabaseMode::ReadWrite
119 },
120 Some(config_path),
121 None,
122 )
123 }
124
125 fn get_lastmod(&self) -> Option<u64> {
126 if self.track_lastmod.is_some_and(|x| x) {
127 let project_dirs = Self::get_project_dirs();
128 let filepath = project_dirs.state_dir()?.join("last_revision");
129 if filepath.exists() {
130 let contents = read_to_string(filepath).ok()?;
131 let num = contents.parse().ok()?;
132 Some(num)
133 } else {
134 None
135 }
136 } else {
137 None
138 }
139 }
140
141 fn set_lastmod(db: ¬much::Database) -> Result<(), Box<dyn std::error::Error>> {
142 let revision = db.revision().revision;
143 let project_dirs = Self::get_project_dirs();
144 let statedir = project_dirs.state_dir().expect("Can't find state dir.");
145 std::fs::create_dir_all(statedir)?;
146 let filepath = statedir.join("last_revision");
147 let mut file = File::create(filepath)?;
148 file.write_all(revision.to_string().as_bytes())?;
149 Ok(())
150 }
151
152 pub fn execute(&self, dry_run: bool) -> Result<(), ConfigError> {
154 let db = self.open_database(dry_run)?;
155 let quiver: Quiver = self.get_rules().try_into()?;
156 quiver.execute(&db, dry_run, self.get_lastmod())?;
157 if self.track_lastmod.is_some_and(|x| x) {
158 Self::set_lastmod(&db).map_err(|x| ConfigError::WriteLastModError(x))?;
159 }
160 Ok(())
161 }
162
163 pub fn read(path: Option<PathBuf>) -> ResultConfig {
165 let config_path = Self::find_config_path(path);
166 let config_contents = File::open(config_path)?;
167 let reader = BufReader::new(config_contents);
168 let read: Config = serde_yml::from_reader(reader)?;
169 Ok(read)
170 }
171}
172
173#[derive(Debug, From, Clone, Deref, DerefMut, AsRef, IntoIterator)]
175pub struct Rules(Vec<Rule>);
176
177#[derive(Debug, Clone, Deserialize)]
178#[serde(transparent)]
179struct ConfigMultiQuery {
180 #[serde(with = "either::serde_untagged")]
181 query: Either<ConfigSingleQuery, Vec<ConfigSingleQuery>>,
182}
183
184#[serde_as]
185#[derive(Debug, Clone, Serialize, Deserialize)]
186#[serde(transparent)]
187pub struct ConfigSingleQuery {
188 #[serde_as(as = "DisplayFromStr")]
189 query: BooleanQuery,
190}
191
192#[derive(Debug, Clone, Deserialize)]
193pub struct ConfigRule {
194 query: ConfigMultiQuery,
195 rewrite: Vector,
196 name: Option<String>,
197}
198
199impl ConfigRule {
200 fn as_rules(self) -> Vec<Rule> {
201 match self.query.query {
202 Either::Left(s) => vec![Rule {
203 query: s.query,
204 rewrite: self.rewrite,
205 name: self.name,
206 }],
207 Either::Right(ss) => ss
208 .iter()
209 .map(|s| Rule {
210 query: s.query.clone(),
211 rewrite: self.rewrite.clone(),
212 name: self.name.clone(),
213 })
214 .collect(),
215 }
216 }
217}
218
219#[derive(Debug, Clone)]
220pub struct Rule {
221 pub query: BooleanQuery,
222 pub rewrite: Vector,
223 pub name: Option<String>,
224}
225
226impl TryFrom<Rule> for Vec<LabelledArrow> {
227 type Error = EffectiveStateError;
228 fn try_from(value: Rule) -> Result<Self, Self::Error> {
229 let label = value.name.unwrap_or_else(|| value.rewrite.to_string());
230 let dnf: DisjunctiveNormalForm<Tag> = value.query.into();
231 let states: Vec<EffectiveState> = dnf
232 .iter()
233 .map(|x| EffectiveState::try_from_conjunction(x.clone()))
234 .try_collect()?;
235 let arrows = states
236 .iter()
237 .map(|s| LabelledArrow {
238 name: label.clone(),
239 arrow: Arrow {
240 source: s.clone(),
241 vector: value.rewrite.clone(),
242 },
243 })
244 .collect();
245 Ok(arrows)
246 }
247}
248
249impl TryFrom<Rules> for Quiver {
250 type Error = EffectiveStateError;
251 fn try_from(value: Rules) -> Result<Self, Self::Error> {
252 let arrows: Vec<Vec<LabelledArrow>> = value
253 .iter()
254 .map(|x| <Vec<LabelledArrow>>::try_from(x.clone()))
255 .try_collect()?;
256 Ok(arrows
257 .iter()
258 .flatten()
259 .map(Clone::clone)
260 .collect::<BTreeSet<_>>()
261 .into())
262 }
263}