feat: impl terminal/normal run launch and better icon lookups

This commit is contained in:
imgurbot12 2023-08-03 17:22:04 -07:00
parent 8a219e6044
commit 2894bb257d
7 changed files with 94 additions and 24 deletions

View file

@ -7,6 +7,7 @@ edition = "2021"
[dependencies]
freedesktop-desktop-entry = "0.5.0"
regex = "1.9.1"
rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" }
rust-ini = { version = "0.19.0", features = ["unicase"] }
serde_json = "1.0.103"

View file

@ -1,11 +1,11 @@
use std::collections::{HashMap, HashSet};
use std::fs::FileType;
use std::path::PathBuf;
use std::{fs::read_to_string, path::Path};
use freedesktop_desktop_entry::DesktopEntry;
use ini::Ini;
use rmenu_plugin::{Action, Entry};
use regex::Regex;
use rmenu_plugin::{Action, Entry, Method};
use walkdir::WalkDir;
static XDG_DATA_ENV: &'static str = "XDG_DATA_DIRS";
@ -167,17 +167,23 @@ fn data_dirs(dir: &str) -> Vec<PathBuf> {
.collect()
}
#[inline(always)]
fn fix_exec(exec: &str, ematch: &Regex) -> String {
ematch.replace_all(exec, "").trim().to_string()
}
/// Parse XDG Desktop Entry into RMenu Entry
fn parse_desktop(path: &Path, locale: Option<&str>) -> Option<Entry> {
fn parse_desktop(path: &Path, locale: Option<&str>, ematch: &Regex) -> Option<Entry> {
let bytes = read_to_string(path).ok()?;
let entry = DesktopEntry::decode(&path, &bytes).ok()?;
let name = entry.name(locale)?.to_string();
let icon = entry.icon().map(|s| s.to_string());
let comment = entry.comment(locale).map(|s| s.to_string());
let terminal = entry.terminal();
let mut actions = match entry.exec() {
Some(exec) => vec![Action {
name: "main".to_string(),
exec: exec.to_string(),
exec: Method::new(fix_exec(exec, ematch), terminal),
comment: None,
}],
None => vec![],
@ -194,7 +200,7 @@ fn parse_desktop(path: &Path, locale: Option<&str>) -> Option<Entry> {
let exec = entry.action_exec(a)?;
Some(Action {
name: name.to_string(),
exec: exec.to_string(),
exec: Method::new(fix_exec(exec, ematch), terminal),
comment: None,
})
}),
@ -208,14 +214,14 @@ fn parse_desktop(path: &Path, locale: Option<&str>) -> Option<Entry> {
}
/// Iterate Path and Parse All `.desktop` files into Entries
fn find_desktops(path: PathBuf, locale: Option<&str>) -> Vec<Entry> {
fn find_desktops(path: PathBuf, locale: Option<&str>, ematch: &Regex) -> Vec<Entry> {
WalkDir::new(path)
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().ends_with(".desktop"))
.filter(|e| e.file_type().is_file())
.filter_map(|e| parse_desktop(e.path(), locale))
.filter_map(|e| parse_desktop(e.path(), locale, ematch))
.collect()
}
@ -236,6 +242,8 @@ fn assign_icons(icons: &Vec<IconGroup>, mut e: Entry) -> Entry {
fn main() {
let locale = Some("en");
let sizes = (32, 64, 128);
// build regex desktop formatter args
let ematch = regex::Regex::new(r"%\w").expect("Failed Regex Compile");
// build a collection of icons for configured themes
let cfgdir = config_dir();
let themes = find_theme(&cfgdir);
@ -252,11 +260,16 @@ fn main() {
.collect();
// add extra icons found in base folders
icons.extend(icon_paths.iter().map(|p| find_icon_extras(p)));
// retrieve desktop applications and assign icons before printing results
data_dirs("applications")
// retrieve desktop applications and sort alphabetically
let mut applications: Vec<Entry> = data_dirs("applications")
.into_iter()
.map(|p| find_desktops(p, locale))
.map(|p| find_desktops(p, locale, &ematch))
.flatten()
.collect();
applications.sort_by_cached_key(|e| e.name.to_owned());
// assign icons and print results
applications
.into_iter()
.map(|e| assign_icons(&icons, e))
.filter_map(|e| serde_json::to_string(&e).ok())
.map(|s| println!("{}", s))

View file

@ -1,15 +1,25 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Method {
Terminal,
Desktop,
Terminal(String),
Run(String),
}
impl Method {
pub fn new(exec: String, terminal: bool) -> Self {
match terminal {
true => Self::Terminal(exec),
false => Self::Run(exec),
}
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Action {
pub name: String,
pub exec: String,
pub exec: Method,
pub comment: Option<String>,
}
@ -17,7 +27,7 @@ impl Action {
pub fn new(exec: &str) -> Self {
Self {
name: "main".to_string(),
exec: exec.to_string(),
exec: Method::Run(exec.to_string()),
comment: None,
}
}

View file

@ -25,4 +25,6 @@ serde_json = "1.0.103"
serde_yaml = "0.9.24"
shell-words = "1.1.0"
shellexpand = "3.1.0"
strfmt = "0.2.4"
thiserror = "1.0.43"
which = "4.4.0"

View file

@ -142,6 +142,7 @@ pub struct Config {
pub plugins: BTreeMap<String, Vec<String>>,
pub keybinds: KeyConfig,
pub window: WindowConfig,
pub terminal: Option<String>,
}
impl Default for Config {
@ -155,6 +156,7 @@ impl Default for Config {
plugins: Default::default(),
keybinds: Default::default(),
window: Default::default(),
terminal: Default::default(),
}
}
}

View file

@ -1,14 +1,56 @@
//! Execution Implementation for Entry Actions
use std::os::unix::process::CommandExt;
use std::process::Command;
use std::{collections::HashMap, os::unix::process::CommandExt};
use rmenu_plugin::Action;
use rmenu_plugin::{Action, Method};
use shell_words::split;
use strfmt::strfmt;
use which::which;
pub fn execute(action: &Action) {
log::info!("executing: {} {:?}", action.name, action.exec);
let args = match shell_words::split(&action.exec) {
/// Find Best Terminal To Execute
fn find_terminal() -> String {
vec![
("alacritty", "-e {cmd}"),
("kitty", "{cmd}"),
("gnome-terminal", "-x {cmd}"),
("foot", "-e {cmd}"),
("xterm", "-C {cmd}"),
]
.into_iter()
.map(|(t, v)| (which(t), v))
.filter(|(c, _)| c.is_ok())
.map(|(c, v)| (c.unwrap(), v))
.map(|(p, v)| {
(
p.to_str()
.expect("Failed to Parse Terminal Path")
.to_owned(),
v,
)
})
.find_map(|(p, v)| Some(format!("{p} {v}")))
.expect("Failed to Find Terminal Executable!")
}
#[inline]
fn parse_args(exec: &str) -> Vec<String> {
match split(exec) {
Ok(args) => args,
Err(err) => panic!("{:?} invalid command {err}", action.exec),
Err(err) => panic!("{:?} invalid command {err}", exec),
}
}
pub fn execute(action: &Action, term: Option<String>) {
log::info!("executing: {} {:?}", action.name, action.exec);
let args = match &action.exec {
Method::Run(exec) => parse_args(&exec),
Method::Terminal(exec) => {
let mut args = HashMap::new();
let terminal = term.unwrap_or_else(find_terminal);
args.insert("cmd".to_string(), exec.to_owned());
let command = strfmt(&terminal, &args).expect("Failed String Format");
parse_args(&command)
}
};
let err = Command::new(&args[0]).args(&args[1..]).exec();
panic!("Command Error: {err:?}");

View file

@ -119,16 +119,16 @@ impl<'a> AppState<'a> {
/// Execute the Current Action
pub fn execute(&self) {
let (pos, subpos) = self.position();
println!("double click {pos} {subpos}");
log::debug!("execute {pos} {subpos}");
let Some(result) = self.results.get(pos) else {
return;
};
println!("result: {result:?}");
log::debug!("result: {result:?}");
let Some(action) = result.actions.get(subpos) else {
return;
};
println!("action: {action:?}");
execute(action);
log::debug!("action: {action:?}");
execute(action, self.app.config.terminal.clone());
}
/// Set Current Key/Action for Later Evaluation