use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileSetError {
NoNextFile,
NoPreviousFile,
WouldEmpty,
}
impl std::fmt::Display for FileSetError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FileSetError::NoNextFile => write!(f, "no next file"),
FileSetError::NoPreviousFile => write!(f, "no previous file"),
FileSetError::WouldEmpty => write!(f, "cannot remove last file"),
}
}
}
#[derive(Debug, Clone)]
pub struct FileSet {
paths: Vec<PathBuf>,
current_index: usize,
}
impl FileSet {
pub fn new(paths: Vec<PathBuf>) -> Self {
Self { paths, current_index: 0 }
}
pub fn current(&self) -> Option<&Path> {
self.paths.get(self.current_index).map(|p| p.as_path())
}
pub fn len(&self) -> usize {
self.paths.len()
}
pub fn current_index(&self) -> usize {
self.current_index
}
pub fn is_empty(&self) -> bool {
self.paths.is_empty()
}
pub fn set_current_index(&mut self, index: usize) {
if self.paths.is_empty() {
return;
}
self.current_index = index.min(self.paths.len() - 1);
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<&Path, FileSetError> {
if self.current_index + 1 >= self.paths.len() {
return Err(FileSetError::NoNextFile);
}
self.current_index += 1;
Ok(self.paths[self.current_index].as_path())
}
pub fn prev(&mut self) -> Result<&Path, FileSetError> {
if self.current_index == 0 {
return Err(FileSetError::NoPreviousFile);
}
self.current_index -= 1;
Ok(self.paths[self.current_index].as_path())
}
pub fn first(&mut self) -> Option<&Path> {
if self.paths.is_empty() {
return None;
}
self.current_index = 0;
Some(self.paths[0].as_path())
}
pub fn last(&mut self) -> Option<&Path> {
if self.paths.is_empty() {
return None;
}
self.current_index = self.paths.len() - 1;
Some(self.paths[self.current_index].as_path())
}
pub fn nth(&self, i: usize) -> Option<&Path> {
self.paths.get(i).map(|p| p.as_path())
}
pub fn append_and_switch(&mut self, path: PathBuf) -> &Path {
self.paths.push(path);
self.current_index = self.paths.len() - 1;
self.paths[self.current_index].as_path()
}
pub fn delete_current(&mut self) -> Result<&Path, FileSetError> {
if self.paths.len() <= 1 {
return Err(FileSetError::WouldEmpty);
}
self.paths.remove(self.current_index);
if self.current_index >= self.paths.len() {
self.current_index = self.paths.len() - 1;
}
Ok(self.paths[self.current_index].as_path())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fs(names: &[&str]) -> FileSet {
FileSet::new(names.iter().map(PathBuf::from).collect())
}
#[test]
fn new_with_paths_sets_current_zero() {
let f = fs(&["a.log", "b.log", "c.log"]);
assert_eq!(f.current_index(), 0);
assert_eq!(f.current(), Some(Path::new("a.log")));
}
#[test]
fn len_reports_total() {
let f = fs(&["a.log", "b.log", "c.log"]);
assert_eq!(f.len(), 3);
}
#[test]
fn next_advances_index() {
let mut f = fs(&["a.log", "b.log", "c.log"]);
assert_eq!(f.next().unwrap(), Path::new("b.log"));
assert_eq!(f.current_index(), 1);
assert_eq!(f.next().unwrap(), Path::new("c.log"));
assert_eq!(f.current_index(), 2);
}
#[test]
fn next_at_last_returns_no_next_file_error() {
let mut f = fs(&["a.log"]);
assert_eq!(f.next().unwrap_err(), FileSetError::NoNextFile);
assert_eq!(f.current_index(), 0);
}
#[test]
fn prev_decrements_index() {
let mut f = fs(&["a.log", "b.log"]);
f.next().unwrap();
assert_eq!(f.prev().unwrap(), Path::new("a.log"));
assert_eq!(f.current_index(), 0);
}
#[test]
fn prev_at_first_returns_no_previous_file_error() {
let mut f = fs(&["a.log", "b.log"]);
assert_eq!(f.prev().unwrap_err(), FileSetError::NoPreviousFile);
assert_eq!(f.current_index(), 0);
}
#[test]
fn first_resets_to_zero() {
let mut f = fs(&["a.log", "b.log", "c.log"]);
f.next().unwrap();
f.next().unwrap();
assert_eq!(f.first(), Some(Path::new("a.log")));
assert_eq!(f.current_index(), 0);
}
#[test]
fn last_jumps_to_count_minus_one() {
let mut f = fs(&["a.log", "b.log", "c.log"]);
assert_eq!(f.last(), Some(Path::new("c.log")));
assert_eq!(f.current_index(), 2);
}
#[test]
fn append_and_switch_grows_list_and_moves_cursor() {
let mut f = fs(&["a.log"]);
let new_path = f.append_and_switch(PathBuf::from("b.log"));
assert_eq!(new_path, Path::new("b.log"));
assert_eq!(f.len(), 2);
assert_eq!(f.current_index(), 1);
}
#[test]
fn delete_current_drops_entry_and_advances() {
let mut f = fs(&["a.log", "b.log", "c.log"]);
f.next().unwrap(); let new_path = f.delete_current().unwrap();
assert_eq!(new_path, Path::new("c.log"));
assert_eq!(f.len(), 2);
assert_eq!(f.current_index(), 1);
}
#[test]
fn delete_current_at_end_moves_back() {
let mut f = fs(&["a.log", "b.log"]);
f.next().unwrap(); let new_path = f.delete_current().unwrap();
assert_eq!(new_path, Path::new("a.log"));
assert_eq!(f.len(), 1);
assert_eq!(f.current_index(), 0);
}
#[test]
fn delete_current_at_start_stays_at_zero() {
let mut f = fs(&["a.log", "b.log", "c.log"]);
let new_path = f.delete_current().unwrap();
assert_eq!(new_path, Path::new("b.log"));
assert_eq!(f.len(), 2);
assert_eq!(f.current_index(), 0);
}
#[test]
fn delete_current_with_single_file_returns_would_empty_error() {
let mut f = fs(&["a.log"]);
assert_eq!(f.delete_current().unwrap_err(), FileSetError::WouldEmpty);
assert_eq!(f.len(), 1);
}
#[test]
fn empty_fileset_returns_none_for_current() {
let f = FileSet::new(Vec::new());
assert_eq!(f.current(), None);
assert!(f.is_empty());
assert_eq!(f.len(), 0);
}
#[test]
fn set_current_index_changes_cursor() {
let mut f = fs(&["a.log", "b.log", "c.log"]);
f.set_current_index(2);
assert_eq!(f.current(), Some(Path::new("c.log")));
f.set_current_index(99); assert_eq!(f.current_index(), 2);
}
#[test]
fn nth_returns_path_or_none() {
let f = fs(&["a.log", "b.log"]);
assert_eq!(f.nth(0), Some(Path::new("a.log")));
assert_eq!(f.nth(1), Some(Path::new("b.log")));
assert_eq!(f.nth(2), None);
}
#[test]
fn next_on_empty_returns_no_next_file_error() {
let mut f = FileSet::new(Vec::new());
assert_eq!(f.next().unwrap_err(), FileSetError::NoNextFile);
}
#[test]
fn prev_on_empty_returns_no_previous_file_error() {
let mut f = FileSet::new(Vec::new());
assert_eq!(f.prev().unwrap_err(), FileSetError::NoPreviousFile);
}
}