projectr 0.4.1

A contextual, MRU sorted, project finder.
Documentation
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, &timestamp): (&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,
        })
    }
}