mirror of
https://github.com/imgurbot12/rmenu.git
synced 2025-02-12 13:15:07 +01:00
feat: impl terminal/normal run launch and better icon lookups
This commit is contained in:
parent
8a219e6044
commit
2894bb257d
7 changed files with 94 additions and 24 deletions
|
@ -7,6 +7,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
freedesktop-desktop-entry = "0.5.0"
|
freedesktop-desktop-entry = "0.5.0"
|
||||||
|
regex = "1.9.1"
|
||||||
rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" }
|
rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" }
|
||||||
rust-ini = { version = "0.19.0", features = ["unicase"] }
|
rust-ini = { version = "0.19.0", features = ["unicase"] }
|
||||||
serde_json = "1.0.103"
|
serde_json = "1.0.103"
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fs::FileType;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::{fs::read_to_string, path::Path};
|
use std::{fs::read_to_string, path::Path};
|
||||||
|
|
||||||
use freedesktop_desktop_entry::DesktopEntry;
|
use freedesktop_desktop_entry::DesktopEntry;
|
||||||
use ini::Ini;
|
use ini::Ini;
|
||||||
use rmenu_plugin::{Action, Entry};
|
use regex::Regex;
|
||||||
|
use rmenu_plugin::{Action, Entry, Method};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
static XDG_DATA_ENV: &'static str = "XDG_DATA_DIRS";
|
static XDG_DATA_ENV: &'static str = "XDG_DATA_DIRS";
|
||||||
|
@ -167,17 +167,23 @@ fn data_dirs(dir: &str) -> Vec<PathBuf> {
|
||||||
.collect()
|
.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
|
/// 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 bytes = read_to_string(path).ok()?;
|
||||||
let entry = DesktopEntry::decode(&path, &bytes).ok()?;
|
let entry = DesktopEntry::decode(&path, &bytes).ok()?;
|
||||||
let name = entry.name(locale)?.to_string();
|
let name = entry.name(locale)?.to_string();
|
||||||
let icon = entry.icon().map(|s| s.to_string());
|
let icon = entry.icon().map(|s| s.to_string());
|
||||||
let comment = entry.comment(locale).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() {
|
let mut actions = match entry.exec() {
|
||||||
Some(exec) => vec![Action {
|
Some(exec) => vec![Action {
|
||||||
name: "main".to_string(),
|
name: "main".to_string(),
|
||||||
exec: exec.to_string(),
|
exec: Method::new(fix_exec(exec, ematch), terminal),
|
||||||
comment: None,
|
comment: None,
|
||||||
}],
|
}],
|
||||||
None => vec![],
|
None => vec![],
|
||||||
|
@ -194,7 +200,7 @@ fn parse_desktop(path: &Path, locale: Option<&str>) -> Option<Entry> {
|
||||||
let exec = entry.action_exec(a)?;
|
let exec = entry.action_exec(a)?;
|
||||||
Some(Action {
|
Some(Action {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
exec: exec.to_string(),
|
exec: Method::new(fix_exec(exec, ematch), terminal),
|
||||||
comment: None,
|
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
|
/// 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)
|
WalkDir::new(path)
|
||||||
.follow_links(true)
|
.follow_links(true)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
.filter(|e| e.file_name().to_string_lossy().ends_with(".desktop"))
|
.filter(|e| e.file_name().to_string_lossy().ends_with(".desktop"))
|
||||||
.filter(|e| e.file_type().is_file())
|
.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()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,6 +242,8 @@ fn assign_icons(icons: &Vec<IconGroup>, mut e: Entry) -> Entry {
|
||||||
fn main() {
|
fn main() {
|
||||||
let locale = Some("en");
|
let locale = Some("en");
|
||||||
let sizes = (32, 64, 128);
|
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
|
// build a collection of icons for configured themes
|
||||||
let cfgdir = config_dir();
|
let cfgdir = config_dir();
|
||||||
let themes = find_theme(&cfgdir);
|
let themes = find_theme(&cfgdir);
|
||||||
|
@ -252,11 +260,16 @@ fn main() {
|
||||||
.collect();
|
.collect();
|
||||||
// add extra icons found in base folders
|
// add extra icons found in base folders
|
||||||
icons.extend(icon_paths.iter().map(|p| find_icon_extras(p)));
|
icons.extend(icon_paths.iter().map(|p| find_icon_extras(p)));
|
||||||
// retrieve desktop applications and assign icons before printing results
|
// retrieve desktop applications and sort alphabetically
|
||||||
data_dirs("applications")
|
let mut applications: Vec<Entry> = data_dirs("applications")
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| find_desktops(p, locale))
|
.map(|p| find_desktops(p, locale, &ematch))
|
||||||
.flatten()
|
.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))
|
.map(|e| assign_icons(&icons, e))
|
||||||
.filter_map(|e| serde_json::to_string(&e).ok())
|
.filter_map(|e| serde_json::to_string(&e).ok())
|
||||||
.map(|s| println!("{}", s))
|
.map(|s| println!("{}", s))
|
||||||
|
|
|
@ -1,15 +1,25 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum Method {
|
pub enum Method {
|
||||||
Terminal,
|
Terminal(String),
|
||||||
Desktop,
|
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)]
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Action {
|
pub struct Action {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub exec: String,
|
pub exec: Method,
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +27,7 @@ impl Action {
|
||||||
pub fn new(exec: &str) -> Self {
|
pub fn new(exec: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: "main".to_string(),
|
name: "main".to_string(),
|
||||||
exec: exec.to_string(),
|
exec: Method::Run(exec.to_string()),
|
||||||
comment: None,
|
comment: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,4 +25,6 @@ serde_json = "1.0.103"
|
||||||
serde_yaml = "0.9.24"
|
serde_yaml = "0.9.24"
|
||||||
shell-words = "1.1.0"
|
shell-words = "1.1.0"
|
||||||
shellexpand = "3.1.0"
|
shellexpand = "3.1.0"
|
||||||
|
strfmt = "0.2.4"
|
||||||
thiserror = "1.0.43"
|
thiserror = "1.0.43"
|
||||||
|
which = "4.4.0"
|
||||||
|
|
|
@ -142,6 +142,7 @@ pub struct Config {
|
||||||
pub plugins: BTreeMap<String, Vec<String>>,
|
pub plugins: BTreeMap<String, Vec<String>>,
|
||||||
pub keybinds: KeyConfig,
|
pub keybinds: KeyConfig,
|
||||||
pub window: WindowConfig,
|
pub window: WindowConfig,
|
||||||
|
pub terminal: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
@ -155,6 +156,7 @@ impl Default for Config {
|
||||||
plugins: Default::default(),
|
plugins: Default::default(),
|
||||||
keybinds: Default::default(),
|
keybinds: Default::default(),
|
||||||
window: Default::default(),
|
window: Default::default(),
|
||||||
|
terminal: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,56 @@
|
||||||
//! Execution Implementation for Entry Actions
|
//! Execution Implementation for Entry Actions
|
||||||
use std::os::unix::process::CommandExt;
|
|
||||||
use std::process::Command;
|
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) {
|
/// Find Best Terminal To Execute
|
||||||
log::info!("executing: {} {:?}", action.name, action.exec);
|
fn find_terminal() -> String {
|
||||||
let args = match shell_words::split(&action.exec) {
|
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,
|
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();
|
let err = Command::new(&args[0]).args(&args[1..]).exec();
|
||||||
panic!("Command Error: {err:?}");
|
panic!("Command Error: {err:?}");
|
||||||
|
|
|
@ -119,16 +119,16 @@ impl<'a> AppState<'a> {
|
||||||
/// Execute the Current Action
|
/// Execute the Current Action
|
||||||
pub fn execute(&self) {
|
pub fn execute(&self) {
|
||||||
let (pos, subpos) = self.position();
|
let (pos, subpos) = self.position();
|
||||||
println!("double click {pos} {subpos}");
|
log::debug!("execute {pos} {subpos}");
|
||||||
let Some(result) = self.results.get(pos) else {
|
let Some(result) = self.results.get(pos) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
println!("result: {result:?}");
|
log::debug!("result: {result:?}");
|
||||||
let Some(action) = result.actions.get(subpos) else {
|
let Some(action) = result.actions.get(subpos) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
println!("action: {action:?}");
|
log::debug!("action: {action:?}");
|
||||||
execute(action);
|
execute(action, self.app.config.terminal.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set Current Key/Action for Later Evaluation
|
/// Set Current Key/Action for Later Evaluation
|
||||||
|
|
Loading…
Reference in a new issue