use std::{ collections::{hash_map::Entry, HashMap}, fmt::Display, io::{BufWriter, Write}, ops::{Deref, DerefMut}, path::PathBuf, time::{Duration, SystemTime}, }; use tracing::trace; use crate::{parser::FilterMap, path::PathMatcher, tmux::Tmux}; #[derive(Default)] pub struct Projects { inner: HashMap, filters: Vec>, excludes: Vec, mtime: bool, } impl Projects { pub fn new(mtime: bool, excludes: Vec) -> Self { Self { mtime, excludes, ..Default::default() } } pub fn add_filter(&mut self, filter: T) { self.filters.push(Box::new(filter)) } pub fn insert(&mut self, item: Project) { let span = tracing::trace_span!("Entry", ?item); let _guard = span.enter(); if self.excludes.iter().any(|p| &item.path_buf == p) { return; } match self.inner.entry(item.path_buf) { Entry::Occupied(mut occupied) if &item.timestamp > occupied.get() => { trace!(?occupied, new_value=?item.timestamp, "New entry is more recent, replacing"); occupied.insert(item.timestamp); } Entry::Occupied(occupied) => { trace!(?occupied, new_value=?item.timestamp, "Previous entry is more recent, skipping"); } Entry::Vacant(v) => { trace!(?item.timestamp, "No previous entry exists, inserting"); v.insert(item.timestamp); } } } pub fn write(&self, writer: W) -> Result<(), std::io::Error> { let mut writer = BufWriter::new(writer); let mut projects: Vec = self.inner.iter().map(Project::from).collect(); projects.sort(); projects .into_iter() .try_for_each(|project| writeln!(writer, "{project}")) } } impl Deref for Projects { type Target = HashMap; fn deref(&self) -> &Self::Target { &self.inner } } impl DerefMut for Projects { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } } impl Extend for Projects { fn extend(&mut self, iter: T) where T: IntoIterator, { for path_buf in iter { if let Some(project) = self.filters.filter_map(path_buf.to_owned()) { self.insert(project) } else if self.mtime { if let Ok(project) = Project::try_from(path_buf) { self.insert(project) } } } } } impl Extend for Projects { fn extend(&mut self, iter: T) where T: IntoIterator, { for project in iter.into_iter() { self.insert(project) } } } impl From for Projects { fn from(value: crate::config::Projects) -> Self { let mut filters: Vec> = Vec::new(); if let Some(pattern) = &value.pattern { filters.push(Box::new(PathMatcher(pattern.to_owned()))); } if value.tmux { filters.push(Box::new(Tmux)); } #[cfg(feature = "git")] if value.git { filters.push(Box::new(crate::git::Git)); } Self { filters, excludes: value.excludes, mtime: value.mtime, ..Default::default() } } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Project { pub timestamp: Duration, pub path_buf: PathBuf, } impl Display for Project { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.path_buf.to_string_lossy()) } } impl From<(PathBuf, Duration)> for Project { fn from((path_buf, timestamp): (PathBuf, Duration)) -> Self { Self { timestamp, path_buf, } } } impl From<(&PathBuf, &Duration)> for Project { fn from((path_buf, ×tamp): (&PathBuf, &Duration)) -> Self { Self { timestamp, path_buf: path_buf.to_owned(), } } } impl TryFrom for Project { type Error = std::io::Error; fn try_from(value: PathBuf) -> Result { let timestamp = value .metadata()? .modified()? .duration_since(SystemTime::UNIX_EPOCH) .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))?; Ok(Self { path_buf: value, timestamp, }) } }