Fix handling of terminal apps | #30, #34

This commit is contained in:
Gregory 2021-04-23 17:33:22 -04:00
parent 1671183954
commit 18b0f0085e
No known key found for this signature in database
GPG key ID: 2E44FAEEDC94B1E2
10 changed files with 362 additions and 411 deletions

558
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,27 +5,28 @@ authors = ["greg <gregory.mkv@gmail.com>"]
edition = "2018" edition = "2018"
license = "MIT" license = "MIT"
description = "Manage mimeapps.list and default applications with ease" description = "Manage mimeapps.list and default applications with ease"
resolver = "2"
[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.2" clap = "3.0.0-beta.2"
url = "2.2.0" url = "2.2.1"
itertools = "0.9.0" itertools = "0.10.0"
json = "0.12.4" json = "0.12.4"
shlex = "0.1.1" shlex = "1.0.0"
thiserror = "1.0.22" thiserror = "1.0.24"
ascii_table = "3.0.2" ascii_table = "3.0.2"
xdg = "2.2.0" xdg = "2.2.0"
mime = "0.3.16" mime = "0.3.16"
regex = { version = "1.4.2", default-features = false, features = ["std"] } mime-db = "1.3.0"
mime-db = "1.1.0"
atty = "0.2.14" atty = "0.2.14"
confy = "0.4.0" confy = "0.4.0"
serde = "1.0.118" serde = { version = "1.0.125", features = ["derive"] }
xdg-mime = "0.3.2" xdg-mime = "0.3.2"
freedesktop_entry_parser = "1.1.1" freedesktop_entry_parser = "1.1.1"
once_cell = "1.5.2" once_cell = "1.7.2"
aho-corasick = "0.7.15"
[profile.release] [profile.release]
opt-level=3 opt-level=3

View file

@ -2,4 +2,4 @@ mod system;
mod user; mod user;
pub use system::SystemApps; pub use system::SystemApps;
pub use user::{MimeApps, Rule as MimeappsRule}; pub use user::{MimeApps, Rule as MimeappsRule, APPS};

View file

@ -6,10 +6,10 @@ use mime::Mime;
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
convert::TryFrom, convert::TryFrom,
ffi::OsStr, ffi::OsString,
}; };
#[derive(Debug, Default)] #[derive(Debug, Default, Clone)]
pub struct SystemApps(pub HashMap<Mime, VecDeque<Handler>>); pub struct SystemApps(pub HashMap<Mime, VecDeque<Handler>>);
impl SystemApps { impl SystemApps {
@ -19,32 +19,34 @@ impl SystemApps {
pub fn get_handler(&self, mime: &Mime) -> Option<Handler> { pub fn get_handler(&self, mime: &Mime) -> Option<Handler> {
Some(self.get_handlers(mime)?.get(0).unwrap().clone()) Some(self.get_handlers(mime)?.get(0).unwrap().clone())
} }
pub fn get_entries(
) -> Result<impl Iterator<Item = (OsString, DesktopEntry)>> {
Ok(xdg::BaseDirectories::new()?
.list_data_files_once("applications")
.into_iter()
.filter(|p| {
p.extension().map(|x| x.to_str()).flatten() == Some("desktop")
})
.filter_map(|p| {
Some((
p.file_name().unwrap().to_owned(),
DesktopEntry::try_from(p.clone()).ok()?,
))
}))
}
pub fn populate() -> Result<Self> { pub fn populate() -> Result<Self> {
let mut map = HashMap::<Mime, VecDeque<Handler>>::with_capacity(50); let mut map = HashMap::<Mime, VecDeque<Handler>>::with_capacity(50);
xdg::BaseDirectories::new()? Self::get_entries()?.for_each(|(_, entry)| {
.get_data_dirs() let (file_name, mimes) = (entry.file_name, entry.mimes);
.into_iter() mimes.into_iter().for_each(|mime| {
.map(|mut data_dir| { map.entry(mime)
data_dir.push("applications"); .or_default()
data_dir .push_back(Handler::assume_valid(file_name.clone()));
})
.filter_map(|data_dir| std::fs::read_dir(data_dir).ok())
.for_each(|dir| {
dir.filter_map(Result::ok)
.filter(|p| {
p.path().extension() == Some(OsStr::new("desktop"))
})
.filter_map(|p| DesktopEntry::try_from(p.path()).ok())
.for_each(|entry| {
let (file_name, mimes) = (entry.file_name, entry.mimes);
mimes.into_iter().for_each(|mime| {
map.entry(mime).or_default().push_back(
Handler::assume_valid(file_name.clone()),
);
});
});
}); });
});
Ok(Self(map)) Ok(Self(map))
} }

View file

@ -1,9 +1,6 @@
use crate::{ use crate::{apps::SystemApps, common::Handler, Error, Result, CONFIG};
apps::SystemApps,
common::{DesktopEntry, Handler},
Error, Result, CONFIG,
};
use mime::Mime; use mime::Mime;
use once_cell::sync::Lazy;
use pest::Parser; use pest::Parser;
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
@ -12,7 +9,9 @@ use std::{
str::FromStr, str::FromStr,
}; };
#[derive(Debug, Default, pest_derive::Parser)] pub static APPS: Lazy<MimeApps> = Lazy::new(|| MimeApps::read().unwrap());
#[derive(Debug, Default, Clone, 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>>,
@ -232,25 +231,18 @@ impl MimeApps {
Ok(()) Ok(())
} }
pub fn list_handlers(&self) -> Result<()> { pub fn list_handlers() -> Result<()> {
use std::{convert::TryFrom, io::Write, os::unix::ffi::OsStrExt}; use std::{io::Write, os::unix::ffi::OsStrExt};
let stdout = std::io::stdout(); let stdout = std::io::stdout();
let mut stdout = stdout.lock(); let mut stdout = stdout.lock();
xdg::BaseDirectories::new()? SystemApps::get_entries()?.for_each(|(_, e)| {
.list_data_files_once("applications") stdout.write_all(e.file_name.as_bytes()).unwrap();
.into_iter() stdout.write_all(b"\t").unwrap();
.filter(|p| { stdout.write_all(e.name.as_bytes()).unwrap();
p.extension().map(|x| x.to_str()).flatten() == Some("desktop") stdout.write_all(b"\n").unwrap();
}) });
.filter_map(|p| DesktopEntry::try_from(p).ok())
.for_each(|e| {
stdout.write_all(e.file_name.as_bytes()).unwrap();
stdout.write_all(b"\t").unwrap();
stdout.write_all(e.name.as_bytes()).unwrap();
stdout.write_all(b"\n").unwrap();
});
Ok(()) Ok(())
} }

View file

@ -1,6 +1,8 @@
use crate::{Error, Result, CONFIG}; use crate::{Error, Result};
use aho_corasick::AhoCorasick;
use mime::Mime; use mime::Mime;
use std::{ use std::{
collections::HashMap,
convert::TryFrom, convert::TryFrom,
ffi::OsString, ffi::OsString,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -15,6 +17,7 @@ pub struct DesktopEntry {
pub(crate) file_name: OsString, pub(crate) file_name: OsString,
pub(crate) term: bool, pub(crate) term: bool,
pub(crate) mimes: Vec<Mime>, pub(crate) mimes: Vec<Mime>,
pub(crate) categories: HashMap<String, ()>,
} }
#[derive(PartialEq, Eq, Copy, Clone)] #[derive(PartialEq, Eq, Copy, Clone)]
@ -47,25 +50,35 @@ impl DesktopEntry {
cmd cmd
}; };
if self.term { if self.term && atty::is(atty::Stream::Stdout) {
cmd.spawn()?; cmd.spawn()?.wait()?;
} else { } else {
cmd.stdout(Stdio::null()).stderr(Stdio::null()).spawn()?; cmd.stdout(Stdio::null()).stderr(Stdio::null()).spawn()?;
}; }
Ok(()) Ok(())
} }
pub fn get_cmd(&self, args: 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 cmd_patterns = &["%f", "%F", "%u", "%U"];
let special = AhoCorasick::new_auto_configured(cmd_patterns);
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" => args.clone(), "%f" | "%F" | "%u" | "%U" => args.clone(),
s if special.is_match(s) => vec![special s if special.is_match(s) => vec![{
.replace_all(s, args.clone().join(" ").as_str()) let mut replaced = String::with_capacity(s.len());
.into()], special.replace_all_with(
s,
&mut replaced,
|_, _, dst| {
dst.push_str(args.clone().join(" ").as_str());
false
},
);
replaced
}],
_ => vec![s], _ => vec![s],
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -73,7 +86,7 @@ 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) {
split = shlex::split(&CONFIG.terminal_emulator) split = shlex::split(&crate::config::Config::terminal()?)
.unwrap() .unwrap()
.into_iter() .into_iter()
.chain(split.into_iter()) .chain(split.into_iter())
@ -108,6 +121,15 @@ fn parse_file(path: &Path) -> Option<DesktopEntry> {
entry.mimes = mimes; entry.mimes = mimes;
} }
"Terminal" => entry.term = attr.value.unwrap() == "true", "Terminal" => entry.term = attr.value.unwrap() == "true",
"Categories" => {
entry.categories = attr
.value
.unwrap()
.split(";")
.filter(|s| !s.is_empty())
.map(|cat| (cat.to_owned(), ()))
.collect();
}
_ => {} _ => {}
} }
} }

