diff --git a/.rustfmt.toml b/.rustfmt.toml index e69de29..be300b2 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -0,0 +1,2 @@ +max_width = 80 +merge-imports = true diff --git a/Cargo.lock b/Cargo.lock index f66db85..9f73356 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,12 +18,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "anyhow" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a60d744a80c30fcb657dfe2c1b22bcb3e814c1a1e3674f32bf5820b570fbff" - [[package]] name = "arrayref" version = "0.3.6" @@ -36,6 +30,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" +[[package]] +name = "ascii_table" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbcbc3963c670677e878a5253f5fef77577ef3821080cfac27779cadf224a9b5" + [[package]] name = "atty" version = "0.2.14" @@ -288,6 +288,26 @@ dependencies = [ "wasi", ] +[[package]] +name = "handlr" +version = "0.1.0" +dependencies = [ + "ascii_table", + "dashmap", + "derive_more", + "dirs", + "itertools", + "json", + "mime_guess", + "pest", + "pest_derive", + "rayon", + "shlex", + "structopt", + "thiserror", + "url", +] + [[package]] name = "heck" version = "0.3.1" @@ -326,6 +346,12 @@ dependencies = [ "either", ] +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + [[package]] name = "lazy_static" version = "1.4.0" @@ -365,23 +391,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "mime" -version = "0.1.0" -dependencies = [ - "anyhow", - "dashmap", - "derive_more", - "dirs", - "itertools", - "mime_guess", - "pest", - "pest_derive", - "rayon", - "structopt", - "url", -] - [[package]] name = "mime" version = "0.3.16" @@ -394,7 +403,7 @@ version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" dependencies = [ - "mime 0.3.16", + "mime", "unicase", ] @@ -584,6 +593,12 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + [[package]] name = "smallvec" version = "1.2.0" @@ -651,6 +666,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b3d3d2ff68104100ab257bb6bb0cb26c901abe4bd4ba15961f3bf867924012" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca972988113b7715266f91250ddb98070d033c62a011fa0fcc57434a649310dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typenum" version = "1.11.2" diff --git a/Cargo.toml b/Cargo.toml index 2bc55fe..459eace 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,11 @@ [package] -name = "mime" +name = "handlr" version = "0.1.0" authors = ["greg "] edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] dirs = "2.0.2" -anyhow = "1.0.28" pest = "2.1.3" pest_derive = "2.1.0" rayon = "1.3.0" @@ -18,3 +15,7 @@ url = "2.1.1" mime_guess = "2.0.3" itertools = "0.9.0" derive_more = {version = "0.99.5", default-features = false, features = ["display"] } +json = "0.12.4" +shlex = "0.1.1" +thiserror = "1.0.14" +ascii_table = "3.0.0" diff --git a/src/common.rs b/src/common.rs index 1bb8c5c..bb68267 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use crate::{Error, Result}; use pest::Parser; use std::convert::TryFrom; use std::path::PathBuf; @@ -12,11 +12,13 @@ impl std::str::FromStr for Mime { Ok(Self(s.to_owned())) } } -#[derive(Debug, derive_more::Display, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[derive( + Debug, derive_more::Display, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, +)] pub struct Handler(String); impl std::str::FromStr for Handler { - type Err = anyhow::Error; + type Err = Error; fn from_str(s: &str) -> Result { Self::resolve(s.to_owned()) } @@ -41,21 +43,31 @@ impl Handler { locally.or(system) } pub fn resolve(name: String) -> Result { - let path = - Self::get_path(&name).ok_or_else(|| anyhow::Error::msg("Handler does not exist"))?; + let path = Self::get_path(&name).ok_or(Error::NotFound)?; DesktopEntry::try_from(path)?; Ok(Self(name)) } pub fn get_entry(&self) -> Result { DesktopEntry::try_from(Self::get_path(&self.0).unwrap()) } - pub fn run(&self, arg: &str) -> Result<()> { - std::process::Command::new("gtk-launch") - .args(&[self.0.as_str(), arg]) + pub fn open(&self, arg: String) -> Result<()> { + let (cmd, args) = self.get_entry()?.get_cmd(Some(arg))?; + std::process::Command::new(cmd) + .args(args) .stdout(std::process::Stdio::null()) .spawn()?; Ok(()) } + pub fn launch(&self, args: Vec) -> Result<()> { + let (cmd, mut base_args) = self.get_entry()?.get_cmd(None)?; + base_args.extend_from_slice(&args); + std::process::Command::new(cmd) + .args(base_args) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn()?; + Ok(()) + } } #[derive(Debug, Clone, pest_derive::Parser, Default, PartialEq, Eq)] @@ -67,8 +79,22 @@ pub struct DesktopEntry { pub(crate) mimes: Vec, } +impl DesktopEntry { + pub fn get_cmd( + &self, + arg: Option, + ) -> Result<(String, Vec)> { + let arg = arg.unwrap_or_default(); + let arg = shlex::quote(&arg); + let replaced = self.exec.replace("%f", &arg).replace("%U", &arg); + + let mut split = shlex::split(&replaced).ok_or(Error::BadCmd)?; + Ok((split.remove(0), split)) + } +} + impl TryFrom for DesktopEntry { - type Error = anyhow::Error; + type Error = Error; fn try_from(p: PathBuf) -> Result { let raw = std::fs::read_to_string(&p)?; let file = Self::parse(Rule::file, &raw)?.next().unwrap(); @@ -84,10 +110,12 @@ impl TryFrom for DesktopEntry { let name = inner_rules.next().unwrap().as_str(); match name { "Name" => { - entry.name = inner_rules.next().unwrap().as_str().into(); + entry.name = + inner_rules.next().unwrap().as_str().into(); } "Exec" => { - entry.exec = inner_rules.next().unwrap().as_str().into(); + entry.exec = + inner_rules.next().unwrap().as_str().into(); } "MimeType" => { let mut mimes = inner_rules @@ -111,7 +139,7 @@ impl TryFrom for DesktopEntry { if !entry.name.is_empty() && !entry.exec.is_empty() { Ok(entry) } else { - Err(anyhow::Error::msg("Invalid desktop entry")) + Err(Error::BadCmd) } } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..9c7ceed --- /dev/null +++ b/src/error.rs @@ -0,0 +1,17 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Parse(#[from] pest::error::Error), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("handler not found")] + NotFound, + #[error("Invalid desktop entry")] + BadCmd, + #[error("Could not find config dir")] + NoConfigDir, + #[error("could not guess mime type")] + Ambiguous, +} + +pub type Result = std::result::Result; diff --git a/src/main.rs b/src/main.rs index 88c03ef..d308d67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,44 +1,61 @@ -use anyhow::Result; +use error::{Error, Result}; use structopt::StructOpt; mod common; +mod error; mod mimeapps; pub use common::{DesktopEntry, Handler, Mime}; #[derive(StructOpt)] -enum Options { +enum Cmd { List, - Open { path: String }, - Get { mime: Mime }, - Set { mime: Mime, handler: Handler }, + Open { + path: String, + }, + Get { + #[structopt(long)] + json: bool, + mime: Mime, + }, + Launch { + mime: Mime, + args: Vec, + }, + Set { + mime: Mime, + handler: Handler, + }, } fn main() -> Result<()> { - let cmd = Options::from_args(); + let mut apps = mimeapps::MimeApps::read()?; - let mut user = mimeapps::MimeApps::read()?; - - match cmd { - Options::Set { mime, handler } => { - user.set_handler(mime, handler)?; + match Cmd::from_args() { + Cmd::Set { mime, handler } => { + apps.set_handler(mime, handler)?; } - Options::Get { mime } => { - println!("{}", user.get_handler(&mime)?); + Cmd::Launch { mime, args } => { + apps.get_handler(&mime)?.launch(args)?; } - Options::Open { path } => match url::Url::parse(&path) { + Cmd::Get { mime, json } => { + apps.show_handler(&mime, json)?; + } + Cmd::Open { path } => match url::Url::parse(&path) { Ok(url) => { let mime = Mime(format!("x-scheme-handler/{}", url.scheme())); - user.get_handler(&mime)?.run(&path)?; + apps.get_handler(&mime)?.open(path)?; } Err(_) => { let guess = mime_guess::from_path(&path) .first_raw() - .ok_or_else(|| anyhow::Error::msg("Could not determine mime type"))?; - user.get_handler(&Mime(guess.to_owned()))?.run(&path)?; + .ok_or(Error::Ambiguous)?; + apps.get_handler(&Mime(guess.to_owned()))?.open(path)?; } }, - _ => {} + Cmd::List => { + apps.print()?; + } }; Ok(()) diff --git a/src/mimeapps.rs b/src/mimeapps.rs index edc5c69..e2d6fc9 100644 --- a/src/mimeapps.rs +++ b/src/mimeapps.rs @@ -1,9 +1,10 @@ -use crate::{DesktopEntry, Handler, Mime}; -use anyhow::Result; +use crate::{DesktopEntry, Error, Handler, Mime, Result}; use dashmap::DashMap; use pest::Parser; -use std::collections::{HashMap, VecDeque}; -use std::path::PathBuf; +use std::{ + collections::{HashMap, VecDeque}, + path::PathBuf, +}; #[derive(Debug, pest_derive::Parser)] #[grammar = "ini.pest"] @@ -17,25 +18,40 @@ impl MimeApps { pub fn set_handler(&mut self, mime: Mime, handler: Handler) -> Result<()> { let handlers = self.default_apps.entry(mime).or_default(); handlers.push_front(handler); - self.print()?; + self.save()?; Ok(()) } pub fn get_handler(&self, mime: &Mime) -> Result { - Ok(self - .default_apps + 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(anyhow::Error::msg("No handlers found"))?) + .ok_or(Error::NotFound) + } + pub fn show_handler(&self, mime: &Mime, output_json: bool) -> Result<()> { + let handler = self.get_handler(mime)?; + let output = if output_json { + let entry = handler.get_entry()?; + (json::object! { + handler: handler.to_string(), + name: entry.name.as_str(), + cmd: entry.get_cmd(None)?.0 + }) + .to_string() + } else { + handler.to_string() + }; + println!("{}", output); + Ok(()) } pub fn path() -> Result { dirs::config_dir() - .map(|mut data_dir| { - data_dir.push("mimeapps.list"); - data_dir + .map(|mut config_dir| { + config_dir.push("mimeapps.list"); + config_dir }) - .ok_or_else(|| anyhow::Error::msg("Could not determine xdg data dir")) + .ok_or(Error::NoConfigDir) } pub fn read() -> Result { let raw_conf = std::fs::read_to_string(Self::path()?)?; @@ -80,9 +96,9 @@ impl MimeApps { "Added Associations" => conf .added_associations .insert(Mime(name.to_owned()), handlers), - "Default Applications" => { - conf.default_apps.insert(Mime(name.to_owned()), handlers) - } + "Default Applications" => conf + .default_apps + .insert(Mime(name.to_owned()), handlers), _ => None, }; } @@ -93,7 +109,7 @@ impl MimeApps { Ok(conf) } - pub fn print(&self) -> Result<()> { + pub fn save(&self) -> Result<()> { use itertools::Itertools; use std::io::{prelude::*, BufWriter}; @@ -123,6 +139,20 @@ impl MimeApps { writer.flush()?; Ok(()) } + pub fn print(&self) -> Result<()> { + use itertools::Itertools; + + let rows = self + .default_apps + .iter() + .sorted() + .map(|(k, v)| vec![k.0.clone(), v.iter().join(", ")]) + .collect::>(); + + ascii_table::AsciiTable::default().print(rows); + + Ok(()) + } } #[derive(Debug)] diff --git a/src/systemapps.rs b/src/systemapps.rs deleted file mode 100644 index 573f0ed..0000000 --- a/src/systemapps.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::{DesktopEntry, Handler, Mime}; -use anyhow::Result; -use dashmap::DashMap; -use std::convert::TryFrom;