mirror of
https://github.com/imgurbot12/rmenu.git
synced 2025-01-26 12:58:08 +01:00
feat: plugin system shamelessly stolen from findex (and customized)
This commit is contained in:
parent
40a64fb533
commit
18e8cb978c
13 changed files with 850 additions and 1 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
/target
|
||||
target
|
||||
Cargo.lock
|
||||
|
|
17
crates/rmenu-plugin/Cargo.toml
Normal file
17
crates/rmenu-plugin/Cargo.toml
Normal file
|
@ -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 }
|
196
crates/rmenu-plugin/src/cache.rs
Normal file
196
crates/rmenu-plugin/src/cache.rs
Normal file
|
@ -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<String, String> {
|
||||
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::<CacheSetting>()
|
||||
.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<Self, Self::Err> {
|
||||
if s.chars().all(char::is_numeric) {
|
||||
let i = s.parse::<u64>().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<String, Entries>,
|
||||
}
|
||||
|
||||
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<Entries> {
|
||||
// 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<F>(&mut self, name: &str, valid: &CacheSetting, func: F) -> Result<Entries>
|
||||
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())
|
||||
}
|
||||
}
|
33
crates/rmenu-plugin/src/internal.rs
Normal file
33
crates/rmenu-plugin/src/internal.rs
Normal file
|
@ -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<dyn Module>,
|
||||
}
|
||||
|
||||
type LoadFunc = unsafe extern "C" fn(&ModuleConfig) -> Box<dyn Module>;
|
||||
|
||||
/* Functions */
|
||||
|
||||
pub unsafe fn load_plugin(path: &str, cfg: &ModuleConfig) -> Result<Plugin, Error> {
|
||||
// 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::<Symbol<LoadFunc>>(b"load_module")?(cfg);
|
||||
Ok(Plugin { lib, module })
|
||||
}
|
66
crates/rmenu-plugin/src/lib.rs
Normal file
66
crates/rmenu-plugin/src/lib.rs
Normal file
|
@ -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<u8>,
|
||||
}
|
||||
|
||||
/// 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<RString>,
|
||||
pub icon: ROption<Icon>,
|
||||
}
|
||||
|
||||
/// Alias for FFI-safe vector of `Entry` object
|
||||
pub type Entries = RVec<Entry>;
|
||||
|
||||
/// Alias for FFI-Safe hashmap for config entries
|
||||
pub type ModuleConfig = RHashMap<RString, RString>;
|
||||
|
||||
/// 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<dyn Module> {
|
||||
let inst = $module::new(config);
|
||||
Box::new(inst)
|
||||
}
|
||||
};
|
||||
}
|
10
crates/rmenu/Cargo.toml
Normal file
10
crates/rmenu/Cargo.toml
Normal file
|
@ -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"] }
|
20
crates/rmenu/src/main.rs
Normal file
20
crates/rmenu/src/main.rs
Normal file
|
@ -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();
|
||||
}
|
16
plugins/drun/Cargo.toml
Normal file
16
plugins/drun/Cargo.toml
Normal file
|
@ -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"
|
135
plugins/drun/src/desktop.rs
Normal file
135
plugins/drun/src/desktop.rs
Normal file
|
@ -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<IconFile>) -> Option<String> {
|
||||
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<IconFile>) -> Option<Icon> {
|
||||
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<String>, icon_paths: &Vec<String>) -> Vec<Entry> {
|
||||
// iterate and collect all existing icon paths
|
||||
let mut imap: HashMap<String, IconFile> = 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
|
||||
}
|
156
plugins/drun/src/lib.rs
Normal file
156
plugins/drun/src/lib.rs
Normal file
|
@ -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<String> {
|
||||
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<String>) -> Vec<String> {
|
||||
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<String>,
|
||||
app_paths: Vec<String>,
|
||||
icon_paths: Vec<String>,
|
||||
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<Entries> {
|
||||
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);
|
15
plugins/run/Cargo.toml
Normal file
15
plugins/run/Cargo.toml
Normal file
|
@ -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"
|
105
plugins/run/src/lib.rs
Normal file
105
plugins/run/src/lib.rs
Normal file
|
@ -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<String> {
|
||||
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<String> {
|
||||
parse_config_paths(&env::var(PATH_VAR).expect("Unable to read $PATH"))
|
||||
}
|
||||
|
||||
/* Module */
|
||||
|
||||
struct Settings {
|
||||
paths: Vec<String>,
|
||||
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<Entries> {
|
||||
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);
|
79
plugins/run/src/run.rs
Normal file
79
plugins/run/src/run.rs
Normal file
|
@ -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<String>) -> Vec<Entry> {
|
||||
let mut execs: HashMap<String, Entry> = 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<Entry> {
|
||||
// let cfg = &settings.run;
|
||||
// let paths = match cfg.paths.as_ref() {
|
||||
// Some(paths) => paths.clone(),
|
||||
// None => get_paths(),
|
||||
// };
|
||||
// find_executables(&paths)
|
||||
// }
|
||||
// }
|
Loading…
Reference in a new issue