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",
]
[[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"

View file

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

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 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)?;

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
#[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"
);
}

View file

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

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),
#[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}")]

View file

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