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<PathBuf, Duration>,
filters: Vec<Box<dyn FilterMap>>,
excludes: Vec<PathBuf>,
mtime: bool,
}
impl Projects {
pub fn new(mtime: bool, excludes: Vec<PathBuf>) -> Self {
Self {
mtime,
excludes,
..Default::default()
}
}
pub fn add_filter<T: FilterMap + 'static>(&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<W: Write>(&self, writer: W) -> Result<(), std::io::Error> {
let mut writer = BufWriter::new(writer);
let mut projects: Vec<Project> = 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<PathBuf, Duration>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Projects {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl Extend<PathBuf> for Projects {
fn extend<T>(&mut self, iter: T)
where
T: IntoIterator<Item = PathBuf>,
{
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<Project> for Projects {
fn extend<T>(&mut self, iter: T)
where
T: IntoIterator<Item = Project>,
{
for project in iter.into_iter() {
self.insert(project)
}
}
}
impl From<crate::config::Projects> for Projects {
fn from(mut value: crate::config::Projects) -> Self {
let mut filters: Vec<Box<dyn FilterMap>> = 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));
}
if value.exclude_cwd {
if let Ok(path) = std::env::current_dir() {
value.excludes.push(path)
}
}
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<PathBuf> for Project {
type Error = std::io::Error;
fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
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,
})
}
}