1use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum FileSetError {
13 NoNextFile,
14 NoPreviousFile,
15 WouldEmpty,
16}
17
18impl std::fmt::Display for FileSetError {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 match self {
21 FileSetError::NoNextFile => write!(f, "no next file"),
22 FileSetError::NoPreviousFile => write!(f, "no previous file"),
23 FileSetError::WouldEmpty => write!(f, "cannot remove last file"),
24 }
25 }
26}
27
28#[derive(Debug, Clone)]
29pub struct FileSet {
30 paths: Vec<PathBuf>,
31 current_index: usize,
32}
33
34impl FileSet {
35 pub fn new(paths: Vec<PathBuf>) -> Self {
40 Self { paths, current_index: 0 }
41 }
42
43 pub fn current(&self) -> Option<&Path> {
44 self.paths.get(self.current_index).map(|p| p.as_path())
45 }
46
47 pub fn len(&self) -> usize {
49 self.paths.len()
50 }
51
52 pub fn current_index(&self) -> usize {
53 self.current_index
54 }
55
56 pub fn is_empty(&self) -> bool {
57 self.paths.is_empty()
58 }
59
60 pub fn set_current_index(&mut self, index: usize) {
63 if self.paths.is_empty() {
64 return;
65 }
66 self.current_index = index.min(self.paths.len() - 1);
67 }
68
69 #[allow(clippy::should_implement_trait)]
72 pub fn next(&mut self) -> Result<&Path, FileSetError> {
73 if self.current_index + 1 >= self.paths.len() {
74 return Err(FileSetError::NoNextFile);
75 }
76 self.current_index += 1;
77 Ok(self.paths[self.current_index].as_path())
78 }
79
80 pub fn prev(&mut self) -> Result<&Path, FileSetError> {
83 if self.current_index == 0 {
84 return Err(FileSetError::NoPreviousFile);
85 }
86 self.current_index -= 1;
87 Ok(self.paths[self.current_index].as_path())
88 }
89
90 pub fn first(&mut self) -> Option<&Path> {
95 if self.paths.is_empty() {
96 return None;
97 }
98 self.current_index = 0;
99 Some(self.paths[0].as_path())
100 }
101
102 pub fn last(&mut self) -> Option<&Path> {
106 if self.paths.is_empty() {
107 return None;
108 }
109 self.current_index = self.paths.len() - 1;
110 Some(self.paths[self.current_index].as_path())
111 }
112
113 pub fn nth(&self, i: usize) -> Option<&Path> {
115 self.paths.get(i).map(|p| p.as_path())
116 }
117
118 pub fn append_and_switch(&mut self, path: PathBuf) -> &Path {
120 self.paths.push(path);
121 self.current_index = self.paths.len() - 1;
122 self.paths[self.current_index].as_path()
123 }
124
125 pub fn delete_current(&mut self) -> Result<&Path, FileSetError> {
129 if self.paths.len() <= 1 {
130 return Err(FileSetError::WouldEmpty);
131 }
132 self.paths.remove(self.current_index);
133 if self.current_index >= self.paths.len() {
134 self.current_index = self.paths.len() - 1;
135 }
136 Ok(self.paths[self.current_index].as_path())
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn fs(names: &[&str]) -> FileSet {
145 FileSet::new(names.iter().map(PathBuf::from).collect())
146 }
147
148 #[test]
149 fn new_with_paths_sets_current_zero() {
150 let f = fs(&["a.log", "b.log", "c.log"]);
151 assert_eq!(f.current_index(), 0);
152 assert_eq!(f.current(), Some(Path::new("a.log")));
153 }
154
155 #[test]
156 fn len_reports_total() {
157 let f = fs(&["a.log", "b.log", "c.log"]);
158 assert_eq!(f.len(), 3);
159 }
160
161 #[test]
162 fn next_advances_index() {
163 let mut f = fs(&["a.log", "b.log", "c.log"]);
164 assert_eq!(f.next().unwrap(), Path::new("b.log"));
165 assert_eq!(f.current_index(), 1);
166 assert_eq!(f.next().unwrap(), Path::new("c.log"));
167 assert_eq!(f.current_index(), 2);
168 }
169
170 #[test]
171 fn next_at_last_returns_no_next_file_error() {
172 let mut f = fs(&["a.log"]);
173 assert_eq!(f.next().unwrap_err(), FileSetError::NoNextFile);
174 assert_eq!(f.current_index(), 0);
175 }
176
177 #[test]
178 fn prev_decrements_index() {
179 let mut f = fs(&["a.log", "b.log"]);
180 f.next().unwrap();
181 assert_eq!(f.prev().unwrap(), Path::new("a.log"));
182 assert_eq!(f.current_index(), 0);
183 }
184
185 #[test]
186 fn prev_at_first_returns_no_previous_file_error() {
187 let mut f = fs(&["a.log", "b.log"]);
188 assert_eq!(f.prev().unwrap_err(), FileSetError::NoPreviousFile);
189 assert_eq!(f.current_index(), 0);
190 }
191
192 #[test]
193 fn first_resets_to_zero() {
194 let mut f = fs(&["a.log", "b.log", "c.log"]);
195 f.next().unwrap();
196 f.next().unwrap();
197 assert_eq!(f.first(), Some(Path::new("a.log")));
198 assert_eq!(f.current_index(), 0);
199 }
200
201 #[test]
202 fn last_jumps_to_count_minus_one() {
203 let mut f = fs(&["a.log", "b.log", "c.log"]);
204 assert_eq!(f.last(), Some(Path::new("c.log")));
205 assert_eq!(f.current_index(), 2);
206 }
207
208 #[test]
209 fn append_and_switch_grows_list_and_moves_cursor() {
210 let mut f = fs(&["a.log"]);
211 let new_path = f.append_and_switch(PathBuf::from("b.log"));
212 assert_eq!(new_path, Path::new("b.log"));
213 assert_eq!(f.len(), 2);
214 assert_eq!(f.current_index(), 1);
215 }
216
217 #[test]
218 fn delete_current_drops_entry_and_advances() {
219 let mut f = fs(&["a.log", "b.log", "c.log"]);
220 f.next().unwrap(); let new_path = f.delete_current().unwrap();
222 assert_eq!(new_path, Path::new("c.log"));
223 assert_eq!(f.len(), 2);
224 assert_eq!(f.current_index(), 1);
225 }
226
227 #[test]
228 fn delete_current_at_end_moves_back() {
229 let mut f = fs(&["a.log", "b.log"]);
230 f.next().unwrap(); let new_path = f.delete_current().unwrap();
232 assert_eq!(new_path, Path::new("a.log"));
233 assert_eq!(f.len(), 1);
234 assert_eq!(f.current_index(), 0);
235 }
236
237 #[test]
238 fn delete_current_at_start_stays_at_zero() {
239 let mut f = fs(&["a.log", "b.log", "c.log"]);
240 let new_path = f.delete_current().unwrap();
242 assert_eq!(new_path, Path::new("b.log"));
243 assert_eq!(f.len(), 2);
244 assert_eq!(f.current_index(), 0);
245 }
246
247 #[test]
248 fn delete_current_with_single_file_returns_would_empty_error() {
249 let mut f = fs(&["a.log"]);
250 assert_eq!(f.delete_current().unwrap_err(), FileSetError::WouldEmpty);
251 assert_eq!(f.len(), 1);
252 }
253
254 #[test]
255 fn empty_fileset_returns_none_for_current() {
256 let f = FileSet::new(Vec::new());
257 assert_eq!(f.current(), None);
258 assert!(f.is_empty());
259 assert_eq!(f.len(), 0);
260 }
261
262 #[test]
263 fn set_current_index_changes_cursor() {
264 let mut f = fs(&["a.log", "b.log", "c.log"]);
265 f.set_current_index(2);
266 assert_eq!(f.current(), Some(Path::new("c.log")));
267 f.set_current_index(99); assert_eq!(f.current_index(), 2);
269 }
270
271 #[test]
272 fn nth_returns_path_or_none() {
273 let f = fs(&["a.log", "b.log"]);
274 assert_eq!(f.nth(0), Some(Path::new("a.log")));
275 assert_eq!(f.nth(1), Some(Path::new("b.log")));
276 assert_eq!(f.nth(2), None);
277 }
278
279 #[test]
280 fn next_on_empty_returns_no_next_file_error() {
281 let mut f = FileSet::new(Vec::new());
282 assert_eq!(f.next().unwrap_err(), FileSetError::NoNextFile);
283 }
284
285 #[test]
286 fn prev_on_empty_returns_no_previous_file_error() {
287 let mut f = FileSet::new(Vec::new());
288 assert_eq!(f.prev().unwrap_err(), FileSetError::NoPreviousFile);
289 }
290}