mirror of
https://github.com/chmln/handlr.git
synced 2024-11-27 11:23:51 +01:00
Implement config and multi-handler selection
This commit is contained in:
parent
935d1daa87
commit
9d726cb6af
9 changed files with 191 additions and 80 deletions
32
Cargo.lock
generated
32
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
53
src/cli.rs
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
|
@ -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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
55
src/config.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}")]
|
||||||
|
|
79
src/main.rs
79
src/main.rs
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue