From 0da454a557923518e5b75543ca5af5f72f07828a Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 8 Apr 2020 15:53:00 -0400 Subject: [PATCH] Implement open,get,set --- .rustfmt.toml | 0 Cargo.lock | 258 ++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 5 + src/common.rs | 58 ++++++++++- src/main.rs | 44 +++++--- src/mimeapps.rs | 168 +++++++++++++++++++++--------- src/systemapps.rs | 37 +------ 7 files changed, 465 insertions(+), 105 deletions(-) create mode 100644 .rustfmt.toml diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..e69de29 diff --git a/Cargo.lock b/Cargo.lock index 1047a35..f66db85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,15 @@ dependencies = [ "const-random", ] +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.28" @@ -27,6 +36,17 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.0" @@ -39,6 +59,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + [[package]] name = "blake2b_simd" version = "0.5.10" @@ -89,6 +115,21 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "clap" +version = "2.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "const-random" version = "0.1.8" @@ -173,6 +214,17 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "derive_more" +version = "0.99.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2323f3f47db9a0e77ce7a300605d8d2098597fc451ed1a97bb1f6411bb550a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.8.1" @@ -236,6 +288,15 @@ dependencies = [ "wasi", ] +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.10" @@ -245,6 +306,26 @@ dependencies = [ "libc", ] +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -263,6 +344,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + [[package]] name = "maybe-uninit" version = "2.0.0" @@ -284,10 +371,31 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime 0.3.16", + "unicase", ] [[package]] @@ -306,6 +414,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + [[package]] name = "pest" version = "2.1.3" @@ -349,6 +463,32 @@ dependencies = [ "sha-1", ] +[[package]] +name = "proc-macro-error" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "syn-mid", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.15" @@ -444,6 +584,42 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "smallvec" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8faa2719539bbe9d77869bfb15d4ee769f99525e707931452c97b693b3f159d" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88b8e18c69496aad6f9ddf4630dd7d585bcaf765786cb415b9aec2fe5a0430" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "1.0.17" @@ -455,6 +631,26 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "typenum" version = "1.11.2" @@ -467,12 +663,74 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" +dependencies = [ + "smallvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + +[[package]] +name = "unicode-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" + [[package]] name = "unicode-xid" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +[[package]] +name = "url" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" +dependencies = [ + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" + +[[package]] +name = "version_check" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 59b6e1d..2bc55fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,8 @@ pest = "2.1.3" pest_derive = "2.1.0" rayon = "1.3.0" dashmap = "3.10.0" +structopt = "0.3.12" +url = "2.1.1" +mime_guess = "2.0.3" +itertools = "0.9.0" +derive_more = {version = "0.99.5", default-features = false, features = ["display"] } diff --git a/src/common.rs b/src/common.rs index a508a6d..1bb8c5c 100644 --- a/src/common.rs +++ b/src/common.rs @@ -3,12 +3,58 @@ use pest::Parser; use std::convert::TryFrom; use std::path::PathBuf; -#[derive(Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct Mime(pub String); -impl std::fmt::Debug for Mime { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.0) +impl std::str::FromStr for Mime { + type Err = std::convert::Infallible; + fn from_str(s: &str) -> Result { + Ok(Self(s.to_owned())) + } +} +#[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; + fn from_str(s: &str) -> Result { + Self::resolve(s.to_owned()) + } +} + +impl Handler { + pub fn assume_valid(name: String) -> Self { + Self(name) + } + pub fn get_path(name: &str) -> Option { + let locally = { + let mut local_dir = dirs::data_dir().unwrap(); + local_dir.push("applications"); + local_dir.push(name); + Some(local_dir).filter(|p| p.exists()) + }; + let system = { + let mut sys = std::path::PathBuf::from("/usr/share/applications"); + sys.push(name); + Some(sys).filter(|p| p.exists()) + }; + 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"))?; + 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]) + .stdout(std::process::Stdio::null()) + .spawn()?; + Ok(()) } } @@ -17,16 +63,18 @@ impl std::fmt::Debug for Mime { pub struct DesktopEntry { pub(crate) name: String, pub(crate) exec: String, + pub(crate) file_name: String, pub(crate) mimes: Vec, } impl TryFrom for DesktopEntry { type Error = anyhow::Error; fn try_from(p: PathBuf) -> Result { - let raw = std::fs::read_to_string(p)?; + let raw = std::fs::read_to_string(&p)?; let file = Self::parse(Rule::file, &raw)?.next().unwrap(); let mut entry = Self::default(); + entry.file_name = p.file_name().unwrap().to_str().unwrap().to_owned(); for line in file.into_inner() { match line.as_rule() { diff --git a/src/main.rs b/src/main.rs index 6a1a5f8..88c03ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,45 @@ use anyhow::Result; +use structopt::StructOpt; mod common; mod mimeapps; -mod systemapps; -pub use common::{DesktopEntry, Mime}; +pub use common::{DesktopEntry, Handler, Mime}; + +#[derive(StructOpt)] +enum Options { + List, + Open { path: String }, + Get { mime: Mime }, + Set { mime: Mime, handler: Handler }, +} fn main() -> Result<()> { - let mut args: Vec<_> = std::env::args().collect(); - let user = mimeapps::MimeApps::read()?; - let sys = systemapps::SystemApps::populate()?; + let cmd = Options::from_args(); - let mime = Mime(args.remove(1)); + let mut user = mimeapps::MimeApps::read()?; - let user_handler = user.get_handler(&mime); - match user_handler { - Some(h) => { - dbg!(&h); + match cmd { + Options::Set { mime, handler } => { + user.set_handler(mime, handler)?; } - None => { - let s = sys.get_handlers(&mime); - dbg!(&s); + Options::Get { mime } => { + println!("{}", user.get_handler(&mime)?); } + Options::Open { path } => match url::Url::parse(&path) { + Ok(url) => { + let mime = Mime(format!("x-scheme-handler/{}", url.scheme())); + user.get_handler(&mime)?.run(&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(()) } diff --git a/src/mimeapps.rs b/src/mimeapps.rs index 7f921c8..edc5c69 100644 --- a/src/mimeapps.rs +++ b/src/mimeapps.rs @@ -1,62 +1,55 @@ -use crate::{DesktopEntry, Mime}; +use crate::{DesktopEntry, Handler, Mime}; use anyhow::Result; +use dashmap::DashMap; use pest::Parser; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::path::PathBuf; -#[derive(Debug, pest_derive::Parser, Default)] +#[derive(Debug, pest_derive::Parser)] #[grammar = "ini.pest"] pub struct MimeApps { - added_associations: HashMap>, - default_apps: HashMap>, -} - -fn handler_exists(name: &str) -> Option { - let locally = { - let mut local_dir = dirs::data_dir().unwrap(); - local_dir.push("applications"); - local_dir.push(name); - Some(local_dir).filter(|p| p.exists()) - }; - let system = { - let mut sys = std::path::PathBuf::from("/usr/share/applications"); - sys.push(name); - Some(sys).filter(|p| p.exists()) - }; - locally.or(system) + added_associations: HashMap>, + default_apps: HashMap>, + system_apps: SystemApps, } impl MimeApps { - pub fn get_handler(&self, mime: &Mime) -> Option { - use std::convert::TryFrom; - - Some( - self.default_apps - .get(mime) - .or_else(|| self.added_associations.get(mime)) - .map(|hs| hs.get(0).unwrap().clone()) - .map(DesktopEntry::try_from) - .map(Result::ok) - .flatten()? - .clone(), - ) + 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()?; + Ok(()) } - pub fn read() -> Result { - let path = dirs::config_dir() + pub fn get_handler(&self, mime: &Mime) -> Result { + Ok(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"))?) + } + pub fn path() -> Result { + dirs::config_dir() .map(|mut data_dir| { data_dir.push("mimeapps.list"); data_dir }) - .ok_or_else(|| anyhow::Error::msg("Could not determine xdg data dir"))?; - - let raw_conf = std::fs::read_to_string(path)?; + .ok_or_else(|| anyhow::Error::msg("Could not determine xdg data dir")) + } + pub fn read() -> Result { + let raw_conf = std::fs::read_to_string(Self::path()?)?; let file = Self::parse(Rule::file, &raw_conf) .expect("unsuccessful parse") // unwrap the parse result .next() .unwrap(); let mut current_section_name = "".to_string(); - let mut conf = Self::default(); + let mut conf = Self { + added_associations: HashMap::default(), + default_apps: HashMap::default(), + system_apps: SystemApps::populate()?, + }; file.into_inner().for_each(|line| { match line.as_rule() { @@ -67,23 +60,29 @@ impl MimeApps { let mut inner_rules = line.into_inner(); // { name ~ "=" ~ value } let name = inner_rules.next().unwrap().as_str(); - let mut handlers = inner_rules - .next() - .unwrap() - .as_str() - .split(";") - .filter_map(handler_exists) - .collect::>(); - handlers.pop(); + let handlers = { + use itertools::Itertools; + use std::str::FromStr; + + inner_rules + .next() + .unwrap() + .as_str() + .split(";") + .filter(|s| !s.is_empty()) + .unique() + .filter_map(|s| Handler::from_str(s).ok()) + .collect::>() + }; if !handlers.is_empty() { match current_section_name.as_str() { "Added Associations" => conf .added_associations - .insert(Mime(name.to_owned()), handlers.to_owned()), - "Default Applications" => conf - .default_apps - .insert(Mime(name.to_owned()), handlers.to_owned()), + .insert(Mime(name.to_owned()), handlers), + "Default Applications" => { + conf.default_apps.insert(Mime(name.to_owned()), handlers) + } _ => None, }; } @@ -94,4 +93,71 @@ impl MimeApps { Ok(conf) } + pub fn print(&self) -> Result<()> { + use itertools::Itertools; + use std::io::{prelude::*, BufWriter}; + + let f = std::fs::OpenOptions::new() + .read(true) + .write(true) + .truncate(true) + .open(Self::path()?)?; + let mut writer = BufWriter::new(f); + + writer.write_all(b"[Added Associations]\n")?; + for (k, v) in self.added_associations.iter().sorted() { + writer.write_all(k.0.as_ref())?; + writer.write_all(b"=")?; + writer.write_all(v.iter().join(";").as_ref())?; + writer.write_all(b";\n")?; + } + + writer.write_all(b"\n[Default Applications]\n")?; + for (k, v) in self.default_apps.iter().sorted() { + writer.write_all(k.0.as_ref())?; + writer.write_all(b"=")?; + writer.write_all(v.iter().join(";").as_ref())?; + writer.write_all(b";\n")?; + } + + writer.flush()?; + Ok(()) + } +} + +#[derive(Debug)] +pub struct SystemApps(pub DashMap>); + +impl SystemApps { + pub fn get_handlers(&self, mime: &Mime) -> Option> { + Some(self.0.get(mime)?.value().clone()) + } + pub fn get_handler(&self, mime: &Mime) -> Option { + Some(self.get_handlers(mime)?.get(0).unwrap().clone()) + } + pub fn populate() -> Result { + use rayon::iter::ParallelBridge; + use rayon::prelude::ParallelIterator; + use std::convert::TryFrom; + + let map = DashMap::>::with_capacity(50); + + std::fs::read_dir("/usr/share/applications")? + .par_bridge() + .filter_map(|path| { + path.ok() + .map(|p| DesktopEntry::try_from(p.path()).ok()) + .flatten() + }) + .for_each(|entry| { + let (file_name, mimes) = (entry.file_name, entry.mimes); + mimes.into_iter().for_each(|mime| { + map.entry(mime) + .or_default() + .push(Handler::assume_valid(file_name.clone())); + }); + }); + + Ok(Self(map)) + } } diff --git a/src/systemapps.rs b/src/systemapps.rs index eec11c6..573f0ed 100644 --- a/src/systemapps.rs +++ b/src/systemapps.rs @@ -1,39 +1,4 @@ -use crate::{DesktopEntry, Mime}; +use crate::{DesktopEntry, Handler, Mime}; use anyhow::Result; use dashmap::DashMap; use std::convert::TryFrom; - -#[derive(Debug)] -pub struct SystemApps(pub DashMap>); - -impl SystemApps { - pub fn get_handlers(&self, mime: &Mime) -> Option> { - Some(self.0.get(mime)?.value().clone()) - } - pub fn populate() -> Result { - use rayon::iter::ParallelBridge; - use rayon::prelude::ParallelIterator; - - let map = DashMap::>::with_capacity(50); - - std::fs::read_dir("/usr/share/applications")? - .par_bridge() - .filter_map(|path| { - path.ok() - .map(|p| DesktopEntry::try_from(p.path()).ok()) - .flatten() - }) - .for_each(|entry| { - let (name, exec, mimes) = (entry.name, entry.exec, entry.mimes); - mimes.into_iter().for_each(|mime| { - map.entry(mime).or_default().push(DesktopEntry { - name: name.clone(), - exec: exec.clone(), - mimes: Vec::new(), - }); - }); - }); - - Ok(Self(map)) - } -}