mirror of
https://github.com/chmln/handlr.git
synced 2024-11-27 03:13:49 +01:00
parent
1671183954
commit
18b0f0085e
10 changed files with 362 additions and 411 deletions
558
Cargo.lock
generated
558
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
17
Cargo.toml
17
Cargo.toml
|
@ -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
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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,32 +19,34 @@ 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| {
|
||||
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()),
|
||||
);
|
||||
});
|
||||
});
|
||||
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()));
|
||||
});
|
||||
});
|
||||
|
||||
Ok(Self(map))
|
||||
}
|
||||
|
|
|
@ -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,25 +231,18 @@ 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| {
|
||||
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();
|
||||
});
|
||||
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();
|
||||
stdout.write_all(b"\n").unwrap();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
17
src/main.rs
17
src/main.rs
|
@ -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
7
src/utils.rs
Normal 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(())
|
||||
}
|
Loading…
Reference in a new issue