mirror of
https://github.com/chmln/handlr.git
synced 2024-11-14 13:39:29 +01:00
misc improvements
This commit is contained in:
parent
06b5355acc
commit
b6c6433aa3
9 changed files with 432 additions and 321 deletions
476
Cargo.lock
generated
476
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
13
Cargo.toml
13
Cargo.toml
|
@ -9,22 +9,23 @@ description = "Manage mimeapps.list and default applications with ease"
|
|||
[dependencies]
|
||||
pest = "2.1.3"
|
||||
pest_derive = "2.1.0"
|
||||
clap = "3.0.0-beta.1"
|
||||
clap = "3.0.0-beta.2"
|
||||
url = "2.1.1"
|
||||
itertools = "0.9.0"
|
||||
json = "0.12.4"
|
||||
shlex = "0.1.1"
|
||||
thiserror = "1.0.19"
|
||||
thiserror = "1.0.22"
|
||||
ascii_table = "3.0.1"
|
||||
xdg = "2.2.0"
|
||||
mime = "0.3.16"
|
||||
regex = { version = "1.3.9", default-features = false, features = ["std"] }
|
||||
mime-db = "0.1.5"
|
||||
regex = { version = "1.4.2", default-features = false, features = ["std"] }
|
||||
mime-db = "1.1.0"
|
||||
atty = "0.2.14"
|
||||
confy = "0.4.0"
|
||||
serde = "1.0.111"
|
||||
serde = "1.0.117"
|
||||
xdg-mime = "0.3.2"
|
||||
freedesktop_entry_parser = "0.2.2"
|
||||
freedesktop_entry_parser = "1.1.0"
|
||||
once_cell = "1.4.1"
|
||||
|
||||
[profile.release]
|
||||
opt-level=3
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
use mime::Mime;
|
||||
use std::{collections::HashMap, convert::TryFrom, ffi::OsStr};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SystemApps(pub HashMap<Mime, Vec<Handler>>);
|
||||
|
||||
impl SystemApps {
|
||||
|
|
122
src/apps/user.rs
122
src/apps/user.rs
|
@ -1,17 +1,18 @@
|
|||
use crate::{
|
||||
apps::SystemApps,
|
||||
common::{DesktopEntry, Handler},
|
||||
Error, Result,
|
||||
Error, Result, CONFIG,
|
||||
};
|
||||
use mime::Mime;
|
||||
use pest::Parser;
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
io::Read,
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
#[derive(Debug, pest_derive::Parser)]
|
||||
#[derive(Debug, Default, pest_derive::Parser)]
|
||||
#[grammar = "common/ini.pest"]
|
||||
pub struct MimeApps {
|
||||
added_associations: HashMap<Mime, VecDeque<Handler>>,
|
||||
|
@ -20,21 +21,17 @@ pub struct MimeApps {
|
|||
}
|
||||
|
||||
impl MimeApps {
|
||||
pub fn add_handler(&mut self, mime: Mime, handler: Handler) -> Result<()> {
|
||||
let handlers = self.default_apps.entry(mime).or_default();
|
||||
handlers.push_back(handler);
|
||||
self.save()?;
|
||||
Ok(())
|
||||
pub fn add_handler(&mut self, mime: Mime, handler: Handler) {
|
||||
self.default_apps
|
||||
.entry(mime)
|
||||
.or_default()
|
||||
.push_back(handler);
|
||||
}
|
||||
pub fn set_handler(&mut self, mime: Mime, handler: Handler) -> Result<()> {
|
||||
self.default_apps.insert(mime, {
|
||||
let mut handlers = VecDeque::with_capacity(1);
|
||||
handlers.push_back(handler);
|
||||
handlers
|
||||
});
|
||||
self.save()?;
|
||||
Ok(())
|
||||
|
||||
pub fn set_handler(&mut self, mime: Mime, handler: Handler) {
|
||||
self.default_apps.insert(mime, vec![handler].into());
|
||||
}
|
||||
|
||||
pub fn remove_handler(&mut self, mime: &Mime) -> Result<()> {
|
||||
if let Some(_removed) = self.default_apps.remove(mime) {
|
||||
self.save()?;
|
||||
|
@ -42,11 +39,20 @@ impl MimeApps {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
pub fn get_handler(&self, mime: &Mime) -> Result<Handler> {
|
||||
let config = crate::config::Config::load()?;
|
||||
|
||||
pub fn get_handler(&self, mime: &Mime) -> Result<Handler> {
|
||||
self.get_handler_from_user(mime)
|
||||
.or_else(|_| {
|
||||
let wildcard =
|
||||
Mime::from_str(&format!("{}/*", mime.type_())).unwrap();
|
||||
self.get_handler_from_user(&wildcard)
|
||||
})
|
||||
.or_else(|_| self.get_handler_from_added_associations(mime))
|
||||
}
|
||||
|
||||
fn get_handler_from_user(&self, mime: &Mime) -> Result<Handler> {
|
||||
match self.default_apps.get(mime) {
|
||||
Some(handlers) if config.enable_selector && handlers.len() > 1 => {
|
||||
Some(handlers) if CONFIG.enable_selector && handlers.len() > 1 => {
|
||||
let handlers = handlers
|
||||
.into_iter()
|
||||
.map(|h| (h, h.get_entry().unwrap().name))
|
||||
|
@ -54,7 +60,7 @@ impl MimeApps {
|
|||
|
||||
let handler = {
|
||||
let name =
|
||||
config.select(handlers.iter().map(|h| h.1.clone()))?;
|
||||
CONFIG.select(handlers.iter().map(|h| h.1.clone()))?;
|
||||
|
||||
handlers
|
||||
.into_iter()
|
||||
|
@ -67,21 +73,21 @@ impl MimeApps {
|
|||
Ok(handler)
|
||||
}
|
||||
Some(handlers) => Ok(handlers.get(0).unwrap().clone()),
|
||||
None => match 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()))
|
||||
{
|
||||
Ok(h) => Ok(h),
|
||||
Err(Error::NotFound(_)) if mime.type_() == "text" => self
|
||||
.get_handler(&mime::TEXT_PLAIN)
|
||||
.map_err(|_| Error::NotFound(mime.to_string())),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
None => Err(Error::NotFound(mime.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_handler_from_added_associations(
|
||||
&self,
|
||||
mime: &Mime,
|
||||
) -> Result<Handler> {
|
||||
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)?;
|
||||
let output = if output_json {
|
||||
|
@ -104,7 +110,16 @@ impl MimeApps {
|
|||
Ok(config)
|
||||
}
|
||||
pub fn read() -> Result<Self> {
|
||||
let raw_conf = std::fs::read_to_string(Self::path()?)?;
|
||||
let raw_conf = {
|
||||
let mut buf = String::new();
|
||||
std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.read(true)
|
||||
.open(Self::path()?)?
|
||||
.read_to_string(&mut buf)?;
|
||||
buf
|
||||
};
|
||||
let file = Self::parse(Rule::file, &raw_conf)?.next().unwrap();
|
||||
|
||||
let mut current_section_name = "".to_string();
|
||||
|
@ -165,6 +180,7 @@ impl MimeApps {
|
|||
|
||||
let f = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(Self::path()?)?;
|
||||
|
@ -226,3 +242,43 @@ impl MimeApps {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wildcard_mimes() -> Result<()> {
|
||||
let mut user_apps = MimeApps::default();
|
||||
user_apps.add_handler(
|
||||
Mime::from_str("video/*").unwrap(),
|
||||
Handler::assume_valid("mpv.desktop".into()),
|
||||
);
|
||||
user_apps.add_handler(
|
||||
Mime::from_str("video/webm").unwrap(),
|
||||
Handler::assume_valid("brave.desktop".into()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
user_apps
|
||||
.get_handler(&Mime::from_str("video/mp4")?)?
|
||||
.to_string(),
|
||||
"mpv.desktop"
|
||||
);
|
||||
assert_eq!(
|
||||
user_apps
|
||||
.get_handler(&Mime::from_str("video/asdf")?)?
|
||||
.to_string(),
|
||||
"mpv.desktop"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
user_apps
|
||||
.get_handler(&Mime::from_str("video/webm")?)?
|
||||
.to_string(),
|
||||
"brave.desktop"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{Error, Result};
|
||||
use crate::{Error, Result, CONFIG};
|
||||
use mime::Mime;
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
|
@ -17,7 +17,7 @@ pub struct DesktopEntry {
|
|||
pub(crate) mimes: Vec<Mime>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[derive(PartialEq, Eq, Copy, Clone)]
|
||||
pub enum Mode {
|
||||
Launch,
|
||||
Open,
|
||||
|
@ -55,16 +55,16 @@ impl DesktopEntry {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
pub fn get_cmd(&self, arg: Vec<String>) -> Result<(String, Vec<String>)> {
|
||||
pub fn get_cmd(&self, args: Vec<String>) -> Result<(String, Vec<String>)> {
|
||||
let special = regex::Regex::new("%(f|F|u|U)").unwrap();
|
||||
|
||||
let mut split = shlex::split(&self.exec)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.flat_map(|s| match s.as_str() {
|
||||
"%f" | "%F" | "%u" | "%U" => arg.clone(),
|
||||
"%f" | "%F" | "%u" | "%U" => args.clone(),
|
||||
s if special.is_match(s) => vec![special
|
||||
.replace_all(s, arg.clone().join(" ").as_str())
|
||||
.replace_all(s, args.clone().join(" ").as_str())
|
||||
.into()],
|
||||
_ => vec![s],
|
||||
})
|
||||
|
@ -73,9 +73,8 @@ impl DesktopEntry {
|
|||
// If the entry expects a terminal (emulator), but this process is not running in one, we
|
||||
// launch a new one.
|
||||
if self.term && !atty::is(atty::Stream::Stdout) {
|
||||
let config = crate::config::Config::load()?;
|
||||
let terminal_emulator_args =
|
||||
shlex::split(&config.terminal_emulator).unwrap();
|
||||
shlex::split(&CONFIG.terminal_emulator).unwrap();
|
||||
split = terminal_emulator_args
|
||||
.into_iter()
|
||||
.chain(split.into_iter())
|
||||
|
@ -87,32 +86,29 @@ impl DesktopEntry {
|
|||
}
|
||||
|
||||
fn parse_file(path: &Path) -> Option<DesktopEntry> {
|
||||
let raw = std::fs::read(&path).ok()?;
|
||||
let parsed = freedesktop_entry_parser::parse_entry(&raw)
|
||||
.filter_map(Result::ok)
|
||||
.find(|s| s.title == b"Desktop Entry")?;
|
||||
let raw_entry = freedesktop_entry_parser::parse_entry(&path).ok()?;
|
||||
let section = raw_entry.section("Desktop Entry");
|
||||
|
||||
let mut entry = DesktopEntry::default();
|
||||
entry.file_name = path.file_name()?.to_owned();
|
||||
|
||||
for attr in parsed.attrs {
|
||||
for attr in section.attrs().into_iter().filter(|a| a.has_value()) {
|
||||
match attr.name {
|
||||
b"Name" if entry.name == "" => {
|
||||
entry.name = String::from_utf8(attr.value.into()).ok()?;
|
||||
"Name" if entry.name == "" => {
|
||||
entry.name = attr.value.unwrap().into();
|
||||
}
|
||||
b"Exec" => {
|
||||
entry.exec = String::from_utf8(attr.value.into()).ok()?
|
||||
}
|
||||
b"MimeType" => {
|
||||
let mut mimes = String::from_utf8(attr.value.into())
|
||||
.ok()?
|
||||
"Exec" => entry.exec = attr.value.unwrap().into(),
|
||||
"MimeType" => {
|
||||
let mut mimes = attr
|
||||
.value
|
||||
.unwrap()
|
||||
.split(";")
|
||||
.filter_map(|m| Mime::from_str(m).ok())
|
||||
.collect::<Vec<_>>();
|
||||
mimes.pop();
|
||||
entry.mimes = mimes;
|
||||
}
|
||||
b"Terminal" => entry.term = attr.value == b"true",
|
||||
"Terminal" => entry.term = attr.value.unwrap() == "true",
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@ impl MimeType {
|
|||
[m] if m == &mime::APPLICATION_OCTET_STREAM => {
|
||||
Err(Error::Ambiguous(ext.into()))
|
||||
}
|
||||
[] => unreachable!(),
|
||||
[guess, ..] => Ok(guess.clone()),
|
||||
[] => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,17 +28,16 @@ impl TryFrom<&str> for MimeType {
|
|||
type Error = Error;
|
||||
|
||||
fn try_from(arg: &str) -> Result<Self> {
|
||||
if let Ok(url) = url::Url::parse(arg) {
|
||||
if url.scheme() == "file" {
|
||||
return Self::try_from(url.path())
|
||||
match url::Url::parse(arg) {
|
||||
Ok(url) if url.scheme() == "file" => {
|
||||
Self::try_from(&*PathBuf::from(url.path()))
|
||||
}
|
||||
Ok(Self(
|
||||
Ok(url) => Ok(Self(
|
||||
format!("x-scheme-handler/{}", url.scheme())
|
||||
.parse::<Mime>()
|
||||
.unwrap(),
|
||||
))
|
||||
} else {
|
||||
Self::try_from(&*PathBuf::from(arg))
|
||||
)),
|
||||
Err(_) => Self::try_from(&*PathBuf::from(arg)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,17 +45,29 @@ impl TryFrom<&str> for MimeType {
|
|||
impl TryFrom<&Path> for MimeType {
|
||||
type Error = Error;
|
||||
fn try_from(path: &Path) -> Result<Self> {
|
||||
match xdg_mime::SharedMimeInfo::new()
|
||||
.guess_mime_type()
|
||||
.path(&path)
|
||||
.guess()
|
||||
.mime_type()
|
||||
{
|
||||
guess if guess == &mime::APPLICATION_OCTET_STREAM => {
|
||||
Err(Error::Ambiguous(path.to_owned()))
|
||||
use mime::APPLICATION_OCTET_STREAM as UNKNOWN;
|
||||
let db = xdg_mime::SharedMimeInfo::new();
|
||||
|
||||
let name_guess = || match path.file_name() {
|
||||
Some(f) => db.get_mime_types_from_file_name(&f.to_string_lossy())
|
||||
[0]
|
||||
.clone(),
|
||||
None => UNKNOWN,
|
||||
};
|
||||
|
||||
let content_guess = db.guess_mime_type().path(&path).guess();
|
||||
|
||||
let mime = match (name_guess(), content_guess.mime_type().clone()) {
|
||||
(m1, m2) if m1 == UNKNOWN && m2 == UNKNOWN => {
|
||||
return Err(Error::Ambiguous(path.to_owned()))
|
||||
}
|
||||
guess => Ok(Self(guess.clone())),
|
||||
}
|
||||
(m1, m2) if m1 == UNKNOWN => m2,
|
||||
(m1, m2) if m2 == UNKNOWN => m1,
|
||||
(m1, m2) if m1 != m2 => m2,
|
||||
(m1, _) => m1,
|
||||
};
|
||||
|
||||
Ok(Self(mime.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,6 +121,12 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filename_priority() -> Result<()> {
|
||||
assert_eq!(MimeType::try_from("./tests/p.html")?.0, "text/html");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ext() -> Result<()> {
|
||||
assert_eq!(".mp3".parse::<MimeOrExtension>()?.0, "audio/mpeg");
|
||||
|
|
|
@ -1,49 +1,31 @@
|
|||
use std::env;
|
||||
use crate::{Error, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn default_terminal() -> std::string::String{
|
||||
match env::var("TERMINAL") {
|
||||
Ok(val) => val,
|
||||
Err(_) => "xterm -e".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_selector() -> std::string::String{
|
||||
"rofi -dmenu -p 'Open With: '".to_owned()
|
||||
}
|
||||
|
||||
fn default_enable_selector() -> bool {
|
||||
false
|
||||
}
|
||||
pub static CONFIG: Lazy<Config> = Lazy::new(Config::load);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
#[serde(default="default_enable_selector")]
|
||||
pub enable_selector: bool,
|
||||
|
||||
#[serde(default="default_selector")]
|
||||
pub selector: String,
|
||||
|
||||
#[serde(default="default_terminal")]
|
||||
pub terminal_emulator: String,
|
||||
}
|
||||
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
// This seems repetitive but serde does not uses Default
|
||||
Config {
|
||||
enable_selector: default_enable_selector(),
|
||||
selector: default_selector(),
|
||||
terminal_emulator: default_terminal()
|
||||
enable_selector: false,
|
||||
selector: std::env::var("TERMINAL").unwrap_or("xterm -e".into()),
|
||||
terminal_emulator: "rofi -dmenu -p 'Open With: '".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Self> {
|
||||
Ok(confy::load("handlr")?)
|
||||
pub fn load() -> Self {
|
||||
confy::load("handlr").unwrap()
|
||||
}
|
||||
|
||||
pub fn select<O: Iterator<Item = String>>(
|
||||
|
|
12
src/main.rs
12
src/main.rs
|
@ -1,4 +1,6 @@
|
|||
use config::CONFIG;
|
||||
use error::{Error, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
mod apps;
|
||||
mod cli;
|
||||
|
@ -12,16 +14,20 @@ fn main() -> Result<()> {
|
|||
use common::MimeType;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
// create config if it doesn't exist
|
||||
Lazy::force(&CONFIG);
|
||||
|
||||
let mut apps = apps::MimeApps::read()?;
|
||||
crate::config::Config::load()?;
|
||||
|
||||
let res = || -> Result<()> {
|
||||
match Cmd::parse() {
|
||||
Cmd::Set { mime, handler } => {
|
||||
apps.set_handler(mime.0, handler)?;
|
||||
apps.set_handler(mime.0, handler);
|
||||
apps.save()?;
|
||||
}
|
||||
Cmd::Add { mime, handler } => {
|
||||
apps.add_handler(mime.0, handler)?;
|
||||
apps.add_handler(mime.0, handler);
|
||||
apps.save()?;
|
||||
}
|
||||
Cmd::Launch { mime, args } => {
|
||||
apps.get_handler(&mime.0)?.launch(args)?;
|
||||
|
|
3
tests/p.html
Normal file
3
tests/p.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<html>
|
||||
<p>asdf</p>
|
||||
</html>
|
Loading…
Reference in a new issue