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

View file

@ -2,4 +2,4 @@ mod system;
mod user;
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::{
collections::{HashMap, VecDeque},
convert::TryFrom,
ffi::OsStr,
ffi::OsString,
};
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
pub struct SystemApps(pub HashMap<Mime, VecDeque<Handler>>);
impl SystemApps {
@ -19,30 +19,32 @@ impl SystemApps {
pub fn get_handler(&self, mime: &Mime) -> Option<Handler> {
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> {
let mut map = HashMap::<Mime, VecDeque<Handler>>::with_capacity(50);
xdg::BaseDirectories::new()?
.get_data_dirs()
.into_iter()
.map(|mut data_dir| {
data_dir.push("applications");
data_dir
})
.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| {
Self::get_entries()?.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()),
);
});
map.entry(mime)
.or_default()
.push_back(Handler::assume_valid(file_name.clone()));
});
});

View file

@ -1,9 +1,6 @@
use crate::{
apps::SystemApps,
common::{DesktopEntry, Handler},
Error, Result, CONFIG,
};
use crate::{apps::SystemApps, common::Handler, Error, Result, CONFIG};
use mime::Mime;
use once_cell::sync::Lazy;
use pest::Parser;
use std::{
collections::{HashMap, VecDeque},
@ -12,7 +9,9 @@ use std::{
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"]
pub struct MimeApps {
added_associations: HashMap<Mime, VecDeque<Handler>>,
@ -232,20 +231,13 @@ impl MimeApps {
Ok(())
}
pub fn list_handlers(&self) -> Result<()> {
use std::{convert::TryFrom, io::Write, os::unix::ffi::OsStrExt};
pub fn list_handlers() -> Result<()> {
use std::{io::Write, os::unix::ffi::OsStrExt};
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
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| DesktopEntry::try_from(p).ok())
.for_each(|e| {
SystemApps::get_entries()?.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();

View file

@ -1,6 +1,8 @@
use crate::{Error, Result, CONFIG};
use crate::{Error, Result};
use aho_corasick::AhoCorasick;
use mime::Mime;
use std::{
collections::HashMap,
convert::TryFrom,
ffi::OsString,
path::{Path, PathBuf},
@ -15,6 +17,7 @@ pub struct DesktopEntry {
pub(crate) file_name: OsString,
pub(crate) term: bool,
pub(crate) mimes: Vec<Mime>,
pub(crate) categories: HashMap<String, ()>,
}
#[derive(PartialEq, Eq, Copy, Clone)]
@ -47,25 +50,35 @@ impl DesktopEntry {
cmd
};
if self.term {
cmd.spawn()?;
if self.term && atty::is(atty::Stream::Stdout) {
cmd.spawn()?.wait()?;
} else {
cmd.stdout(Stdio::null()).stderr(Stdio::null()).spawn()?;
};
}
Ok(())
}
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)
.unwrap()
.into_iter()
.flat_map(|s| match s.as_str() {
"%f" | "%F" | "%u" | "%U" => args.clone(),
s if special.is_match(s) => vec![special
.replace_all(s, args.clone().join(" ").as_str())
.into()],
s if special.is_match(s) => vec![{
let mut replaced = String::with_capacity(s.len());
special.replace_all_with(
s,
&mut replaced,
|_, _, dst| {
dst.push_str(args.clone().join(" ").as_str());
false
},
);
replaced
}],
_ => vec![s],
})
.collect::<Vec<_>>();
@ -73,7 +86,7 @@ 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) {
split = shlex::split(&CONFIG.terminal_emulator)
split = shlex::split(&crate::config::Config::terminal()?)
.unwrap()
.into_iter()
.chain(split.into_iter())
@ -108,6 +121,15 @@ fn parse_file(path: &Path) -> Option<DesktopEntry> {
entry.mimes = mimes;
}
"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 serde::{Deserialize, Serialize};
use std::str::FromStr;
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 enable_selector: bool,
pub selector: String,
pub terminal_emulator: String,
}
impl Default for Config {
fn default() -> Self {
// This seems repetitive but serde does not uses Default
Config {
enable_selector: false,
selector: "rofi -dmenu -p 'Open With: '".into(),
terminal_emulator: std::env::var("TERMINAL").unwrap_or("xterm -e".into()),
}
}
}
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 {
confy::load("handlr").unwrap()
}

View file

@ -22,6 +22,9 @@ pub enum Error {
Selector(String),
#[error("selection 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>;

View file

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