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

View file

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

View file

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

View file

@ -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",
_ => {}
}
}

View file

@ -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");

View file

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

View file

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

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