summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorToby Vincent <tobyv13@gmail.com>2022-11-17 23:12:10 -0600
committerToby Vincent <tobyv13@gmail.com>2022-11-17 23:12:10 -0600
commit09be0362d42034e343b64de08618c995b63c90fe (patch)
tree23f44d4e36cd767ea86f524db6016d64819eaf9b /src
parent082d11db68efa63f6da1be29cc9dc95b8f9e1735 (diff)
feat: get intitial finder working
Diffstat (limited to 'src')
-rw-r--r--src/cli.rs53
-rw-r--r--src/config.rs76
-rw-r--r--src/error.rs10
-rw-r--r--src/finder.rs93
-rw-r--r--src/finder/config.rs53
-rw-r--r--src/finder/error.rs12
-rw-r--r--src/main.rs53
-rw-r--r--src/paths.rs26
-rw-r--r--src/paths/config.rs107
-rw-r--r--src/paths/error.rs5
10 files changed, 325 insertions, 163 deletions
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<PathBuf>,
+ pub(crate) paths: Vec<PathBuf>,
/// 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<Cli> 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<value::Map<Profile, value::Dict>> {
+ 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<T: Provider>(provider: T) -> figment::error::Result<Config> {
+ pub fn extract<T: Provider>(provider: T) -> figment::error::Result<Config> {
Figment::from(provider).extract()
}
@@ -52,7 +54,7 @@ impl Provider for Config {
}
fn data(&self) -> figment::error::Result<value::Map<Profile, value::Dict>> {
- Serialized::defaults(Config::default()).data()
+ Serialized::defaults(Self::default()).data()
}
}
@@ -63,3 +65,69 @@ impl TryFrom<Figment> 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<T> = std::result::Result<T, Error>;
#[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<String>,
+}
impl Finder {
- pub fn new(config: &Config) -> Result<Self> {
- Command::new(&config.program)
- .args(&config.args)
+ pub fn new() -> Result<Self> {
+ Self::from_provider(Config::figment())
+ }
+
+ /// Extract `Config` from `provider` to construct new `Finder`
+ pub fn from_provider<T: Provider>(provider: T) -> Result<Self> {
+ Config::extract(&provider)
+ .map(|config| Finder {
+ program: config.program,
+ args: config.args,
+ })
+ .map_err(Into::into)
+ }
+
+ pub fn spawn(self) -> Result<FinderChild> {
+ 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<V>(&mut self, directories: V) -> Result<()>
+impl FinderChild {
+ pub fn find<V, I>(mut self, items: V) -> Result<Output>
where
- V: IntoIterator<Item = PathBuf>,
+ V: IntoIterator<Item = I>,
+ I: AsRef<OsStr>,
{
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<Output> {
- 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<Child> 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<Finder> 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<String>,
}
impl Config {
- pub fn from<T: Provider>(provider: T) -> figment::error::Result<Config> {
+ // Extract the configuration from any `Provider`
+ pub fn extract<T: Provider>(provider: T) -> figment::error::Result<Config> {
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<value::Map<Profile, value::Dict>> {
- 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<T> = std::result::Result<T, Error>;
#[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<E: Error>(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<PathEntry>) -> Self {
+ pub fn new() -> Result<Self> {
+ Self::from_provider(Config::figment())
+ }
+
+ /// Extract `Config` from `provider` to construct new `Paths`
+ pub fn from_provider<T: Provider>(provider: T) -> Result<Self> {
+ Config::extract(&provider)
+ .map_err(Into::into)
+ .map(Into::into)
+ }
+}
+
+impl From<Vec<PathEntry>> for Paths {
+ fn from(value: Vec<PathEntry>) -> 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<Config> 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<T: Provider>(provider: T) -> figment::error::Result<Config> {
+ // Extract the configuration from any `Provider`
+ pub fn extract<T: Provider>(provider: T) -> figment::error::Result<Config> {
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<value::Map<Profile, value::Dict>> {
- 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<usize>,
}
-impl std::str::FromStr for PathEntry {
- type Err = std::convert::Infallible;
+impl From<PathBuf> for PathEntry {
+ fn from(path: PathBuf) -> Self {
+ Self {
+ path,
+ ..Default::default()
+ }
+ }
+}
+
+impl FromStr for PathEntry {
+ type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- Ok(PathEntry {
- path: s.to_string().into(),
- ..Default::default()
- })
+ s.parse().map(PathBuf::into)
+ }
+}
+
+impl<'de> Deserialize<'de> for PathEntry {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ #[derive(Deserialize)]
+ #[serde(untagged)]
+ enum Variants {
+ String(String),
+ Struct {
+ path: PathBuf,
+ #[serde(default)]
+ hidden: bool,
+ #[serde(default)]
+ recurse: Option<usize>,
+ },
+ }
+
+ 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<T> = std::result::Result<T, Error>;
+
#[derive(thiserror::Error, Debug)]
pub enum Error {
+ #[error("Config error: {0}")]
+ Config(#[from] figment::error::Error),
+
#[error("Ignore error: {0}")]
Ignore(#[from] ignore::Error),
}