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 append_and_switch(&mut self, path: PathBuf) -> &Path {
115 self.paths.push(path);
116 self.current_index = self.paths.len() - 1;
117 self.paths[self.current_index].as_path()
118 }
119
120 pub fn delete_current(&mut self) -> Result<&Path, FileSetError> {
124 if self.paths.len() <= 1 {
125 return Err(FileSetError::WouldEmpty);
126 }
127 self.paths.remove(self.current_index);
128 if self.current_index >= self.paths.len() {
129 self.current_index = self.paths.len() - 1;
130 }
131 Ok(self.paths[self.current_index].as_path())
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn fs(names: &[&str]) -> FileSet {
140 FileSet::new(names.iter().map(PathBuf::from).collect())
141 }
142
143 #[test]
144 fn new_with_paths_sets_current_zero() {
145 let f = fs(&["a.log", "b.log", "c.log"]);
146 assert_eq!(f.current_index(), 0);
147 assert_eq!(f.current(), Some(Path::new("a.log")));
148 }
149
150 #[test]
151 fn len_reports_total() {
152 let f = fs(&["a.log", "b.log", "c.log"]);
153 assert_eq!(f.len(), 3);
154 }
155
156 #[test]
157 fn next_advances_index() {
158 let mut f = fs(&["a.log", "b.log", "c.log"]);
159 assert_eq!(f.next().unwrap(), Path::new("b.log"));
160 assert_eq!(f.current_index(), 1);
161 assert_eq!(f.next().unwrap(), Path::new("c.log"));
162 assert_eq!(f.current_index(), 2);
163 }
164
165 #[test]
166 fn next_at_last_returns_no_next_file_error() {
167 let mut f = fs(&["a.log"]);
168 assert_eq!(f.next().unwrap_err(), FileSetError::NoNextFile);
169 assert_eq!(f.current_index(), 0);
170 }
171
172 #[test]
173 fn prev_decrements_index() {
174 let mut f = fs(&["a.log", "b.log"]);
175 f.next().unwrap();
176 assert_eq!(f.prev().unwrap(), Path::new("a.log"));
177 assert_eq!(f.current_index(), 0);
178 }
179
180 #[test]
181 fn prev_at_first_returns_no_previous_file_error() {
182 let mut f = fs(&["a.log", "b.log"]);
183 assert_eq!(f.prev().unwrap_err(), FileSetError::NoPreviousFile);
184 assert_eq!(f.current_index(), 0);
185 }
186
187 #[test]
188 fn first_resets_to_zero() {
189 let mut f = fs(&["a.log", "b.log", "c.log"]);
190 f.next().unwrap();
191 f.next().unwrap();
192 assert_eq!(f.first(), Some(Path::new("a.log")));
193 assert_eq!(f.current_index(), 0);
194 }
195
196 #[test]
197 fn last_jumps_to_count_minus_one() {
198 let mut f = fs(&["a.log", "b.log", "c.log"]);
199 assert_eq!(f.last(), Some(Path::new("c.log")));
200 assert_eq!(f.current_index(), 2);
201 }
202
203 #[test]
204 fn append_and_switch_grows_list_and_moves_cursor() {
205 let mut f = fs(&["a.log"]);
206 let new_path = f.append_and_switch(PathBuf::from("b.log"));
207 assert_eq!(new_path, Path::new("b.log"));
208 assert_eq!(f.len(), 2);
209 assert_eq!(f.current_index(), 1);
210 }
211
212 #[test]
213 fn delete_current_drops_entry_and_advances() {
214 let mut f = fs(&["a.log", "b.log", "c.log"]);
215 f.next().unwrap(); let new_path = f.delete_current().unwrap();
217 assert_eq!(new_path, Path::new("c.log"));
218 assert_eq!(f.len(), 2);
219 assert_eq!(f.current_index(), 1);
220 }
221
222 #[test]
223 fn delete_current_at_end_moves_back() {
224 let mut f = fs(&["a.log", "b.log"]);
225 f.next().unwrap(); let new_path = f.delete_current().unwrap();
227 assert_eq!(new_path, Path::new("a.log"));
228 assert_eq!(f.len(), 1);
229 assert_eq!(f.current_index(), 0);
230 }
231
232 #[test]
233 fn delete_current_at_start_stays_at_zero() {
234 let mut f = fs(&["a.log", "b.log", "c.log"]);
235 let new_path = f.delete_current().unwrap();
237 assert_eq!(new_path, Path::new("b.log"));
238 assert_eq!(f.len(), 2);
239 assert_eq!(f.current_index(), 0);
240 }
241
242 #[test]
243 fn delete_current_with_single_file_returns_would_empty_error() {
244 let mut f = fs(&["a.log"]);
245 assert_eq!(f.delete_current().unwrap_err(), FileSetError::WouldEmpty);
246 assert_eq!(f.len(), 1);
247 }
248
249 #[test]
250 fn empty_fileset_returns_none_for_current() {
251 let f = FileSet::new(Vec::new());
252 assert_eq!(f.current(), None);
253 assert!(f.is_empty());
254 assert_eq!(f.len(), 0);
255 }
256
257 #[test]
258 fn set_current_index_changes_cursor() {
259 let mut f = fs(&["a.log", "b.log", "c.log"]);
260 f.set_current_index(2);
261 assert_eq!(f.current(), Some(Path::new("c.log")));
262 f.set_current_index(99); assert_eq!(f.current_index(), 2);
264 }
265
266 #[test]
267 fn next_on_empty_returns_no_next_file_error() {
268 let mut f = FileSet::new(Vec::new());
269 assert_eq!(f.next().unwrap_err(), FileSetError::NoNextFile);
270 }
271
272 #[test]
273 fn prev_on_empty_returns_no_previous_file_error() {
274 let mut f = FileSet::new(Vec::new());
275 assert_eq!(f.prev().unwrap_err(), FileSetError::NoPreviousFile);
276 }
277}