From 2894bb257dc231e3b223d8fe36c1a8ce8d404415 Mon Sep 17 00:00:00 2001 From: imgurbot12 Date: Thu, 3 Aug 2023 17:22:04 -0700 Subject: [PATCH] feat: impl terminal/normal run launch and better icon lookups --- plugin-desktop/Cargo.toml | 1 + plugin-desktop/src/main.rs | 33 ++++++++++++++++------- rmenu-plugin/src/lib.rs | 18 ++++++++++--- rmenu/Cargo.toml | 2 ++ rmenu/src/config.rs | 2 ++ rmenu/src/exec.rs | 54 +++++++++++++++++++++++++++++++++----- rmenu/src/state.rs | 8 +++--- 7 files changed, 94 insertions(+), 24 deletions(-) diff --git a/plugin-desktop/Cargo.toml b/plugin-desktop/Cargo.toml index baa4064..956c433 100644 --- a/plugin-desktop/Cargo.toml +++ b/plugin-desktop/Cargo.toml @@ -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" diff --git a/plugin-desktop/src/main.rs b/plugin-desktop/src/main.rs index 13a35a2..0cbb306 100644 --- a/plugin-desktop/src/main.rs +++ b/plugin-desktop/src/main.rs @@ -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 { .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 { +fn parse_desktop(path: &Path, locale: Option<&str>, ematch: &Regex) -> Option { 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 { 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 { } /// Iterate Path and Parse All `.desktop` files into Entries -fn find_desktops(path: PathBuf, locale: Option<&str>) -> Vec { +fn find_desktops(path: PathBuf, locale: Option<&str>, ematch: &Regex) -> Vec { 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, 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 = 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)) diff --git a/rmenu-plugin/src/lib.rs b/rmenu-plugin/src/lib.rs index 47eab2f..e766c2c 100644 --- a/rmenu-plugin/src/lib.rs +++ b/rmenu-plugin/src/lib.rs @@ -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, } @@ -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, } } diff --git a/rmenu/Cargo.toml b/rmenu/Cargo.toml index 7c0b8cd..9d12acc 100644 --- a/rmenu/Cargo.toml +++ b/rmenu/Cargo.toml @@ -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" diff --git a/rmenu/src/config.rs b/rmenu/src/config.rs index aa1fef5..a0567be 100644 --- a/rmenu/src/config.rs +++ b/rmenu/src/config.rs @@ -142,6 +142,7 @@ pub struct Config { pub plugins: BTreeMap>, pub keybinds: KeyConfig, pub window: WindowConfig, + pub terminal: Option, } impl Default for Config { @@ -155,6 +156,7 @@ impl Default for Config { plugins: Default::default(), keybinds: Default::default(), window: Default::default(), + terminal: Default::default(), } } } diff --git a/rmenu/src/exec.rs b/rmenu/src/exec.rs index 703936f..f5858a4 100644 --- a/rmenu/src/exec.rs +++ b/rmenu/src/exec.rs @@ -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 { + 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) { + 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:?}"); diff --git a/rmenu/src/state.rs b/rmenu/src/state.rs index 77c0afa..c0458aa 100644 --- a/rmenu/src/state.rs +++ b/rmenu/src/state.rs @@ -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