View file

@ -1,6 +1,8 @@
use crate::{Error, Result}; use crate::{apps::SystemApps, common::Handler, Error, Result};
use mime::Mime;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr;
pub static CONFIG: Lazy<Config> = Lazy::new(Config::load); pub static CONFIG: Lazy<Config> = Lazy::new(Config::load);
@ -9,21 +11,56 @@ pub static CONFIG: Lazy<Config> = Lazy::new(Config::load);
pub struct Config { pub struct Config {
pub enable_selector: bool, pub enable_selector: bool,
pub selector: String, pub selector: 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
Config { Config {
enable_selector: false, enable_selector: false,
selector: "rofi -dmenu -p 'Open With: '".into(), selector: "rofi -dmenu -p 'Open With: '".into(),
terminal_emulator: std::env::var("TERMINAL").unwrap_or("xterm -e".into()),
} }
} }
} }
impl Config { impl Config {
pub fn terminal() -> Result<String> {
let terminal_entry = crate::apps::APPS
.get_handler(&Mime::from_str("x-scheme-handler/terminal").unwrap())
.ok()
.map(|h| h.get_entry().ok())
.flatten();
terminal_entry
.or_else(|| {
let entry = SystemApps::get_entries()
.ok()?
.find(|(_handler, entry)| {
entry.categories.contains_key("TerminalEmulator")
})
.map(|e| e.clone())?;
crate::utils::notify(
"handlr",
&format!(
"Guessed terminal emulator: {}.\n\nIf this is wrong, use `handlr set x-scheme-handler/terminal` to update it.",
entry.0.to_string_lossy()
)
).ok()?;
let mut apps = (*crate::apps::APPS).clone();
apps.set_handler(
Mime::from_str("x-scheme-handler/terminal").unwrap(),
Handler::assume_valid(entry.0),
);
apps.save().ok()?;
Some(entry.1)
})
.map(|e| e.exec)
.ok_or(Error::NoTerminal)
}
pub fn load() -> Self { pub fn load() -> Self {
confy::load("handlr").unwrap() confy::load("handlr").unwrap()
} }

View file

@ -22,6 +22,9 @@ pub enum Error {
Selector(String), Selector(String),
#[error("selection cancelled")] #[error("selection cancelled")]
Cancelled, Cancelled,
#[error("Please specify the default terminal with handlr set x-scheme-handler/terminal")]
NoTerminal,
} }
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;

View file

@ -7,18 +7,18 @@ mod cli;
mod common; mod common;
mod config; mod config;
mod error; mod error;
mod utils;
fn main() -> Result<()> { fn main() -> Result<()> {
use clap::Clap; use clap::Clap;
use cli::Cmd; use cli::Cmd;
use common::{MimeType, Handler}; use common::{Handler, MimeType};
use std::convert::TryFrom; use std::{collections::HashMap, convert::TryFrom};
use std::collections::HashMap;
// create config if it doesn't exist // create config if it doesn't exist
Lazy::force(&CONFIG); Lazy::force(&CONFIG);
let mut apps = apps::MimeApps::read()?; let mut apps = (*apps::APPS).clone();
let res = || -> Result<()> { let res = || -> Result<()> {
match Cmd::parse() { match Cmd::parse() {
@ -37,7 +37,8 @@ fn main() -> Result<()> {
apps.show_handler(&mime.0, json)?; apps.show_handler(&mime.0, json)?;
} }
Cmd::Open { paths } => { Cmd::Open { paths } => {
let mut handlers: HashMap<Handler, Vec<String>> = HashMap::new(); let mut handlers: HashMap<Handler, Vec<String>> =
HashMap::new();
for path in paths.into_iter() { for path in paths.into_iter() {
let mime = MimeType::try_from(path.as_str())?.0; let mime = MimeType::try_from(path.as_str())?.0;
let handler = apps.get_handler(&mime)?; let handler = apps.get_handler(&mime)?;
@ -58,7 +59,7 @@ fn main() -> Result<()> {
mimes, mimes,
} => { } => {
if desktop_files { if desktop_files {
apps.list_handlers()?; apps::MimeApps::list_handlers()?;
} else if mimes { } else if mimes {
common::db_autocomplete()?; common::db_autocomplete()?;
} }
@ -76,9 +77,7 @@ fn main() -> Result<()> {
std::process::exit(1); std::process::exit(1);
} }
(Err(e), false) => { (Err(e), false) => {
std::process::Command::new("notify-send") utils::notify("handlr error", &e.to_string())?;
.args(&["handlr error", &e.to_string()])
.spawn()?;
std::process::exit(1); std::process::exit(1);
} }
_ => Ok(()), _ => Ok(()),

7
src/utils.rs Normal file
View file

@ -0,0 +1,7 @@
use crate::Result;
pub fn notify(title: &str, msg: &str) -> Result<()> {
std::process::Command::new("notify-send")
.args(&["-t", "10000", title, msg])
.spawn()?;
Ok(())
}