summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cli.rs103
-rw-r--r--src/config.rs116
-rw-r--r--src/lib.rs2
-rw-r--r--src/main.rs23
-rw-r--r--src/project/path.rs19
-rw-r--r--src/search.rs82
-rw-r--r--src/search/entry.rs36
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()
}
}
diff --git a/src/lib.rs b/src/lib.rs
index 687cb44..b54d897 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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 }
}
}