diff --git a/.gitignore b/.gitignore index ea8c4bf..a9d37c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/target +target +Cargo.lock diff --git a/crates/rmenu-plugin/Cargo.toml b/crates/rmenu-plugin/Cargo.toml new file mode 100644 index 0000000..d1982cd --- /dev/null +++ b/crates/rmenu-plugin/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rmenu-plugin" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +cache = ["dep:bincode", "dep:serde"] +rmenu_internals = ["dep:libloading"] + +[dependencies] +abi_stable = "0.11.1" +bincode = { version = "1.3.3", optional = true } +lastlog = { version = "0.2.3", features = ["libc"] } +libloading = { version = "0.7.4", optional = true } +serde = { version = "1.0.152", features = ["derive"], optional = true } diff --git a/crates/rmenu-plugin/src/cache.rs b/crates/rmenu-plugin/src/cache.rs new file mode 100644 index 0000000..67a175d --- /dev/null +++ b/crates/rmenu-plugin/src/cache.rs @@ -0,0 +1,196 @@ +/* + * Plugin Optional Cache Implementation for Convenient Storage + */ +use std::collections::HashMap; +use std::io::{Error, ErrorKind, Result, Write}; +use std::path::Path; +use std::path::PathBuf; +use std::time::{Duration, SystemTime}; +use std::{env, fs}; + +use abi_stable::{std_types::RString, StableAbi}; +use lastlog::search_self; + +use super::{Entries, ModuleConfig}; + +/* Variables */ + +static HOME: &str = "HOME"; +static XDG_CACHE_HOME: &str = "XDG_CACHE_HOME"; + +/* Functions */ + +/// Retrieve xdg-cache directory or get default +pub fn get_cache_dir() -> std::result::Result { + let path = if let Ok(xdg) = env::var(XDG_CACHE_HOME) { + Path::new(&xdg).to_path_buf() + } else if let Ok(home) = env::var(HOME) { + Path::new(&home).join(".cache") + } else { + Path::new("/tmp/").to_path_buf() + }; + match path.join("rmenu").to_str() { + Some(s) => Ok(s.to_owned()), + None => Err(format!("Failed to read $XDG_CACHE_DIR or $HOME")), + } +} + +/// Retrieve standard cache-path variable from config or set to default +#[inline] +pub fn get_cache_dir_setting(cfg: &ModuleConfig) -> PathBuf { + Path::new( + match cfg.get("cache_path") { + Some(path) => RString::from(path.as_str()), + None => RString::from(get_cache_dir().unwrap()), + } + .as_str(), + ) + .to_path_buf() +} + +/// Retrieve standard cache-mode variable from config or set to default +#[inline] +pub fn get_cache_setting(cfg: &ModuleConfig, default: CacheSetting) -> CacheSetting { + cfg.get("cache_mode") + .unwrap_or(&RString::from("")) + .parse::() + .unwrap_or(default) +} + +/* Module */ + +/// Configured Module Cache settings +#[repr(C)] +#[derive(Debug, Clone, PartialEq, StableAbi)] +pub enum CacheSetting { + Never, + Forever, + OnLogin, + After(u64), +} + +impl std::str::FromStr for CacheSetting { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + if s.chars().all(char::is_numeric) { + let i = s.parse::().map_err(|e| format!("{e}"))?; + return Ok(CacheSetting::After(i)); + }; + match s { + "never" => Ok(CacheSetting::Never), + "forever" => Ok(CacheSetting::Forever), + "login" => Ok(CacheSetting::OnLogin), + _ => Err(format!("invalid value: {:?}", s)), + } + } +} + +/// Simple Entry cache implementation +pub struct Cache { + path: PathBuf, + cache: HashMap, +} + +impl Cache { + pub fn new(path: PathBuf) -> Self { + Self { + path, + cache: HashMap::new(), + } + } + + // attempt to read given cache entry from disk + fn load(&self, name: &str, valid: &CacheSetting) -> Result { + // skip all checks if caching is disabled + let expr_err = Error::new(ErrorKind::InvalidData, "cache expired"); + if valid == &CacheSetting::Never { + return Err(expr_err); + } + // check if the given path exists + let path = self.path.join(name); + if !path.is_file() { + return Err(Error::new(ErrorKind::NotFound, "no such file")); + } + // get last-modified date of file + let meta = path.metadata()?; + let modified = meta.modified()?; + // handle expiration based on cache-setting + match valid { + CacheSetting::Never => return Err(expr_err), + CacheSetting::Forever => {} + CacheSetting::OnLogin => { + // expire content if it was last modified before last login + if let Ok(record) = search_self() { + if let Some(lastlog) = record.last_login.into() { + if modified <= lastlog { + return Err(expr_err); + } + } + } + } + CacheSetting::After(secs) => { + // expire content if it was last modified longer than duration + let now = SystemTime::now(); + let duration = Duration::from_secs(*secs); + if now - duration >= modified { + return Err(expr_err); + } + } + }; + // load entries from bincode + let data = fs::read(path)?; + let entries: Entries = bincode::deserialize(&data).map_err(|_| ErrorKind::InvalidData)?; + Ok(entries) + } + + /// Returns true if the given key was found in cache memory + pub fn contains_key(&self, name: &str) -> bool { + self.cache.contains_key(name) + } + + /// Read all cached entries associated w/ the specified key + pub fn read(&mut self, name: &str, valid: &CacheSetting) -> Result<&Entries> { + // return cache entry if already cached + if self.cache.contains_key(name) { + return Ok(self.cache.get(name).expect("read failed cache grab")); + } + // load entries from cache on disk + let entries = self.load(name, valid)?; + self.cache.insert(name.to_owned(), entries); + Ok(self.cache.get(name).expect("cached entries are missing?")) + } + + /// Write all entries associated w/ the specified key to cache + pub fn write(&mut self, name: &str, valid: &CacheSetting, entries: Entries) -> Result<()> { + // write to runtime cache reguardless of cache settings + self.cache.insert(name.to_owned(), entries); + // skip caching if disabled + if valid == &CacheSetting::Never { + return Ok(()); + } + // retrieve entries passed to cache + let entries = self.cache.get(name).expect("write failed cache grab"); + // serialize entries and write to cache + let path = self.path.join(name); + let mut f = fs::File::create(path)?; + let data = bincode::serialize(entries).map_err(|_| ErrorKind::InvalidInput)?; + f.write_all(&data)?; + Ok(()) + } + + /// easy functional wrapper that ensures data is always loaded once and then cached + pub fn wrap(&mut self, name: &str, valid: &CacheSetting, func: F) -> Result + where + F: FnOnce() -> Entries, + { + let entries = match self.read(name, valid) { + Ok(entries) => entries, + Err(_) => { + self.write(name, valid, func())?; + self.read(name, valid)? + } + }; + Ok(entries.clone()) + } +} diff --git a/crates/rmenu-plugin/src/internal.rs b/crates/rmenu-plugin/src/internal.rs new file mode 100644 index 0000000..b299556 --- /dev/null +++ b/crates/rmenu-plugin/src/internal.rs @@ -0,0 +1,33 @@ +/* + * Internal Library Loading Implementation + */ +use abi_stable::std_types::{RBox, RHashMap, RString}; +use libloading::{Error, Library, Symbol}; + +use super::{Module, ModuleConfig}; + +/* Types */ + +pub struct Plugin { + pub lib: Library, + pub module: Box, +} + +type LoadFunc = unsafe extern "C" fn(&ModuleConfig) -> Box; + +/* Functions */ + +pub unsafe fn load_plugin(path: &str, cfg: &ModuleConfig) -> Result { + // Load and initialize library + #[cfg(target_os = "linux")] + let lib: Library = { + // Load library with `RTLD_NOW | RTLD_NODELETE` to fix a SIGSEGV + // https://github.com/nagisa/rust_libloading/issues/41#issuecomment-448303856 + libloading::os::unix::Library::open(Some(path), 0x2 | 0x1000)?.into() + }; + #[cfg(not(target_os = "linux"))] + let lib = Library::new(path.as_ref())?; + // load module object and generate plugin + let module = lib.get::>(b"load_module")?(cfg); + Ok(Plugin { lib, module }) +} diff --git a/crates/rmenu-plugin/src/lib.rs b/crates/rmenu-plugin/src/lib.rs new file mode 100644 index 0000000..fe42572 --- /dev/null +++ b/crates/rmenu-plugin/src/lib.rs @@ -0,0 +1,66 @@ +#[cfg(feature = "rmenu_internals")] +pub mod internal; + +#[cfg(feature = "cache")] +pub mod cache; + +use abi_stable::std_types::*; +use abi_stable::{sabi_trait, StableAbi}; + +#[cfg(feature = "cache")] +use serde::{Deserialize, Serialize}; + +/// Configured Entry Execution settings +#[repr(C)] +#[derive(Debug, Clone, StableAbi)] +#[cfg_attr(feature = "cache", derive(Serialize, Deserialize))] +pub enum Exec { + Command(RString), + Terminal(RString), +} + +/// Module search-entry Icon configuration +#[repr(C)] +#[derive(Debug, Clone, StableAbi)] +#[cfg_attr(feature = "cache", derive(Serialize, Deserialize))] +pub struct Icon { + pub name: RString, + pub data: RVec, +} + +/// Module single search-entry +#[repr(C)] +#[derive(Debug, Clone, StableAbi)] +#[cfg_attr(feature = "cache", derive(Serialize, Deserialize))] +pub struct Entry { + pub name: RString, + pub exec: Exec, + pub comment: ROption, + pub icon: ROption, +} + +/// Alias for FFI-safe vector of `Entry` object +pub type Entries = RVec; + +/// Alias for FFI-Safe hashmap for config entries +pub type ModuleConfig = RHashMap; + +/// Module trait abstraction for all search plugins +#[sabi_trait] +pub trait Module { + extern "C" fn name(&self) -> RString; + extern "C" fn prefix(&self) -> RString; + extern "C" fn search(&mut self, search: RString) -> Entries; +} + +/// Generates the required rmenu plugin resources +#[macro_export] +macro_rules! export_plugin { + ($module:ident) => { + #[no_mangle] + extern "C" fn load_module(config: &ModuleConfig) -> Box { + let inst = $module::new(config); + Box::new(inst) + } + }; +} diff --git a/crates/rmenu/Cargo.toml b/crates/rmenu/Cargo.toml new file mode 100644 index 0000000..a6131d7 --- /dev/null +++ b/crates/rmenu/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rmenu" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +abi_stable = "0.11.1" +rmenu-plugin = { version = "0.1.0", path = "../rmenu-plugin", features = ["rmenu_internals"] } diff --git a/crates/rmenu/src/main.rs b/crates/rmenu/src/main.rs new file mode 100644 index 0000000..f367f8a --- /dev/null +++ b/crates/rmenu/src/main.rs @@ -0,0 +1,20 @@ +use abi_stable::std_types::{RHashMap, RString}; +use rmenu_plugin::internal::load_plugin; + +static PLUGIN: &str = "../../plugins/run/target/release/librun.so"; + +fn test() { + let mut cfg = RHashMap::new(); + // cfg.insert(RString::from("ignore_case"), RString::from("true")); + + let mut plugin = unsafe { load_plugin(PLUGIN, &cfg).unwrap() }; + let results = plugin.module.search(RString::from("br")); + for result in results.into_iter() { + println!("{} - {:?}", result.name, result.comment); + } + println!("ayy lmao done!"); +} + +fn main() { + test(); +} diff --git a/plugins/drun/Cargo.toml b/plugins/drun/Cargo.toml new file mode 100644 index 0000000..1303f60 --- /dev/null +++ b/plugins/drun/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "drun" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +abi_stable = "0.11.1" +freedesktop_entry_parser = "1.3.0" +regex = "1.7.0" +rmenu-plugin = { version = "0.1.0", path = "../../crates/rmenu-plugin", features = ["cache"] } +walkdir = "2.3.2" diff --git a/plugins/drun/src/desktop.rs b/plugins/drun/src/desktop.rs new file mode 100644 index 0000000..a69e265 --- /dev/null +++ b/plugins/drun/src/desktop.rs @@ -0,0 +1,135 @@ +use std::collections::HashMap; +use std::fs; + +use abi_stable::std_types::{RNone, ROption, RSome, RString, RVec}; +use freedesktop_entry_parser::parse_entry; +use walkdir::{DirEntry, WalkDir}; + +use rmenu_plugin::*; + +/* Types */ + +#[derive(Debug)] +struct IconFile { + name: String, + path: String, + size: u64, +} + +/* Functions */ + +// filter out invalid icon entries +fn is_icon(entry: &DirEntry) -> bool { + entry.file_type().is_dir() + || entry + .file_name() + .to_str() + .map(|s| s.ends_with(".svg") || s.ends_with(".png")) + .unwrap_or(false) +} + +// filter out invalid desktop entries +fn is_desktop(entry: &DirEntry) -> bool { + entry.file_type().is_dir() + || entry + .file_name() + .to_str() + .map(|s| s.ends_with(".desktop")) + .unwrap_or(false) +} + +// correlate name w/ best matched icon +#[inline] +fn match_icon(name: &str, icons: &Vec) -> Option { + for icon in icons.iter() { + if icon.name == name { + return Some(icon.path.to_owned()); + } + let Some((fname, _)) = icon.name.rsplit_once('.') else { continue }; + if fname == name { + return Some(icon.path.to_owned()); + } + } + None +} + +// correlate name w/ best matched icon and read into valid entry +#[inline] +fn read_icon(name: &str, icons: &Vec) -> Option { + let path = match_icon(name, icons)?; + let Ok(data) = fs::read(&path) else { return None }; + Some(Icon { + name: RString::from(name), + data: RVec::from(data), + }) +} + +// retrieve master-list of all possible xdg-application entries from filesystem +pub fn load_entries(app_paths: &Vec, icon_paths: &Vec) -> Vec { + // iterate and collect all existing icon paths + let mut imap: HashMap = HashMap::new(); + for path in icon_paths.into_iter() { + let walker = WalkDir::new(path).into_iter(); + for entry in walker.filter_entry(is_icon) { + let Ok(dir) = entry else { continue }; + let Ok(meta) = dir.metadata() else { continue }; + let Some(name) = dir.file_name().to_str() else { continue }; + let Some(path) = dir.path().to_str() else { continue; }; + let size = meta.len(); + // find the biggest icon file w/ the same name + let pathstr = path.to_owned(); + if let Some(icon) = imap.get_mut(name) { + if icon.size < size { + icon.path = pathstr; + icon.size = size; + } + continue; + } + imap.insert( + name.to_owned(), + IconFile { + name: name.to_owned(), + path: pathstr, + size, + }, + ); + } + } + // parse application entries + let icons = imap.into_values().collect(); + let mut entries = vec![]; + for path in app_paths.into_iter() { + let walker = WalkDir::new(path).into_iter(); + for entry in walker.filter_entry(is_desktop) { + let Ok(dir) = entry else { continue }; + let Ok(file) = parse_entry(dir.path()) else { continue }; + let desktop = file.section("Desktop Entry"); + let Some(name) = desktop.attr("Name") else { continue }; + let Some(exec) = desktop.attr("Exec") else { continue }; + let terminal = desktop.attr("Terminal").unwrap_or("") == "true"; + // parse icon + let icon = match desktop.attr("Icon") { + Some(name) => ROption::from(read_icon(name, &icons)), + None => RNone, + }; + // parse comment + let comment = match desktop.attr("Comment") { + Some(attr) => RSome(RString::from(attr)), + None => RNone, + }; + // convert exec/terminal into command + let command = match terminal { + true => Exec::Terminal(RString::from(exec)), + false => Exec::Command(RString::from(exec)), + }; + // generate entry + entries.push(Entry { + name: RString::from(name), + exec: command, + comment, + icon, + }); + } + } + entries +} diff --git a/plugins/drun/src/lib.rs b/plugins/drun/src/lib.rs new file mode 100644 index 0000000..dde9c48 --- /dev/null +++ b/plugins/drun/src/lib.rs @@ -0,0 +1,156 @@ +use std::env; +use std::io::Result; +use std::path::{Path, PathBuf}; + +use abi_stable::std_types::*; +use regex::{Regex, RegexBuilder}; +use rmenu_plugin::{cache::*, *}; + +mod desktop; +use desktop::load_entries; + +/* Variables */ + +static NAME: &str = "drun"; +static PREFIX: &str = "app"; + +static XDG_DATA_DIRS: &str = "XDG_DATA_DIRS"; + +static DEFAULT_XDG_PATHS: &str = "/usr/share/"; +static DEFAULT_APP_PATHS: &str = ""; +static DEFAULT_ICON_PATHS: &str = "/usr/share/pixmaps/"; + +/* Functions */ + +// parse path string into separate path entries +#[inline] +fn parse_config_paths(paths: String) -> Vec { + env::split_paths(&paths) + .map(|s| s.to_str().expect("invalid path").to_owned()) + .collect() +} + +// retrieve default xdg-paths using xdg environment variable when possible +#[inline] +fn default_xdg_paths() -> String { + if let Ok(paths) = env::var(XDG_DATA_DIRS) { + return paths.to_owned(); + } + DEFAULT_XDG_PATHS.to_owned() +} + +// append joined xdg-paths to app/icon path results +#[inline] +fn apply_paths(join: &str, paths: &Vec) -> Vec { + paths + .iter() + .map(|s| { + Path::new(s) + .join(join) + .to_str() + .expect("Unable to join PATH") + .to_owned() + }) + .collect() +} + +// regex validate if the following entry matches the given regex expression +#[inline] +pub fn is_match(entry: &Entry, search: &Regex) -> bool { + if search.is_match(&entry.name) { + return true; + }; + if let RSome(comment) = entry.comment.as_ref() { + return search.is_match(&comment); + } + false +} + +/* Macros */ + +macro_rules! pathify { + ($cfg:expr, $key:expr, $other:expr) => { + parse_config_paths(match $cfg.get($key) { + Some(path) => path.as_str().to_owned(), + None => $other, + }) + }; +} + +/* Plugin */ + +struct Settings { + xdg_paths: Vec, + app_paths: Vec, + icon_paths: Vec, + cache_path: PathBuf, + cache_mode: CacheSetting, + ignore_case: bool, +} + +struct DesktopRun { + cache: Cache, + settings: Settings, +} + +impl DesktopRun { + pub fn new(cfg: &ModuleConfig) -> Self { + let settings = Settings { + xdg_paths: pathify!(cfg, "xdg_paths", default_xdg_paths()), + app_paths: pathify!(cfg, "app_paths", DEFAULT_APP_PATHS.to_owned()), + icon_paths: pathify!(cfg, "icon_paths", DEFAULT_ICON_PATHS.to_owned()), + ignore_case: cfg + .get("ignore_case") + .unwrap_or(&RString::from("true")) + .parse() + .unwrap_or(true), + cache_path: get_cache_dir_setting(cfg), + cache_mode: get_cache_setting(cfg, CacheSetting::OnLogin), + }; + Self { + cache: Cache::new(settings.cache_path.clone()), + settings, + } + } + + fn load(&mut self) -> Result { + self.cache.wrap(NAME, &self.settings.cache_mode, || { + // configure paths w/ xdg-paths + let mut app_paths = apply_paths("applications", &self.settings.xdg_paths); + let mut icon_paths = apply_paths("icons", &self.settings.xdg_paths); + app_paths.append(&mut self.settings.app_paths.clone()); + icon_paths.append(&mut self.settings.icon_paths.clone()); + // generate search results + let mut entries = load_entries(&app_paths, &icon_paths); + entries.sort_by_cached_key(|s| s.name.to_owned()); + RVec::from(entries) + }) + } +} + +impl Module for DesktopRun { + extern "C" fn name(&self) -> RString { + RString::from(NAME) + } + extern "C" fn prefix(&self) -> RString { + RString::from(PREFIX) + } + extern "C" fn search(&mut self, search: RString) -> Entries { + // compile regex expression for the given search + let mut matches = RVec::new(); + let Ok(rgx) = RegexBuilder::new(search.as_str()) + .case_insensitive(self.settings.ignore_case) + .build() else { return matches }; + // retrieve entries based on declared modes + let Ok(entries) = self.load() else { return matches }; + // search existing entries for matching regex expr + for entry in entries.into_iter() { + if is_match(&entry, &rgx) { + matches.push(entry); + } + } + matches + } +} + +export_plugin!(DesktopRun); diff --git a/plugins/run/Cargo.toml b/plugins/run/Cargo.toml new file mode 100644 index 0000000..b6beef0 --- /dev/null +++ b/plugins/run/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "run" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +abi_stable = "0.11.1" +regex = "1.7.0" +rmenu-plugin = { version = "0.1.0", path = "../../crates/rmenu-plugin", features = ["cache"] } +walkdir = "2.3.2" diff --git a/plugins/run/src/lib.rs b/plugins/run/src/lib.rs new file mode 100644 index 0000000..48dcd2f --- /dev/null +++ b/plugins/run/src/lib.rs @@ -0,0 +1,105 @@ +/*! + * Binary/Executable App Search Module + */ + +use std::env; +use std::io::Result; +use std::path::PathBuf; + +use abi_stable::std_types::{RString, RVec}; +use regex::RegexBuilder; +use rmenu_plugin::{cache::*, *}; + +mod run; +use run::find_executables; + +/* Variables */ + +static NAME: &str = "run"; +static PREFIX: &str = "exec"; + +static PATH_VAR: &str = "PATH"; + +/* Functions */ + +// parse path string into separate path entries +#[inline] +fn parse_config_paths(paths: &str) -> Vec { + env::split_paths(paths) + .map(|s| s.to_str().expect("invalid path").to_owned()) + .collect() +} + +// get all paths listed in $PATH env variable +#[inline] +fn get_paths() -> Vec { + parse_config_paths(&env::var(PATH_VAR).expect("Unable to read $PATH")) +} + +/* Module */ + +struct Settings { + paths: Vec, + cache_path: PathBuf, + cache_mode: CacheSetting, + ignore_case: bool, +} + +struct Run { + cache: Cache, + settings: Settings, +} + +impl Run { + pub fn new(cfg: &ModuleConfig) -> Self { + let settings = Settings { + ignore_case: cfg + .get("ignore_case") + .unwrap_or(&RString::from("true")) + .parse() + .unwrap_or(true), + paths: match cfg.get("paths") { + Some(paths) => parse_config_paths(paths.as_str()), + None => get_paths(), + }, + cache_path: get_cache_dir_setting(cfg), + cache_mode: get_cache_setting(cfg, CacheSetting::After(30)), + }; + Run { + cache: Cache::new(settings.cache_path.clone()), + settings, + } + } + + fn load(&mut self) -> Result { + self.cache.wrap(NAME, &self.settings.cache_mode, || { + RVec::from(find_executables(&self.settings.paths)) + }) + } +} + +impl Module for Run { + extern "C" fn name(&self) -> RString { + RString::from(NAME) + } + extern "C" fn prefix(&self) -> RString { + RString::from(PREFIX) + } + extern "C" fn search(&mut self, search: RString) -> Entries { + // compile regex expression for the given search + let mut matches = RVec::new(); + let Ok(rgx) = RegexBuilder::new(search.as_str()) + .case_insensitive(self.settings.ignore_case) + .build() else { return matches }; + // load entries and evaluate matches + let entries = self.load().expect("failed to parse through $PATH"); + for entry in entries.into_iter() { + if rgx.is_match(entry.name.as_str()) { + matches.push(entry); + } + } + matches + } +} + +export_plugin!(Run); diff --git a/plugins/run/src/run.rs b/plugins/run/src/run.rs new file mode 100644 index 0000000..790a658 --- /dev/null +++ b/plugins/run/src/run.rs @@ -0,0 +1,79 @@ +use std::collections::HashMap; +use std::os::unix::fs::PermissionsExt; + +use abi_stable::std_types::{RNone, RString}; +use rmenu_plugin::{Entry, Exec}; +use walkdir::{DirEntry, WalkDir}; + +/* Functions */ + +// check if file is executable +fn is_exec(entry: &DirEntry) -> bool { + if entry.file_type().is_dir() { + return true; + } + let Ok(meta) = entry.metadata() else { return false }; + meta.permissions().mode() & 0o111 != 0 +} + +// find all executables within the given paths +#[inline] +pub fn find_executables(paths: &Vec) -> Vec { + let mut execs: HashMap = HashMap::new(); + for path in paths.iter() { + let walker = WalkDir::new(path).into_iter(); + for entry in walker.filter_entry(is_exec) { + let Ok(dir) = entry else { continue }; + let Some(name) = dir.file_name().to_str() else { continue }; + let Some(path) = dir.path().to_str() else { continue; }; + // check if entry already exists but replace on longer path + if let Some(entry) = execs.get(name) { + if let Exec::Terminal(ref exec) = entry.exec { + if exec.len() >= path.len() { + continue; + } + } + } + execs.insert( + name.to_owned(), + Entry { + name: RString::from(name), + exec: Exec::Terminal(RString::from(path)), + comment: RNone, + icon: RNone, + }, + ); + } + } + execs.into_values().collect() +} + +/* Module */ + +// pub struct RunModule {} +// +// impl Module for RunModule { +// fn name(&self) -> &str { +// "run" +// } +// +// fn mode(&self) -> Mode { +// Mode::Run +// } +// +// fn cache_setting(&self, settings: &Settings) -> Cache { +// match settings.run.cache.as_ref() { +// Some(cache) => cache.clone(), +// None => Cache::After(Duration::new(30, 0)), +// } +// } +// +// fn load(&self, settings: &Settings) -> Vec { +// let cfg = &settings.run; +// let paths = match cfg.paths.as_ref() { +// Some(paths) => paths.clone(), +// None => get_paths(), +// }; +// find_executables(&paths) +// } +// }