use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use clap::ValueEnum;
use crate::strategy::Nexter;
pub mod factory;
mod strategy;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Eq, PartialEq, Clone, ValueEnum)]
pub enum NexterType {
First,
Last,
Previous,
Next,
Random,
Keep
}
impl NexterType {
fn build(&self) -> strategy::Strategy {
match self {
NexterType::First => strategy::Strategy::First(strategy::First{}),
NexterType::Last => strategy::Strategy::Last(strategy::Last{}),
NexterType::Previous => strategy::Strategy::Previous(strategy::Previous{}),
NexterType::Next => strategy::Strategy::Next(strategy::Next{}),
NexterType::Random => strategy::Strategy::Random(strategy::Random{}),
NexterType::Keep => strategy::Strategy::Keep(strategy::Keep{}),
}
}
}
impl FromStr for NexterType {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"first" => Ok(NexterType::First),
"last" => Ok(NexterType::Last),
"previous" => Ok(NexterType::Previous),
"next" => Ok(NexterType::Next),
"random" => Ok(NexterType::Random),
"keep" => Ok(NexterType::Keep),
_ => Err(Error::UnknownNexterType(s.to_string())),
}
}
}
#[derive(Debug)]
pub enum Error {
Array(Vec<Error>),
Fatal(String),
Io(std::io::Error),
NotFound(PathBuf),
NoParent(PathBuf),
NotDir(PathBuf),
NotFile(PathBuf),
UnknownNexterType(String),
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Io(e) => write!(f, "I/O error: {e}"),
Error::NotDir(path) => write!(f, "{}: Not a directory", path.display()),
Error::NoParent(path) => write!(f, "{}: No parent directory", path.display()),
Error::Array(array) => array
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
.fmt(f),
Error::NotFile(path) => write!(f, "{}: Not a file", path.display()),
Error::NotFound(path) => write!(f, "{}: Not found", path.display()),
Error::Fatal(message) => write!(f, "Fatal error: {message}"),
Error::UnknownNexterType(s) => write!(f, "Unknown nexter type: {s}"),
}
}
}
pub trait Nextable {
fn current_index(&self) -> Option<usize>;
fn dirs(&self) -> &Dirs;
fn next(&self, nexter: NexterType) -> Option<Dir<'_>> {
self.next_with(nexter, 1)
}
fn next_with(&self, nexter: NexterType, step: i32) -> Option<Dir<'_>>;
}
#[derive(Debug, Clone)]
pub struct Dirs {
entries: Vec<PathBuf>,
parent: PathBuf,
current: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct Dir<'a> {
siblings: &'a Dirs,
index: usize,
}
impl Dir<'_> {
fn new(siblings: &Dirs, index: usize) -> Dir<'_> {
log::trace!("Dir::new(index={index})");
Dir {
siblings,
index,
}
}
pub fn path(&self) -> &Path {
&self.siblings.entries[self.index]
}
pub fn index(&self) -> usize {
self.index
}
pub fn dirs(self) -> Dirs {
Dirs {
entries: self.siblings.entries.clone(),
parent: self.siblings.parent.clone(),
current: Some(self.index),
}
}
}
impl Nextable for Dir<'_> {
fn current_index(&self) -> Option<usize> {
Some(self.index)
}
fn dirs(&self) -> &Dirs {
self.siblings
}
fn next_with(&self, nexter: NexterType, step: i32) -> Option<Dir<'_>> {
match nexter.build() {
strategy::Strategy::First(strategy) => strategy.next_with(self, step),
strategy::Strategy::Last(strategy) => strategy.next_with(self, step),
strategy::Strategy::Previous(strategy) => strategy.next_with(self, step),
strategy::Strategy::Next(strategy) => strategy.next_with(self, step),
strategy::Strategy::Random(strategy) => strategy.next_with(self, step),
strategy::Strategy::Keep(strategy) => strategy.next_with(self, step),
}
}
}
impl Nextable for Dirs {
fn current_index(&self) -> Option<usize> {
self.current
}
fn dirs(&self) -> &Dirs {
self
}
fn next_with(&self, nexter: NexterType, step: i32) -> Option<Dir<'_>> {
match nexter.build() {
strategy::Strategy::First(strategy) => strategy.next_with(self, step),
strategy::Strategy::Last(strategy) => strategy.next_with(self, step),
strategy::Strategy::Previous(strategy) => strategy.next_with(self, step),
strategy::Strategy::Next(strategy) => strategy.next_with(self, step),
strategy::Strategy::Random(strategy) => strategy.next_with(self, step),
strategy::Strategy::Keep(strategy) => strategy.next_with(self, step),
}
}
}
impl Dirs {
pub fn new(base_dir: PathBuf, entries: Vec<PathBuf>) -> Self {
Dirs {
entries,
parent: base_dir,
current: None,
}
}
pub fn new_with_wd(base_dir: PathBuf, entries: Vec<PathBuf>, cwd: PathBuf) -> Result<Self> {
match factory::find_current(&base_dir, &entries, &Some(cwd.clone())) {
None => {
log::debug!("Dirs::new_with_wd: current directory not found in siblings");
Err(Error::NotFound(cwd))
}
current => Ok(Dirs {
entries,
parent: base_dir,
current,
}),
}
}
pub fn parent(&self) -> &Path {
self.parent.as_path()
}
pub fn current(&self) -> Option<Dir<'_>> {
self.current.map(|index| Dir::new(self, index))
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn directories(&self) -> impl Iterator<Item = &PathBuf> {
self.entries.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::factory::*;
#[test]
fn test_nexter_type_from_str() {
for (name, expected) in [
("first", NexterType::First),
("last", NexterType::Last),
("previous", NexterType::Previous),
("next", NexterType::Next),
("random", NexterType::Random),
("keep", NexterType::Keep),
("NEXT", NexterType::Next), ] {
assert_eq!(name.parse::<NexterType>().unwrap(), expected, "{name}");
}
let e = "unknown"
.parse::<NexterType>()
.expect_err("unknown is not a name of the nexter type");
assert!(matches!(e, Error::UnknownNexterType(_)), "{e}");
}
#[test]
fn test_error_display_io() {
let err = Error::Io(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"denied",
));
assert_eq!(format!("{err}"), "I/O error: denied");
}
#[test]
fn test_error_display_array() {
let err = Error::Array(vec![
Error::NotDir(PathBuf::from("/path/to/file")),
Error::NotFound(PathBuf::from("/path/to/dir")),
]);
assert_eq!(
format!("{err}"),
"/path/to/file: Not a directory, /path/to/dir: Not found"
);
}
#[test]
fn test_dir_is_nextable() {
let config = &Config::new_with_wd("testdata/basic", false, "testdata/basic/c");
let dirs = DirsFactory::create_with(config).expect("Failed to create Dirs");
let d = dirs.next(NexterType::Next).expect("the next directory of c");
assert_eq!(d.current_index(), Some(3));
assert_eq!(Nextable::dirs(&d).len(), 26);
for (nexter, name) in [
(NexterType::Next, "testdata/basic/e"),
(NexterType::Previous, "testdata/basic/c"),
(NexterType::First, "testdata/basic/a"),
(NexterType::Last, "testdata/basic/z"),
(NexterType::Keep, "testdata/basic/d"),
] {
let found = d.next(nexter.clone()).unwrap_or_else(|| panic!("{nexter:?}"));
assert!(found.path().ends_with(name), "{nexter:?}: {:?}", found.path());
}
let found = d.next(NexterType::Random).expect("the random directory");
assert!(dirs.directories().any(|dir| dir == found.path()));
let from_d = d.dirs();
assert_eq!(from_d.current().map(|c| c.index()), Some(3));
assert_eq!(from_d.len(), 26);
}
#[test]
fn test_dirs_new_with_wd() {
let base = PathBuf::from("testdata/basic");
let entries = vec![
PathBuf::from("testdata/basic/a"),
PathBuf::from("testdata/basic/b"),
];
let dirs =
Dirs::new_with_wd(base.clone(), entries.clone(), PathBuf::from("testdata/basic/b"))
.expect("b is in the entries");
assert_eq!(dirs.current().map(|c| c.index()), Some(1));
let e = Dirs::new_with_wd(base, entries, PathBuf::from("testdata/basic/z"))
.expect_err("z is not in the entries");
assert!(matches!(e, Error::NotFound(_)), "{e}");
}
#[test]
fn test_directories() {
let dirs = DirsFactory::create("testdata/worried").expect("Failed to create Dirs");
let names = dirs
.directories()
.map(|d| d.file_name().unwrap().to_string_lossy().to_string())
.collect::<Vec<_>>();
assert_eq!(names, vec!["dir with spaces", "multibyte_chars_\u{1f44d}"]);
}
#[test]
fn test_error_display_not_dir() {
let err = Error::NotDir(PathBuf::from("/path/to/file"));
assert_eq!(
format!("{}", err),
"/path/to/file: Not a directory".to_string()
);
}
#[test]
fn test_error_display_not_file() {
let err = Error::NotFile(PathBuf::from("/path/to/file"));
assert_eq!(
format!("{}", err),
"/path/to/file: Not a file".to_string()
);
}
#[test]
fn test_error_display_no_parent() {
let err = Error::NoParent(PathBuf::from("/path/to/file"));
assert_eq!(
format!("{}", err),
"/path/to/file: No parent directory".to_string()
);
}
#[test]
fn test_error_display_fatal() {
let err = Error::Fatal("Some fatal error".into());
assert_eq!(
format!("{}", err),
"Fatal error: Some fatal error".to_string()
);
}
#[test]
fn test_error_display_unknown_nexter_type() {
let err = Error::UnknownNexterType("unknown".into());
assert_eq!(
format!("{}", err),
"Unknown nexter type: unknown".to_string()
);
}
#[test]
fn test_empty_dirs() {
let dirs = Dirs::new(PathBuf::from("testdata"), vec![]);
assert!(dirs.is_empty());
assert_eq!(dirs.len(), 0);
assert!(dirs.current().is_none());
for nexter in [
NexterType::First,
NexterType::Last,
NexterType::Previous,
NexterType::Next,
NexterType::Random,
NexterType::Keep,
] {
assert!(
dirs.next(nexter.clone()).is_none(),
"{nexter:?}: should find no directory"
);
}
}
#[test]
fn test_dirs_new() {
let dirs = DirsFactory::create(PathBuf::from("testdata/basic"))
.expect("Failed to create Dirs");
assert!(dirs.current().is_none());
let dir = dirs.next_with(NexterType::Next, 3)
.expect("Failed to get next directory");
assert_eq!(dir.path().file_name().unwrap(), "c");
assert_eq!(dirs.len(), 26);
assert_eq!(dir.index(), 2);
assert!(dirs.next(NexterType::Previous).is_none());
assert!(dirs.next(NexterType::Keep).is_none());
assert_eq!(
dirs.next(NexterType::Next).map(|d| d.index()),
Some(0),
"the next of the unknown position is the first directory"
);
}
#[test]
fn test_worried_dirs() {
let config = Config::new_with_wd("testdata/worried", false, "dir with spaces");
let dirs = DirsFactory::create_with(&config);
assert!(dirs.is_ok());
let dirs = dirs.unwrap();
assert_eq!(dirs.len(), 2);
assert_eq!(dirs.current, Some(0));
}
#[test]
fn test_dir_dot() {
let dirs = DirsFactory::create(PathBuf::from("."))
.expect("Failed to create Dirs");
assert!(dirs.current().is_none());
assert_eq!(
dirs.next(NexterType::Next)
.map(|d| d.path().file_name().unwrap().to_string_lossy().to_string()),
Some(String::from(".bin"))
);
}
#[test]
fn test_dirs() {
let config = &Config::new_with_wd("testdata/basic", false, "d");
let dirs = DirsFactory::create_with(config)
.expect("Failed to create Dirs");
assert_eq!(dirs.parent(), Path::new("testdata/basic"));
assert_eq!(dirs.current().unwrap().index(), 3);
assert!(!dirs.is_empty());
assert_eq!(dirs.len(), 26);
}
#[test]
fn test_dir_from_file() {
let dirs = DirsFactory::create_from_file("testdata/basic/dirlist.txt",
&Config::new("testdata/basic", true))
.expect("Failed to create Dirs from file");
assert_eq!(dirs.len(), 3);
assert!(dirs.current().is_none());
assert_eq!(dirs.parent, Path::new("testdata/basic"));
}
#[test]
fn test_nexter_first() {
let config = &Config::new_with_wd("testdata/basic", false, "c");
let dirs = DirsFactory::create_with(config)
.expect("Failed to create Dirs");
match &dirs.next(NexterType::First) {
Some(p) => assert!(p.path().ends_with("testdata/basic/a")),
None => panic!("unexpected None"),
}
}
#[test]
fn test_nexter_keep() {
let config = &Config::new_with_wd("testdata/basic", false, "c");
let dirs = DirsFactory::create_with(config)
.expect("Failed to create Dirs");
match dirs.next(NexterType::Keep) {
Some(p) => assert!(p.path().ends_with("testdata/basic/c")),
None => panic!("unexpected None"),
}
}
#[test]
fn test_nexter_last() {
let config = &Config::new_with_wd("testdata/basic", false, "c");
let dirs = DirsFactory::create_with(config)
.expect("Failed to create Dirs");
match dirs.next(NexterType::Last) {
Some(p) => assert!(p.path().ends_with("testdata/basic/z")),
None => panic!("unexpected None"),
}
}
#[test]
fn test_nexter_next() {
let config = &Config::new_with_wd("testdata/basic", false, "c");
let dirs = DirsFactory::create_with(config)
.expect("Failed to create Dirs");
match dirs.next_with(NexterType::Next, 1) {
Some(p) => assert!(p.path().ends_with("testdata/basic/d")),
None => panic!("unexpected None"),
}
match dirs.next_with(NexterType::Next, 2) {
Some(p) => assert!(p.path().ends_with("testdata/basic/e"), "{:?}", p.path()),
None => panic!("unexpected None"),
}
match dirs.next_with(NexterType::Next, 23) {
Some(p) => assert!(p.path().ends_with("testdata/basic/z"), "{:?}", p.path()),
None => panic!("unexpected None"),
}
match dirs.next_with(NexterType::Next, 24) {
None => {}
Some(p) => panic!("unexpected {:?}", p.path()),
}
}
#[test]
fn test_nexter_with_negative_step() {
let config = &Config::new_with_wd("testdata/basic", false, "k");
let dirs = DirsFactory::create_with(config)
.expect("Failed to create Dirs");
match dirs.next_with(NexterType::Next, -3) {
Some(p) => assert!(p.path().ends_with("testdata/basic/h")),
None => panic!("unexpected None"),
}
match dirs.next_with(NexterType::Previous, -3) {
Some(p) => assert!(p.path().ends_with("testdata/basic/n")),
None => panic!("unexpected None"),
}
match dirs.next_with(NexterType::Next, 0) {
Some(p) => assert!(p.path().ends_with("testdata/basic/k")),
None => panic!("unexpected None"),
}
assert!(dirs.next_with(NexterType::Next, -11).is_none());
}
#[test]
fn test_nexter_prev() {
let config = &Config::new_with_wd("testdata/basic", false, "k");
let dirs = DirsFactory::create_with(config)
.expect("Failed to create Dirs");
match dirs.next(NexterType::Previous) {
Some(p) => assert!(p.path().ends_with("testdata/basic/j")),
None => panic!("unexpected None"),
}
match dirs.next_with(NexterType::Previous, 1) {
Some(p) => assert!(p.path().ends_with("testdata/basic/j")),
None => panic!("unexpected None"),
}
match dirs.next_with(NexterType::Previous, 4) {
Some(p) => assert!(p.path().ends_with("testdata/basic/g")),
None => panic!("unexpected None"),
}
match dirs.next_with(NexterType::Previous, 10) {
Some(p) => assert!(p.path().ends_with("testdata/basic/a")),
None => panic!("unexpected None"),
}
if let Some(p) = dirs.next_with(NexterType::Previous, 11) {
panic!("unexpected {:?}", p.path())
}
}
}