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, ×tamp): (&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}