projectr/
project.rs

1use std::{
2    collections::{hash_map::Entry, HashMap},
3    fmt::Display,
4    io::{BufWriter, Write},
5    ops::{Deref, DerefMut},
6    path::PathBuf,
7    time::{Duration, SystemTime},
8};
9
10use tracing::trace;
11
12use crate::{parser::FilterMap, path::PathMatcher, tmux::Tmux};
13
14#[derive(Default)]
15pub struct Projects {
16    inner: HashMap<PathBuf, Duration>,
17    filters: Vec<Box<dyn FilterMap>>,
18    excludes: Vec<PathBuf>,
19    mtime: bool,
20}
21
22impl Projects {
23    pub fn new(mtime: bool, excludes: Vec<PathBuf>) -> Self {
24        Self {
25            mtime,
26            excludes,
27            ..Default::default()
28        }
29    }
30
31    pub fn add_filter<T: FilterMap + 'static>(&mut self, filter: T) {
32        self.filters.push(Box::new(filter))
33    }
34
35    pub fn insert(&mut self, item: Project) {
36        let span = tracing::trace_span!("Entry", ?item);
37        let _guard = span.enter();
38
39        if self.excludes.iter().any(|p| &item.path_buf == p) {
40            return;
41        }
42
43        match self.inner.entry(item.path_buf) {
44            Entry::Occupied(mut occupied) if &item.timestamp > occupied.get() => {
45                trace!(?occupied, new_value=?item.timestamp, "New entry is more recent, replacing");
46                occupied.insert(item.timestamp);
47            }
48            Entry::Occupied(occupied) => {
49                trace!(?occupied, new_value=?item.timestamp, "Previous entry is more recent, skipping");
50            }
51            Entry::Vacant(v) => {
52                trace!(?item.timestamp, "No previous entry exists, inserting");
53                v.insert(item.timestamp);
54            }
55        }
56    }
57
58    pub fn write<W: Write>(&self, writer: W) -> Result<(), std::io::Error> {
59        let mut writer = BufWriter::new(writer);
60        let mut projects: Vec<Project> = self.inner.iter().map(Project::from).collect();
61
62        projects.sort();
63
64        projects
65            .into_iter()
66            .try_for_each(|project| writeln!(writer, "{project}"))
67    }
68}
69
70impl Deref for Projects {
71    type Target = HashMap<PathBuf, Duration>;
72
73    fn deref(&self) -> &Self::Target {
74        &self.inner
75    }
76}
77
78impl DerefMut for Projects {
79    fn deref_mut(&mut self) -> &mut Self::Target {
80        &mut self.inner
81    }
82}
83
84impl Extend<PathBuf> for Projects {
85    fn extend<T>(&mut self, iter: T)
86    where
87        T: IntoIterator<Item = PathBuf>,
88    {
89        for path_buf in iter {
90            if let Some(project) = self.filters.filter_map(path_buf.to_owned()) {
91                self.insert(project)
92            } else if self.mtime {
93                if let Ok(project) = Project::try_from(path_buf) {
94                    self.insert(project)
95                }
96            }
97        }
98    }
99}
100
101impl Extend<Project> for Projects {
102    fn extend<T>(&mut self, iter: T)
103    where
104        T: IntoIterator<Item = Project>,
105    {
106        for project in iter.into_iter() {
107            self.insert(project)
108        }
109    }
110}
111
112impl From<crate::config::Projects> for Projects {
113    fn from(mut value: crate::config::Projects) -> Self {
114        let mut filters: Vec<Box<dyn FilterMap>> = Vec::new();
115
116        if let Some(pattern) = &value.pattern {
117            filters.push(Box::new(PathMatcher(pattern.to_owned())));
118        }
119
120        if value.tmux {
121            filters.push(Box::new(Tmux));
122        }
123
124        #[cfg(feature = "git")]
125        if value.git {
126            filters.push(Box::new(crate::git::Git));
127        }
128
129        if value.exclude_cwd {
130            if let Ok(path) = std::env::current_dir() {
131                value.excludes.push(path)
132            }
133        }
134
135        Self {
136            filters,
137            excludes: value.excludes,
138            mtime: value.mtime,
139            ..Default::default()
140        }
141    }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
145pub struct Project {
146    pub timestamp: Duration,
147    pub path_buf: PathBuf,
148}
149
150impl Display for Project {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        write!(f, "{}", self.path_buf.to_string_lossy())
153    }
154}
155
156impl From<(PathBuf, Duration)> for Project {
157    fn from((path_buf, timestamp): (PathBuf, Duration)) -> Self {
158        Self {
159            timestamp,
160            path_buf,
161        }
162    }
163}
164
165impl From<(&PathBuf, &Duration)> for Project {
166    fn from((path_buf, &timestamp): (&PathBuf, &Duration)) -> Self {
167        Self {
168            timestamp,
169            path_buf: path_buf.to_owned(),
170        }
171    }
172}
173
174impl TryFrom<PathBuf> for Project {
175    type Error = std::io::Error;
176
177    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
178        let timestamp = value
179            .metadata()?
180            .modified()?
181            .duration_since(SystemTime::UNIX_EPOCH)
182            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))?;
183
184        Ok(Self {
185            path_buf: value,
186            timestamp,
187        })
188    }
189}