From 1334d9f1d08a27ea7fa4cd4228ac1fdf44bdd552 Mon Sep 17 00:00:00 2001 From: Toby Vincent Date: Tue, 1 Nov 2022 17:48:54 -0500 Subject: feat: impl ignore crate to walk directories --- src/cli.rs | 39 ++++++---- src/config.rs | 247 ++++++++++++++++++++++++++++++++++++++++++---------------- src/error.rs | 9 +++ src/lib.rs | 22 ++++++ src/main.rs | 4 +- 5 files changed, 239 insertions(+), 82 deletions(-) (limited to 'src') diff --git a/src/cli.rs b/src/cli.rs index 5da1036..68bc665 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,22 +1,29 @@ -use crate::{Config, Result}; +use crate::{config::Paths, Config, Result}; use clap::{Args, Parser}; -use std::fs::File; -use std::sync::Arc; +use std::path::PathBuf; use tracing_subscriber::{filter::LevelFilter, Layer, Registry}; /// 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 + pub(crate) path: Vec, + + /// Add additional directory to search results. Can be specified multiple times + #[arg(short, long)] + pub(crate) directory: Vec, + #[command(flatten)] pub verbose: Verbosity, + /// Allows traversal into hidden directories when searching + #[arg(long)] + pub(crate) hidden: bool, + /// Connect to ssh host #[arg(short, long)] pub ssh: Option, - - #[command(flatten)] - pub config: Config, } impl Cli { @@ -30,16 +37,20 @@ impl Cli { layers.push(fmt_layer); - if self.config.enable_logging { - let file = File::create(&self.config.log_file)?; - let log_layer = tracing_subscriber::fmt::layer() - .with_writer(Arc::new(file)) - .boxed(); - layers.push(log_layer); - }; - Ok(layers) } + + pub fn as_config(&self) -> Config { + Config { + paths: Paths { + search: self.path.to_owned(), + add: self.directory.to_owned(), + hidden: self.hidden, + }, + finder: Default::default(), + logging: Default::default(), + } + } } #[derive(Debug, Default, Args)] diff --git a/src/config.rs b/src/config.rs index 42614ec..2c61b88 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,64 +1,99 @@ use crate::{Error, Result}; -use clap::{Args, Parser}; use figment::{ providers::{Env, Format, Serialized, Toml}, Figment, }; +use ignore::WalkBuilder; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::{fmt::Display, fs::File, path::PathBuf, sync::Arc}; +use tracing::{info, warn}; +use tracing_subscriber::{Layer, Registry}; -/// Simple program to manage projects and ssh hosts using tmux -#[derive(Debug, Parser, Serialize, Deserialize)] -#[command(author, version, about)] +#[derive(Debug, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct Config { - /// Path to search for directories - pub(crate) path: Vec, - - /// Stand alone directory. Can be specified multiple times - #[arg(short, long)] - pub(crate) project: Vec, - - /// Allows traversal into hidden directories when searching - #[arg(long)] - pub(crate) hidden: bool, - - #[arg(skip)] + pub(crate) paths: Paths, pub(crate) finder: Finder, - - #[arg(skip)] - pub(crate) enable_logging: bool, - - #[arg(skip)] - pub(crate) log_file: PathBuf, + pub(crate) logging: Logging, } impl Config { - pub fn extract(&self) -> Result { + pub fn extract(self) -> Result { Figment::from(Serialized::defaults(self)) .merge(Toml::file("tmuxr.toml")) .merge(Env::prefixed("TMUXR_")) .extract() .map_err(Error::from) } + + pub fn as_layer(&self) -> Result + Send + Sync>>> { + let mut layers = Vec::new(); + + if self.logging.enabled { + let file = File::create(&self.logging.path)?; + let log_layer = tracing_subscriber::fmt::layer() + .with_writer(Arc::new(file)) + .boxed(); + layers.push(log_layer); + }; + + Ok(layers) + } } -impl Default for Config { - fn default() -> Self { - Self { - path: Default::default(), - project: Default::default(), - hidden: Default::default(), - finder: Default::default(), - enable_logging: Default::default(), - log_file: dirs::cache_dir() - .map(|p| p.join("tmuxr")) - .unwrap_or_default() - .join("tmuxr.log"), - } +#[derive(Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub(crate) struct Paths { + pub(crate) search: Vec, + pub(crate) add: Vec, + pub(crate) hidden: bool, +} + +impl Display for Paths { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Vec::::from(self) + .into_iter() + .filter_map(|p: PathBuf| { + p.to_str().map(|s| s.to_owned()).or_else(|| { + warn!("Failed to convert path to str"); + None + }) + }) + .map(|s| writeln!(f, "{}", s)) + .collect::() + } +} + +impl From<&Paths> for Vec { + fn from(value: &Paths) -> Self { + let mut dirs = value.search.to_owned().into_iter(); + let first = dirs.nth(0).unwrap_or_default(); + + let mut path_bufs = dirs + .fold( + WalkBuilder::new(first) + .standard_filters(true) + .max_depth(Some(1)), + |walk, dir| walk.add(dir), + ) + .build() + .filter_map(|res| { + res.map_err(|err| { + warn!("Error while walking directory: {:?}", err); + err + }) + .ok() + }) + .map(|entry| entry.into_path()) + .collect::>(); + + info!("{:?}", value.add); + info!("{:?}", path_bufs); + + path_bufs.extend(value.add.to_owned()); + path_bufs } } -#[derive(Debug, Args, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub(crate) struct Finder { pub(crate) program: String, pub(crate) args: Vec, @@ -69,46 +104,126 @@ impl Default for Finder { Self { program: "fzf-tmux".into(), args: vec![ - "--".into(), - "--multi".into(), - "--print-query".into(), - "-d/".into(), - "--preview-window='right,75%,<80(up,75%,border-bottom)'".into(), - "--preview='sel={}; less ${sel:-{q}} 2>/dev/null'".into(), - ], + "--", + "--multi", + "--print-query", + "-d/", + "--preview-window='right,75%,<80(up,75%,border-bottom)'", + "--preview='sel={}; less ${sel:-{q}} 2>/dev/null'", + ] + .into_iter() + .map(Into::into) + .collect(), } } } -#[derive(Debug, Args, Serialize, Deserialize)] -pub(crate) struct Directory { +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Logging { + pub(crate) enabled: bool, pub(crate) path: PathBuf, - pub(crate) root: bool, - pub(crate) hidden: bool, -} - -#[derive(Debug, Default)] -pub(crate) struct Directories { - search_dirs: Vec, - root_dirs: Vec, } -impl From> for Directories { - fn from(value: Vec) -> Self { - value.iter().fold(Directories::default(), |mut acc, d| { - match d.root { - true => acc.root_dirs.push(d.path.to_owned()), - false => acc.search_dirs.push(d.path.to_owned()), - }; - acc - }) +impl Default for Logging { + fn default() -> Self { + Self { + enabled: Default::default(), + path: dirs::cache_dir() + .map(|p| p.join("tmuxr")) + .unwrap_or_default() + .join("tmuxr.log"), + } } } #[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] + 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'", + ] + + [logging] + enabled = true + path = "/tmp/tmuxr/test/tmuxr.log" + "#, + )?; + + let config: Config = Figment::from(Serialized::defaults(Config::default())) + .merge(Toml::file("tmuxr.toml")) + .extract()?; + + assert_eq!(config, Config::default()); + + Ok(()) + }); + } + #[test] - fn test_start() { - assert_eq!(1, 1); + fn custom() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "tmuxr.toml", + r#" + [paths] + search = [ "/tmp/tmuxr/test/projects" ] + add = [ "/tmp/tmuxr/test/other_projects" ] + hidden = true + + [finder] + program = "fzf" + args = ["-0", "-1", "--preview='cat'"] + + [logging] + enabled = true + log_file = "/tmp/tmuxr/test/tmuxr.log" + "#, + )?; + + let config: Config = Figment::from(Serialized::defaults(Config::default())) + .merge(Toml::file("tmuxr.toml")) + .extract()?; + + assert_eq!( + config, + Config { + paths: Paths { + search: vec!["/tmp/tmuxr/test/projects".into()], + add: vec!["/tmp/tmuxr/test/extra_project".into()], + hidden: true, + }, + finder: Finder { + program: "fzf".into(), + args: vec!["-0".into(), "-1".into(), "--preview='cat'".into()], + }, + logging: Logging { + enabled: true, + path: "/tmp/tmuxr/test/tmuxr.log".into() + }, + } + ); + + Ok(()) + }); } } diff --git a/src/error.rs b/src/error.rs index e423493..5b0fea8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,4 +7,13 @@ pub enum Error { #[error("Config error: {0:?}")] Config(#[from] figment::error::Error), + + #[error("Ignore error: {0:?}")] + Ignore(#[from] ignore::Error), + + #[error("Process error: {0:?}")] + Finder(String), + + #[error("Path error: {0:?}")] + Path(String), } diff --git a/src/lib.rs b/src/lib.rs index 872ca50..acaa475 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,9 @@ +use std::{ + io::Write, + process::{Command, Stdio}, +}; +use tracing::info; + pub use crate::cli::Cli; pub use crate::config::Config; pub use crate::error::{Error, Result}; @@ -8,5 +14,21 @@ mod error; #[tracing::instrument()] pub fn run(config: &Config) -> Result<()> { + let mut finder = Command::new(&config.finder.program) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .args(&config.finder.args) + .spawn()?; + + finder + .stdin + .as_mut() + .ok_or_else(|| Error::Finder("Failed to get finder's stdin".to_string()))? + .write_all(config.paths.to_string().as_bytes())?; + + let output = finder.wait_with_output()?; + + info!("{:?}", output); + Ok(()) } diff --git a/src/main.rs b/src/main.rs index 40fccd6..c9b21fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,10 @@ use clap::Parser; -use tmuxr::{Cli, Result}; +use tmuxr::{Cli, Config, Result}; use tracing_subscriber::prelude::*; fn main() -> Result<()> { let cli = Cli::parse(); - let config = cli.config.extract()?; + let config = Config::extract(cli.as_config())?; tracing_subscriber::registry().with(cli.as_layer()?).init(); -- cgit v1.2.3-70-g09d2