diff options
-rw-r--r-- | src/cli.rs | 103 | ||||
-rw-r--r-- | src/config.rs | 116 | ||||
-rw-r--r-- | src/lib.rs | 2 | ||||
-rw-r--r-- | src/main.rs | 23 | ||||
-rw-r--r-- | src/project/path.rs | 19 | ||||
-rw-r--r-- | src/search.rs | 82 | ||||
-rw-r--r-- | src/search/entry.rs | 36 |
7 files changed, 138 insertions, 243 deletions
diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index 6f7ee5d..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,103 +0,0 @@ -use clap::{Args, Parser}; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use tracing::{metadata::LevelFilter, Level}; - -use crate::{config::SearchEntryConfig, Config}; - -/// Tool for listing project directories. -#[derive(Debug, Clone, Default, Parser, Serialize, Deserialize)] -#[command(author, version, about)] -pub struct Cli { - #[command(flatten)] - pub projects: Projects, - - #[command(flatten)] - pub verbosity: Verbosity, -} - -#[derive(Debug, Default, Clone, Args, Serialize, Deserialize)] -#[serde(into = "Config")] -pub struct Projects { - /// Directory to search. - /// - /// Directories are searched recursively based on `--max-depth`. - paths: Vec<PathBuf>, - - /// Max depth to recurse. - /// - /// Setting to 0 will only use the supplied directory. - #[arg(short = 'd', long, default_value = "1")] - max_depth: Option<usize>, - - /// Recurse into hidden directories. - /// - /// Traverse into hidden directories while searching. A directory is considered hidden - /// if its name starts with a `.` sign (dot). If `--max-depth` is 0, this has no effect. - #[arg(long)] - hidden: bool, - - /// Match directories containing item named <PATTERN> - #[arg(long, short)] - pattern: Option<String>, - - /// Match git repositories - #[cfg(feature = "git")] - #[arg(long, short, default_value_t = true)] - git: bool, -} - -impl From<Projects> for Config { - fn from(value: Projects) -> Self { - let paths = value - .paths - .iter() - .cloned() - .map(|path_buf| SearchEntryConfig { - path_buf, - hidden: value.hidden, - max_depth: value.max_depth, - pattern: value.pattern.to_owned(), - - #[cfg(feature = "git")] - git: value.git, - }) - .collect(); - - Config { paths } - } -} - -#[derive(Debug, Default, Clone, Args, Serialize, Deserialize)] -pub struct Verbosity { - /// Print additional information per occurrence. - /// - /// Conflicts with `--quiet`. - #[arg(short, long, global = true, action = clap::ArgAction::Count, conflicts_with = "quiet")] - pub verbose: u8, - - /// Suppress all output. - /// - /// Conflicts with `--verbose`. - #[arg(short, long, global = true, conflicts_with = "verbose")] - pub quiet: bool, -} - -impl From<Verbosity> for Option<Level> { - fn from(value: Verbosity) -> Self { - match 1 + value.verbose - u8::from(value.quiet) { - 0 => None, - 1 => Some(Level::ERROR), - 2 => Some(Level::WARN), - 3 => Some(Level::INFO), - 4 => Some(Level::DEBUG), - _ => Some(Level::TRACE), - } - } -} - -impl From<Verbosity> for LevelFilter { - fn from(value: Verbosity) -> Self { - Option::<Level>::from(value).into() - } -} diff --git a/src/config.rs b/src/config.rs index cc1f28f..358a258 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,91 +1,49 @@ -use serde::{Deserialize, Serialize}; -use std::{convert::Infallible, path::PathBuf, str::FromStr}; +use clap::{Args, Parser}; +use tracing::{metadata::LevelFilter, Level}; -#[serde_with::serde_as] -#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] -pub struct Config { - #[serde_as(as = "Vec<serde_with::PickFirst<(_, serde_with::DisplayFromStr)>>")] - pub paths: Vec<SearchEntryConfig>, -} +use crate::search::Search; -#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] -#[serde(default)] -pub struct SearchEntryConfig { - pub path_buf: PathBuf, - pub hidden: bool, - pub max_depth: Option<usize>, - pub pattern: Option<String>, - #[cfg(feature = "git")] - pub git: bool, -} +/// Tool for listing project directories. +#[derive(Debug, Clone, Default, Parser)] +#[command(author, version, about)] +pub struct Config { + #[command(flatten)] + pub search: Search, -impl SearchEntryConfig { - pub fn new(path_buf: PathBuf) -> Self { - Self { - path_buf, - ..Default::default() - } - } + #[command(flatten)] + pub verbosity: Verbosity, } -impl From<PathBuf> for SearchEntryConfig { - fn from(path_buf: PathBuf) -> Self { - Self::new(path_buf) - } +#[derive(Debug, Default, Clone, Args)] +pub struct Verbosity { + /// Print additional information per occurrence. + /// + /// Conflicts with `--quiet`. + #[arg(short, long, global = true, action = clap::ArgAction::Count, conflicts_with = "quiet")] + pub verbose: u8, + + /// Suppress all output. + /// + /// Conflicts with `--verbose`. + #[arg(short, long, global = true, conflicts_with = "verbose")] + pub quiet: bool, } -impl FromStr for SearchEntryConfig { - type Err = Infallible; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - s.parse().map(|path_buf| Self { - path_buf, - ..Default::default() - }) +impl From<&Verbosity> for Option<Level> { + fn from(value: &Verbosity) -> Self { + match 1 + value.verbose - u8::from(value.quiet) { + 0 => None, + 1 => Some(Level::ERROR), + 2 => Some(Level::WARN), + 3 => Some(Level::INFO), + 4 => Some(Level::DEBUG), + _ => Some(Level::TRACE), + } } } -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_extract_config() { - let s = r#" - paths = [ - "/path/to/projects", - { path_buf = "/path/to/other_projects", hidden = true, max_depth = 1 }, - { path_buf = "/path/to/another_project", max_depth = 0 } - ] - "#; - - let config: Config = toml::from_str(s).unwrap(); - - assert_eq!( - config, - Config { - paths: Vec::from([ - SearchEntryConfig { - path_buf: "/path/to/projects".into(), - hidden: false, - max_depth: None, - ..Default::default() - }, - SearchEntryConfig { - path_buf: "/path/to/other_projects".into(), - hidden: true, - max_depth: Some(1), - ..Default::default() - }, - SearchEntryConfig { - path_buf: "/path/to/another_project".into(), - hidden: false, - max_depth: Some(0), - ..Default::default() - }, - ]), - } - ); +impl From<&Verbosity> for LevelFilter { + fn from(value: &Verbosity) -> Self { + Option::<Level>::from(value).into() } } @@ -1,10 +1,8 @@ -pub use crate::cli::Cli; pub use crate::config::Config; pub use crate::error::{Error, Result}; pub mod project; pub mod search; -mod cli; mod config; mod error; diff --git a/src/main.rs b/src/main.rs index 8470651..7f3b90d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,40 +1,29 @@ use anyhow::{Context, Result}; use clap::Parser; -use figment::{ - providers::{Env, Format, Serialized, Toml}, - Figment, -}; -use projectr::{project::ProjectItem, search::Search, Cli, Config}; +use projectr::{project::ProjectItem, Config}; use tokio::signal; #[tokio::main] async fn main() -> Result<()> { - let cli = Cli::parse(); - - let config: Config = Figment::new() - .merge(Figment::from(Serialized::defaults(&cli.projects))) - .merge(Toml::file("projectr.toml")) - .merge(Env::prefixed("PROJECTR_")) - .extract() - .context("Failed to extract config")?; + let cli = Config::parse(); tracing_subscriber::fmt::fmt() .pretty() .with_writer(std::io::stderr) - .with_max_level(cli.verbosity) + .with_max_level(&cli.verbosity) .init(); let res = tokio::select! { res = signal::ctrl_c() => res.map_err(Into::into), - res = run(&config) => res.context("Failed to run projectr"), + res = run(cli) => res.context("Failed to run projectr"), }; res } #[tracing::instrument] -pub async fn run(config: &Config) -> Result<()> { - let mut projects: Vec<ProjectItem> = Search::from(config.paths.to_owned()).collect(); +pub async fn run(config: Config) -> Result<()> { + let mut projects: Vec<ProjectItem> = config.search.into_iter().collect(); projects.sort_unstable_by_key(|p| *p.timestamp()); diff --git a/src/project/path.rs b/src/project/path.rs index fad746c..5954ff9 100644 --- a/src/project/path.rs +++ b/src/project/path.rs @@ -5,16 +5,23 @@ use tracing::debug; use super::{ProjectItem, ProjectParser}; #[derive(Debug, Clone)] -pub struct PathMatcher(pub String); +pub enum PathMatcher { + All(PathBuf), + Pattern(String), +} impl ProjectParser for PathMatcher { #[tracing::instrument] fn parse(&self, path_buf: PathBuf) -> Option<ProjectItem> { - if path_buf.join(&self.0).exists() { - Some(Box::new(PathProject::new(path_buf))) - } else { - debug!("Failed to match pattern in directory"); - None + match self { + PathMatcher::All(p) if &path_buf != p => Some(Box::new(PathProject::new(path_buf))), + PathMatcher::Pattern(p) if path_buf.join(p).exists() => { + Some(Box::new(PathProject::new(path_buf))) + } + _ => { + debug!("Failed to match pattern in directory"); + None + } } } } diff --git a/src/search.rs b/src/search.rs index 6b30293..888179e 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,16 +1,76 @@ +use std::path::PathBuf; + +use clap::Args; + +use crate::project::ProjectItem; + use self::entry::SearchEntry; -use crate::{config::SearchEntryConfig, project::ProjectItem}; pub mod entry; -type EntryIter = std::vec::IntoIter<SearchEntryConfig>; +type EntryIter = std::vec::IntoIter<PathBuf>; +#[derive(Debug, Default, Clone, Args)] pub struct Search { + /// Directory to search. + /// + /// Directories are searched recursively based on `--max-depth`. + pub paths: Vec<PathBuf>, + + #[command(flatten)] + pub filter: Filters, +} + +#[derive(Debug, Default, Clone, Args)] +pub struct Filters { + /// Match all child directories + #[arg(long, short, conflicts_with_all = ["pattern", "git"])] + pub all: bool, + + /// Max depth to recurse. + /// + /// Setting to 0 will only use the supplied directory. + #[arg(short = 'd', long, default_value = "1")] + pub max_depth: Option<usize>, + + /// Recurse into hidden directories. + /// + /// Traverse into hidden directories while searching. A directory is considered hidden + /// if its name starts with a `.` sign (dot). If `--max-depth` is 0, this has no effect. + #[arg(long)] + pub hidden: bool, + + /// Match directories containing item named <PATTERN> + #[arg(long, short)] + pub pattern: Option<String>, + + /// Match git repositories + #[cfg(feature = "git")] + #[arg(long, short, default_value_t = true)] + pub git: bool, +} + +impl IntoIterator for Search { + type Item = ProjectItem; + + type IntoIter = SearchIter; + + fn into_iter(self) -> Self::IntoIter { + SearchIter { + iter: self.paths.into_iter(), + config: self.filter, + curr: None, + } + } +} + +pub struct SearchIter { iter: EntryIter, + config: Filters, curr: Option<SearchEntry>, } -impl std::fmt::Debug for Search { +impl std::fmt::Debug for SearchIter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Projects") .field("paths_iter", &self.iter) @@ -18,19 +78,7 @@ impl std::fmt::Debug for Search { } } -impl<T> From<T> for Search -where - T: IntoIterator<IntoIter = EntryIter>, -{ - fn from(value: T) -> Self { - Self { - iter: value.into_iter(), - curr: None, - } - } -} - -impl Iterator for Search { +impl Iterator for SearchIter { type Item = ProjectItem; #[tracing::instrument] @@ -38,7 +86,7 @@ impl Iterator for Search { match self.curr.as_mut().and_then(|c| c.next()) { Some(proj) => Some(proj), None => { - self.curr = Some(self.iter.next()?.into()); + self.curr = Some(SearchEntry::new(self.iter.next()?, &self.config)); self.next() } } diff --git a/src/search/entry.rs b/src/search/entry.rs index eb845e1..16dcd8b 100644 --- a/src/search/entry.rs +++ b/src/search/entry.rs @@ -1,11 +1,11 @@ +use std::path::PathBuf; + use ignore::{Walk, WalkBuilder}; use tracing::error; -use crate::{ - config::SearchEntryConfig, - project::{path::PathMatcher, ProjectParser, ProjectParserGroup}, - search::ProjectItem, -}; +use crate::project::{path::PathMatcher, ProjectItem, ProjectParser, ProjectParserGroup}; + +use super::Filters; pub struct SearchEntry { parsers: ProjectParserGroup, @@ -13,17 +13,15 @@ pub struct SearchEntry { } impl SearchEntry { - fn new(config: &SearchEntryConfig) -> Self { - let iter = WalkBuilder::new(&config.path_buf) - .standard_filters(true) - .max_depth(config.max_depth) - .hidden(!config.hidden) - .build(); - + pub fn new(path_buf: PathBuf, config: &Filters) -> Self { let mut parsers = ProjectParserGroup::new(); + if config.all { + parsers.push(Box::new(PathMatcher::All(path_buf.to_owned()))) + } + if let Some(s) = config.pattern.as_ref() { - parsers.push(Box::new(PathMatcher(s.to_owned()))); + parsers.push(Box::new(PathMatcher::Pattern(s.to_owned()))); }; #[cfg(feature = "git")] @@ -31,13 +29,13 @@ impl SearchEntry { parsers.push(Box::new(crate::project::git::GitMatcher)); }; - Self { parsers, iter } - } -} + let iter = WalkBuilder::new(path_buf) + .standard_filters(true) + .max_depth(config.max_depth) + .hidden(!config.hidden) + .build(); -impl From<SearchEntryConfig> for SearchEntry { - fn from(config: SearchEntryConfig) -> Self { - Self::new(&config) + Self { parsers, iter } } } |