diff options
-rw-r--r-- | Cargo.lock | 1 | ||||
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | src/cli.rs | 41 | ||||
-rw-r--r-- | src/config.rs | 91 | ||||
-rw-r--r-- | src/directories.rs | 52 | ||||
-rw-r--r-- | src/directories/config.rs | 26 | ||||
-rw-r--r-- | src/error.rs | 2 | ||||
-rw-r--r-- | src/finder/config.rs | 61 | ||||
-rw-r--r-- | src/finder/error.rs | 1 | ||||
-rw-r--r-- | src/lib.rs | 4 | ||||
-rw-r--r-- | src/main.rs | 11 | ||||
-rw-r--r-- | src/paths.rs | 113 | ||||
-rw-r--r-- | src/paths/config.rs | 107 | ||||
-rw-r--r-- | src/paths/error.rs (renamed from src/directories/error.rs) | 2 |
14 files changed, 321 insertions, 194 deletions
@@ -844,6 +844,7 @@ dependencies = [ "serde", "sled", "ssh_cfg", + "tempfile", "thiserror", "tmux_interface", "tracing", @@ -24,3 +24,6 @@ tmux_interface = { version = "0.2.1", default-features = false, features = [ ] } tracing = { version = "0.1.37", features = ["attributes"] } tracing-subscriber = "0.3.16" + +[dev-dependencies] +tempfile = "3.3.0" @@ -2,24 +2,32 @@ use clap::{Args, Parser}; use std::path::PathBuf; use tracing_subscriber::{filter::LevelFilter, Layer, Registry}; +use crate::paths::PathEntry; + /// Simple program to manage projects and ssh hosts using tmux #[derive(Parser, Debug)] #[command(author, version, about)] pub struct Cli { - /// Path to search recursively for directories + /// Path to directories pub(crate) path: Vec<PathBuf>, - /// Add additional directory to search results. Can be specified multiple times - #[arg(short, long)] - pub(crate) directory: Vec<PathBuf>, - - #[command(flatten)] - pub verbose: Verbosity, + /// Max depth to recurse. + /// + /// By default, no limit is set. Setting to 0 will only use the supplied directory. + #[arg(short = 'd', long)] + pub(crate) max_depth: Option<usize>, - /// Allows traversal into hidden directories when searching + /// Recurse into hidden directories. + /// + /// Include hidden directories when traversing directories. (default: hidden directories + /// are skipped). A Directory is considered to be hidden if its name starts with a `.` + /// sign (dot). If `max-depth` is set to 0, this has no effect (As no recursion happens). #[arg(long)] pub(crate) hidden: bool, + #[command(flatten)] + pub verbose: Verbosity, + /// Connect to ssh host #[arg(short, long)] pub ssh: Option<String>, @@ -36,11 +44,18 @@ impl Cli { } // TODO: replace this with `impl Figment for Cli` - pub fn as_config(&self) -> crate::directories::Config { - crate::directories::Config { - search: self.path.to_owned(), - add: self.directory.to_owned(), - hidden: self.hidden, + pub fn as_config(&self) -> crate::paths::Config { + crate::paths::Config { + paths: self + .path + .to_owned() + .into_iter() + .map(|p| PathEntry { + path: p, + hidden: self.hidden, + recurse: self.max_depth, + }) + .collect(), } } } diff --git a/src/config.rs b/src/config.rs index a2305f1..4ba59b4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,7 +8,7 @@ use tracing_subscriber::{Layer, Registry}; pub struct Config { pub log_enabled: bool, pub log_file: PathBuf, - pub paths: crate::directories::Config, + pub paths: crate::paths::Config, pub finder: crate::finder::Config, } @@ -63,92 +63,3 @@ impl TryFrom<Figment> for Config { value.extract().map_err(Into::into) } } - -#[cfg(test)] -mod tests { - use crate::{finder, directories}; - - use super::*; - use figment::providers::{Format, Serialized, Toml}; - - #[test] - fn defaults() { - figment::Jail::expect_with(|jail| { - jail.create_file( - "tmuxr.toml", - r#" - log_enabled = false - - [paths] - search = [] - add = [] - hidden = false - - [finder] - program = "fzf-tmux" - args = [ - "--", - "--multi", - "--print-query", - "-d/", - "--preview-window='right,75%,<80(up,75%,border-bottom)'", - "--preview='sel={}; less ${sel:-{q}} 2>/dev/null'", - ] - "#, - )?; - - let config: Config = Figment::from(Serialized::defaults(Config::default())) - .merge(Toml::file("tmuxr.toml")) - .extract()?; - - assert_eq!(config, Config::default()); - - Ok(()) - }); - } - - #[test] - fn custom() { - figment::Jail::expect_with(|jail| { - jail.create_file( - "tmuxr.toml", - r#" - log_enabled = true - log_file = "/tmp/tmuxr/test/tmuxr.log" - - [paths] - search = [ "/tmp/tmuxr/test/projects" ] - add = [ "/tmp/tmuxr/test/other_projects" ] - hidden = true - - [finder] - program = "fzf" - args = ["-0", "-1", "--preview='cat'"] - "#, - )?; - - let config: Config = Figment::from(Serialized::defaults(Config::default())) - .merge(Toml::file("tmuxr.toml")) - .extract()?; - - assert_eq!( - config, - Config { - paths: directories::Config { - search: vec!["/tmp/tmuxr/test/projects".into()], - add: vec!["/tmp/tmuxr/test/extra_project".into()], - hidden: true, - }, - finder: finder::Config { - program: "fzf".into(), - args: vec!["-0".into(), "-1".into(), "--preview='cat'".into()], - }, - log_enabled: true, - log_file: "/tmp/tmuxr/test/tmuxr.log".into() - } - ); - - Ok(()) - }); - } -} diff --git a/src/directories.rs b/src/directories.rs deleted file mode 100644 index 62a566f..0000000 --- a/src/directories.rs +++ /dev/null @@ -1,52 +0,0 @@ -use ignore::WalkBuilder; -use std::{ops::Deref, path::PathBuf}; - -pub use config::Config; -pub use error::{Error, Result}; - -mod config; -mod error; - -#[derive(Debug, PartialEq, Eq, Default)] -pub struct Directories { - config: Config, - pub directories: Vec<PathBuf>, -} - -impl Directories { - pub fn new(config: &Config) -> Directories { - Directories { - config: config.to_owned(), - directories: config.add.to_owned(), - } - } - - pub fn walk(&mut self) -> Result<&mut Directories> { - let mut dirs = self.config.search.iter().cloned(); - - // Taking first element is neccissary due to requirement of an initial item - // in the WalkBuilder API - // - // See: https://github.com/BurntSushi/ripgrep/issues/1761 - let first = dirs.next().unwrap_or_default(); - let mut walk = WalkBuilder::new(first); - let walk = walk.standard_filters(true).max_depth(Some(1)); - - let results = dirs - .fold(walk, |walk, dir| walk.add(dir)) - .build() - .map(|r| r.map(|d| d.into_path())) - .collect::<std::result::Result<Vec<_>, _>>(); - - self.directories.extend(results?); - Ok(self) - } -} - -impl Deref for Directories { - type Target = Vec<PathBuf>; - - fn deref(&self) -> &Self::Target { - &self.directories - } -} diff --git a/src/directories/config.rs b/src/directories/config.rs deleted file mode 100644 index ec66d0c..0000000 --- a/src/directories/config.rs +++ /dev/null @@ -1,26 +0,0 @@ -use figment::{providers::Serialized, value, Figment, Metadata, Profile, Provider}; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] -pub struct Config { - pub(crate) search: Vec<PathBuf>, - pub(crate) add: Vec<PathBuf>, - pub(crate) hidden: bool, -} - -impl Config { - pub fn from<T: Provider>(provider: T) -> figment::error::Result<Config> { - Figment::from(provider).extract() - } -} - -impl Provider for Config { - fn metadata(&self) -> Metadata { - Metadata::named("Tmuxr directory config") - } - - fn data(&self) -> figment::error::Result<value::Map<Profile, value::Dict>> { - Serialized::defaults(Config::default()).data() - } -} diff --git a/src/error.rs b/src/error.rs index fe94978..efd7193 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,7 +9,7 @@ pub enum Error { Config(#[from] figment::error::Error), #[error("Directories error: {0:?}")] - Directories(#[from] crate::directories::Error), + Directories(#[from] crate::paths::Error), #[error("Finder error: {0:?}")] Finder(#[from] crate::finder::Error), diff --git a/src/finder/config.rs b/src/finder/config.rs index bc39556..d0a0570 100644 --- a/src/finder/config.rs +++ b/src/finder/config.rs @@ -41,3 +41,64 @@ impl Provider for Config { Serialized::defaults(Config::default()).data() } } + +#[cfg(test)] +mod tests { + use super::*; + use figment::providers::{Format, Serialized, Toml}; + + #[test] + fn defaults() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "tmuxr.toml", + r#" + program = "fzf-tmux" + args = [ + "--", + "--multi", + "--print-query", + "-d/", + "--preview-window='right,75%,<80(up,75%,border-bottom)'", + "--preview='sel={}; less ${sel:-{q}} 2>/dev/null'", + ] + "#, + )?; + + let config: Config = Figment::from(Serialized::defaults(Config::default())) + .merge(Toml::file("tmuxr.toml")) + .extract()?; + + assert_eq!(config, Config::default()); + + Ok(()) + }); + } + + #[test] + fn custom() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "tmuxr.toml", + r#" + program = "fzf" + args = ["-0", "-1", "--preview='cat'"] + "#, + )?; + + let config: Config = Figment::from(Serialized::defaults(Config::default())) + .merge(Toml::file("tmuxr.toml")) + .extract()?; + + assert_eq!( + config, + Config { + program: "fzf".into(), + args: vec!["-0".into(), "-1".into(), "--preview='cat'".into()], + } + ); + + Ok(()) + }); + } +} diff --git a/src/finder/error.rs b/src/finder/error.rs index 46d39c4..181da21 100644 --- a/src/finder/error.rs +++ b/src/finder/error.rs @@ -8,4 +8,3 @@ pub enum Error { #[error("Stdin error: Failed to get finder's stdin")] Stdin, } - @@ -1,11 +1,11 @@ pub use crate::cli::Cli; pub use crate::config::Config; -pub use crate::directories::Directories; pub use crate::error::{Error, Result}; pub use crate::finder::Finder; +pub use crate::paths::Paths; mod cli; mod config; -mod directories; mod error; mod finder; +mod paths; diff --git a/src/main.rs b/src/main.rs index 5a6679e..ff29b97 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,10 @@ -use std::{fs::File, sync::Arc}; - use clap::Parser; use figment::{ providers::{Env, Format, Serialized, Toml}, Figment, }; -use tmuxr::{Cli, Config, Directories, Finder, Result}; +use std::{fs::File, sync::Arc}; +use tmuxr::{Cli, Config, Finder, Paths, Result}; use tracing::info; use tracing_subscriber::prelude::*; @@ -50,12 +49,10 @@ fn init_subscriber(cli: &Cli, config: &Config) -> Result<()> { #[tracing::instrument()] pub fn run(config: &Config) -> Result<()> { - let mut directories = Directories::new(&config.paths); - directories.walk()?; - + let directories = Paths::from(&config.paths); let mut finder = Finder::new(&config.finder)?; - finder.write_path_buf_vectored(directories.directories)?; + finder.write_path_buf_vectored(directories)?; let output = finder.wait_with_output()?; diff --git a/src/paths.rs b/src/paths.rs new file mode 100644 index 0000000..de6a2dd --- /dev/null +++ b/src/paths.rs @@ -0,0 +1,113 @@ +use ignore::{Walk, WalkBuilder}; +use std::{path::PathBuf, vec::IntoIter}; +use tracing::warn; + +pub use config::{Config, PathEntry}; +pub use error::Error; + +mod config; +mod error; + +#[derive(Default)] +pub struct Paths { + path_entries: Vec<PathEntry>, + paths_iter: Option<IntoIter<PathEntry>>, + iter: Option<Walk>, +} + +impl Paths { + pub fn new(path_entries: Vec<PathEntry>) -> Self { + Self { + path_entries, + ..Default::default() + } + } +} + +impl From<&Config> for Paths { + fn from(value: &Config) -> Self { + Paths { + path_entries: value.paths.to_owned(), + ..Default::default() + } + } +} + +impl Iterator for Paths { + type Item = PathBuf; + + fn next(&mut self) -> Option<Self::Item> { + loop { + match self.iter.as_mut().and_then(|iter| iter.next()) { + Some(Ok(d)) => return Some(d.into_path()), + Some(Err(err)) => warn!("{:?}", err), + None => match self.paths_iter.as_mut() { + Some(paths_iter) => { + let next = paths_iter.next()?; + self.iter = Some( + WalkBuilder::new(next.path) + .standard_filters(true) + .max_depth(next.recurse) + .hidden(next.hidden) + .build(), + ); + } + None => self.paths_iter = Some(self.path_entries.to_owned().into_iter()), + }, + }; + } + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use super::*; + + #[test] + fn test_iteration() { + let test_dir = tempfile::Builder::new() + .tempdir() + .expect("Failed to create tmp directory"); + + let test_path = test_dir.path().to_owned(); + + let mut projects = Vec::from([ + test_path.join("projects"), + test_path.join("projects/project1"), + test_path.join("projects/project2"), + test_path.join("project3"), + test_path.join("other_projects/project3"), + ]); + + projects.iter().for_each(|project| { + fs::create_dir_all(project).expect("Failed to create test project directory"); + }); + + let directories = Paths::new(Vec::from([ + PathEntry { + path: test_path.join("projects"), + hidden: false, + recurse: Some(1), + }, + PathEntry { + path: test_path.join("project3"), + hidden: false, + recurse: Some(0), + }, + PathEntry { + path: test_path.join("other_projects/project3"), + hidden: false, + recurse: Some(0), + }, + ])); + + let mut actual = directories.into_iter().collect::<Vec<PathBuf>>(); + + projects.sort(); + actual.sort(); + + assert_eq!(projects, actual); + } +} diff --git a/src/paths/config.rs b/src/paths/config.rs new file mode 100644 index 0000000..72c5da7 --- /dev/null +++ b/src/paths/config.rs @@ -0,0 +1,107 @@ +use figment::{providers::Serialized, value, Figment, Metadata, Profile, Provider}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +pub struct Config { + pub(crate) paths: Vec<PathEntry>, +} + +impl Config { + pub fn from<T: Provider>(provider: T) -> figment::error::Result<Config> { + Figment::from(provider).extract() + } +} + +impl Provider for Config { + fn metadata(&self) -> Metadata { + Metadata::named("Tmuxr path config") + } + + fn data(&self) -> figment::error::Result<value::Map<Profile, value::Dict>> { + Serialized::defaults(Config::default()).data() + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +pub struct PathEntry { + pub path: PathBuf, + pub hidden: bool, + pub recurse: Option<usize>, +} + +impl std::str::FromStr for PathEntry { + type Err = std::convert::Infallible; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(PathEntry { + path: s.to_string().into(), + ..Default::default() + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use figment::providers::{Format, Serialized, Toml}; + + #[test] + fn defaults() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "tmuxr.toml", + r#" + paths = [] + "#, + )?; + + let config: Config = Figment::from(Serialized::defaults(Config::default())) + .merge(Toml::file("tmuxr.toml")) + .extract()?; + + assert_eq!(config, Config::default()); + + Ok(()) + }); + } + + #[test] + fn custom() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "tmuxr.toml", + r#" + paths = [ + "/tmp/projects", + { path = "/tmp/tmuxr/test/other_projects", recursive = false, hidden = true }, + ] + "#, + )?; + + let config: Config = Figment::from(Serialized::defaults(Config::default())) + .merge(Toml::file("tmuxr.toml")) + .extract()?; + + assert_eq!( + config, + Config { + paths: Vec::from([ + PathEntry { + path: "/tmp/tmuxr/test/project_1".into(), + hidden: false, + recurse: None, + }, + PathEntry { + path: "/tmp/tmuxr/test/projects".into(), + hidden: false, + recurse: Some(1), + } + ]), + } + ); + + Ok(()) + }); + } +} diff --git a/src/directories/error.rs b/src/paths/error.rs index 8509b08..13a69ea 100644 --- a/src/directories/error.rs +++ b/src/paths/error.rs @@ -1,5 +1,3 @@ -pub type Result<T> = std::result::Result<T, Error>; - #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Ignore error: {0:?}")] |