From 6fe171c3988dc6f82c0478aad546faa3a134d7a1 Mon Sep 17 00:00:00 2001 From: imgurbot12 Date: Mon, 17 Jul 2023 22:49:07 -0700 Subject: [PATCH] feat: complete rewrite for v2 --- Cargo.toml | 5 + crates/rmenu-plugin/Cargo.toml | 17 --- crates/rmenu-plugin/src/cache.rs | 196 ---------------------------- crates/rmenu-plugin/src/internal.rs | 32 ----- crates/rmenu-plugin/src/lib.rs | 67 ---------- crates/rmenu/Cargo.toml | 17 --- crates/rmenu/src/config.rs | 101 -------------- crates/rmenu/src/exec.rs | 0 crates/rmenu/src/gui/mod.rs | 57 -------- crates/rmenu/src/gui_old/icons.rs | 94 ------------- crates/rmenu/src/gui_old/mod.rs | 188 -------------------------- crates/rmenu/src/gui_old/page.rs | 107 --------------- crates/rmenu/src/main.rs | 55 -------- crates/rmenu/src/plugins.rs | 33 ----- plugins/drun/Cargo.toml | 16 --- plugins/drun/src/desktop.rs | 136 ------------------- plugins/drun/src/lib.rs | 156 ---------------------- plugins/run/Cargo.toml | 15 --- plugins/run/src/lib.rs | 105 --------------- plugins/run/src/run.rs | 79 ----------- rmenu-plugin/Cargo.toml | 9 ++ rmenu-plugin/src/lib.rs | 33 +++++ rmenu/Cargo.toml | 18 +++ rmenu/public/default.css | 40 ++++++ rmenu/src/gui.rs | 69 ++++++++++ rmenu/src/main.rs | 69 ++++++++++ 26 files changed, 243 insertions(+), 1471 deletions(-) create mode 100644 Cargo.toml delete mode 100644 crates/rmenu-plugin/Cargo.toml delete mode 100644 crates/rmenu-plugin/src/cache.rs delete mode 100644 crates/rmenu-plugin/src/internal.rs delete mode 100644 crates/rmenu-plugin/src/lib.rs delete mode 100644 crates/rmenu/Cargo.toml delete mode 100644 crates/rmenu/src/config.rs delete mode 100644 crates/rmenu/src/exec.rs delete mode 100644 crates/rmenu/src/gui/mod.rs delete mode 100644 crates/rmenu/src/gui_old/icons.rs delete mode 100644 crates/rmenu/src/gui_old/mod.rs delete mode 100644 crates/rmenu/src/gui_old/page.rs delete mode 100644 crates/rmenu/src/main.rs delete mode 100644 crates/rmenu/src/plugins.rs delete mode 100644 plugins/drun/Cargo.toml delete mode 100644 plugins/drun/src/desktop.rs delete mode 100644 plugins/drun/src/lib.rs delete mode 100644 plugins/run/Cargo.toml delete mode 100644 plugins/run/src/lib.rs delete mode 100644 plugins/run/src/run.rs create mode 100644 rmenu-plugin/Cargo.toml create mode 100644 rmenu-plugin/src/lib.rs create mode 100644 rmenu/Cargo.toml create mode 100644 rmenu/public/default.css create mode 100644 rmenu/src/gui.rs create mode 100644 rmenu/src/main.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..57aa7db --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = [ + "rmenu", + "rmenu-plugin" +] diff --git a/crates/rmenu-plugin/Cargo.toml b/crates/rmenu-plugin/Cargo.toml deleted file mode 100644 index d1982cd..0000000 --- a/crates/rmenu-plugin/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[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 deleted file mode 100644 index 67a175d..0000000 --- a/crates/rmenu-plugin/src/cache.rs +++ /dev/null @@ -1,196 +0,0 @@ -/* - * 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 deleted file mode 100644 index f4388ec..0000000 --- a/crates/rmenu-plugin/src/internal.rs +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Internal Library Loading Implementation - */ -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 deleted file mode 100644 index be1c685..0000000 --- a/crates/rmenu-plugin/src/lib.rs +++ /dev/null @@ -1,67 +0,0 @@ -#[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 path: 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 deleted file mode 100644 index e37e107..0000000 --- a/crates/rmenu/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[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" -clap = { version = "4.0.32", features = ["derive"] } -dioxus = "0.3.2" -dioxus-desktop = "0.3.0" -log = "0.4.17" -rmenu-plugin = { version = "0.1.0", path = "../rmenu-plugin", features = ["rmenu_internals"] } -serde = { version = "1.0.152", features = ["derive"] } -shellexpand = "3.0.0" -toml = "0.5.10" diff --git a/crates/rmenu/src/config.rs b/crates/rmenu/src/config.rs deleted file mode 100644 index 5347557..0000000 --- a/crates/rmenu/src/config.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::{env, fs}; - -use rmenu_plugin::ModuleConfig; -use serde::{Deserialize, Serialize}; -use shellexpand::tilde; - -/* Variables */ - -static HOME: &str = "HOME"; -static XDG_CONIFG_HOME: &str = "XDG_CONIFG_HOME"; - -/* Types */ - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct PluginConfig { - pub prefix: String, - pub path: String, - pub config: ModuleConfig, -} - -#[derive(Serialize, Deserialize)] -pub struct RMenuConfig { - pub terminal: String, - pub icon_size: f32, - pub centered: Option, - pub window_pos: Option<[f32; 2]>, - pub window_size: Option<[f32; 2]>, - pub result_size: Option, - pub decorate_window: bool, -} - -#[derive(Serialize, Deserialize)] -pub struct Config { - pub rmenu: RMenuConfig, - pub plugins: HashMap, -} - -impl Default for Config { - fn default() -> Self { - Self { - rmenu: RMenuConfig { - terminal: "foot".to_owned(), - icon_size: 20.0, - centered: Some(true), - window_pos: None, - window_size: Some([500.0, 300.0]), - result_size: Some(15), - decorate_window: false, - }, - plugins: HashMap::new(), - } - } -} - -/* Functions */ - -#[inline] -fn get_config_dir() -> PathBuf { - if let Ok(config) = env::var(XDG_CONIFG_HOME) { - return Path::new(&config).join("rmenu").to_path_buf(); - } - if let Ok(home) = env::var(HOME) { - return Path::new(&home).join(".config").join("rmenu").to_path_buf(); - } - panic!("cannot find config directory!") -} - -pub fn load_config(path: Option) -> Config { - // determine path based on arguments - let fpath = match path.clone() { - Some(path) => Path::new(&tilde(&path).to_string()).to_path_buf(), - None => get_config_dir().join("config.toml"), - }; - // read existing file or write default and read it back - let mut config = match fpath.exists() { - false => { - // write default config to standard location - let config = Config::default(); - if path.is_none() { - let dir = get_config_dir(); - if !dir.exists() { - fs::create_dir(dir).expect("failed to make config dir"); - } - let default = toml::to_string(&config).unwrap(); - fs::write(fpath, default).expect("failed to write default config"); - } - config - } - true => { - let config = fs::read_to_string(fpath).expect("unable to read config"); - toml::from_str(&config).expect("broken config") - } - }; - // expand plugin paths - for plugin in config.plugins.values_mut() { - plugin.path = tilde(&plugin.path).to_string(); - } - config -} diff --git a/crates/rmenu/src/exec.rs b/crates/rmenu/src/exec.rs deleted file mode 100644 index e69de29..0000000 diff --git a/crates/rmenu/src/gui/mod.rs b/crates/rmenu/src/gui/mod.rs deleted file mode 100644 index ecc49a4..0000000 --- a/crates/rmenu/src/gui/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ - -use dioxus::prelude::*; -use dioxus_desktop::{Config, WindowBuilder}; - -use rmenu_plugin::Entry; - -use crate::{config, plugins::Plugins}; - -/// Spawn GUI instance with the specified config and plugins -pub fn launch_gui(cfg: config::Config, plugins: Plugins) -> Result<(), String> { - // simple_logger::init_with_level(log::Level::Debug).unwrap(); - let size = cfg.rmenu.window_size.unwrap_or([550.0, 350.0]); - let gui = GUI::new(cfg, plugins); - dioxus_desktop::launch_cfg( - App, - Config::default().with_window(WindowBuilder::new().with_resizable(true).with_inner_size( - dioxus_desktop::wry::application::dpi::LogicalSize::new(size[0], size[1]), - )), - ); - Ok(()) -} - -struct GUI { - plugins: Plugins, - search: String, - config: config::Config, -} - -impl GUI { - pub fn new(config: config::Config, plugins: Plugins) -> Self { - Self { - config, - plugins, - search: "".to_owned(), - } - } - - fn search(&mut self, search: &str) -> Vec { - self.plugins.search(search) - } - - pub fn app(&mut self, cx: Scope) -> Element { - let results = self.search(""); - cx.render(rsx! { - div { - h1 { "Hello World!" } - result.iter().map(|entry| { - div { - div { entry.name } - div { entry.comment } - } - }) - } - }) - } - -} diff --git a/crates/rmenu/src/gui_old/icons.rs b/crates/rmenu/src/gui_old/icons.rs deleted file mode 100644 index a692e45..0000000 --- a/crates/rmenu/src/gui_old/icons.rs +++ /dev/null @@ -1,94 +0,0 @@ -/* - * GUI Icon Cache/Loading utilities - */ -use std::sync::Arc; -use std::thread; - -use dashmap::{mapref::one::Ref, DashMap}; -use egui_extras::RetainedImage; -use log::debug; - -use rmenu_plugin::{Entry, Icon}; - -/* Types */ - -type Cache = DashMap; -type IconRef<'a> = Ref<'a, String, RetainedImage>; - -/* Functions */ - -// load result entry icons into cache in background w/ given chunk-size per thread -pub fn load_images(cache: &mut IconCache, chunk_size: usize, results: &Vec) { - // retrieve icons from results - let icons: Vec = results - .iter() - .filter_map(|r| r.icon.clone().into()) - .collect(); - for chunk in icons.chunks(chunk_size).into_iter() { - cache.save_background(Vec::from(chunk)); - } -} - -/* Cache */ - -// spawn multiple threads to load image objects into cache from search results - -pub struct IconCache { - c: Arc, -} - -impl IconCache { - pub fn new() -> Self { - Self { - c: Arc::new(Cache::new()), - } - } - - // save icon to cache if not already saved - pub fn save(&mut self, icon: &Icon) -> Result<(), String> { - let name = icon.name.as_str(); - let path = icon.path.as_str(); - if !self.c.contains_key(name) { - self.c.insert( - name.to_owned(), - if path.ends_with(".svg") { - RetainedImage::from_svg_bytes(path, &icon.data)? - } else { - RetainedImage::from_image_bytes(path, &icon.data)? - }, - ); - } - Ok(()) - } - - // load an image from the given icon-cache - #[inline] - pub fn load(&mut self, icon: &Icon) -> Result, String> { - self.save(icon)?; - Ok(self - .c - .get(icon.name.as_str()) - .expect("failed to load saved image")) - } - - // save list of icon-entries in the background - pub fn save_background(&mut self, icons: Vec) { - let mut cache = self.clone(); - thread::spawn(move || { - for icon in icons.iter() { - if let Err(err) = cache.save(&icon) { - debug!("icon {} failed to load: {}", icon.name.as_str(), err); - }; - } - debug!("background task loaded {} icons", icons.len()); - }); - } -} - -impl Clone for IconCache { - fn clone(&self) -> Self { - Self { - c: Arc::clone(&self.c), - } - } -} diff --git a/crates/rmenu/src/gui_old/mod.rs b/crates/rmenu/src/gui_old/mod.rs deleted file mode 100644 index 408e353..0000000 --- a/crates/rmenu/src/gui_old/mod.rs +++ /dev/null @@ -1,188 +0,0 @@ -/*! - * Rmenu - Egui implementation - */ -use std::process::exit; - -use eframe::egui; - -mod icons; -mod page; -use icons::{load_images, IconCache}; -use page::Paginator; - -use crate::{config::Config, plugins::Plugins}; - -// v1: -//TODO: fix grid so items expand entire length of window -//TODO: remove prefix and name specification from module definition -//TODO: allow specifying prefix in search to limit enabled plugins -//TODO: build in the actual execute and close part -//TODO: build compilation and install script for easy setup -//TODO: allow for close-on-defocus option in config? - -// v2: -//TODO: look into dynamic rendering w/ a custom style config - maybe even css? -//TODO: add additonal plugins: file-browser, browser-url, etc... - -/* Function */ - -// spawn gui application and run it -pub fn launch_gui(cfg: Config, plugins: Plugins) -> Result<(), eframe::Error> { - let pos = match cfg.rmenu.window_pos { - Some(pos) => Some(egui::pos2(pos[0], pos[1])), - None => None, - }; - let size = cfg.rmenu.window_size.unwrap_or([550.0, 350.0]); - let options = eframe::NativeOptions { - transparent: true, - always_on_top: true, - decorated: cfg.rmenu.decorate_window, - centered: cfg.rmenu.centered.unwrap_or(false), - initial_window_pos: pos, - initial_window_size: Some(egui::vec2(size[0], size[1])), - ..Default::default() - }; - let gui = GUI::new(cfg, plugins); - eframe::run_native("rmenu", options, Box::new(|_cc| Box::new(gui))) -} - -/* Implementation */ - -struct GUI { - plugins: Plugins, - search: String, - images: IconCache, - config: Config, - page: Paginator, -} - -impl GUI { - pub fn new(config: Config, plugins: Plugins) -> Self { - let mut gui = Self { - plugins, - search: "".to_owned(), - images: IconCache::new(), - page: Paginator::new(config.rmenu.result_size.clone().unwrap_or(15)), - config, - }; - // pre-run empty search to generate cache - gui.search(); - gui - } - - // complete search based on current internal search variable - fn search(&mut self) { - let results = self.plugins.search(&self.search); - if results.len() > 0 { - load_images(&mut self.images, 20, &results); - } - self.page.reset(results); - } - - #[inline] - fn keyboard(&mut self, ctx: &egui::Context) { - // tab / ctrl+tab controls - if ctx.input().key_pressed(egui::Key::Tab) { - match ctx.input().modifiers.ctrl { - true => self.page.focus_up(1), - false => self.page.focus_down(1), - }; - } - // arrow-key controls - if ctx.input().key_pressed(egui::Key::ArrowUp) { - self.page.focus_up(1); - } - if ctx.input().key_pressed(egui::Key::ArrowDown) { - self.page.focus_down(1) - } - // pageup / pagedown controls - if ctx.input().key_pressed(egui::Key::PageUp) { - self.page.focus_up(5); - } - if ctx.input().key_pressed(egui::Key::PageDown) { - self.page.focus_down(5); - } - // exit controls - if ctx.input().key_pressed(egui::Key::Escape) { - exit(1); - } - } -} - -impl GUI { - // implement simple topbar searchbar - #[inline] - fn simple_search(&mut self, ui: &mut egui::Ui) { - let size = ui.available_size(); - ui.horizontal(|ui| { - ui.spacing_mut().text_edit_width = size.x; - let search = egui::TextEdit::singleline(&mut self.search).frame(false); - let object = ui.add(search); - if object.changed() { - self.search(); - } - }); - } - - // check if results contain any icons at all - #[inline] - fn has_icons(&self) -> bool { - self.page - .iter() - .filter(|r| r.icon.is_some()) - .peekable() - .peek() - .is_some() - } - - #[inline] - fn grid_highlight(&self) -> Box Option> { - let focus = self.page.row_focus(); - Box::new(move |row, style| { - if row == focus { - return Some(egui::Rgba::from(style.visuals.faint_bg_color)); - } - None - }) - } - - // implement simple scrolling grid-based results pannel - #[inline] - fn simple_results(&mut self, ui: &mut egui::Ui) { - let results = self.page.iter(); - let has_icons = self.has_icons(); - egui::Grid::new("results") - .with_row_color(self.grid_highlight()) - .show(ui, |ui| { - for record in results { - // render icons (if any were present in set) - if has_icons { - ui.horizontal(|ui| { - if let Some(icon) = record.icon.as_ref().into_option() { - if let Ok(image) = self.images.load(&icon) { - let xy = self.config.rmenu.icon_size; - image.show_size(ui, egui::vec2(xy, xy)); - } - } - }); - } - // render content - ui.label(record.name.as_str()); - if let Some(comment) = record.comment.as_ref().into_option() { - ui.label(comment.as_str()); - } - ui.end_row(); - } - }); - } -} - -impl eframe::App for GUI { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - egui::CentralPanel::default().show(ctx, |ui| { - self.keyboard(ctx); - self.simple_search(ui); - self.simple_results(ui); - }); - } -} diff --git a/crates/rmenu/src/gui_old/page.rs b/crates/rmenu/src/gui_old/page.rs deleted file mode 100644 index fe6049f..0000000 --- a/crates/rmenu/src/gui_old/page.rs +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Result Paginator Implementation - */ -use std::cmp::min; - -use rmenu_plugin::Entry; - -/// Plugin results paginator implementation -pub struct Paginator { - page: usize, - page_size: usize, - results: Vec, - focus: usize, -} - -impl Paginator { - pub fn new(page_size: usize) -> Self { - Self { - page: 0, - page_size, - results: vec![], - focus: 0, - } - } - - #[inline(always)] - fn lower_bound(&self) -> usize { - self.page * self.page_size - } - - #[inline(always)] - fn upper_bound(&self) -> usize { - (self.page + 1) * self.page_size - } - - fn set_focus(&mut self, focus: usize) { - self.focus = focus; - if self.focus < self.lower_bound() { - self.page -= 1; - } - if self.focus >= self.upper_bound() { - self.page += 1; - } - } - - /// reset paginator location and replace internal results - pub fn reset(&mut self, results: Vec) { - self.page = 0; - self.focus = 0; - self.results = results; - } - - /// calculate zeroed focus based on index in iterator - #[inline] - pub fn row_focus(&self) -> usize { - self.focus - self.lower_bound() - } - - /// shift focus up a certain number of rows - #[inline] - pub fn focus_up(&mut self, shift: usize) { - self.set_focus(self.focus - min(shift, self.focus)); - } - - /// shift focus down a certain number of rows - pub fn focus_down(&mut self, shift: usize) { - let results = self.results.len(); - let max_pos = if results > 0 { results - 1 } else { 0 }; - self.set_focus(min(self.focus + shift, max_pos)); - } - - /// Generate page-size iterator - #[inline] - pub fn iter(&self) -> PageIter { - PageIter::new(self.lower_bound(), self.upper_bound(), &self.results) - } -} - -/// Paginator bounds iterator implementation -pub struct PageIter<'a> { - stop: usize, - cursor: usize, - results: &'a Vec, -} - -impl<'a> PageIter<'a> { - pub fn new(start: usize, stop: usize, results: &'a Vec) -> Self { - Self { - stop, - results, - cursor: start, - } - } -} - -impl<'a> Iterator for PageIter<'a> { - type Item = &'a Entry; - - fn next(&mut self) -> Option { - if self.cursor >= self.stop { - return None; - } - let result = self.results.get(self.cursor); - self.cursor += 1; - result - } -} diff --git a/crates/rmenu/src/main.rs b/crates/rmenu/src/main.rs deleted file mode 100644 index 63f2a90..0000000 --- a/crates/rmenu/src/main.rs +++ /dev/null @@ -1,55 +0,0 @@ -use clap::Parser; - -mod config; -mod gui; -mod plugins; - -use config::{load_config, PluginConfig}; -use gui::launch_gui; -use plugins::Plugins; - -/* Types */ - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Args { - /// configuration file to read from - #[arg(short, long)] - pub config: Option, - /// terminal command override - #[arg(short, long)] - pub term: Option, - /// declared and enabled plugin modes - #[arg(short, long)] - pub show: Option>, -} - -fn main() { - // parse cli-args and use it to load the config - let args = Args::parse(); - let mut config = load_config(args.config); - // update config based on other cli-args - if let Some(term) = args.term.as_ref() { - config.rmenu.terminal = term.to_owned() - } - // load relevant plugins based on configured options - let enabled = args.show.unwrap_or_else(|| vec!["drun".to_owned()]); - let plugin_configs: Vec = config - .plugins - .clone() - .into_iter() - .filter(|(k, _)| enabled.contains(k)) - .map(|(_, v)| v) - .collect(); - // error if plugins-list is empty - if plugin_configs.len() != enabled.len() { - let missing: Vec<&String> = enabled - .iter() - .filter(|p| !config.plugins.contains_key(p.as_str())) - .collect(); - panic!("no plugin configurations for: {:?}", missing); - } - // spawn gui instance w/ config and enabled plugins - let plugins = Plugins::new(enabled, plugin_configs); - launch_gui(config, plugins).expect("gui crashed") -} diff --git a/crates/rmenu/src/plugins.rs b/crates/rmenu/src/plugins.rs deleted file mode 100644 index 1b876ea..0000000 --- a/crates/rmenu/src/plugins.rs +++ /dev/null @@ -1,33 +0,0 @@ -use abi_stable::std_types::RString; -use rmenu_plugin::internal::{load_plugin, Plugin}; -use rmenu_plugin::Entry; - -use super::config::PluginConfig; - -/// Convenient wrapper used to execute configured plugins -pub struct Plugins { - plugins: Vec, -} - -impl Plugins { - pub fn new(enable: Vec, plugins: Vec) -> Self { - Self { - plugins: plugins - .into_iter() - .map(|p| unsafe { load_plugin(&p.path, &p.config) }.expect("failed to load plugin")) - .filter(|plugin| enable.contains(&plugin.module.name().as_str().to_owned())) - .collect(), - } - } - - /// complete search w/ the configured plugins - pub fn search(&mut self, search: &str) -> Vec { - let mut entries = vec![]; - for plugin in self.plugins.iter_mut() { - let found = plugin.module.search(RString::from(search)); - entries.append(&mut found.into()); - continue; - } - entries - } -} diff --git a/plugins/drun/Cargo.toml b/plugins/drun/Cargo.toml deleted file mode 100644 index 1303f60..0000000 --- a/plugins/drun/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[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 deleted file mode 100644 index d2975ec..0000000 --- a/plugins/drun/src/desktop.rs +++ /dev/null @@ -1,136 +0,0 @@ -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), - path: RString::from(path), - 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 deleted file mode 100644 index cc5092e..0000000 --- a/plugins/drun/src/lib.rs +++ /dev/null @@ -1,156 +0,0 @@ -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/:/usr/local/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 deleted file mode 100644 index b6beef0..0000000 --- a/plugins/run/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[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 deleted file mode 100644 index 48dcd2f..0000000 --- a/plugins/run/src/lib.rs +++ /dev/null @@ -1,105 +0,0 @@ -/*! - * 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 deleted file mode 100644 index 790a658..0000000 --- a/plugins/run/src/run.rs +++ /dev/null @@ -1,79 +0,0 @@ -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) -// } -// } diff --git a/rmenu-plugin/Cargo.toml b/rmenu-plugin/Cargo.toml new file mode 100644 index 0000000..c20b9ba --- /dev/null +++ b/rmenu-plugin/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "rmenu-plugin" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0.171", features = ["derive"] } diff --git a/rmenu-plugin/src/lib.rs b/rmenu-plugin/src/lib.rs new file mode 100644 index 0000000..b4e20a5 --- /dev/null +++ b/rmenu-plugin/src/lib.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum Method { + Terminal, + Desktop, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Action { + pub exec: String, + pub comment: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Entry { + pub name: String, + pub actions: BTreeMap, + pub comment: Option, + pub icon: Option, +} + +impl Entry { + pub fn new(name: &str) -> Self { + Self { + name: name.to_owned(), + actions: Default::default(), + comment: Default::default(), + icon: Default::default(), + } + } +} diff --git a/rmenu/Cargo.toml b/rmenu/Cargo.toml new file mode 100644 index 0000000..8c3d0fc --- /dev/null +++ b/rmenu/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "rmenu" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.3.15", features = ["derive"] } +dioxus = "0.3.2" +rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" } +serde_json = "1.0.103" + +[target.'cfg(any(unix, windows))'.dependencies] +dioxus-desktop = { version = "0.3.0" } + +[target.'cfg(target_family = "wasm")'.dependencies] +dioxus-web = { version = "0.3.1" } diff --git a/rmenu/public/default.css b/rmenu/public/default.css new file mode 100644 index 0000000..48f3270 --- /dev/null +++ b/rmenu/public/default.css @@ -0,0 +1,40 @@ +main { + display: flex; + flex-direction: column; + justify-content: center; +} + +input { + min-width: 99%; +} + +div.result { + display: flex; + align-items: center; + justify-content: left; +} + +div.result > div { + margin: 2px 5px; +} + +div.result > div.icon { + width: 4%; + overflow: hidden; + display: flex; + justify-content: center; +} + +div.result > div.icon > img { + width: 100%; + height: 100%; + object-fit: cover; +} + +div.result > div.name { + width: 30%; +} + +div.result > div.comment { + flex: 1; +} diff --git a/rmenu/src/gui.rs b/rmenu/src/gui.rs new file mode 100644 index 0000000..9844481 --- /dev/null +++ b/rmenu/src/gui.rs @@ -0,0 +1,69 @@ +#![allow(non_snake_case)] +use dioxus::prelude::*; +use rmenu_plugin::Entry; + +use crate::App; + +pub fn run(app: App) { + #[cfg(target_family = "wasm")] + dioxus_web::launch(App, app, dioxus_web::Config::default()); + + #[cfg(any(windows, unix))] + dioxus_desktop::launch_with_props(App, app, dioxus_desktop::Config::default()); +} + +#[derive(PartialEq, Props)] +struct GEntry<'a> { + o: &'a Entry, +} + +fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> { + cx.render(rsx! { + div { + class: "result", + div { + class: "icon", + if let Some(icon) = cx.props.o.icon.as_ref() { + cx.render(rsx! { img { src: "{icon}" } }) + } + } + div { + class: "name", + "{cx.props.o.name}" + } + div { + class: "comment", + if let Some(comment) = cx.props.o.comment.as_ref() { + format!("- {comment}") + } + } + } + }) +} + +fn App(cx: Scope) -> Element { + let search = use_state(cx, || "".to_string()); + let results = &cx.props.entries; + let searchstr = search.as_str(); + let results_rendered = results + .iter() + .filter(|entry| { + if entry.name.contains(searchstr) { + return true; + } + if let Some(comment) = entry.comment.as_ref() { + return comment.contains(searchstr); + } + false + }) + .map(|entry| cx.render(rsx! { TableEntry{ o: entry } })); + + cx.render(rsx! { + style { "{cx.props.css}" } + input { + value: "{search}", + oninput: move |evt| search.set(evt.value.clone()), + } + results_rendered + }) +} diff --git a/rmenu/src/main.rs b/rmenu/src/main.rs new file mode 100644 index 0000000..1708deb --- /dev/null +++ b/rmenu/src/main.rs @@ -0,0 +1,69 @@ +use std::fs::{read_to_string, File}; +use std::io::{prelude::*, BufReader, Error}; + +mod gui; + +use clap::*; +use rmenu_plugin::Entry; + +/// Rofi Clone (Built with Rust) +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct Args { + #[arg(short, long, default_value_t=String::from("-"))] + input: String, + #[arg(short, long)] + json: bool, + #[arg(short, long)] + msgpack: bool, + #[arg(short, long)] + run: Vec, + #[arg(long)] + css: Option, +} + +//TODO: improve search w/ options for regex/case-insensivity/modes? +//TODO: improve looks and css + +/// Application State for GUI +#[derive(Debug, PartialEq)] +pub struct App { + css: String, + name: String, + entries: Vec, +} + +fn default(args: &Args) -> Result { + // read entries from specified input + let fpath = if args.input == "-" { + "/dev/stdin" + } else { + &args.input + }; + let file = File::open(fpath)?; + let reader = BufReader::new(file); + let mut entries = vec![]; + for line in reader.lines() { + let entry = serde_json::from_str::(&line?)?; + entries.push(entry); + } + // generate app object based on configured args + let css = args + .css + .clone() + .unwrap_or("rmenu/public/default.css".to_owned()); + let args = App { + name: "default".to_string(), + css: read_to_string(css)?, + entries, + }; + Ok(args) +} + +fn main() { + let cli = Args::parse(); + let app = default(&cli).unwrap(); + println!("{:?}", app); + gui::run(app); +}