misc improvements

This commit is contained in:
Gregory 2020-11-04 00:05:56 -05:00
parent 06b5355acc
commit b6c6433aa3
No known key found for this signature in database
GPG key ID: 2E44FAEEDC94B1E2
9 changed files with 432 additions and 321 deletions

476
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,22 +9,23 @@ description = "Manage mimeapps.list and default applications with ease"
[dependencies] [dependencies]
pest = "2.1.3" pest = "2.1.3"
pest_derive = "2.1.0" pest_derive = "2.1.0"
clap = "3.0.0-beta.1" clap = "3.0.0-beta.2"
url = "2.1.1" url = "2.1.1"
itertools = "0.9.0" itertools = "0.9.0"
json = "0.12.4" json = "0.12.4"
shlex = "0.1.1" shlex = "0.1.1"
thiserror = "1.0.19" thiserror = "1.0.22"
ascii_table = "3.0.1" ascii_table = "3.0.1"
xdg = "2.2.0" xdg = "2.2.0"
mime = "0.3.16" mime = "0.3.16"
regex = { version = "1.3.9", default-features = false, features = ["std"] } regex = { version = "1.4.2", default-features = false, features = ["std"] }
mime-db = "0.1.5" mime-db = "1.1.0"
atty = "0.2.14" atty = "0.2.14"
confy = "0.4.0" confy = "0.4.0"
serde = "1.0.111" serde = "1.0.117"
xdg-mime = "0.3.2" xdg-mime = "0.3.2"
freedesktop_entry_parser = "0.2.2" freedesktop_entry_parser = "1.1.0"
once_cell = "1.4.1"
[profile.release] [profile.release]
opt-level=3 opt-level=3

View file

