diff --git a/Cargo.lock b/Cargo.lock index 23ceae2..5d562cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,6 +185,17 @@ dependencies = [ "syn 1.0.30", ] +[[package]] +name = "confy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2913470204e9e8498a0f31f17f90a0de801ae92c8c5ac18c49af4819e6786697" +dependencies = [ + "directories", + "serde", + "toml", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -237,6 +248,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "directories" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c" +dependencies = [ + "cfg-if", + "dirs-sys", +] + [[package]] name = "dirs" version = "1.0.5" @@ -425,6 +446,7 @@ dependencies = [ "ascii_table", "atty", "clap", + "confy", "itertools", "json", "mime", @@ -434,6 +456,7 @@ dependencies = [ "pest", "pest_derive", "regex", + "serde", "shlex", "thiserror", "url", @@ -1506,6 +1529,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +dependencies = [ + "serde", +] + [[package]] name = "tower-service" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 082582c..097c5db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ mime-db = "0.1.5" xdg-mime = { git = "https://github.com/ebassi/xdg-mime-rs" } atty = "0.2.14" notify-rust = "4.0.0-rc.1" +confy = "0.4.0" +serde = "1.0.111" [profile.release] opt-level=3 diff --git a/src/apps/user.rs b/src/apps/user.rs index 5aacf4a..8306a59 100644 --- a/src/apps/user.rs +++ b/src/apps/user.rs @@ -1,4 +1,8 @@ -use crate::{apps::SystemApps, DesktopEntry, Error, Handler, Result}; +use crate::{ + apps::SystemApps, + common::{DesktopEntry, Handler}, + Error, Result, +}; use mime::Mime; use pest::Parser; use std::{ @@ -39,12 +43,28 @@ impl MimeApps { Ok(()) } pub fn get_handler(&self, mime: &Mime) -> Result<Handler> { - self.default_apps - .get(mime) - .or_else(|| self.added_associations.get(mime)) - .map(|hs| hs.get(0).unwrap().clone()) - .or_else(|| self.system_apps.get_handler(mime)) - .ok_or(Error::NotFound(mime.to_string())) + let config = crate::config::Config::load()?; + + match self.default_apps.get(mime) { + Some(handlers) if config.enable_selector && handlers.len() > 1 => { + let handlers = handlers + .into_iter() + .map(|h| (h, h.get_entry().unwrap().name)) + .collect::<Vec<_>>(); + let selected = + config.select(handlers.iter().map(|h| h.1.clone()))?; + let selected = + handlers.into_iter().find(|h| h.1 == selected).unwrap().0; + Ok(selected.clone()) + } + Some(handlers) => Ok(handlers.get(0).unwrap().clone()), + None => self + .added_associations + .get(mime) + .map(|h| h.get(0).unwrap().clone()) + .or_else(|| self.system_apps.get_handler(mime)) + .ok_or(Error::NotFound(mime.to_string())), + } } pub fn show_handler(&self, mime: &Mime, output_json: bool) -> Result<()> { let handler = self.get_handler(mime)?; diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..9c5be89 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,53 @@ +use crate::common::{Handler, MimeOrExtension}; + +#[derive(clap::Clap)] +#[clap(global_setting = clap::AppSettings::DeriveDisplayOrder)] +#[clap(global_setting = clap::AppSettings::DisableHelpSubcommand)] +#[clap(version = clap::crate_version!())] +pub enum Cmd { + /// List default apps and the associated handlers + List, + + /// Open a path/URL with its default handler + Open { + #[clap(required = true)] + paths: Vec<String>, + }, + + /// Set the default handler for mime/extension + Set { + mime: MimeOrExtension, + handler: Handler, + }, + + /// Unset the default handler for mime/extension + Unset { mime: MimeOrExtension }, + + /// Launch the handler for specified extension/mime with optional arguments + Launch { + mime: MimeOrExtension, + args: Vec<String>, + }, + + /// Get handler for this mime/extension + Get { + #[clap(long)] + json: bool, + mime: MimeOrExtension, + }, + + /// Add a handler for given mime/extension + /// Note that the first handler is the default + Add { + mime: MimeOrExtension, + handler: Handler, + }, + + #[clap(setting = clap::AppSettings::Hidden)] + Autocomplete { + #[clap(short)] + desktop_files: bool, + #[clap(short)] + mimes: bool, + }, +} diff --git a/src/common/mime_types.rs b/src/common/mime_types.rs index d12fdd3..5aa5bc3 100644 --- a/src/common/mime_types.rs +++ b/src/common/mime_types.rs @@ -8,9 +8,9 @@ use std::{ // A mime derived from a path or URL #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct FlexibleMime(pub Mime); +pub struct MimeType(pub Mime); -impl TryFrom<&str> for FlexibleMime { +impl TryFrom<&str> for MimeType { type Error = Error; fn try_from(arg: &str) -> Result<Self> { @@ -26,7 +26,7 @@ impl TryFrom<&str> for FlexibleMime { } } -impl TryFrom<&Path> for FlexibleMime { +impl TryFrom<&Path> for MimeType { type Error = Error; fn try_from(path: &Path) -> Result<Self> { let guess = SHARED_MIME_DB.guess_mime_type().path(path).guess(); @@ -47,7 +47,7 @@ impl FromStr for MimeOrExtension { type Err = Error; fn from_str(s: &str) -> Result<Self> { if s.starts_with(".") { - Ok(Self(FlexibleMime::try_from(s)?.0)) + Ok(Self(MimeType::try_from(s)?.0)) } else { Ok(Self(Mime::from_str(s)?)) } @@ -69,11 +69,11 @@ mod tests { #[test] fn from_path_with_extension() { assert_eq!( - FlexibleMime::try_from(".pdf").unwrap().0, + MimeType::try_from(".pdf").unwrap().0, mime::APPLICATION_PDF ); assert_eq!( - FlexibleMime::try_from(".").unwrap().0.essence_str(), + MimeType::try_from(".").unwrap().0.essence_str(), "inode/directory" ); } diff --git a/src/common/mod.rs b/src/common/mod.rs index 9568d8d..e1403f1 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -6,4 +6,4 @@ mod mime_types; pub use self::db::{autocomplete as db_autocomplete, SHARED_MIME_DB}; pub use desktop_entry::{DesktopEntry, Rule as PestRule}; pub use handler::Handler; -pub use mime_types::{FlexibleMime, MimeOrExtension}; +pub use mime_types::{MimeOrExtension, MimeType}; diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..35b63b4 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,55 @@ +use crate::Result; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub enable_selector: bool, + pub selector: String, +} + +impl Default for Config { + fn default() -> Self { + Config { + enable_selector: false, + selector: "rofi -dmenu".to_owned(), + } + } +} + +impl Config { + pub fn load() -> Result<Self> { + Ok(confy::load("handlr")?) + } + + pub fn select<O: Iterator<Item = String>>( + &self, + mut opts: O, + ) -> Result<String> { + use itertools::Itertools; + use std::{ + io::prelude::*, + process::{Command, Stdio}, + }; + + let process = { + let mut split = shlex::split(&self.selector).unwrap(); + let (cmd, args) = (split.remove(0), split); + Command::new(cmd) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()? + }; + + process + .stdin + .unwrap() + .write_all(opts.join("\n").as_bytes())?; + + let mut output = String::with_capacity(24); + process.stdout.unwrap().read_to_string(&mut output)?; + let output = output.trim_end().to_owned(); + + Ok(output) + } +} diff --git a/src/error.rs b/src/error.rs index 08e6cba..4d4d08c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,6 +8,8 @@ pub enum Error { Notify(#[from] notify_rust::error::Error), #[error(transparent)] Xdg(#[from] xdg::BaseDirectoriesError), + #[error(transparent)] + Config(#[from] confy::ConfyError), #[error("no handler defined for this mime/extension")] NotFound(String), #[error("could not figure out the mime type .{0}")] diff --git a/src/main.rs b/src/main.rs index 8969c20..a60c88c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,68 +1,19 @@ -use clap::Clap; use error::{Error, Result}; -use notify_rust::Notification; -use std::convert::TryFrom; mod apps; +mod cli; mod common; +mod config; mod error; -use common::{DesktopEntry, FlexibleMime, Handler, MimeOrExtension}; - -#[derive(Clap)] -#[clap(global_setting = clap::AppSettings::DeriveDisplayOrder)] -#[clap(global_setting = clap::AppSettings::DisableHelpSubcommand)] -#[clap(version = clap::crate_version!())] -enum Cmd { - /// List default apps and the associated handlers - List, - - /// Open a path/URL with its default handler - Open { - #[clap(required = true)] - path: Vec<String>, - }, - - /// Set the default handler for mime/extension - Set { - mime: MimeOrExtension, - handler: Handler, - }, - - /// Unset the default handler for mime/extension - Unset { mime: MimeOrExtension }, - - /// Launch the handler for specified extension/mime with optional arguments - Launch { - mime: MimeOrExtension, - args: Vec<String>, - }, - - /// Get handler for this mime/extension - Get { - #[clap(long)] - json: bool, - mime: MimeOrExtension, - }, - - /// Add a handler for given mime/extension - /// Note that the first handler is the default - Add { - mime: MimeOrExtension, - handler: Handler, - }, - - #[clap(setting = clap::AppSettings::Hidden)] - Autocomplete { - #[clap(short)] - desktop_files: bool, - #[clap(short)] - mimes: bool, - }, -} - fn main() -> Result<()> { + use clap::Clap; + use cli::Cmd; + use common::MimeType; + use std::convert::TryFrom; + let mut apps = apps::MimeApps::read()?; + crate::config::Config::load()?; let res = || -> Result<()> { match Cmd::parse() { @@ -78,14 +29,9 @@ fn main() -> Result<()> { Cmd::Get { mime, json } => { apps.show_handler(&mime.0, json)?; } - Cmd::Open { path } => { - std::process::Command::new("notify-send") - .arg(&format!("{:?}", path)) - .spawn()?; - apps.get_handler( - &FlexibleMime::try_from(path.get(0).unwrap().as_str())?.0, - )? - .launch(path)?; + Cmd::Open { paths } => { + let mime = MimeType::try_from(paths[0].as_str())?.0; + apps.get_handler(&mime)?.launch(paths)?; } Cmd::List => { apps.print()?; @@ -110,12 +56,13 @@ fn main() -> Result<()> { match (res, atty::is(atty::Stream::Stdout)) { (Err(e), true) => eprintln!("{}", e), (Err(e), false) => { - Notification::new() + notify_rust::Notification::new() .summary("handlr error") .body(&e.to_string()) .show()?; } _ => {} }; + Ok(()) }