Implement config and multi-handler selection

This commit is contained in:
Gregory 2020-06-06 01:51:58 -04:00
parent 935d1daa87
commit 9d726cb6af
No known key found for this signature in database
GPG key ID: 2E44FAEEDC94B1E2
9 changed files with 191 additions and 80 deletions

32
Cargo.lock generated
View file

@ -185,6 +185,17 @@ dependencies = [
"syn 1.0.30", "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]] [[package]]
name = "constant_time_eq" name = "constant_time_eq"
version = "0.1.5" version = "0.1.5"
@ -237,6 +248,16 @@ dependencies = [
"generic-array", "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]] [[package]]
name = "dirs" name = "dirs"
version = "1.0.5" version = "1.0.5"
@ -425,6 +446,7 @@ dependencies = [
"ascii_table", "ascii_table",
"atty", "atty",
"clap", "clap",
"confy",
"itertools", "itertools",
"json", "json",
"mime", "mime",
@ -434,6 +456,7 @@ dependencies = [
"pest", "pest",
"pest_derive", "pest_derive",
"regex", "regex",
"serde",
"shlex", "shlex",
"thiserror", "thiserror",
"url", "url",
@ -1506,6 +1529,15 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "toml"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.0" version = "0.3.0"

View file

@ -24,6 +24,8 @@ mime-db = "0.1.5"
xdg-mime = { git = "https://github.com/ebassi/xdg-mime-rs" } xdg-mime = { git = "https://github.com/ebassi/xdg-mime-rs" }
atty = "0.2.14" atty = "0.2.14"
notify-rust = "4.0.0-rc.1" notify-rust = "4.0.0-rc.1"
confy = "0.4.0"
serde = "1.0.111"
[profile.release] [profile.release]
opt-level=3 opt-level=3

View file

@ -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 mime::Mime;
use pest::Parser; use pest::Parser;
use std::{ use std::{
@ -39,12 +43,28 @@ impl MimeApps {
Ok(()) Ok(())
} }
pub fn get_handler(&self, mime: &Mime) -> Result<Handler> { pub fn get_handler(&self, mime: &Mime) -> Result<Handler> {
self.default_apps let config = crate::config::Config::load()?;
.get(mime)
.or_else(|| self.added_associations.get(mime)) match self.default_apps.get(mime) {
.map(|hs| hs.get(0).unwrap().clone()) Some(handlers) if config.enable_selector && handlers.len() > 1 => {
.or_else(|| self.system_apps.get_handler(mime)) let handlers = handlers
.ok_or(Error::NotFound(mime.to_string())) .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<()> { pub fn show_handler(&self, mime: &Mime, output_json: bool) -> Result<()> {
let handler = self.get_handler(mime)?; let handler = self.get_handler(mime)?;

53
src/cli.rs Normal file
View file

@ -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,
},
}

View file

@ -8,9 +8,9 @@ use std::{
// A mime derived from a path or URL // A mime derived from a path or URL
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] #[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; type Error = Error;
fn try_from(arg: &str) -> Result<Self> { 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; type Error = Error;
fn try_from(path: &Path) -> Result<Self> { fn try_from(path: &Path) -> Result<Self> {
let guess = SHARED_MIME_DB.guess_mime_type().path(path).guess(); let guess = SHARED_MIME_DB.guess_mime_type().path(path).guess();
@ -47,7 +47,7 @@ impl FromStr for MimeOrExtension {
type Err = Error; type Err = Error;
fn from_str(s: &str) -> Result<Self> { fn from_str(s: &str) -> Result<Self> {
if s.starts_with(".") { if s.starts_with(".") {
Ok(Self(FlexibleMime::try_from(s)?.0)) Ok(Self(MimeType::try_from(s)?.0))
} else { } else {
Ok(Self(Mime::from_str(s)?)) Ok(Self(Mime::from_str(s)?))
} }
@ -69,11 +69,11 @@ mod tests {
#[test] #[test]
fn from_path_with_extension() { fn from_path_with_extension() {
assert_eq!( assert_eq!(
FlexibleMime::try_from(".pdf").unwrap().0, MimeType::try_from(".pdf").unwrap().0,
mime::APPLICATION_PDF mime::APPLICATION_PDF
); );
assert_eq!( assert_eq!(
FlexibleMime::try_from(".").unwrap().0.essence_str(), MimeType::try_from(".").unwrap().0.essence_str(),
"inode/directory" "inode/directory"
); );
} }

View file

@ -6,4 +6,4 @@ mod mime_types;
pub use self::db::{autocomplete as db_autocomplete, SHARED_MIME_DB}; pub use self::db::{autocomplete as db_autocomplete, SHARED_MIME_DB};
pub use desktop_entry::{DesktopEntry, Rule as PestRule}; pub use desktop_entry::{DesktopEntry, Rule as PestRule};
pub use handler::Handler; pub use handler::Handler;
pub use mime_types::{FlexibleMime, MimeOrExtension}; pub use mime_types::{MimeOrExtension, MimeType};

55
src/config.rs Normal file
View file

@ -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)
}
}

View file

@ -8,6 +8,8 @@ pub enum Error {
Notify(#[from] notify_rust::error::Error), Notify(#[from] notify_rust::error::Error),
#[error(transparent)] #[error(transparent)]
Xdg(#[from] xdg::BaseDirectoriesError), Xdg(#[from] xdg::BaseDirectoriesError),
#[error(transparent)]
Config(#[from] confy::ConfyError),
#[error("no handler defined for this mime/extension")] #[error("no handler defined for this mime/extension")]
NotFound(String), NotFound(String),
#[error("could not figure out the mime type .{0}")] #[error("could not figure out the mime type .{0}")]

View file

@ -1,68 +1,19 @@
use clap::Clap;
use error::{Error, Result}; use error::{Error, Result};
use notify_rust::Notification;
use std::convert::TryFrom;
mod apps; mod apps;
mod cli;
mod common; mod common;
mod config;
mod error; 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<()> { fn main() -> Result<()> {
use clap::Clap;
use cli::Cmd;
use common::MimeType;
use std::convert::TryFrom;
let mut apps = apps::MimeApps::read()?; let mut apps = apps::MimeApps::read()?;
crate::config::Config::load()?;
let res = || -> Result<()> { let res = || -> Result<()> {
match Cmd::parse() { match Cmd::parse() {
@ -78,14 +29,9 @@ fn main() -> Result<()> {
Cmd::Get { mime, json } => { Cmd::Get { mime, json } => {
apps.show_handler(&mime.0, json)?; apps.show_handler(&mime.0, json)?;
} }
Cmd::Open { path } => { Cmd::Open { paths } => {
std::process::Command::new("notify-send") let mime = MimeType::try_from(paths[0].as_str())?.0;
.arg(&format!("{:?}", path)) apps.get_handler(&mime)?.launch(paths)?;
.spawn()?;
apps.get_handler(
&FlexibleMime::try_from(path.get(0).unwrap().as_str())?.0,
)?
.launch(path)?;
} }
Cmd::List => { Cmd::List => {
apps.print()?; apps.print()?;
@ -110,12 +56,13 @@ fn main() -> Result<()> {
match (res, atty::is(atty::Stream::Stdout)) { match (res, atty::is(atty::Stream::Stdout)) {
(Err(e), true) => eprintln!("{}", e), (Err(e), true) => eprintln!("{}", e),
(Err(e), false) => { (Err(e), false) => {
Notification::new() notify_rust::Notification::new()
.summary("handlr error") .summary("handlr error")
.body(&e.to_string()) .body(&e.to_string())
.show()?; .show()?;
} }
_ => {} _ => {}
}; };
Ok(()) Ok(())
} }