From 09be0362d42034e343b64de08618c995b63c90fe Mon Sep 17 00:00:00 2001 From: Toby Vincent Date: Thu, 17 Nov 2022 23:12:10 -0600 Subject: feat: get intitial finder working --- src/cli.rs | 53 ++++++++++++++++--------- src/config.rs | 76 ++++++++++++++++++++++++++++++++++-- src/error.rs | 10 +++++ src/finder.rs | 93 ++++++++++++++++++++++++++++++-------------- src/finder/config.rs | 53 ++++++++----------------- src/finder/error.rs | 12 ++++-- src/main.rs | 53 ++++++++++++------------- src/paths.rs | 26 ++++++++++--- src/paths/config.rs | 107 +++++++++++++++++++++++++++++++++------------------ src/paths/error.rs | 5 +++ 10 files changed, 325 insertions(+), 163 deletions(-) (limited to 'src') diff --git a/src/cli.rs b/src/cli.rs index 353c293..657d05d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,15 +1,18 @@ use clap::{Args, Parser}; +use figment::{providers::Serialized, value, Metadata, Profile, Provider}; +use serde::Serialize; use std::path::PathBuf; use tracing_subscriber::{filter::LevelFilter, Layer, Registry}; -use crate::paths::PathEntry; +use crate::{paths::PathEntry, Config}; /// Simple program to manage projects and ssh hosts using tmux -#[derive(Parser, Debug)] +#[derive(Debug, Clone, Default, Parser, Serialize)] #[command(author, version, about)] +#[serde(into = "Config")] pub struct Cli { /// Path to directories - pub(crate) path: Vec, + pub(crate) paths: Vec, /// Max depth to recurse. /// @@ -42,25 +45,39 @@ impl Cli { vec![fmt_layer] } +} - // TODO: replace this with `impl Figment for Cli` - pub fn as_config(&self) -> crate::paths::Config { - crate::paths::Config { - paths: self - .path - .iter() - .cloned() - .map(|p| PathEntry { - path: p, - hidden: self.hidden, - recurse: self.max_depth, - }) - .collect(), +impl From for Config { + fn from(value: Cli) -> Self { + Config { + paths: crate::paths::Config { + paths: value + .paths + .iter() + .cloned() + .map(|p| PathEntry { + path: p, + hidden: value.hidden, + recurse: value.max_depth, + }) + .collect(), + }, + ..Default::default() } } } -#[derive(Debug, Default, Args)] +impl Provider for Cli { + fn metadata(&self) -> Metadata { + Metadata::named("Tmuxr cli provider") + } + + fn data(&self) -> figment::error::Result> { + Serialized::defaults(Self::default()).data() + } +} + +#[derive(Debug, Default, Clone, Args)] pub struct Verbosity { /// Print additional information per occurrence #[arg(short, long, action = clap::ArgAction::Count, conflicts_with = "quiet")] @@ -93,7 +110,7 @@ impl From<&Verbosity> for LevelFilter { #[cfg(test)] mod tests { #[test] - fn test_start() { + fn test_cli_parse() { assert_eq!(1, 1); } } diff --git a/src/config.rs b/src/config.rs index 4ba59b4..d31eca8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,15 +5,17 @@ use std::{fs::File, path::PathBuf, sync::Arc}; use tracing_subscriber::{Layer, Registry}; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] pub struct Config { - pub log_enabled: bool, - pub log_file: PathBuf, + #[serde(flatten)] pub paths: crate::paths::Config, pub finder: crate::finder::Config, + pub log_enabled: bool, + pub log_file: PathBuf, } impl Config { - pub fn from(provider: T) -> figment::error::Result { + pub fn extract(provider: T) -> figment::error::Result { Figment::from(provider).extract() } @@ -52,7 +54,7 @@ impl Provider for Config { } fn data(&self) -> figment::error::Result> { - Serialized::defaults(Config::default()).data() + Serialized::defaults(Self::default()).data() } } @@ -63,3 +65,69 @@ impl TryFrom for Config { value.extract().map_err(Into::into) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::paths::PathEntry; + use figment::providers::{Format, Serialized, Toml}; + + #[test] + fn test_extract() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "tmuxr.toml", + r#" + log_enabled = true + log_file = "/path/to/log_file" + paths = [ + "/path/to/projects", + { path = "/path/to/other_projects", recurse = 1, hidden = true }, + { path = "/path/to/another_project", recurse = 0 }, + ] + + [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: crate::paths::Config { + paths: Vec::from([ + PathEntry { + path: "/path/to/projects".into(), + hidden: false, + recurse: None, + }, + PathEntry { + path: "/path/to/other_projects".into(), + hidden: true, + recurse: Some(1), + }, + PathEntry { + path: "/path/to/another_project".into(), + hidden: false, + recurse: Some(0), + }, + ]), + }, + finder: crate::finder::Config { + program: "fzf".into(), + args: vec!["-0".into(), "-1".into(), "--preview='cat'".into()], + }, + log_enabled: true, + log_file: "/path/to/log_file".into() + } + ); + + Ok(()) + }); + } +} diff --git a/src/error.rs b/src/error.rs index 9e20809..509dba2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,9 @@ +use std::process::{ExitCode, Termination}; + pub type Result = std::result::Result; #[derive(thiserror::Error, Debug)] +#[repr(u8)] pub enum Error { #[error("IO error: {0}")] IO(#[from] std::io::Error), @@ -14,3 +17,10 @@ pub enum Error { #[error("Finder error: {0}")] Finder(#[from] crate::finder::Error), } + +impl Termination for Error { + fn report(self) -> ExitCode { + eprintln!("{}", self); + ExitCode::FAILURE + } +} diff --git a/src/finder.rs b/src/finder.rs index db92c83..c5b5bd5 100644 --- a/src/finder.rs +++ b/src/finder.rs @@ -1,8 +1,9 @@ +use figment::Provider; use std::{ + ffi::OsStr, io::Write, ops::{Deref, DerefMut}, os::unix::prelude::OsStrExt, - path::PathBuf, process::{Child, Command, Output, Stdio}, }; @@ -12,41 +13,57 @@ pub use error::{Error, Result}; mod config; mod error; -pub struct Finder(Child); +pub struct Finder { + program: String, + args: Vec, +} impl Finder { - pub fn new(config: &Config) -> Result { - Command::new(&config.program) - .args(&config.args) + pub fn new() -> Result { + Self::from_provider(Config::figment()) + } + + /// Extract `Config` from `provider` to construct new `Finder` + pub fn from_provider(provider: T) -> Result { + Config::extract(&provider) + .map(|config| Finder { + program: config.program, + args: config.args, + }) + .map_err(Into::into) + } + + pub fn spawn(self) -> Result { + Command::new(self.program) + .args(self.args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() - .map(Into::into) + .map(FinderChild) .map_err(Into::into) } +} - pub fn into_inner(self) -> Child { - self.0 - } +pub struct FinderChild(Child); - pub fn write_path_buf_vectored(&mut self, directories: V) -> Result<()> +impl FinderChild { + pub fn find(mut self, items: V) -> Result where - V: IntoIterator, + V: IntoIterator, + I: AsRef, { let stdin = self.stdin.as_mut().ok_or(Error::Stdin)?; - directories.into_iter().try_for_each(|path_buf| { - stdin - .write_all(path_buf.into_os_string().as_bytes()) - .map_err(From::from) - }) - } - pub fn wait_with_output(self) -> Result { - self.into_inner().wait_with_output().map_err(From::from) + items.into_iter().try_for_each(|item| -> Result<()> { + stdin.write_all(item.as_ref().as_bytes())?; + stdin.write_all("\n".as_bytes()).map_err(From::from) + })?; + + self.0.wait_with_output().map_err(Into::into) } } -impl Deref for Finder { +impl Deref for FinderChild { type Target = Child; fn deref(&self) -> &Self::Target { @@ -54,20 +71,40 @@ impl Deref for Finder { } } -impl DerefMut for Finder { +impl DerefMut for FinderChild { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl From for Finder { - fn from(value: Child) -> Self { - Self(value) +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_output() { + let items = Vec::from(["item1"]); + + let finder = Finder { + program: "fzf".into(), + args: ["-1".into()].into(), + }; + + let selected = finder.spawn().unwrap().find(items).unwrap().stdout; + assert_eq!(selected.as_slice(), "item1\n".as_bytes()) } -} -impl From for Child { - fn from(value: Finder) -> Child { - value.0 + #[test] + #[ignore] + fn test_selection() { + let items = Vec::from(["item1", "item2", "item3", "item4"]); + + let finder = Finder { + program: "fzf".into(), + args: [].into(), + }; + + let selected = finder.spawn().unwrap().find(items).unwrap().stdout; + assert_eq!(selected.as_slice(), "item1\n".as_bytes()) } } diff --git a/src/finder/config.rs b/src/finder/config.rs index d0a0570..916637f 100644 --- a/src/finder/config.rs +++ b/src/finder/config.rs @@ -2,32 +2,37 @@ use figment::{providers::Serialized, value, Figment, Metadata, Profile, Provider use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(default)] pub struct Config { pub program: String, pub args: Vec, } impl Config { - pub fn from(provider: T) -> figment::error::Result { + // Extract the configuration from any `Provider` + pub fn extract(provider: T) -> figment::error::Result { Figment::from(provider).extract() } + + // Provide a default provider, a `Figment`. + pub fn figment() -> Figment { + Figment::from(Config::default()) + } } impl Default for Config { fn default() -> Self { Self { - program: "fzf-tmux".into(), - args: vec![ - "--", + program: "fzf".into(), + args: [ "--multi", "--print-query", "-d/", - "--preview-window='right,75%,<80(up,75%,border-bottom)'", + "--preview-window=right,75%,<80(up,75%,border-bottom)", "--preview='sel={}; less ${sel:-{q}} 2>/dev/null'", ] - .into_iter() .map(Into::into) - .collect(), + .to_vec(), } } } @@ -38,7 +43,7 @@ impl Provider for Config { } fn data(&self) -> figment::error::Result> { - Serialized::defaults(Config::default()).data() + Serialized::defaults(Self::default()).data() } } @@ -48,35 +53,7 @@ mod tests { 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() { + fn test_extract() { figment::Jail::expect_with(|jail| { jail.create_file( "tmuxr.toml", @@ -94,7 +71,7 @@ mod tests { config, Config { program: "fzf".into(), - args: vec!["-0".into(), "-1".into(), "--preview='cat'".into()], + args: ["-0", "-1", "--preview='cat'"].map(Into::into).to_vec(), } ); diff --git a/src/finder/error.rs b/src/finder/error.rs index 6ea5017..a8009a8 100644 --- a/src/finder/error.rs +++ b/src/finder/error.rs @@ -2,9 +2,15 @@ pub type Result = std::result::Result; #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("IO error: {0}")] - IO(#[from] std::io::Error), + #[error("Child process has not been spawned")] + NoChild, - #[error("Stdin error: Failed to get finder's stdin")] + #[error("Failed to get Child's stdin")] Stdin, + + #[error("Config error: {0}")] + Config(#[from] figment::error::Error), + + #[error("IO error: {0}")] + IO(#[from] std::io::Error), } diff --git a/src/main.rs b/src/main.rs index ff29b97..8ec7006 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use figment::{ providers::{Env, Format, Serialized, Toml}, Figment, }; -use std::{fs::File, sync::Arc}; +use std::{error::Error, fs::File, sync::Arc}; use tmuxr::{Cli, Config, Finder, Paths, Result}; use tracing::info; use tracing_subscriber::prelude::*; @@ -11,37 +11,38 @@ use tracing_subscriber::prelude::*; fn main() -> Result<()> { let cli = Cli::parse(); - let figment = Figment::from(Serialized::defaults(Config::default())) + let config = Figment::from(Serialized::defaults(Config::default())) + .merge(Serialized::defaults(&cli)) .merge(Toml::file("tmuxr.toml")) - .merge(Env::prefixed("TMUXR_")); + .merge(Env::prefixed("TMUXR_")) + .extract() + .map_err(eprintln)?; - let config = Config::try_from(figment)?; - - if let Err(err) = init_subscriber(&cli, &config) { - eprintln!("Failed to initialize logging: {:?}", err) - } + init_subscriber(&cli, &config).map_err(eprintln)?; + run(&config).map_err(eprintln) +} - run(&config) +fn eprintln(err: E) -> E { + eprintln!("{}", err); + err } fn init_subscriber(cli: &Cli, config: &Config) -> Result<()> { - let mut layers = Vec::new(); + let stdout_layer = tracing_subscriber::fmt::layer() + .pretty() + .with_filter(cli.verbose.as_filter()); - if config.log_enabled { + let log_layer = if config.log_enabled { let file = File::create(&config.log_file)?; - let log_layer = tracing_subscriber::fmt::layer() - .with_writer(Arc::new(file)) - .boxed(); - layers.push(log_layer); + let log_layer = tracing_subscriber::fmt::layer().with_writer(Arc::new(file)); + Some(log_layer) + } else { + None }; tracing_subscriber::registry() - .with( - tracing_subscriber::fmt::layer() - .pretty() - .with_filter(cli.verbose.as_filter()), - ) - .with(layers) + .with(stdout_layer) + .with(log_layer) .init(); Ok(()) @@ -49,14 +50,10 @@ fn init_subscriber(cli: &Cli, config: &Config) -> Result<()> { #[tracing::instrument()] pub fn run(config: &Config) -> Result<()> { - let directories = Paths::from(&config.paths); - let mut finder = Finder::new(&config.finder)?; - - finder.write_path_buf_vectored(directories)?; - - let output = finder.wait_with_output()?; + let paths = Paths::from_provider(config)?; + let selected = Finder::from_provider(config)?.spawn()?.find(paths)?; - info!("{:?}", output); + info!("{:?}", selected); Ok(()) } diff --git a/src/paths.rs b/src/paths.rs index 8f0153c..d9c1214 100644 --- a/src/paths.rs +++ b/src/paths.rs @@ -1,9 +1,10 @@ +use figment::Provider; use ignore::{Walk, WalkBuilder}; use std::{path::PathBuf, vec::IntoIter}; use tracing::warn; pub use config::{Config, PathEntry}; -pub use error::Error; +pub use error::{Error, Result}; mod config; mod error; @@ -14,17 +15,30 @@ pub struct Paths { } impl Paths { - pub fn new(path_entries: Vec) -> Self { + pub fn new() -> Result { + Self::from_provider(Config::figment()) + } + + /// Extract `Config` from `provider` to construct new `Paths` + pub fn from_provider(provider: T) -> Result { + Config::extract(&provider) + .map_err(Into::into) + .map(Into::into) + } +} + +impl From> for Paths { + fn from(value: Vec) -> Self { Self { - paths_iter: path_entries.into_iter(), + paths_iter: value.into_iter(), iter: None, } } } -impl From<&Config> for Paths { - fn from(value: &Config) -> Self { - Self::new(value.paths.to_owned()) +impl From for Paths { + fn from(value: Config) -> Self { + value.paths.into() } } diff --git a/src/paths/config.rs b/src/paths/config.rs index 72c5da7..eea176f 100644 --- a/src/paths/config.rs +++ b/src/paths/config.rs @@ -1,6 +1,6 @@ use figment::{providers::Serialized, value, Figment, Metadata, Profile, Provider}; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use serde::{Deserialize, Deserializer, Serialize}; +use std::{convert::Infallible, path::PathBuf, str::FromStr}; #[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] pub struct Config { @@ -8,9 +8,15 @@ pub struct Config { } impl Config { - pub fn from(provider: T) -> figment::error::Result { + // Extract the configuration from any `Provider` + pub fn extract(provider: T) -> figment::error::Result { Figment::from(provider).extract() } + + // Provide a default provider, a `Figment`. + pub fn figment() -> Figment { + Figment::from(Config::default()) + } } impl Provider for Config { @@ -19,25 +25,64 @@ impl Provider for Config { } fn data(&self) -> figment::error::Result> { - Serialized::defaults(Config::default()).data() + Serialized::defaults(Self::default()).data() } } -#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize)] pub struct PathEntry { pub path: PathBuf, pub hidden: bool, pub recurse: Option, } -impl std::str::FromStr for PathEntry { - type Err = std::convert::Infallible; +impl From for PathEntry { + fn from(path: PathBuf) -> Self { + Self { + path, + ..Default::default() + } + } +} + +impl FromStr for PathEntry { + type Err = Infallible; fn from_str(s: &str) -> Result { - Ok(PathEntry { - path: s.to_string().into(), - ..Default::default() - }) + s.parse().map(PathBuf::into) + } +} + +impl<'de> Deserialize<'de> for PathEntry { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum Variants { + String(String), + Struct { + path: PathBuf, + #[serde(default)] + hidden: bool, + #[serde(default)] + recurse: Option, + }, + } + + match Variants::deserialize(deserializer)? { + Variants::String(s) => s.parse().map_err(serde::de::Error::custom), + Variants::Struct { + path, + hidden, + recurse, + } => Ok(Self { + path, + hidden, + recurse, + }), + } } } @@ -47,34 +92,15 @@ mod tests { 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() { + fn test_extract() { figment::Jail::expect_with(|jail| { jail.create_file( "tmuxr.toml", r#" paths = [ - "/tmp/projects", - { path = "/tmp/tmuxr/test/other_projects", recursive = false, hidden = true }, + "/path/to/projects", + { path = "/path/to/other_projects", recurse = 1, hidden = true }, + { path = "/path/to/another_project", recurse = 0 }, ] "#, )?; @@ -88,15 +114,20 @@ mod tests { Config { paths: Vec::from([ PathEntry { - path: "/tmp/tmuxr/test/project_1".into(), + path: "/path/to/projects".into(), hidden: false, recurse: None, }, PathEntry { - path: "/tmp/tmuxr/test/projects".into(), - hidden: false, + path: "/path/to/other_projects".into(), + hidden: true, recurse: Some(1), - } + }, + PathEntry { + path: "/path/to/another_project".into(), + hidden: false, + recurse: Some(0), + }, ]), } ); diff --git a/src/paths/error.rs b/src/paths/error.rs index 08fdc89..3a5a580 100644 --- a/src/paths/error.rs +++ b/src/paths/error.rs @@ -1,5 +1,10 @@ +pub type Result = std::result::Result; + #[derive(thiserror::Error, Debug)] pub enum Error { + #[error("Config error: {0}")] + Config(#[from] figment::error::Error), + #[error("Ignore error: {0}")] Ignore(#[from] ignore::Error), } -- cgit v1.2.3-70-g09d2