From 6cca8338d9ae36b842f9f61174b1cf17bf08a833 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Mon, 24 Mar 2025 11:50:05 +0900 Subject: [PATCH 1/9] separate the source codes into library (API) and CLI interface. --- Cargo.toml | 34 +--- cli/Cargo.toml | 17 ++ cli/src/cli.rs | 85 ++++++++ {src => cli/src}/init.rs | 16 +- {src => cli/src}/main.rs | 65 +++--- cli/src/printer.rs | 92 +++++++++ lib/Cargo.toml | 15 ++ lib/src/lib.rs | 420 +++++++++++++++++++++++++++++++++++++++ src/cli.rs | 72 ------- src/dirs.rs | 176 ---------------- src/nexter.rs | 163 --------------- src/printer.rs | 67 ------- wix/main.wxs | 228 --------------------- 13 files changed, 689 insertions(+), 761 deletions(-) create mode 100644 cli/Cargo.toml create mode 100644 cli/src/cli.rs rename {src => cli/src}/init.rs (58%) rename {src => cli/src}/main.rs (68%) create mode 100644 cli/src/printer.rs create mode 100644 lib/Cargo.toml create mode 100644 lib/src/lib.rs delete mode 100644 src/cli.rs delete mode 100644 src/dirs.rs delete mode 100644 src/nexter.rs delete mode 100644 src/printer.rs delete mode 100644 wix/main.wxs diff --git a/Cargo.toml b/Cargo.toml index ba47794..e3ef55e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,38 +1,20 @@ -[package] -name = "sibling" -version = "2.0.0-beta-7" -description = "get next/previous sibling directory name." +[workspace] +members = [ "lib", "cli" ] + +resolver = "3" + +[workspace.package] +version = "2.0.0-beta-10" repository = "https://github.com/tamada/sibling" homepage = "https://tamada.github.io/sibling" readme = "README.md" authors = [ "Haruaki Tamada " ] -license = "WTFL" +license-file = "LICENSE" categories = ["command-line-utilities"] edition = "2021" -[package.metadata.wix] -upgrade-guid = "D4DB88AD-BA23-4F88-A9B4-74C774EBEA37" -path-guid = "5A8D6CFE-2D9B-4F9E-80F2-439C0D9E43BC" -license = false -eula = false - -[dependencies] -clap = { version = "4.5.23", features = ["derive"] } -rand = "0.8.5" -rust-embed = "8.5.0" - -[build-dependencies] -clap = { version = "4.5.5", features = ["derive"] } -clap_complete = "4.5.38" -toml = "0.8.19" - -# The profile that 'cargo dist' will build with -[profile.dist] -inherits = "release" -lto = "thin" - # Config for 'cargo dist' [workspace.metadata.dist] # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..6135f0f --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "sibling-cli" +readme = "README.md" +description = "get next/previous sibling directory name." +categories = ["command-line-utilities"] +repository.workspace = true +homepage.workspace = true +version.workspace = true +authors.workspace = true +edition.workspace = true + +[dependencies] +clap = { version = "4.5.23", features = ["derive"] } +clap_complete = "4.5.38" +rust-embed = "8.5.0" +sibling = { path = "../lib"} +toml = "0.8.19" diff --git a/cli/src/cli.rs b/cli/src/cli.rs new file mode 100644 index 0000000..f2e368b --- /dev/null +++ b/cli/src/cli.rs @@ -0,0 +1,85 @@ +use std::path::PathBuf; + +use clap::Parser; + +#[derive(Debug, Parser)] +#[clap(version, author, about, arg_required_else_help = true)] +pub struct CliOpts { + #[clap(flatten)] + pub(crate) p_opts: PrintingOpts, + + #[arg( + short, + long, + help = "specify the number of times to execute sibling", + value_name = "COUNT", + default_value_t = 1 + )] + pub step: usize, + + #[arg( + long, + help = "generate the initialize script for the shell", + value_name = "SHELL", + hide = true, + default_missing_value = "bash" + )] + pub init: Option, + + #[arg(short = 't', long = "type", help = "specify the nexter type", value_enum, default_value_t = sibling::NexterType::Next, value_name = "TYPE", ignore_case = true)] + pub nexter: sibling::NexterType, + + #[arg( + short, + long, + help = "directory list from file, if FILE is \"-\", reads from stdin.", + value_name = "FILE" + )] + pub input: Option, + + #[arg(index = 1, help = "the target directory", value_name = "DIR")] + pub dirs: Vec, +} + +#[derive(Debug, Parser)] +pub(crate) struct PrintingOpts { + #[arg( + long, + help = "print the result in the csv format", + default_value_t = false, + hide = true + )] + pub csv: bool, + + #[arg( + short, + long, + help = "print the directory name in the absolute path", + default_value_t = false + )] + pub absolute: bool, + + #[arg( + short, + long, + help = "list the sibling directories", + default_value_t = false + )] + pub list: bool, + + #[arg( + short, + long, + help = "print the progress of traversing directories", + default_value_t = false + )] + pub progress: bool, + + #[arg( + short = 'P', + long, + help = "print parent directory, when no more sibling directories are found", + default_value_t = false + )] + pub parent: bool, +} diff --git a/src/init.rs b/cli/src/init.rs similarity index 58% rename from src/init.rs rename to cli/src/init.rs index 0ec592f..084921f 100644 --- a/src/init.rs +++ b/cli/src/init.rs @@ -1,21 +1,29 @@ use rust_embed::Embed; -use crate::cli::{Result, SiblingError}; +use sibling::{Result, SiblingError}; #[derive(Embed)] -#[folder = "assets/init"] +#[folder = "../assets/init"] struct Assets; pub(crate) fn generate_init_script(shell_name: String) -> Result { let script_file = match shell_name.to_lowercase().as_str() { "bash" => "init.bash", "zsh" => "init.bash", - _ => return Err(SiblingError::Fatal(format!("{}: Unsupported shell", shell_name))), + _ => { + return Err(SiblingError::Fatal(format!( + "{}: Unsupported shell", + shell_name + ))) + } }; match Assets::get(script_file) { Some(file) => match std::str::from_utf8(file.data.as_ref()) { Ok(script) => Ok(script.to_string()), - Err(_) => Err(SiblingError::Fatal(format!("{}: Invalid script", script_file))), + Err(_) => Err(SiblingError::Fatal(format!( + "{}: Invalid script", + script_file + ))), }, None => Err(SiblingError::NotFound(script_file.into())), } diff --git a/src/main.rs b/cli/src/main.rs similarity index 68% rename from src/main.rs rename to cli/src/main.rs index 2b3de80..0df52c6 100644 --- a/src/main.rs +++ b/cli/src/main.rs @@ -1,42 +1,51 @@ use std::path::PathBuf; use std::vec; +use crate::cli::{CliOpts, PrintingOpts}; use clap::Parser; -use crate::cli::{CliOpts, PrintingOpts, Result, SiblingError}; -use crate::nexter::Nexter; +use sibling::Nexter; +use sibling::{Result, SiblingError}; mod cli; -mod dirs; mod init; -mod nexter; mod printer; -fn perform_impl(mut dirs: dirs::Dirs, nexter: &Box, step: i32, opts: &PrintingOpts) -> Result { - nexter.next(&mut dirs, step); - printer::result_string(&dirs, opts) +fn perform_impl( + dirs: sibling::Dirs, + nexter: &dyn Nexter, + step: usize, + opts: &PrintingOpts, +) -> Result { + let next = dirs.next_with(nexter, step); + printer::result_string(&dirs, next, opts) } fn perform_from_file(opts: CliOpts) -> Vec> { - let nexter = nexter::build_nexter(opts.nexter); + let nexter = sibling::build_nexter(opts.nexter); let r = match opts.input { None => Err(SiblingError::Fatal("input is not specified".into())), - Some(file) => match dirs::Dirs::new_from_file(file) { + Some(file) => match sibling::Dirs::new_from_file(file) { Err(e) => Err(e), - Ok(dirs) => perform_impl(dirs, &nexter, opts.step, &opts.p_opts), - } + Ok(dirs) => perform_impl(dirs, nexter.as_ref(), opts.step, &opts.p_opts), + }, }; vec![r] } -fn perform_each(dir: std::path::PathBuf, nexter: &Box, step: i32, opts: &PrintingOpts) -> Result { - match dirs::Dirs::new(dir) { +fn perform_each( + dir: std::path::PathBuf, + nexter: &dyn Nexter, + step: usize, + opts: &PrintingOpts, +) -> Result { + match sibling::Dirs::new(dir) { Err(e) => Err(e), Ok(dirs) => perform_impl(dirs, nexter, step, opts), } } fn perform_sibling(opts: CliOpts) -> Vec> { - let nexter = nexter::build_nexter(opts.nexter); + let nexter = sibling::build_nexter(opts.nexter); let target_dirs = if opts.dirs.is_empty() { vec![std::env::current_dir().unwrap()] } else { @@ -47,9 +56,9 @@ fn perform_sibling(opts: CliOpts) -> Vec> { let dir = if dir == PathBuf::from(".") { std::env::current_dir().unwrap() } else { - std::path::PathBuf::from(dir) + dir }; - let r = perform_each(dir, &nexter, opts.step, &opts.p_opts); + let r = perform_each(dir, nexter.as_ref(), opts.step, &opts.p_opts); result.push(r); } result @@ -72,10 +81,10 @@ fn print_error(e: &SiblingError) { SiblingError::NoParent(path) => eprintln!("{:?}: no parent directory", path), SiblingError::Array(array) => { array.iter().for_each(print_error); - }, + } SiblingError::NotFile(path) => eprintln!("{:?}: not a file", path), SiblingError::NotFound(path) => eprintln!("{:?}: not found", path), - SiblingError::Fatal(message) => eprintln!("fatal error: {}", message) + SiblingError::Fatal(message) => eprintln!("fatal error: {}", message), } } @@ -109,15 +118,21 @@ mod tests { assert!(opts_r.is_ok()); let r = perform(opts_r.unwrap()); assert_eq!(r.len(), 1); - match r.get(0).unwrap() { - Err(e) => print_error(&e), + match r.first().unwrap() { + Err(e) => print_error(e), Ok(result) => println!("{}", result), } } #[test] fn test_from_file() { - let opts_r = cli::CliOpts::try_parse_from(vec!["sibling", "--input", "testdata/dirlist.txt", "--type", "previous"]); + let opts_r = cli::CliOpts::try_parse_from(vec![ + "sibling", + "--input", + "testdata/dirlist.txt", + "--type", + "previous", + ]); if let Err(e) = &opts_r { eprintln!("{}", e); @@ -125,9 +140,9 @@ mod tests { assert!(opts_r.is_ok()); let r = perform(opts_r.unwrap()); assert_eq!(r.len(), 1); - match r.get(0).unwrap() { - Err(e) => print_error(&e), - Ok(result) => assert_eq!(result, "testdata/a") + match r.first().unwrap() { + Err(e) => print_error(e), + Ok(result) => assert_eq!(result, "testdata/a"), } } -} \ No newline at end of file +} diff --git a/cli/src/printer.rs b/cli/src/printer.rs new file mode 100644 index 0000000..900fc14 --- /dev/null +++ b/cli/src/printer.rs @@ -0,0 +1,92 @@ +use std::path::PathBuf; + +use crate::cli::PrintingOpts; +use sibling::{Dir, Dirs, Result}; + +pub(crate) fn result_string( + dirs: &Dirs, + next: Option>, + opts: &PrintingOpts, +) -> Result { + if opts.csv { + csv_string(dirs, next, opts.absolute) + } else if next.is_some() && next.clone().unwrap().is_last_item() { + no_more_dir_string(dirs, opts) + } else if opts.list { + list_string(dirs, next, opts) + } else { + result_string_impl(dirs, next, opts) + } +} + +fn csv_string(dirs: &Dirs, next: Option>, absolute: bool) -> Result { + let current = dirs.current(); + Ok(format!( + r##""{}","{}",{},{},{}"##, + pathbuf_to_string(Some(dirs.current().path()), absolute), + pathbuf_to_string(next.clone().map(|p| p.path()), absolute), + current.index() + 1, + next.map(|n| n.index() as i32 + 1).unwrap_or(-1), + dirs.len() + )) +} + +fn no_more_dir_string(dirs: &Dirs, opts: &PrintingOpts) -> Result { + if opts.parent { + Ok(pathbuf_to_string(Some(dirs.parent()), opts.absolute)) + } else { + Ok(String::from("no more sibling directory")) + } +} + +fn list_string(dirs: &Dirs, next: Option>, opts: &PrintingOpts) -> Result { + let mut result = vec![]; + let current = dirs.current(); + let next_index = next.clone().map(|n| n.index() as i32).unwrap_or(-1); + for (i, dir) in dirs.directories().enumerate() { + let prefix = if i as i32 == next_index { + "> " + } else if i == current.index() { + "* " + } else { + " " + }; + result.push(format!( + "{:>4} {}{}", + i + 1, + prefix, + pathbuf_to_string(Some(dir.to_path_buf()), opts.absolute) + )); + } + Ok(result.join("\n")) +} + +fn result_string_impl(dirs: &Dirs, next: Option>, opts: &PrintingOpts) -> Result { + let r = if opts.progress { + format!( + "{} ({}/{})", + pathbuf_to_string(next.clone().map(|n| n.path()), opts.absolute), + next.map(|n| n.index() as i32).unwrap_or(-1) + 1, + dirs.len() + ) + } else { + pathbuf_to_string(next.map(|n| n.path()), opts.absolute).to_string() + }; + Ok(r) +} + +fn pathbuf_to_string(path: Option, absolute: bool) -> String { + match path { + Some(p) => { + if absolute { + std::fs::canonicalize(p) + .unwrap() + .to_string_lossy() + .to_string() + } else { + p.to_string_lossy().to_string() + } + } + None => "".to_string(), + } +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml new file mode 100644 index 0000000..1f84f6f --- /dev/null +++ b/lib/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sibling" +readme = "README.md" +description = "The API for sibling (get next/previous sibling directory name)." +categories = ["command-line-utilities"] +version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true + +[dependencies] +rand = "0.9.0" +rust-embed = "8.5.0" +clap = { version = "4.5.23", features = ["derive"] } diff --git a/lib/src/lib.rs b/lib/src/lib.rs new file mode 100644 index 0000000..a2385fb --- /dev/null +++ b/lib/src/lib.rs @@ -0,0 +1,420 @@ +//! The sibling library. +//! +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; + +use clap::ValueEnum; + +pub type Result = std::result::Result; + +/// The type of the nexter. +#[derive(Debug, Eq, PartialEq, Clone, ValueEnum)] +pub enum NexterType { + First, + Last, + Previous, + Next, + Random, + Keep, +} + +/// The error type for sibling. +#[derive(Debug)] +pub enum SiblingError { + Io(std::io::Error), + NotFound(PathBuf), + NoParent(PathBuf), + NotDir(PathBuf), + NotFile(PathBuf), + Fatal(String), + Array(Vec), +} + +/// The struct of directory list. +#[derive(Debug, Clone)] +pub struct Dirs { + dirs: Vec, + parent: PathBuf, + current: usize, +} + +/// The struct of directory. +#[derive(Debug, Clone)] +pub struct Dir<'a> { + dirs: &'a Dirs, + index: usize, + last_item: bool, +} + +impl Dir<'_> { + /// Create a new `Dir` instance. + pub fn new(dirs: &Dirs, index: usize) -> Dir<'_> { + Dir { + dirs, + index, + last_item: false, + } + } + + /// Create a new `Dir` instance with the last item flag. + pub fn new_of_last_item(dirs: &Dirs, index: usize) -> Dir<'_> { + Dir { + dirs, + index, + last_item: true, + } + } + + pub fn path(&self) -> PathBuf { + self.dirs.dirs[self.index].clone() + } + + pub fn index(&self) -> usize { + self.index + } + + pub fn is_last_item(&self) -> bool { + self.last_item + } +} + +impl Dirs { + pub fn new(current_dir: PathBuf) -> Result { + if current_dir == PathBuf::from(".") { + match std::env::current_dir() { + Ok(dir) => build_dirs(dir.clone().parent(), dir), + Err(e) => Err(SiblingError::Io(e)), + } + } else if current_dir.exists() { + if current_dir.is_dir() { + let current = std::fs::canonicalize(¤t_dir).unwrap(); + build_dirs(current.clone().parent(), current) + } else { + Err(SiblingError::NotDir(current_dir)) + } + } else { + Err(SiblingError::NotFound(current_dir)) + } + } + + pub fn new_from_file(file: String) -> Result { + if file == "-" { + return build_from_reader(Box::new(std::io::stdin().lock())); + } + let path = PathBuf::from(file); + if !path.exists() { + Err(SiblingError::NotFound(path)) + } else if path.is_dir() { + Err(SiblingError::NotFile(path)) + } else { + build_from_list(path) + } + } + + pub fn parent(&self) -> PathBuf { + self.parent.clone() + } + + pub fn current(&self) -> Dir<'_> { + Dir::new(self, self.current) + } + + pub fn is_empty(&self) -> bool { + self.dirs.is_empty() + } + + pub fn len(&self) -> usize { + self.dirs.len() + } + + pub fn next(&self, nexter: &dyn Nexter) -> Option> { + self.next_with(nexter, 1) + } + + pub fn next_with(&self, nexter: &dyn Nexter, step: usize) -> Option> { + nexter.next(self, step as i32) + } + + pub fn directories(&self) -> impl Iterator { + self.dirs.iter() + } +} + +fn build_dirs(parent: Option<&Path>, current: PathBuf) -> Result { + let parent = match parent { + Some(p) => p, + None => return Err(SiblingError::NoParent(current)), + }; + let mut errs = vec![]; + let dirs = collect_dirs(parent, &mut errs); + if !errs.is_empty() { + Err(SiblingError::Array(errs)) + } else { + let current_index = find_current(&dirs, ¤t); + Ok(Dirs { + dirs, + parent: parent.to_path_buf(), + current: current_index, + }) + } +} + +fn collect_dirs(parent: &Path, errs: &mut Vec) -> Vec { + let mut dirs = vec![]; + if let Ok(entries) = parent.read_dir() { + for entry in entries { + match entry { + Ok(entry) => { + let path = entry.path(); + if path.is_dir() { + dirs.push(path); + } + } + Err(e) => errs.push(SiblingError::Io(e)), + }; + } + } + dirs.sort(); + dirs +} + +fn find_current(dirs: &[PathBuf], current: &PathBuf) -> usize { + dirs.iter().position(|dir| dir == current).unwrap_or(0) +} + +fn build_from_reader(reader: Box) -> Result { + let lines = reader + .lines() + .filter_map(|line| line.map(|n| n.trim().to_string()).ok()) + .collect::>(); + let base = if let Some(base) = lines.iter().find(|l| l.starts_with("parent:")) { + base.chars().skip(7).collect::().trim().to_string() + } else { + ".".to_string() + }; + let dirs = lines + .iter() + .filter(|l| !l.starts_with("parent:")) + .map(PathBuf::from) + .collect::>(); + let current = find_current_dir_index(&dirs); + Ok(Dirs { + dirs, + parent: PathBuf::from(base), + current, + }) +} + +fn find_current_dir_index(dirs: &[PathBuf]) -> usize { + if let Ok(pwd) = std::env::current_dir() { + let cwd = PathBuf::from("."); + if let Some(pos) = dirs + .iter() + .position(|dir| dir == &cwd || pwd.ends_with(dir)) + { + return pos; + } + } + 0 +} + +fn build_from_list(filename: PathBuf) -> Result { + if let Ok(f) = std::fs::File::open(filename) { + let reader = BufReader::new(f); + build_from_reader(Box::new(reader)) + } else { + Err(SiblingError::Io(std::io::Error::last_os_error())) + } +} + +pub trait Nexter { + fn next<'a>(&self, dirs: &'a Dirs, step: i32) -> Option>; +} + +pub struct NexterFactory {} + +impl NexterFactory { + pub fn build(nexter_type: NexterType) -> Box { + match nexter_type { + NexterType::First => Box::new(First {}), + NexterType::Last => Box::new(Last {}), + NexterType::Previous => Box::new(Previous {}), + NexterType::Next => Box::new(Next {}), + NexterType::Random => Box::new(Random {}), + NexterType::Keep => Box::new(Keep {}), + } + } +} + +pub fn build_nexter(nexter_type: NexterType) -> Box { + match nexter_type { + NexterType::First => Box::new(First {}), + NexterType::Last => Box::new(Last {}), + NexterType::Previous => Box::new(Previous {}), + NexterType::Next => Box::new(Next {}), + NexterType::Random => Box::new(Random {}), + NexterType::Keep => Box::new(Keep {}), + } +} + +struct First {} +struct Last {} +struct Previous {} +struct Next {} +struct Random {} +struct Keep {} + +impl Nexter for First { + fn next<'a>(&self, dirs: &'a Dirs, _step: i32) -> Option> { + Some(Dir::new_of_last_item(dirs, 0)) + } +} + +impl Nexter for Last { + fn next<'a>(&self, dirs: &'a Dirs, _step: i32) -> Option> { + let next = dirs.dirs.len() - 1; + Some(Dir::new_of_last_item(dirs, next)) + } +} + +impl Nexter for Previous { + fn next<'a>(&self, dirs: &'a Dirs, _step: i32) -> Option> { + next_impl(dirs, -_step) + } +} + +impl Nexter for Next { + fn next<'a>(&self, dirs: &'a Dirs, _step: i32) -> Option> { + next_impl(dirs, _step) + } +} + +impl Nexter for Random { + fn next<'a>(&self, dirs: &'a Dirs, _step: i32) -> Option> { + use rand::Rng; + let mut rng = rand::rng(); + let next = rng.random_range(0..dirs.dirs.len()) as usize; + Some(Dir::new(dirs, next)) + } +} + +impl Nexter for Keep { + fn next<'a>(&self, dirs: &'a Dirs, _step: i32) -> Option> { + Some(dirs.current()) + } +} + +fn next_impl(dirs: &Dirs, step: i32) -> Option> { + let next = dirs.current as i32 + step; + if next < 0 || next >= dirs.dirs.len() as i32 { + None + } else if next == 0 { + Some(Dir::new_of_last_item(dirs, 0)) + } else if next == dirs.dirs.len() as i32 - 1 { + Some(Dir::new_of_last_item(dirs, dirs.dirs.len() - 1)) + } else { + Some(Dir::new(dirs, next as usize)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dirs_new() { + let dirs = Dirs::new(PathBuf::from("testdata/d")); + assert!(dirs.is_ok()); + let dirs = dirs.unwrap(); + assert_eq!(dirs.dirs.len(), 26); + assert_eq!(dirs.current, 3); + } + + #[test] + fn test_dir_dot() { + let dirs = Dirs::new(PathBuf::from(".")); + assert!(dirs.is_ok()); + let dirs = dirs.unwrap(); + assert_eq!( + dirs.current().path().file_name().map(|s| s.to_str()), + Some("sibling".into()) + ); + } + + #[test] + fn test_dir_from_file() { + let dirs = Dirs::new_from_file("../testdata/dirlist.txt".into()); + assert!(dirs.is_ok()); + let dirs = dirs.unwrap(); + assert_eq!(dirs.dirs.len(), 4); + assert_eq!(dirs.current, 1); + assert_eq!(dirs.parent, PathBuf::from("../testdata")); + } + + #[test] + fn test_nexter_first() { + let dirs = Dirs::new("../testdata/c".into()).unwrap(); + let nexter = build_nexter(NexterType::First); + match nexter.next(&dirs, 1) { + Some(p) => assert!(p.path().ends_with("../testdata/a")), + None => panic!("unexpected None"), + } + } + + #[test] + fn test_nexter_last() { + let dirs = Dirs::new("../testdata/k".into()).unwrap(); + let nexter = build_nexter(NexterType::Last); + match nexter.next(&dirs, 1) { + Some(p) => assert!(p.path().ends_with("../testdata/z")), + None => panic!("unexpected None"), + } + } + + #[test] + fn test_nexter_next() { + let dirs = Dirs::new("../testdata/c".into()).unwrap(); + let nexter = build_nexter(NexterType::Next); + match nexter.next(&dirs, 1) { + Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/d")), + None => panic!("unexpected None"), + } + match nexter.next(&dirs, 2) { + Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/e")), + None => panic!("unexpected None"), + } + match nexter.next(&dirs, 26) { + Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/z")), + None => panic!("unexpected None"), + } + match nexter.next(&dirs, 1) { + None => {} + Some(p) => panic!("unexpected {:?}", p.path()), + } + } + + #[test] + fn test_nexter_prev() { + let dirs = Dirs::new("../testdata/k".into()).unwrap(); + let nexter = build_nexter(NexterType::Previous); + match nexter.next(&dirs, 1) { + Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/j")), + None => panic!("unexpected None"), + } + match nexter.next(&dirs, 1) { + Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/j")), + None => panic!("unexpected None"), + } + match nexter.next(&dirs, 4) { + Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/g")), + None => panic!("unexpected None"), + } + match nexter.next(&dirs, 26) { + Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/a")), + None => panic!("unexpected None"), + } + if let Some(p) = nexter.next(&dirs, 1) { + panic!("unexpected {:?}", p.path()) + } + } +} diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index c60c112..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::path::PathBuf; - -use clap::{Parser, ValueEnum}; - -pub type Result = std::result::Result; - -#[derive(Debug)] -pub enum SiblingError { - Io(std::io::Error), - NotFound(PathBuf), - NoParent(PathBuf), - NotDir(PathBuf), - NotFile(PathBuf), - Fatal(String), - Array(Vec), -} - -#[derive(Debug, Eq, PartialEq, Clone, ValueEnum)] -pub enum NexterType { - First, - Last, - Previous, - Next, - Random, - Keep, -} - -#[derive(Debug, Parser)] -#[clap( - version, - author, - about, - arg_required_else_help = true, -)] -pub struct CliOpts { - #[clap(flatten)] - pub(crate) p_opts: PrintingOpts, - - #[arg(short, long, help = "specify the number of times to execute sibling", value_name = "COUNT", default_value_t = 1)] - pub step: i32, - - #[arg(long, help = "generate the initialize script for the shell", value_name = "SHELL", hide = true, default_missing_value = "bash")] - pub init: Option, - - #[arg(short = 't', long = "type", help = "specify the nexter type", value_enum, default_value_t = NexterType::Next, value_name = "TYPE", ignore_case = true)] - pub nexter: NexterType, - - #[arg(short, long, help = "directory list from file, if FILE is \"-\", reads from stdin.", value_name = "FILE")] - pub input: Option, - - #[arg(index = 1, help = "the target directory", value_name = "DIR")] - pub dirs: Vec, -} - -#[derive(Debug, Parser)] -pub(crate) struct PrintingOpts { - #[arg(long, help = "print the result in the csv format", default_value_t = false, hide = true)] - pub csv: bool, - - #[arg(short, long, help = "print the directory name in the absolute path", default_value_t = false)] - pub absolute: bool, - - #[arg(short, long, help = "list the sibling directories", default_value_t = false)] - pub list: bool, - - #[arg(short, long, help = "print the progress of traversing directories", default_value_t = false)] - pub progress: bool, - - #[arg(short = 'P', long, help = "print parent directory, when no more sibling directories are found", default_value_t = false)] - pub parent: bool, -} - diff --git a/src/dirs.rs b/src/dirs.rs deleted file mode 100644 index 9446ca2..0000000 --- a/src/dirs.rs +++ /dev/null @@ -1,176 +0,0 @@ -use std::io::{BufRead, BufReader}; -use std::path::{Path, PathBuf}; - -use crate::cli::{Result, SiblingError}; - -pub struct Dirs { - pub(crate) dirs: Vec, - pub(crate) parent: PathBuf, - pub(crate) current: usize, - pub(crate) next: i32, - pub(crate) no_more_dir: bool, -} - -impl Dirs { - pub fn new(current_dir: PathBuf) -> Result { - if current_dir == PathBuf::from(".") { - match std::env::current_dir() { - Ok(dir) => - build_dirs(dir.clone().parent(), dir), - Err(e) => Err(SiblingError::Io(e)), - } - } else if current_dir.exists() { - if current_dir.is_dir() { - let current = std::fs::canonicalize(¤t_dir).unwrap(); - build_dirs(current.clone().parent(), current) - } else { - Err(SiblingError::NotDir(current_dir)) - } - } else { - Err(SiblingError::NotFound(current_dir)) - } - } - pub fn new_from_file(file: String) -> Result { - if file == "-" { - return build_from_reader(Box::new(std::io::stdin().lock())); - } - let path = PathBuf::from(file); - if !path.exists() { - return Err(SiblingError::NotFound(path)); - } else if path.is_dir() { - return Err(SiblingError::NotFile(path)); - } else { - return build_from_list(path); - } - } - - pub fn current_path(&self) -> PathBuf { - self.dirs[self.current].clone() - } - - pub fn next_path(&self) -> Option { - if self.next >= 0 && self.next < self.dirs.len() as i32 { - Some(self.dirs[self.next as usize].clone()) - } else { - None - } - } -} - -fn build_dirs(parent: Option<&Path>, current: PathBuf) -> Result { - let parent = match parent { - Some(p) => p, - None => return Err(SiblingError::NoParent(current)), - }; - let mut errs = vec![]; - let dirs = collect_dirs(&parent, &mut errs); - if !errs.is_empty() { - Err(SiblingError::Array(errs)) - } else { - let current_index = find_current(&dirs, ¤t); - Ok(Dirs { - dirs, - parent: parent.to_path_buf(), - current: current_index, - next: -1, - no_more_dir: false, - }) - } -} - -fn collect_dirs(parent: &Path, errs: &mut Vec) -> Vec { - let mut dirs = vec![]; - if let Ok(entries) = parent.read_dir() { - for entry in entries { - match entry { - Ok(entry) => { - let path = entry.path(); - if path.is_dir() { - dirs.push(path); - } - }, - Err(e) => errs.push(SiblingError::Io(e)) - }; - } - } - dirs.sort(); - dirs -} - -fn find_current(dirs: &[PathBuf], current: &PathBuf) -> usize { - dirs.iter().position(|dir| dir == current).unwrap_or(0) -} - -fn build_from_reader(reader: Box) -> Result { - let lines = reader.lines() - .filter_map(|line| line.map(|n| n.trim().to_string()).ok()) - .collect::>(); - let base = if let Some(base) = lines.iter().find(|l| l.starts_with("parent:")) { - base.chars().skip(7).collect::().trim().to_string() - } else { - ".".to_string() - }; - let dirs = lines.iter() - .filter(|l| !l.starts_with("parent:")) - .map(|line| PathBuf::from(line)) - .collect::>(); - let current = find_current_dir_index(&dirs); - Ok(Dirs { - dirs, - parent: PathBuf::from(base), - current: current, - next: -1, - no_more_dir: false, - }) -} - -fn find_current_dir_index(dirs: &Vec) -> usize { - if let Ok(pwd) = std::env::current_dir() { - let cwd = PathBuf::from("."); - if let Some(pos) = dirs.iter().position(|dir| dir == &cwd || pwd.ends_with(dir)) { - return pos; - } - } - 0 as usize -} - -fn build_from_list(filename: PathBuf) -> Result { - if let Ok(f) = std::fs::File::open(filename) { - let reader = BufReader::new(f); - build_from_reader(Box::new(reader)) - } else { - Err(SiblingError::Io(std::io::Error::last_os_error())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dirs_new() { - let dirs = Dirs::new(PathBuf::from("testdata/d")); - assert!(dirs.is_ok()); - let dirs = dirs.unwrap(); - assert_eq!(dirs.dirs.len(), 26); - assert_eq!(dirs.current, 3); - } - - #[test] - fn test_dir_dot() { - let dirs = Dirs::new(PathBuf::from(".")); - assert!(dirs.is_ok()); - let dirs = dirs.unwrap(); - assert_eq!(dirs.current_path().file_name().map(|s| s.to_str()), Some("sibling".into())); - } - - #[test] - fn test_dir_from_file() { - let dirs = Dirs::new_from_file("testdata/dirlist.txt".into()); - assert!(dirs.is_ok()); - let dirs = dirs.unwrap(); - assert_eq!(dirs.dirs.len(), 4); - assert_eq!(dirs.current, 1); - assert_eq!(dirs.parent, PathBuf::from("testdata")); - } -} diff --git a/src/nexter.rs b/src/nexter.rs deleted file mode 100644 index 46e6668..0000000 --- a/src/nexter.rs +++ /dev/null @@ -1,163 +0,0 @@ -use std::path::PathBuf; - -use crate::dirs::Dirs; -use crate::cli::NexterType; - -pub trait Nexter { - fn next(&self, dirs: &mut Dirs, step: i32) -> Option; -} - -pub fn build_nexter(nexter_type: NexterType) -> Box { - match nexter_type { - NexterType::First => Box::new(First {}), - NexterType::Last => Box::new(Last {}), - NexterType::Previous => Box::new(Previous {}), - NexterType::Next => Box::new(Next {}), - NexterType::Random => Box::new(Random {}), - NexterType::Keep => Box::new(Keep {}), - } -} - -struct First {} -struct Last {} -struct Previous {} -struct Next {} -struct Random {} -struct Keep {} - -impl Nexter for First { - fn next(&self, dirs: &mut Dirs, _step: i32) -> Option { - dirs.next = 0; - dirs.dirs.first().map(|p| p.to_path_buf()) - } -} - -impl Nexter for Last { - fn next(&self, dirs: &mut Dirs, _step: i32) -> Option { - dirs.next = (dirs.dirs.len() - 1) as i32; - dirs.dirs.last().map(|p| p.to_path_buf()) - } -} - -impl Nexter for Previous { - fn next(&self, dirs: &mut Dirs, step: i32) -> Option { - next_impl(dirs, -step) - } -} - -impl Nexter for Next { - fn next(&self, dirs: &mut Dirs, step: i32) -> Option { - next_impl(dirs, step) - } -} - -impl Nexter for Random { - fn next(&self, dirs: &mut Dirs, _step: i32) -> Option { - use rand::Rng; - let mut rng = rand::thread_rng(); - dirs.next = rng.gen_range(0..dirs.dirs.len()) as i32; - dirs.dirs.get(dirs.next as usize).map(|p| p.to_path_buf()) - } -} - -impl Nexter for Keep { - fn next(&self, dirs: &mut Dirs, _step: i32) -> Option { - dirs.next = dirs.current as i32; - dirs.dirs.get(dirs.current).map(|p| p.to_path_buf()) - } -} - -fn next_impl(dirs: &mut Dirs, step: i32) -> Option { - if dirs.no_more_dir { - return None; - } - dirs.next = dirs.current as i32 + step; - let r = if dirs.next < 0 { - dirs.next = 0; - dirs.no_more_dir = true; - dirs.dirs.first() - } else if dirs.next >= dirs.dirs.len() as i32 { - dirs.next = (dirs.dirs.len() - 1) as i32; - dirs.no_more_dir = true; - dirs.dirs.last() - } else { - dirs.dirs.get(dirs.next as usize) - }; - r.map(|f| f.to_path_buf()) -} - -#[cfg(test)] -mod tests { - use core::panic; - - use super::*; - use crate::dirs::Dirs; - - #[test] - fn test_nexter_first() { - let mut dirs = Dirs::new("testdata/c".into()).unwrap(); - let nexter = build_nexter(NexterType::First); - match nexter.next(&mut dirs, 1) { - Some(p) => assert!(p.ends_with("testdata/a")), - None => panic!("unexpected None"), - } - } - - #[test] - fn test_nexter_last() { - let mut dirs = Dirs::new("testdata/k".into()).unwrap(); - let nexter = build_nexter(NexterType::Last); - match nexter.next(&mut dirs, 1) { - Some(p) => assert!(p.ends_with("testdata/z")), - None => panic!("unexpected None"), - } - } - - #[test] - fn test_nexter_next() { - let mut dirs = Dirs::new("testdata/c".into()).unwrap(); - let nexter = build_nexter(NexterType::Next); - match nexter.next(&mut dirs, 1) { - Some(p) => assert!(p.ends_with("testdata/d")), - None => panic!("unexpected None"), - } - match nexter.next(&mut dirs, 2) { - Some(p) => assert!(p.ends_with("testdata/e")), - None => panic!("unexpected None"), - } - match nexter.next(&mut dirs, 26) { - Some(p) => assert!(p.ends_with("testdata/z")), - None => panic!("unexpected None"), - } - match nexter.next(&mut dirs, 1) { - None => assert!(true), - Some(p) => panic!("unexpected {:?}", p), - } - } - - #[test] - fn test_nexter_prev() { - let mut dirs = Dirs::new("testdata/k".into()).unwrap(); - let nexter = build_nexter(NexterType::Previous); - match nexter.next(&mut dirs, 1) { - Some(p) => assert!(p.ends_with("testdata/j")), - None => panic!("unexpected None"), - } - match nexter.next(&mut dirs, 1) { - Some(p) => assert!(p.ends_with("testdata/j")), - None => panic!("unexpected None"), - } - match nexter.next(&mut dirs, 4) { - Some(p) => assert!(p.ends_with("testdata/g")), - None => panic!("unexpected None"), - } - match nexter.next(&mut dirs, 26) { - Some(p) => assert!(p.ends_with("testdata/a")), - None => panic!("unexpected None"), - } - match nexter.next(&mut dirs, 1) { - None => assert!(true), - Some(p) => panic!("unexpected {:?}", p), - } - } -} \ No newline at end of file diff --git a/src/printer.rs b/src/printer.rs deleted file mode 100644 index da3c5ad..0000000 --- a/src/printer.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::path::PathBuf; - -use crate::cli::{PrintingOpts, Result}; -use crate::dirs::Dirs; - -pub(crate) fn result_string(dirs: &Dirs, opts: &PrintingOpts) -> Result { - if opts.csv { - csv_string(dirs, opts.absolute) - } else if dirs.no_more_dir { - no_more_dir_string(dirs, opts) - } else if opts.list { - list_string(dirs, opts) - } else { - result_string_impl(dirs, opts) - } -} - -fn csv_string(dirs: &Dirs, absolute: bool) -> Result { - Ok(format!(r##""{}","{}",{},{},{}"##, - pathbuf_to_string(Some(dirs.current_path()), absolute), - pathbuf_to_string(dirs.next_path(), absolute), - dirs.current + 1, dirs.next + 1, dirs.dirs.len())) -} - -fn no_more_dir_string(dirs: &Dirs, opts: &PrintingOpts) -> Result { - if opts.parent { - Ok(pathbuf_to_string(Some(dirs.parent.clone()), opts.absolute)) - } else { - Ok(String::from("no more sibling directory")) - } -} - -fn list_string(dirs: &Dirs, opts: &PrintingOpts) -> Result { - let mut result = vec![]; - for (i, dir) in dirs.dirs.iter().enumerate() { - let prefix = if i == dirs.next as usize { "> " } - else if i == dirs.current { - "* " - } else { - " " - }; - result.push(format!("{:>4} {}{}", i + 1, prefix, pathbuf_to_string(Some(dir.to_path_buf()), opts.absolute))); - } - Ok(result.join("\n")) -} - -fn result_string_impl(dirs: &Dirs, opts: &PrintingOpts) -> Result { - let r = if opts.progress { - format!("{} ({}/{})", pathbuf_to_string(dirs.next_path(), opts.absolute), dirs.next + 1, dirs.dirs.len()) - } else { - pathbuf_to_string(dirs.next_path(), opts.absolute).to_string() - }; - Ok(r) -} - -fn pathbuf_to_string(path: Option, absolute: bool) -> String { - match path { - Some(p) => { - if absolute { - std::fs::canonicalize(p).unwrap().to_string_lossy().to_string() - } else { - p.to_string_lossy().to_string() - } - }, - None => "".to_string(), - } -} \ No newline at end of file diff --git a/wix/main.wxs b/wix/main.wxs deleted file mode 100644 index 5a2b318..0000000 --- a/wix/main.wxs +++ /dev/null @@ -1,228 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - 1 - - - - - - - - - - - - - - - - - - From 0bb7bbeb8a7cc5e0aa12db1034119db7722261b6 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Mon, 24 Mar 2025 11:51:13 +0900 Subject: [PATCH 2/9] feat: add options for generating completion files and specify output directory --- assets/completions/bash/sibling | 6 +++- assets/completions/elvish/sibling | 2 ++ assets/completions/fish/sibling | 2 ++ assets/completions/powershell/sibling | 2 ++ assets/completions/zsh/_sibling | 2 ++ build.rs | 40 --------------------------- cli/src/cli.rs | 24 ++++++++++++++++ cli/src/gencomp.rs | 26 +++++++++++++++++ cli/src/main.rs | 7 +++++ 9 files changed, 70 insertions(+), 41 deletions(-) delete mode 100644 build.rs create mode 100644 cli/src/gencomp.rs diff --git a/assets/completions/bash/sibling b/assets/completions/bash/sibling index f0a26cb..0fec1af 100644 --- a/assets/completions/bash/sibling +++ b/assets/completions/bash/sibling @@ -19,7 +19,7 @@ _sibling() { case "${cmd}" in sibling) - opts="-a -l -p -P -s -t -i -h -V --csv --absolute --list --progress --parent --step --init --type --input --help --version [DIR]..." + opts="-a -l -p -P -s -t -i -h -V --csv --absolute --list --progress --parent --step --init --type --input --generate-completion-files --completion-out-dir --help --version [DIR]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -53,6 +53,10 @@ _sibling() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --completion-out-dir) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; diff --git a/assets/completions/elvish/sibling b/assets/completions/elvish/sibling index f4922ba..e5b4991 100644 --- a/assets/completions/elvish/sibling +++ b/assets/completions/elvish/sibling @@ -25,6 +25,7 @@ set edit:completion:arg-completer[sibling] = {|@words| cand --type 'specify the nexter type' cand -i 'directory list from file, if FILE is "-", reads from stdin.' cand --input 'directory list from file, if FILE is "-", reads from stdin.' + cand --completion-out-dir 'Output directory of completion files' cand --csv 'print the result in the csv format' cand -a 'print the directory name in the absolute path' cand --absolute 'print the directory name in the absolute path' @@ -34,6 +35,7 @@ set edit:completion:arg-completer[sibling] = {|@words| cand --progress 'print the progress of traversing directories' cand -P 'print parent directory, when no more sibling directories are found' cand --parent 'print parent directory, when no more sibling directories are found' + cand --generate-completion-files 'Generate completion files' cand -h 'Print help' cand --help 'Print help' cand -V 'Print version' diff --git a/assets/completions/fish/sibling b/assets/completions/fish/sibling index 274ca9a..375edec 100644 --- a/assets/completions/fish/sibling +++ b/assets/completions/fish/sibling @@ -2,10 +2,12 @@ complete -c sibling -s s -l step -d 'specify the number of times to execute sibl complete -c sibling -l init -d 'generate the initialize script for the shell' -r complete -c sibling -s t -l type -d 'specify the nexter type' -r -f -a "{first\t'',last\t'',previous\t'',next\t'',random\t'',keep\t''}" complete -c sibling -s i -l input -d 'directory list from file, if FILE is "-", reads from stdin.' -r +complete -c sibling -l completion-out-dir -d 'Output directory of completion files' -r -F complete -c sibling -l csv -d 'print the result in the csv format' complete -c sibling -s a -l absolute -d 'print the directory name in the absolute path' complete -c sibling -s l -l list -d 'list the sibling directories' complete -c sibling -s p -l progress -d 'print the progress of traversing directories' complete -c sibling -s P -l parent -d 'print parent directory, when no more sibling directories are found' +complete -c sibling -l generate-completion-files -d 'Generate completion files' complete -c sibling -s h -l help -d 'Print help' complete -c sibling -s V -l version -d 'Print version' diff --git a/assets/completions/powershell/sibling b/assets/completions/powershell/sibling index c889f55..9d40388 100644 --- a/assets/completions/powershell/sibling +++ b/assets/completions/powershell/sibling @@ -28,6 +28,7 @@ Register-ArgumentCompleter -Native -CommandName 'sibling' -ScriptBlock { [CompletionResult]::new('--type', '--type', [CompletionResultType]::ParameterName, 'specify the nexter type') [CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'directory list from file, if FILE is "-", reads from stdin.') [CompletionResult]::new('--input', '--input', [CompletionResultType]::ParameterName, 'directory list from file, if FILE is "-", reads from stdin.') + [CompletionResult]::new('--completion-out-dir', '--completion-out-dir', [CompletionResultType]::ParameterName, 'Output directory of completion files') [CompletionResult]::new('--csv', '--csv', [CompletionResultType]::ParameterName, 'print the result in the csv format') [CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'print the directory name in the absolute path') [CompletionResult]::new('--absolute', '--absolute', [CompletionResultType]::ParameterName, 'print the directory name in the absolute path') @@ -37,6 +38,7 @@ Register-ArgumentCompleter -Native -CommandName 'sibling' -ScriptBlock { [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'print the progress of traversing directories') [CompletionResult]::new('-P', '-P ', [CompletionResultType]::ParameterName, 'print parent directory, when no more sibling directories are found') [CompletionResult]::new('--parent', '--parent', [CompletionResultType]::ParameterName, 'print parent directory, when no more sibling directories are found') + [CompletionResult]::new('--generate-completion-files', '--generate-completion-files', [CompletionResultType]::ParameterName, 'Generate completion files') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') diff --git a/assets/completions/zsh/_sibling b/assets/completions/zsh/_sibling index 955804a..1cacbbd 100644 --- a/assets/completions/zsh/_sibling +++ b/assets/completions/zsh/_sibling @@ -22,6 +22,7 @@ _sibling() { '--type=[specify the nexter type]:TYPE:(first last previous next random keep)' \ '-i+[directory list from file, if FILE is "-", reads from stdin.]:FILE:_default' \ '--input=[directory list from file, if FILE is "-", reads from stdin.]:FILE:_default' \ +'--completion-out-dir=[Output directory of completion files]:DIR:_files' \ '--csv[print the result in the csv format]' \ '-a[print the directory name in the absolute path]' \ '--absolute[print the directory name in the absolute path]' \ @@ -31,6 +32,7 @@ _sibling() { '--progress[print the progress of traversing directories]' \ '-P[print parent directory, when no more sibling directories are found]' \ '--parent[print parent directory, when no more sibling directories are found]' \ +'--generate-completion-files[Generate completion files]' \ '-h[Print help]' \ '--help[Print help]' \ '-V[Print version]' \ diff --git a/build.rs b/build.rs deleted file mode 100644 index 5dcdace..0000000 --- a/build.rs +++ /dev/null @@ -1,40 +0,0 @@ -use clap::{Command, CommandFactory}; -use clap_complete::Shell; -use std::fs::File; -use std::path::Path; - -include!("src/cli.rs"); - -fn generate(s: Shell, app: &mut Command, appname: &str, outdir: &Path, file: String) { - let destfile = outdir.join(file); - std::fs::create_dir_all(destfile.parent().unwrap()).unwrap(); - let mut dest = File::create(destfile).unwrap(); - - clap_complete::generate(s, app, appname, &mut dest); -} - -fn parse_cargo_toml() -> toml::Value { - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml"); - let file = match std::fs::read_to_string(path) { - Ok(f) => f, - Err(e) => panic!("{}", e), - }; - - file.parse().unwrap() -} - -fn main() { - let table = parse_cargo_toml(); - let appname = table["package"]["name"].as_str().unwrap(); - - let mut app = CliOpts::command(); - app.set_bin_name(appname); - - let outdir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("target/completions/"); - - generate(Shell::Bash, &mut app, appname, &outdir, format!("bash/{}", appname)); - generate(Shell::Elvish, &mut app, appname, &outdir, format!("elvish/{}", appname)); - generate(Shell::Fish, &mut app, appname, &outdir, format!("fish/{}", appname)); - generate(Shell::PowerShell, &mut app, appname, &outdir, format!("powershell/{}", appname)); - generate(Shell::Zsh, &mut app, appname, &outdir, format!("zsh/_{}", appname)); -} diff --git a/cli/src/cli.rs b/cli/src/cli.rs index f2e368b..5484ba6 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -39,6 +39,30 @@ pub struct CliOpts { #[arg(index = 1, help = "the target directory", value_name = "DIR")] pub dirs: Vec, + + #[cfg(debug_assertions)] + #[clap(flatten)] + pub(crate) compopts: CompletionOpts, +} + +#[cfg(debug_assertions)] +#[derive(Parser, Debug)] +pub(crate) struct CompletionOpts { + #[arg( + long = "generate-completion-files", + help = "Generate completion files", + hide = true + )] + pub(crate) completion: bool, + + #[arg( + long = "completion-out-dir", + value_name = "DIR", + default_value = "assets/completions", + help = "Output directory of completion files", + hide = true + )] + pub(crate) dest: PathBuf, } #[derive(Debug, Parser)] diff --git a/cli/src/gencomp.rs b/cli/src/gencomp.rs new file mode 100644 index 0000000..f4432ad --- /dev/null +++ b/cli/src/gencomp.rs @@ -0,0 +1,26 @@ +#[cfg(debug_assertions)] +use clap::{Command, CommandFactory}; +use clap_complete::Shell; +use std::fs::File; +use std::path::{Path, PathBuf}; + +fn generate_impl(s: Shell, app: &mut Command, appname: &str, outdir: &Path, file: String) { + let destfile = outdir.join(file); + std::fs::create_dir_all(destfile.parent().unwrap()).unwrap(); + let mut dest = File::create(destfile).unwrap(); + + clap_complete::generate(s, app, appname, &mut dest); +} + +pub fn generate(outdir: PathBuf) { + let appname = "sibling"; + + let mut app = crate::cli::CliOpts::command(); + app.set_bin_name(appname); + + generate_impl(Shell::Bash, &mut app, appname, &outdir, format!("bash/{}", appname)); + generate_impl(Shell::Elvish, &mut app, appname, &outdir, format!("elvish/{}", appname)); + generate_impl(Shell::Fish, &mut app, appname, &outdir, format!("fish/{}", appname)); + generate_impl(Shell::PowerShell, &mut app, appname, &outdir, format!("powershell/{}", appname)); + generate_impl(Shell::Zsh, &mut app, appname, &outdir, format!("zsh/_{}", appname)); +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 0df52c6..3834daa 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -7,6 +7,7 @@ use sibling::Nexter; use sibling::{Result, SiblingError}; mod cli; +mod gencomp; mod init; mod printer; @@ -96,6 +97,12 @@ fn main() { args.collect() }; let opts = cli::CliOpts::parse_from(args); + if cfg!(debug_assertions) { + #[cfg(debug_assertions)] + if opts.compopts.completion { + return gencomp::generate(opts.compopts.dest); + } + } for item in perform(opts) { match item { Ok(result) => println!("{}", result), From 2422e2d83a9da43bd69e6ea06a4214bd9a17b69d Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Mon, 24 Mar 2025 11:51:26 +0900 Subject: [PATCH 3/9] feat: add Justfile for task automation and build processes --- Justfile | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 Justfile diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..667fb17 --- /dev/null +++ b/Justfile @@ -0,0 +1,42 @@ +[private] +@default: help + +VERSION := `grep '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/g'` + +# show help message +@help: + echo "Task runner for Gixor {{ VERSION }} with Just" + echo "Usage: just " + echo "" + just --list + +formats: + cargo fmt -- --emit=files + +clippy: + cargo clippy --all-targets --all-features -- -D warnings + +build target = "": formats clippy + cargo build {{ target }} + +test: (build "") + cargo llvm-cov --lcov --output-path target/coverage.lcov + +# Generate completion files +completions: + cargo run -- --generate-completion-files --completion-out-dir assets/completions + +prepare_site_build: + test -d docs/public || git worktree add -f docs/public gh-pages + +# Generate the document site with Hugo +site: prepare_site_build + hugo -s site + +# Build the docker image for gixor +docker: + docker build -t ghcr.io/tamada/btmeister:latest -t ghcr.io/tamada/btmeister:{{VERSION}} . + +# Build the docker image for multiple platforms and push them into ghcr.io +docker_buildx: + docker buildx build --platform linux/arm64/v8,linux/amd64 --output=type=image,push=true -t ghcr.io/tamada/btmeister:latest -t ghcr.io/tamada/btmeister:{{VERSION}} . \ No newline at end of file From 68a271ce774f439999544329abebf3687ab57155 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Mon, 24 Mar 2025 15:19:00 +0900 Subject: [PATCH 4/9] refactor: encapsulate generation logic in a module and adjust visibility --- cli/src/gencomp.rs | 49 +++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/cli/src/gencomp.rs b/cli/src/gencomp.rs index f4432ad..09b5a3b 100644 --- a/cli/src/gencomp.rs +++ b/cli/src/gencomp.rs @@ -1,26 +1,35 @@ #[cfg(debug_assertions)] -use clap::{Command, CommandFactory}; -use clap_complete::Shell; -use std::fs::File; -use std::path::{Path, PathBuf}; +mod gencomp { + use clap::{Command, CommandFactory}; + use clap_complete::Shell; + use std::fs::File; + use std::path::{Path, PathBuf}; -fn generate_impl(s: Shell, app: &mut Command, appname: &str, outdir: &Path, file: String) { - let destfile = outdir.join(file); - std::fs::create_dir_all(destfile.parent().unwrap()).unwrap(); - let mut dest = File::create(destfile).unwrap(); - - clap_complete::generate(s, app, appname, &mut dest); -} + #[cfg(debug_assertions)] + fn generate_impl(s: Shell, app: &mut Command, appname: &str, outdir: &Path, file: String) { + let destfile = outdir.join(file); + std::fs::create_dir_all(destfile.parent().unwrap()).unwrap(); + let mut dest = File::create(destfile).unwrap(); + + clap_complete::generate(s, app, appname, &mut dest); + } -pub fn generate(outdir: PathBuf) { - let appname = "sibling"; + pub(super) fn generate(outdir: PathBuf) { + let appname = "sibling"; - let mut app = crate::cli::CliOpts::command(); - app.set_bin_name(appname); + let mut app = crate::cli::CliOpts::command(); + app.set_bin_name(appname); - generate_impl(Shell::Bash, &mut app, appname, &outdir, format!("bash/{}", appname)); - generate_impl(Shell::Elvish, &mut app, appname, &outdir, format!("elvish/{}", appname)); - generate_impl(Shell::Fish, &mut app, appname, &outdir, format!("fish/{}", appname)); - generate_impl(Shell::PowerShell, &mut app, appname, &outdir, format!("powershell/{}", appname)); - generate_impl(Shell::Zsh, &mut app, appname, &outdir, format!("zsh/_{}", appname)); + generate_impl(Shell::Bash, &mut app, appname, &outdir, format!("bash/{}", appname)); + generate_impl(Shell::Elvish, &mut app, appname, &outdir, format!("elvish/{}", appname)); + generate_impl(Shell::Fish, &mut app, appname, &outdir, format!("fish/{}", appname)); + generate_impl(Shell::PowerShell, &mut app, appname, &outdir, format!("powershell/{}", appname)); + generate_impl(Shell::Zsh, &mut app, appname, &outdir, format!("zsh/_{}", appname)); + } } + +#[allow(dead_code, unused_variables)] +pub(crate) fn generate(outdir: std::path::PathBuf) { + #[cfg(debug_assertions)] + gencomp::generate(outdir); +} \ No newline at end of file From 1f1269c1133d9306c7ada75a723ef6d9fccb39fb Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Mon, 24 Mar 2025 17:16:13 +0900 Subject: [PATCH 5/9] refactor: rename module from gencomp to generator and improve path handling in Dirs --- cli/src/gencomp.rs | 48 +++++++++++++++++++++++++++++++++-------- lib/src/lib.rs | 54 ++++++++++++++++++++++++---------------------- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/cli/src/gencomp.rs b/cli/src/gencomp.rs index 09b5a3b..5bc08a4 100644 --- a/cli/src/gencomp.rs +++ b/cli/src/gencomp.rs @@ -1,5 +1,5 @@ #[cfg(debug_assertions)] -mod gencomp { +mod generator { use clap::{Command, CommandFactory}; use clap_complete::Shell; use std::fs::File; @@ -10,7 +10,7 @@ mod gencomp { let destfile = outdir.join(file); std::fs::create_dir_all(destfile.parent().unwrap()).unwrap(); let mut dest = File::create(destfile).unwrap(); - + clap_complete::generate(s, app, appname, &mut dest); } @@ -20,16 +20,46 @@ mod gencomp { let mut app = crate::cli::CliOpts::command(); app.set_bin_name(appname); - generate_impl(Shell::Bash, &mut app, appname, &outdir, format!("bash/{}", appname)); - generate_impl(Shell::Elvish, &mut app, appname, &outdir, format!("elvish/{}", appname)); - generate_impl(Shell::Fish, &mut app, appname, &outdir, format!("fish/{}", appname)); - generate_impl(Shell::PowerShell, &mut app, appname, &outdir, format!("powershell/{}", appname)); - generate_impl(Shell::Zsh, &mut app, appname, &outdir, format!("zsh/_{}", appname)); + generate_impl( + Shell::Bash, + &mut app, + appname, + &outdir, + format!("bash/{}", appname), + ); + generate_impl( + Shell::Elvish, + &mut app, + appname, + &outdir, + format!("elvish/{}", appname), + ); + generate_impl( + Shell::Fish, + &mut app, + appname, + &outdir, + format!("fish/{}", appname), + ); + generate_impl( + Shell::PowerShell, + &mut app, + appname, + &outdir, + format!("powershell/{}", appname), + ); + generate_impl( + Shell::Zsh, + &mut app, + appname, + &outdir, + format!("zsh/_{}", appname), + ); } } #[allow(dead_code, unused_variables)] pub(crate) fn generate(outdir: std::path::PathBuf) { #[cfg(debug_assertions)] - gencomp::generate(outdir); -} \ No newline at end of file + generator::generate(outdir); +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index a2385fb..ebb9ca3 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -79,7 +79,8 @@ impl Dir<'_> { } impl Dirs { - pub fn new(current_dir: PathBuf) -> Result { + pub fn new>(current_dir: P) -> Result { + let current_dir = current_dir.as_ref(); if current_dir == PathBuf::from(".") { match std::env::current_dir() { Ok(dir) => build_dirs(dir.clone().parent(), dir), @@ -87,17 +88,18 @@ impl Dirs { } } else if current_dir.exists() { if current_dir.is_dir() { - let current = std::fs::canonicalize(¤t_dir).unwrap(); + let current = std::fs::canonicalize(current_dir).unwrap(); build_dirs(current.clone().parent(), current) } else { - Err(SiblingError::NotDir(current_dir)) + Err(SiblingError::NotDir(current_dir.to_path_buf())) } } else { - Err(SiblingError::NotFound(current_dir)) + Err(SiblingError::NotFound(current_dir.to_path_buf())) } } - pub fn new_from_file(file: String) -> Result { + pub fn new_from_file>(file: S) -> Result { + let file = file.as_ref(); if file == "-" { return build_from_reader(Box::new(std::io::stdin().lock())); } @@ -323,7 +325,7 @@ mod tests { #[test] fn test_dirs_new() { - let dirs = Dirs::new(PathBuf::from("testdata/d")); + let dirs = Dirs::new(PathBuf::from("../testdata/d")); assert!(dirs.is_ok()); let dirs = dirs.unwrap(); assert_eq!(dirs.dirs.len(), 26); @@ -332,7 +334,7 @@ mod tests { #[test] fn test_dir_dot() { - let dirs = Dirs::new(PathBuf::from(".")); + let dirs = Dirs::new(PathBuf::from("..")); assert!(dirs.is_ok()); let dirs = dirs.unwrap(); assert_eq!( @@ -343,51 +345,51 @@ mod tests { #[test] fn test_dir_from_file() { - let dirs = Dirs::new_from_file("../testdata/dirlist.txt".into()); + let dirs = Dirs::new_from_file("../testdata/dirlist.txt"); assert!(dirs.is_ok()); let dirs = dirs.unwrap(); assert_eq!(dirs.dirs.len(), 4); assert_eq!(dirs.current, 1); - assert_eq!(dirs.parent, PathBuf::from("../testdata")); + assert_eq!(dirs.parent, PathBuf::from("testdata")); } #[test] fn test_nexter_first() { - let dirs = Dirs::new("../testdata/c".into()).unwrap(); + let dirs = Dirs::new("../testdata/c").unwrap(); let nexter = build_nexter(NexterType::First); match nexter.next(&dirs, 1) { - Some(p) => assert!(p.path().ends_with("../testdata/a")), + Some(p) => assert!(p.path().ends_with("testdata/a")), None => panic!("unexpected None"), } } #[test] fn test_nexter_last() { - let dirs = Dirs::new("../testdata/k".into()).unwrap(); + let dirs = Dirs::new("../testdata/k").unwrap(); let nexter = build_nexter(NexterType::Last); match nexter.next(&dirs, 1) { - Some(p) => assert!(p.path().ends_with("../testdata/z")), + Some(p) => assert!(p.path().ends_with("testdata/z")), None => panic!("unexpected None"), } } #[test] fn test_nexter_next() { - let dirs = Dirs::new("../testdata/c".into()).unwrap(); + let dirs = Dirs::new("../testdata/c").unwrap(); let nexter = build_nexter(NexterType::Next); match nexter.next(&dirs, 1) { - Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/d")), + Some(p) => assert!(p.path().ends_with("testdata/d")), None => panic!("unexpected None"), } match nexter.next(&dirs, 2) { - Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/e")), + Some(p) => assert!(p.path().ends_with("testdata/e"), "{:?}", p.path()), None => panic!("unexpected None"), } - match nexter.next(&dirs, 26) { - Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/z")), + match nexter.next(&dirs, 23) { + Some(p) => assert!(p.path().ends_with("testdata/z"), "{:?}", p.path()), None => panic!("unexpected None"), } - match nexter.next(&dirs, 1) { + match nexter.next(&dirs, 24) { None => {} Some(p) => panic!("unexpected {:?}", p.path()), } @@ -395,25 +397,25 @@ mod tests { #[test] fn test_nexter_prev() { - let dirs = Dirs::new("../testdata/k".into()).unwrap(); + let dirs = Dirs::new("../testdata/k").unwrap(); let nexter = build_nexter(NexterType::Previous); match nexter.next(&dirs, 1) { - Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/j")), + Some(p) => assert!(p.path().ends_with("testdata/j")), None => panic!("unexpected None"), } match nexter.next(&dirs, 1) { - Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/j")), + Some(p) => assert!(p.path().ends_with("testdata/j")), None => panic!("unexpected None"), } match nexter.next(&dirs, 4) { - Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/g")), + Some(p) => assert!(p.path().ends_with("testdata/g")), None => panic!("unexpected None"), } - match nexter.next(&dirs, 26) { - Some(p) => assert_eq!(p.path(), PathBuf::from("../testdata/a")), + match nexter.next(&dirs, 10) { + Some(p) => assert!(p.path().ends_with("testdata/a")), None => panic!("unexpected None"), } - if let Some(p) = nexter.next(&dirs, 1) { + if let Some(p) = nexter.next(&dirs, 11) { panic!("unexpected {:?}", p.path()) } } From fbe7f1bf30afeb6fdc34f1c1d19131915583de90 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Fri, 18 Jul 2025 13:35:54 +0900 Subject: [PATCH 6/9] fix: handle potential error when creating destination file in generate_impl function --- .github/workflows/publish.yaml | 12 ++++++------ cli/src/gencomp.rs | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 377f2db..7b6f43a 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -73,27 +73,27 @@ jobs: - os: ubuntu-latest target: aarch64-unknown-linux-musl artifact_name: ${{ needs.documents.outputs.appname }} - asset_name: ${{ needs.documents.outputs.appname }}-${{ needs.documents.outputs.tag }}_linux_amd64 + asset_name: ${{ needs.documents.outputs.appname }}-${{ needs.documents.outputs.tag }}_linux_arm64 - os: ubuntu-latest target: x86_64-unknown-linux-musl artifact_name: ${{ needs.documents.outputs.appname }} - asset_name: ${{ needs.documents.outputs.appname }}-${{ needs.documents.outputs.tag }}_linux_arm64 + asset_name: ${{ needs.documents.outputs.appname }}-${{ needs.documents.outputs.tag }}_linux_amd64 # - os: ubuntu-latest # target: aarch64-pc-windows-gnullvm # artifact_name: ${{ needs.documents.outputs.appname }}.exe - # asset_name: ${{ needs.documents.outputs.appname }}-${{ needs.documents.outputs.tag }}_windows_amd64 + # asset_name: ${{ needs.documents.outputs.appname }}-${{ needs.documents.outputs.tag }}_windows_arm64 # - os: ubuntu-latest # target: x86_64-pc-windows-gnu # artifact_name: ${{ needs.documents.outputs.appname }}.exe - # asset_name: ${{ needs.documents.outputs.appname }}-${{ needs.documents.outputs.tag }}_windows_arm64 + # asset_name: ${{ needs.documents.outputs.appname }}-${{ needs.documents.outputs.tag }}_windows_amd64 - os: macos-latest target: aarch64-apple-darwin artifact_name: ${{ needs.documents.outputs.appname }} - asset_name: ${{ needs.documents.outputs.appname }}-${{ needs.documents.outputs.tag }}_darwin_amd64 + asset_name: ${{ needs.documents.outputs.appname }}-${{ needs.documents.outputs.tag }}_darwin_arm64 - os: macos-latest target: x86_64-apple-darwin artifact_name: ${{ needs.documents.outputs.appname }} - asset_name: ${{ needs.documents.outputs.appname }}-${{ needs.documents.outputs.tag }}_darwin_arm64 + asset_name: ${{ needs.documents.outputs.appname }}-${{ needs.documents.outputs.tag }}_darwin_amd64 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/cli/src/gencomp.rs b/cli/src/gencomp.rs index 5bc08a4..b8599fd 100644 --- a/cli/src/gencomp.rs +++ b/cli/src/gencomp.rs @@ -9,9 +9,9 @@ mod generator { fn generate_impl(s: Shell, app: &mut Command, appname: &str, outdir: &Path, file: String) { let destfile = outdir.join(file); std::fs::create_dir_all(destfile.parent().unwrap()).unwrap(); - let mut dest = File::create(destfile).unwrap(); - - clap_complete::generate(s, app, appname, &mut dest); + if let Ok(mut dest) = File::create(destfile) { + clap_complete::generate(s, app, appname, &mut dest); + } } pub(super) fn generate(outdir: PathBuf) { From ce5d783e9e3eea9ae856a1e0d3760f6f170eb296 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 18 Jul 2025 04:36:15 +0000 Subject: [PATCH 7/9] update version to 2.0.1, ready to publish v2.0.1 --- Cargo.toml | 2 +- docs/config.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e3ef55e..4be2239 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = [ "lib", "cli" ] resolver = "3" [workspace.package] -version = "2.0.0-beta-10" +version = "2.0.1" repository = "https://github.com/tamada/sibling" homepage = "https://tamada.github.io/sibling" readme = "README.md" diff --git a/docs/config.toml b/docs/config.toml index a9a3fc1..2d84b97 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -15,7 +15,7 @@ pygmentsStyle = "pygments" project_tagline = "get next/previous sibling directory name" dateFormat = "2006-01-02" katex = true - version = "2.0.0-beta-7" + version = "2.0.1" footer = "[![GitHub](https://img.shields.io/badge/GitHub-tamada/sibling-blueviolet.svg?logo=github)](https://github.com/tamada/sibling) Made with [Hugo](https://gohugo.io/). Theme by [Cayman](https://github.com/zwbetz-gh/cayman-hugo-theme). Deployed to [GitHub Pages](https://pages.github.com/)." [menu] From 9ca3158366d52ba1b8fe5c1bc5cfd849e59e6750 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Fri, 18 Jul 2025 14:23:47 +0900 Subject: [PATCH 8/9] refactor: replace build_nexter function with NexterFactory::build for consistency --- cli/src/main.rs | 4 ++-- lib/src/lib.rs | 19 ++++--------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 3834daa..f05f283 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -22,7 +22,7 @@ fn perform_impl( } fn perform_from_file(opts: CliOpts) -> Vec> { - let nexter = sibling::build_nexter(opts.nexter); + let nexter = sibling::NexterFactory::build(opts.nexter); let r = match opts.input { None => Err(SiblingError::Fatal("input is not specified".into())), Some(file) => match sibling::Dirs::new_from_file(file) { @@ -46,7 +46,7 @@ fn perform_each( } fn perform_sibling(opts: CliOpts) -> Vec> { - let nexter = sibling::build_nexter(opts.nexter); + let nexter = sibling::NexterFactory::build(opts.nexter); let target_dirs = if opts.dirs.is_empty() { vec![std::env::current_dir().unwrap()] } else { diff --git a/lib/src/lib.rs b/lib/src/lib.rs index ebb9ca3..d8ee4d4 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -248,17 +248,6 @@ impl NexterFactory { } } -pub fn build_nexter(nexter_type: NexterType) -> Box { - match nexter_type { - NexterType::First => Box::new(First {}), - NexterType::Last => Box::new(Last {}), - NexterType::Previous => Box::new(Previous {}), - NexterType::Next => Box::new(Next {}), - NexterType::Random => Box::new(Random {}), - NexterType::Keep => Box::new(Keep {}), - } -} - struct First {} struct Last {} struct Previous {} @@ -356,7 +345,7 @@ mod tests { #[test] fn test_nexter_first() { let dirs = Dirs::new("../testdata/c").unwrap(); - let nexter = build_nexter(NexterType::First); + let nexter = NexterFactory::build(NexterType::First); match nexter.next(&dirs, 1) { Some(p) => assert!(p.path().ends_with("testdata/a")), None => panic!("unexpected None"), @@ -366,7 +355,7 @@ mod tests { #[test] fn test_nexter_last() { let dirs = Dirs::new("../testdata/k").unwrap(); - let nexter = build_nexter(NexterType::Last); + let nexter = NexterFactory::build(NexterType::Last); match nexter.next(&dirs, 1) { Some(p) => assert!(p.path().ends_with("testdata/z")), None => panic!("unexpected None"), @@ -376,7 +365,7 @@ mod tests { #[test] fn test_nexter_next() { let dirs = Dirs::new("../testdata/c").unwrap(); - let nexter = build_nexter(NexterType::Next); + let nexter = NexterFactory::build(NexterType::Next); match nexter.next(&dirs, 1) { Some(p) => assert!(p.path().ends_with("testdata/d")), None => panic!("unexpected None"), @@ -398,7 +387,7 @@ mod tests { #[test] fn test_nexter_prev() { let dirs = Dirs::new("../testdata/k").unwrap(); - let nexter = build_nexter(NexterType::Previous); + let nexter = NexterFactory::build(NexterType::Previous); match nexter.next(&dirs, 1) { Some(p) => assert!(p.path().ends_with("testdata/j")), None => panic!("unexpected None"), From 1eb58e496120da5f698b896cf04b5e3bb16d8350 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Fri, 18 Jul 2025 17:16:51 +0900 Subject: [PATCH 9/9] refactor: simplify string formatting in generate_init_script and print_error functions --- cli/src/gencomp.rs | 10 +++++----- cli/src/init.rs | 6 ++---- cli/src/main.rs | 20 ++++++++++---------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/cli/src/gencomp.rs b/cli/src/gencomp.rs index b8599fd..bb88961 100644 --- a/cli/src/gencomp.rs +++ b/cli/src/gencomp.rs @@ -25,35 +25,35 @@ mod generator { &mut app, appname, &outdir, - format!("bash/{}", appname), + format!("bash/{appname}"), ); generate_impl( Shell::Elvish, &mut app, appname, &outdir, - format!("elvish/{}", appname), + format!("elvish/{appname}"), ); generate_impl( Shell::Fish, &mut app, appname, &outdir, - format!("fish/{}", appname), + format!("fish/{appname}"), ); generate_impl( Shell::PowerShell, &mut app, appname, &outdir, - format!("powershell/{}", appname), + format!("powershell/{appname}"), ); generate_impl( Shell::Zsh, &mut app, appname, &outdir, - format!("zsh/_{}", appname), + format!("zsh/_{appname}"), ); } } diff --git a/cli/src/init.rs b/cli/src/init.rs index 084921f..f0d8518 100644 --- a/cli/src/init.rs +++ b/cli/src/init.rs @@ -12,8 +12,7 @@ pub(crate) fn generate_init_script(shell_name: String) -> Result { "zsh" => "init.bash", _ => { return Err(SiblingError::Fatal(format!( - "{}: Unsupported shell", - shell_name + "{shell_name}: Unsupported shell" ))) } }; @@ -21,8 +20,7 @@ pub(crate) fn generate_init_script(shell_name: String) -> Result { Some(file) => match std::str::from_utf8(file.data.as_ref()) { Ok(script) => Ok(script.to_string()), Err(_) => Err(SiblingError::Fatal(format!( - "{}: Invalid script", - script_file + "{script_file}: Invalid script" ))), }, None => Err(SiblingError::NotFound(script_file.into())), diff --git a/cli/src/main.rs b/cli/src/main.rs index f05f283..d73bb9c 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -77,15 +77,15 @@ fn perform(opts: CliOpts) -> Vec> { fn print_error(e: &SiblingError) { match e { - SiblingError::Io(e) => eprintln!("I/O error: {}", e), - SiblingError::NotDir(path) => eprintln!("{:?}: Not a directory", path), - SiblingError::NoParent(path) => eprintln!("{:?}: no parent directory", path), + SiblingError::Io(e) => eprintln!("I/O error: {e}"), + SiblingError::NotDir(path) => eprintln!("{path:?}: Not a directory"), + SiblingError::NoParent(path) => eprintln!("{path:?}: no parent directory"), SiblingError::Array(array) => { array.iter().for_each(print_error); } - SiblingError::NotFile(path) => eprintln!("{:?}: not a file", path), - SiblingError::NotFound(path) => eprintln!("{:?}: not found", path), - SiblingError::Fatal(message) => eprintln!("fatal error: {}", message), + SiblingError::NotFile(path) => eprintln!("{path:?}: not a file"), + SiblingError::NotFound(path) => eprintln!("{path:?}: not found"), + SiblingError::Fatal(message) => eprintln!("fatal error: {message}"), } } @@ -105,7 +105,7 @@ fn main() { } for item in perform(opts) { match item { - Ok(result) => println!("{}", result), + Ok(result) => println!("{result}"), Err(e) => print_error(&e), } } @@ -120,14 +120,14 @@ mod tests { let opts_r = cli::CliOpts::try_parse_from(vec!["sibling", "."]); if let Err(e) = &opts_r { - eprintln!("{}", e); + eprintln!("{e}"); } assert!(opts_r.is_ok()); let r = perform(opts_r.unwrap()); assert_eq!(r.len(), 1); match r.first().unwrap() { Err(e) => print_error(e), - Ok(result) => println!("{}", result), + Ok(result) => println!("{result}"), } } @@ -142,7 +142,7 @@ mod tests { ]); if let Err(e) = &opts_r { - eprintln!("{}", e); + eprintln!("{e}"); } assert!(opts_r.is_ok()); let r = perform(opts_r.unwrap());