@ -5,7 +5,7 @@ use crate::{
use mime::Mime; use mime::Mime;
use std::{collections::HashMap, convert::TryFrom, ffi::OsStr}; use std::{collections::HashMap, convert::TryFrom, ffi::OsStr};
#[derive(Debug)] #[derive(Debug, Default)]
pub struct SystemApps(pub HashMap<Mime, Vec<Handler>>); pub struct SystemApps(pub HashMap<Mime, Vec<Handler>>);
impl SystemApps { impl SystemApps {

View file

@ -1,17 +1,18 @@
use crate::{ use crate::{
apps::SystemApps, apps::SystemApps,
common::{DesktopEntry, Handler}, common::{DesktopEntry, Handler},
Error, Result, Error, Result, CONFIG,
}; };
use mime::Mime; use mime::Mime;
use pest::Parser; use pest::Parser;
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
io::Read,
path::PathBuf, path::PathBuf,
str::FromStr, str::FromStr,
}; };
#[derive(Debug, pest_derive::Parser)] #[derive(Debug, Default, pest_derive::Parser)]
#[grammar = "common/ini.pest"] #[grammar = "common/ini.pest"]
pub struct MimeApps { pub struct MimeApps {
added_associations: HashMap<Mime, VecDeque<Handler>>, added_associations: HashMap<Mime, VecDeque<Handler>>,
@ -20,21 +21,17 @@ pub struct MimeApps {
} }
impl MimeApps { impl MimeApps {
pub fn add_handler(&mut self, mime: Mime, handler: Handler) -> Result<()> { pub fn add_handler(&mut self, mime: Mime, handler: Handler) {
let handlers = self.default_apps.entry(mime).or_default(); self.default_apps
handlers.push_back(handler); .entry(mime)
self.save()?; .or_default()
Ok(()) .push_back(handler);
} }
pub fn set_handler(&mut self, mime: Mime, handler: Handler) -> Result<()> {
self.default_apps.insert(mime, { pub fn set_handler(&mut self, mime: Mime, handler: Handler) {
let mut handlers = VecDeque::with_capacity(1); self.default_apps.insert(mime, vec![handler].into());
handlers.push_back(handler);
handlers
});
self.save()?;
Ok(())
} }
pub fn remove_handler(&mut self, mime: &Mime) -> Result<()> { pub fn remove_handler(&mut self, mime: &Mime) -> Result<()> {
if let Some(_removed) = self.default_apps.remove(mime) { if let Some(_removed) = self.default_apps.remove(mime) {
self.save()?; self.save()?;
@ -42,11 +39,20 @@ impl MimeApps {
Ok(()) 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) { 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 let handlers = handlers
.into_iter() .into_iter()
.map(|h| (h, h.get_entry().unwrap().name)) .map(|h| (h, h.get_entry().unwrap().name))
@ -54,7 +60,7 @@ impl MimeApps {
let handler = { let handler = {
let name = let name =
config.select(handlers.iter().map(|h| h.1.clone()))?; CONFIG.select(handlers.iter().map(|h| h.1.clone()))?;
handlers handlers
.into_iter() .into_iter()
@ -67,21 +73,21 @@ impl MimeApps {
Ok(handler) Ok(handler)
} }
Some(handlers) => Ok(handlers.get(0).unwrap().clone()), Some(handlers) => Ok(handlers.get(0).unwrap().clone()),
None => match self None => Err(Error::NotFound(mime.to_string())),
.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),
},
} }
} }
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<()> { pub fn show_handler(&self, mime: &Mime, output_json: bool) -> Result<()> {
let handler = self.get_handler(mime)?; let handler = self.get_handler(mime)?;
let output = if output_json { let output = if output_json {
@ -104,7 +110,16 @@ impl MimeApps {
Ok(config) Ok(config)
} }
pub fn read() -> Result<Self> { 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 file = Self::parse(Rule::file, &raw_conf)?.next().unwrap();
let mut current_section_name = "".to_string(); let mut current_section_name = "".to_string();
@ -165,6 +180,7 @@ impl MimeApps {
let f = std::fs::OpenOptions::new() let f = std::fs::OpenOptions::new()
.read(true) .read(true)
.create(true)
.write(true) .write(true)
.truncate(true) .truncate(true)
.open(Self::path()?)?; .open(Self::path()?)?;
@ -226,3 +242,43 @@ impl MimeApps {
Ok(()) 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(())
}
}

View file

@ -1,4 +1,4 @@
use crate::{Error, Result}; use crate::{Error, Result, CONFIG};
use mime::Mime; use mime::Mime;
use std::{ use std::{
convert::TryFrom, convert::TryFrom,
@ -17,7 +17,7 @@ pub struct DesktopEntry {
pub(crate) mimes: Vec<Mime>, pub(crate) mimes: Vec<Mime>,
} }
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq, Copy, Clone)]
pub enum Mode { pub enum Mode {
Launch, Launch,
Open, Open,
@ -55,16 +55,16 @@ impl DesktopEntry {
Ok(()) 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 special = regex::Regex::new("%(f|F|u|U)").unwrap();
let mut split = shlex::split(&self.exec) let mut split = shlex::split(&self.exec)
.unwrap() .unwrap()
.into_iter() .into_iter()
.flat_map(|s| match s.as_str() { .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 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()], .into()],
_ => vec![s], _ => vec![s],
}) })
@ -73,9 +73,8 @@ impl DesktopEntry {
// If the entry expects a terminal (emulator), but this process is not running in one, we // If the entry expects a terminal (emulator), but this process is not running in one, we
// launch a new one. // launch a new one.
if self.term && !atty::is(atty::Stream::Stdout) { if self.term && !atty::is(atty::Stream::Stdout) {
let config = crate::config::Config::load()?;
let terminal_emulator_args = let terminal_emulator_args =
shlex::split(&config.terminal_emulator).unwrap(); shlex::split(&CONFIG.terminal_emulator).unwrap();
split = terminal_emulator_args split = terminal_emulator_args
.into_iter() .into_iter()
.chain(split.into_iter()) .chain(split.into_iter())
@ -87,32 +86,29 @@ impl DesktopEntry {
} }
fn parse_file(path: &Path) -> Option<DesktopEntry> { fn parse_file(path: &Path) -> Option<DesktopEntry> {
let raw = std::fs::read(&path).ok()?; let raw_entry = freedesktop_entry_parser::parse_entry(&path).ok()?;
let parsed = freedesktop_entry_parser::parse_entry(&raw) let section = raw_entry.section("Desktop Entry");
.filter_map(Result::ok)
.find(|s| s.title == b"Desktop Entry")?;
let mut entry = DesktopEntry::default(); let mut entry = DesktopEntry::default();
entry.file_name = path.file_name()?.to_owned(); 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 { match attr.name {
b"Name" if entry.name == "" => { "Name" if entry.name == "" => {
entry.name = String::from_utf8(attr.value.into()).ok()?; entry.name = attr.value.unwrap().into();
} }
b"Exec" => { "Exec" => entry.exec = attr.value.unwrap().into(),
entry.exec = String::from_utf8(attr.value.into()).ok()? "MimeType" => {
} let mut mimes = attr
b"MimeType" => { .value
let mut mimes = String::from_utf8(attr.value.into()) .unwrap()
.ok()?
.split(";") .split(";")
.filter_map(|m| Mime::from_str(m).ok()) .filter_map(|m| Mime::from_str(m).ok())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
mimes.pop(); mimes.pop();
entry.mimes = mimes; entry.mimes = mimes;
} }
b"Terminal" => entry.term = attr.value == b"true", "Terminal" => entry.term = attr.value.unwrap() == "true",
_ => {} _ => {}
} }
} }

View file

@ -18,8 +18,8 @@ impl MimeType {
[m] if m == &mime::APPLICATION_OCTET_STREAM => { [m] if m == &mime::APPLICATION_OCTET_STREAM => {
Err(Error::Ambiguous(ext.into())) Err(Error::Ambiguous(ext.into()))
} }
[] => unreachable!(),
[guess, ..] => Ok(guess.clone()), [guess, ..] => Ok(guess.clone()),
[] => unreachable!(),
} }
} }
} }
@ -28,17 +28,16 @@ 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> {
if let Ok(url) = url::Url::parse(arg) { match url::Url::parse(arg) {
if url.scheme() == "file" { Ok(url) if url.scheme() == "file" => {
return Self::try_from(url.path()) Self::try_from(&*PathBuf::from(url.path()))
} }
Ok(Self( Ok(url) => Ok(Self(
format!("x-scheme-handler/{}", url.scheme()) format!("x-scheme-handler/{}", url.scheme())
.parse::<Mime>() .parse::<Mime>()
.unwrap(), .unwrap(),
)) )),
} else { Err(_) => Self::try_from(&*PathBuf::from(arg)),
Self::try_from(&*PathBuf::from(arg))
} }
} }
} }
@ -46,17 +45,29 @@ impl TryFrom<&str> for MimeType {
impl TryFrom<&Path> for MimeType { 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> {
match xdg_mime::SharedMimeInfo::new() use mime::APPLICATION_OCTET_STREAM as UNKNOWN;
.guess_mime_type() let db = xdg_mime::SharedMimeInfo::new();
.path(&path)
.guess() let name_guess = || match path.file_name() {
.mime_type() Some(f) => db.get_mime_types_from_file_name(&f.to_string_lossy())
{ [0]
guess if guess == &mime::APPLICATION_OCTET_STREAM => { .clone(),
Err(Error::Ambiguous(path.to_owned())) 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(()) Ok(())
} }
#[test]
fn filename_priority() -> Result<()> {
assert_eq!(MimeType::try_from("./tests/p.html")?.0, "text/html");
Ok(())
}
#[test] #[test]
fn from_ext() -> Result<()> { fn from_ext() -> Result<()> {
assert_eq!(".mp3".parse::<MimeOrExtension>()?.0, "audio/mpeg"); assert_eq!(".mp3".parse::<MimeOrExtension>()?.0, "audio/mpeg");

View file

@ -1,49 +1,31 @@
use std::env;
use crate::{Error, Result}; use crate::{Error, Result};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
fn default_terminal() -> std::string::String{ pub static CONFIG: Lazy<Config> = Lazy::new(Config::load);
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
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(default)]
pub struct Config { pub struct Config {
#[serde(default="default_enable_selector")]
pub enable_selector: bool, pub enable_selector: bool,
#[serde(default="default_selector")]
pub selector: String, pub selector: String,
#[serde(default="default_terminal")]
pub terminal_emulator: String, pub terminal_emulator: String,
} }
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
// This seems repetitive but serde does not uses Default // This seems repetitive but serde does not uses Default
Config { Config {
enable_selector: default_enable_selector(), enable_selector: false,
selector: default_selector(), selector: std::env::var("TERMINAL").unwrap_or("xterm -e".into()),
terminal_emulator: default_terminal() terminal_emulator: "rofi -dmenu -p 'Open With: '".into(),
} }
} }
} }
impl Config { impl Config {
pub fn load() -> Result<Self> { pub fn load() -> Self {
Ok(confy::load("handlr")?) confy::load("handlr").unwrap()
} }
pub fn select<O: Iterator<Item = String>>( pub fn select<O: Iterator<Item = String>>(

View file

@ -1,4 +1,6 @@
use config::CONFIG;
use error::{Error, Result}; use error::{Error, Result};
use once_cell::sync::Lazy;
mod apps; mod apps;
mod cli; mod cli;
@ -12,16 +14,20 @@ fn main() -> Result<()> {
use common::MimeType; use common::MimeType;
use std::convert::TryFrom; use std::convert::TryFrom;
// create config if it doesn't exist
Lazy::force(&CONFIG);
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() {
Cmd::Set { mime, handler } => { Cmd::Set { mime, handler } => {
apps.set_handler(mime.0, handler)?; apps.set_handler(mime.0, handler);
apps.save()?;
} }
Cmd::Add { mime, handler } => { Cmd::Add { mime, handler } => {
apps.add_handler(mime.0, handler)?; apps.add_handler(mime.0, handler);
apps.save()?;
} }
Cmd::Launch { mime, args } => { Cmd::Launch { mime, args } => {
apps.get_handler(&mime.0)?.launch(args)?; apps.get_handler(&mime.0)?.launch(args)?;

3
tests/p.html Normal file
View file

@ -0,0 +1,3 @@
<html>
<p>asdf</p>
</html>