Skip to main content

nm_tagrewriter/
config.rs

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    /// If true, track the last modification commit on each run,
31    /// and on future runs, only retag messages that have been modified.
32    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    /// Get the default config path.
58    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    /// Get the config path based on values.
67    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    /// Get notmuch config file as specified by rules in notmuch-config(1)
84    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    /// Open the notmuch database R/W
109    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: &notmuch::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    // TODO: move elsewhere, in app
153    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    /// Read the configuration.
164    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/// Wrapper around a vector of `Rule's
174#[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}