mirror of
https://github.com/imgurbot12/rmenu.git
synced 2025-01-27 13:28:03 +01:00
feat: more work on rmenu implementation
This commit is contained in:
parent
18e8cb978c
commit
5e114ef36a
9 changed files with 681 additions and 15 deletions
|
@ -1,7 +1,6 @@
|
|||
/*
|
||||
* Internal Library Loading Implementation
|
||||
*/
|
||||
use abi_stable::std_types::{RBox, RHashMap, RString};
|
||||
use libloading::{Error, Library, Symbol};
|
||||
|
||||
use super::{Module, ModuleConfig};
|
||||
|
|
|
@ -7,4 +7,18 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
abi_stable = "0.11.1"
|
||||
clap = { version = "4.0.32", features = ["derive"] }
|
||||
dashmap = "5.4.0"
|
||||
eframe = "0.20.1"
|
||||
egui = "0.20.1"
|
||||
egui_extras = { version = "0.20.0", features = ["svg", "image"] }
|
||||
image = { version = "0.24.5", default-features = false, features = ["png"] }
|
||||
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"
|
||||
|
||||
[patch.crates-io]
|
||||
eframe = { git = "https://github.com/imgurbot12/egui", branch="feat/grid-color" }
|
||||
egui = { git = "https://github.com/imgurbot12/egui", branch="feat/grid-color" }
|
||||
|
|
94
crates/rmenu/src/config.rs
Normal file
94
crates/rmenu/src/config.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
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 window_width: f32,
|
||||
pub window_height: f32,
|
||||
pub result_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub rmenu: RMenuConfig,
|
||||
pub plugins: HashMap<String, PluginConfig>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rmenu: RMenuConfig {
|
||||
terminal: "foot".to_owned(),
|
||||
icon_size: 20.0,
|
||||
window_width: 500.0,
|
||||
window_height: 300.0,
|
||||
result_size: 15,
|
||||
},
|
||||
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<String>) -> 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() {
|
||||
fs::create_dir(get_config_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
|
||||
}
|
193
crates/rmenu/src/gui/gui.rs.bak
Normal file
193
crates/rmenu/src/gui/gui.rs.bak
Normal file
|
@ -0,0 +1,193 @@
|
|||
use std::cmp::min;
|
||||
use std::process::exit;
|
||||
|
||||
use eframe::egui;
|
||||
use eframe::egui::ScrollArea;
|
||||
|
||||
use super::exec::exec_command;
|
||||
use super::icons::{background_load, IconCache};
|
||||
use super::modules::{Entries, Mode, ModuleSearch, Settings};
|
||||
|
||||
/* Application */
|
||||
|
||||
pub struct App {
|
||||
modules: ModuleSearch,
|
||||
search: String,
|
||||
results: Option<Entries>,
|
||||
focus: usize,
|
||||
images: IconCache,
|
||||
}
|
||||
|
||||
// application class-related functions and utilities
|
||||
impl App {
|
||||
pub fn new(modes: Vec<Mode>, settings: Settings) -> Self {
|
||||
let modules = ModuleSearch::new(modes, settings).expect("module search failed");
|
||||
let mut app = Self {
|
||||
search: "".to_owned(),
|
||||
modules,
|
||||
results: None,
|
||||
focus: 0,
|
||||
images: IconCache::new(),
|
||||
};
|
||||
app.search();
|
||||
app
|
||||
}
|
||||
|
||||
fn search(&mut self) {
|
||||
// assign values
|
||||
self.focus = 0;
|
||||
self.results = self.modules.search(&self.search, 0).ok();
|
||||
// load icons in background
|
||||
if let Some(results) = self.results.as_ref() {
|
||||
background_load(&mut self.images, 20, results);
|
||||
}
|
||||
}
|
||||
|
||||
// shift focus based on size of results and scope of valid range
|
||||
fn shift_focus(&mut self, shift: i32) {
|
||||
// handle shifts up
|
||||
if shift < 0 {
|
||||
let change = shift.abs() as usize;
|
||||
if change > self.focus {
|
||||
self.focus = 0;
|
||||
return;
|
||||
}
|
||||
self.focus -= change;
|
||||
return;
|
||||
}
|
||||
// handle shifts down
|
||||
let max_pos = if let Some(r) = self.results.as_ref() {
|
||||
r.len() - 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
self.focus = min(self.focus + shift as usize, max_pos);
|
||||
}
|
||||
}
|
||||
|
||||
// ui component functions
|
||||
impl App {
|
||||
// implement keyboard navigation controls between menu items
|
||||
#[inline]
|
||||
fn keyboard_controls(&mut self, ctx: &egui::Context) {
|
||||
// tab/ctrl+tab controls
|
||||
if ctx.input().key_pressed(egui::Key::Tab) {
|
||||
if ctx.input().modifiers.ctrl {
|
||||
self.shift_focus(-1);
|
||||
} else {
|
||||
self.shift_focus(1);
|
||||
};
|
||||
}
|
||||
// arrow-key controls
|
||||
if ctx.input().key_pressed(egui::Key::ArrowUp) && self.focus > 0 {
|
||||
self.shift_focus(-1);
|
||||
}
|
||||
if ctx.input().key_pressed(egui::Key::ArrowDown) {
|
||||
self.shift_focus(1);
|
||||
}
|
||||
// pageup/down controls
|
||||
if ctx.input().key_pressed(egui::Key::PageUp) {
|
||||
self.shift_focus(-5);
|
||||
}
|
||||
if ctx.input().key_pressed(egui::Key::PageDown) {
|
||||
self.shift_focus(5);
|
||||
}
|
||||
// escape
|
||||
if ctx.input().key_pressed(egui::Key::Escape) {
|
||||
exit(1);
|
||||
}
|
||||
// enter - app selection
|
||||
if ctx.input().key_pressed(egui::Key::Enter) {
|
||||
let Some(results) = self.results.as_ref() else { return };
|
||||
let Some(entry) = results.get(self.focus) else { return };
|
||||
exec_command(&entry.exec, entry.terminal);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = ui.text_edit_singleline(&mut self.search);
|
||||
if search.changed() {
|
||||
self.search();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// implement simple scrolling grid-based results panel
|
||||
fn simple_results(&mut self, ui: &mut egui::Ui) {
|
||||
let focus = self.focus;
|
||||
ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show_viewport(ui, |ui, viewport| {
|
||||
// calculate top/bottom positions and size of each row
|
||||
let top_pos = viewport.min.y;
|
||||
let bottom_pos = viewport.max.y;
|
||||
let spacing = ui.spacing();
|
||||
let row_height = spacing.interact_size.y + spacing.item_spacing.y;
|
||||
// render results and their related fields
|
||||
let Some(found) = self.results.as_ref() else { return };
|
||||
let results = found.clone();
|
||||
egui::Grid::new("results")
|
||||
.with_row_color(move |row, style| {
|
||||
if row == focus {
|
||||
return Some(egui::Rgba::from(style.visuals.faint_bg_color));
|
||||
};
|
||||
None
|
||||
})
|
||||
.show(ui, |ui| {
|
||||
let has_icons = results
|
||||
.iter()
|
||||
.filter(|r| r.icon.is_some())
|
||||
.peekable()
|
||||
.peek()
|
||||
.is_some();
|
||||
for (n, record) in results.into_iter().enumerate() {
|
||||
let y = n as f32 * row_height;
|
||||
// load and render image field
|
||||
// content is contained within a horizontal to keep
|
||||
// scroll-pos from updating when icon renderings
|
||||
// change
|
||||
if has_icons {
|
||||
ui.horizontal(|ui| {
|
||||
// only render images that display within view window
|
||||
if n == 0 || (y < bottom_pos && y > top_pos) {
|
||||
if let Some(icon) = record.icon.as_ref() {
|
||||
if let Ok(image) = self.images.load(icon) {
|
||||
let size = egui::vec2(20.0, 20.0);
|
||||
image.show_size(ui, size);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
ui.label("");
|
||||
});
|
||||
}
|
||||
// render text fields
|
||||
let label = ui.label(&record.name);
|
||||
if n == self.focus {
|
||||
label.scroll_to_me(None)
|
||||
}
|
||||
if let Some(extra) = record.comment.as_ref() {
|
||||
ui.label(extra);
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
self.keyboard_controls(ctx);
|
||||
self.simple_search(ui);
|
||||
self.simple_results(ui);
|
||||
});
|
||||
}
|
||||
}
|
93
crates/rmenu/src/gui/icons.rs
Normal file
93
crates/rmenu/src/gui/icons.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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<String, RetainedImage>;
|
||||
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<Entry>) {
|
||||
// retrieve icons from results
|
||||
let icons: Vec<Icon> = 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<Cache>,
|
||||
}
|
||||
|
||||
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();
|
||||
if !self.c.contains_key(name) {
|
||||
self.c.insert(
|
||||
name.to_owned(),
|
||||
if name.ends_with(".svg") {
|
||||
RetainedImage::from_svg_bytes(name, &icon.data)?
|
||||
} else {
|
||||
RetainedImage::from_image_bytes(name, &icon.data)?
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// load an image from the given icon-cache
|
||||
#[inline]
|
||||
pub fn load(&mut self, icon: &Icon) -> Result<IconRef<'_>, 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<Icon>) {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
205
crates/rmenu/src/gui/mod.rs
Normal file
205
crates/rmenu/src/gui/mod.rs
Normal file
|
@ -0,0 +1,205 @@
|
|||
/*!
|
||||
* Rmenu - Egui implementation
|
||||
*/
|
||||
use std::cmp::min;
|
||||
|
||||
use eframe::egui;
|
||||
use rmenu_plugin::Entry;
|
||||
|
||||
mod icons;
|
||||
use icons::{load_images, IconCache};
|
||||
|
||||
use crate::{config::Config, plugins::Plugins};
|
||||
|
||||
/* Function */
|
||||
|
||||
// spawn gui application and run it
|
||||
pub fn launch_gui(cfg: Config, plugins: Plugins) -> Result<(), eframe::Error> {
|
||||
let options = eframe::NativeOptions {
|
||||
initial_window_size: Some(egui::vec2(cfg.rmenu.window_width, cfg.rmenu.window_height)),
|
||||
..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,
|
||||
results: Vec<Entry>,
|
||||
focus: usize,
|
||||
focus_updated: bool,
|
||||
images: IconCache,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl GUI {
|
||||
pub fn new(config: Config, plugins: Plugins) -> Self {
|
||||
let mut gui = Self {
|
||||
config,
|
||||
plugins,
|
||||
search: "".to_owned(),
|
||||
results: vec![],
|
||||
focus: 0,
|
||||
focus_updated: false,
|
||||
images: IconCache::new(),
|
||||
};
|
||||
// pre-run empty search to generate cache
|
||||
gui.search();
|
||||
gui
|
||||
}
|
||||
|
||||
// complete search based on current internal search variable
|
||||
fn search(&mut self) {
|
||||
// update variables and complete search
|
||||
self.focus = 0;
|
||||
self.results = self.plugins.search(&self.search);
|
||||
self.focus_updated = true;
|
||||
// load icons in background
|
||||
if self.results.len() > 0 {
|
||||
load_images(&mut self.images, 20, &self.results);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_focus(&mut self, focus: usize) {
|
||||
self.focus = focus;
|
||||
self.focus_updated = true;
|
||||
}
|
||||
|
||||
// shift focus up a certain number of rows
|
||||
#[inline]
|
||||
fn focus_up(&mut self, shift: usize) {
|
||||
self.set_focus(self.focus - min(shift, self.focus));
|
||||
}
|
||||
|
||||
// shift focus down a certain number of rows
|
||||
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));
|
||||
}
|
||||
|
||||
#[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.focus_down(1),
|
||||
false => self.focus_up(1),
|
||||
};
|
||||
}
|
||||
// arrow-key controls
|
||||
if ctx.input().key_pressed(egui::Key::ArrowUp) {
|
||||
self.focus_up(1);
|
||||
}
|
||||
if ctx.input().key_pressed(egui::Key::ArrowDown) {
|
||||
self.focus_down(1)
|
||||
}
|
||||
// pageup / pagedown controls
|
||||
if ctx.input().key_pressed(egui::Key::PageUp) {
|
||||
self.focus_up(5);
|
||||
}
|
||||
if ctx.input().key_pressed(egui::Key::PageDown) {
|
||||
self.focus_down(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = ui.text_edit_singleline(&mut self.search);
|
||||
if search.changed() {
|
||||
self.search();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// check if results contain any icons at all
|
||||
#[inline]
|
||||
fn has_icons(&self) -> bool {
|
||||
self.results
|
||||
.iter()
|
||||
.filter(|r| r.icon.is_some())
|
||||
.peekable()
|
||||
.peek()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn grid_highlight(&self) -> Box<dyn Fn(usize, &egui::Style) -> Option<egui::Rgba>> {
|
||||
let focus = self.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) {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show_viewport(ui, |ui, viewport| {
|
||||
// calculate top/bottom positions and size of rows
|
||||
let spacing = ui.spacing();
|
||||
let top_y = viewport.min.y;
|
||||
let bot_y = viewport.max.y;
|
||||
let row_h = spacing.interact_size.y + spacing.item_spacing.y;
|
||||
// render results and their related fields
|
||||
let results = &self.results;
|
||||
let has_icons = self.has_icons();
|
||||
egui::Grid::new("results")
|
||||
.with_row_color(self.grid_highlight())
|
||||
.show(ui, |ui| {
|
||||
for (n, record) in results.iter().enumerate() {
|
||||
// render icon if enabled and within visible bounds
|
||||
if has_icons {
|
||||
ui.horizontal(|ui| {
|
||||
let y = n as f32 * row_h;
|
||||
if n == 0 || (y < bot_y && y > top_y) {
|
||||
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 main label
|
||||
let label = ui.label(record.name.as_str());
|
||||
// scroll to laebl when focus shifts
|
||||
if n == self.focus && self.focus_updated {
|
||||
label.scroll_to_me(None);
|
||||
self.focus_updated = false;
|
||||
}
|
||||
// render comment (if any)
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,20 +1,55 @@
|
|||
use abi_stable::std_types::{RHashMap, RString};
|
||||
use rmenu_plugin::internal::load_plugin;
|
||||
use clap::Parser;
|
||||
|
||||
static PLUGIN: &str = "../../plugins/run/target/release/librun.so";
|
||||
mod config;
|
||||
mod gui;
|
||||
mod plugins;
|
||||
|
||||
fn test() {
|
||||
let mut cfg = RHashMap::new();
|
||||
// cfg.insert(RString::from("ignore_case"), RString::from("true"));
|
||||
use config::{load_config, PluginConfig};
|
||||
use gui::launch_gui;
|
||||
use plugins::Plugins;
|
||||
|
||||
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!");
|
||||
/* Types */
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// configuration file to read from
|
||||
#[arg(short, long)]
|
||||
pub config: Option<String>,
|
||||
/// terminal command override
|
||||
#[arg(short, long)]
|
||||
pub term: Option<String>,
|
||||
/// declared and enabled plugin modes
|
||||
#[arg(short, long)]
|
||||
pub show: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
test();
|
||||
// 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<PluginConfig> = 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")
|
||||
}
|
||||
|
|
33
crates/rmenu/src/plugins.rs
Normal file
33
crates/rmenu/src/plugins.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
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<Plugin>,
|
||||
}
|
||||
|
||||
impl Plugins {
|
||||
pub fn new(enable: Vec<String>, plugins: Vec<PluginConfig>) -> 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<Entry> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ static PREFIX: &str = "app";
|
|||
|
||||
static XDG_DATA_DIRS: &str = "XDG_DATA_DIRS";
|
||||
|
||||
static DEFAULT_XDG_PATHS: &str = "/usr/share/";
|
||||
static DEFAULT_XDG_PATHS: &str = "/usr/share/:/usr/local/share";
|
||||
static DEFAULT_APP_PATHS: &str = "";
|
||||
static DEFAULT_ICON_PATHS: &str = "/usr/share/pixmaps/";
|
||||
|
||||
|
|
Loading…
Reference in a new issue