From 07db986da1a35c49b5c5e949aa9d11bf3c954c8f Mon Sep 17 00:00:00 2001 From: imgurbot12 Date: Mon, 7 Aug 2023 15:18:28 -0700 Subject: [PATCH] feat: default config, cleaner code, added plugin-cache system --- rmenu/Cargo.toml | 2 + rmenu/public/config.yaml | 19 +++++++++ rmenu/src/cache.rs | 90 ++++++++++++++++++++++++++++++++++++++++ rmenu/src/config.rs | 56 ++++++++++++++++++++++++- rmenu/src/gui.rs | 2 - rmenu/src/main.rs | 38 ++++++++++++----- rmenu/src/state.rs | 14 +++---- 7 files changed, 200 insertions(+), 21 deletions(-) create mode 100644 rmenu/public/config.yaml create mode 100644 rmenu/src/cache.rs diff --git a/rmenu/Cargo.toml b/rmenu/Cargo.toml index 940d06d..de0807f 100644 --- a/rmenu/Cargo.toml +++ b/rmenu/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bincode = "1.3.3" cached = "0.44.0" clap = { version = "4.3.15", features = ["derive"] } dioxus = "0.3.2" @@ -13,6 +14,7 @@ dioxus-desktop = "0.3.0" env_logger = "0.10.0" heck = "0.4.1" keyboard-types = "0.6.2" +lastlog = { version = "0.2.3", features = ["libc"] } log = "0.4.19" once_cell = "1.18.0" png = "0.17.9" diff --git a/rmenu/public/config.yaml b/rmenu/public/config.yaml new file mode 100644 index 0000000..0d47b72 --- /dev/null +++ b/rmenu/public/config.yaml @@ -0,0 +1,19 @@ +use_icons: true +ignore_case: true +search_regex: false + +plugins: + run: + exec: ["~/.config/rmenu/run"] + cache: 300 + drun: + exec: ["~/.config/rmenu/drun"] + cache: onlogin + +keybinds: + exec: ["Enter"] + exit: ["Escape"] + move_up: ["Arrow-Up", "Shift+Tab"] + move_down: ["Arrow-Down", "Tab"] + open_menu: ["Arrow-Right"] + close_menu: ["Arrow-Left"] diff --git a/rmenu/src/cache.rs b/rmenu/src/cache.rs new file mode 100644 index 0000000..97b75a8 --- /dev/null +++ b/rmenu/src/cache.rs @@ -0,0 +1,90 @@ +//! RMenu Plugin Result Cache +use std::fs; +use std::io::Write; +use std::path::PathBuf; +use std::time::{Duration, SystemTime}; + +use once_cell::sync::Lazy; +use rmenu_plugin::Entry; +use thiserror::Error; + +use crate::config::{CacheSetting, PluginConfig}; +use crate::CONFIG_DIR; + +static CONFIG_PATH: Lazy = + Lazy::new(|| PathBuf::from(shellexpand::tilde(CONFIG_DIR).to_string())); + +#[derive(Debug, Error)] +pub enum CacheError { + #[error("Cache Not Available")] + NotAvailable, + #[error("Cache Invalid")] + InvalidCache, + #[error("Cache Expired")] + CacheExpired, + #[error("Cache File Error")] + FileError(#[from] std::io::Error), + #[error("Encoding Error")] + EncodingError(#[from] bincode::Error), +} + +#[inline] +fn cache_file(name: &str) -> PathBuf { + CONFIG_PATH.join(format!("{name}.cache")) +} + +/// Read Entries from Cache (if Valid and Available) +pub fn read_cache(name: &str, cfg: &PluginConfig) -> Result, CacheError> { + // confirm cache exists + let path = cache_file(name); + if !path.exists() { + return Err(CacheError::NotAvailable); + } + // get file modified date + let meta = path.metadata()?; + let modified = meta.modified()?; + // confirm cache is not expired + match cfg.cache { + CacheSetting::NoCache => return Err(CacheError::InvalidCache), + CacheSetting::Never => {} + CacheSetting::OnLogin => { + if let Ok(record) = lastlog::search_self() { + if let Some(last) = record.last_login.into() { + if modified <= last { + return Err(CacheError::CacheExpired); + } + } + } + } + CacheSetting::AfterSeconds(secs) => { + let now = SystemTime::now(); + let duration = Duration::from_secs(secs as u64); + let diff = now + .duration_since(modified) + .unwrap_or_else(|_| Duration::from_secs(0)); + if diff >= duration { + return Err(CacheError::CacheExpired); + } + } + } + // attempt to read content + let data = fs::read(path)?; + let results: Vec = bincode::deserialize(&data)?; + Ok(results) +} + +/// Write Results to Cache (if Allowed) +pub fn write_cache(name: &str, cfg: &PluginConfig, entries: &Vec) -> Result<(), CacheError> { + // write cache if allowed + match cfg.cache { + CacheSetting::NoCache => {} + _ => { + let path = cache_file(name); + println!("write! {:?}", path); + let data = bincode::serialize(entries)?; + let mut f = fs::File::create(path)?; + f.write_all(&data)?; + } + } + Ok(()) +} diff --git a/rmenu/src/config.rs b/rmenu/src/config.rs index a0567be..08ce35e 100644 --- a/rmenu/src/config.rs +++ b/rmenu/src/config.rs @@ -18,6 +18,7 @@ fn mod_from_str(s: &str) -> Option { } } +/// Single GUI Keybind for Configuration #[derive(Debug, PartialEq)] pub struct Keybind { pub mods: Modifiers, @@ -73,6 +74,7 @@ impl<'de> Deserialize<'de> for Keybind { } } +/// Global GUI Keybind Settings Options #[derive(Debug, PartialEq, Deserialize)] #[serde(default)] pub struct KeyConfig { @@ -97,6 +99,7 @@ impl Default for KeyConfig { } } +/// GUI Desktop Window Configuration Settings #[derive(Debug, PartialEq, Deserialize)] pub struct WindowConfig { pub title: String, @@ -131,6 +134,57 @@ impl Default for WindowConfig { } } +/// Cache Settings for Configured RMenu Plugins +#[derive(Debug, PartialEq)] +pub enum CacheSetting { + NoCache, + Never, + OnLogin, + AfterSeconds(usize), +} + +impl FromStr for CacheSetting { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "never" => Ok(Self::Never), + "false" | "disable" | "disabled" => Ok(Self::NoCache), + "true" | "login" | "onlogin" => Ok(Self::OnLogin), + _ => { + let secs: usize = s + .parse() + .map_err(|_| format!("Invalid Cache Setting: {s:?}"))?; + Ok(Self::AfterSeconds(secs)) + } + } + } +} + +impl<'de> Deserialize<'de> for CacheSetting { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s: &str = Deserialize::deserialize(deserializer)?; + CacheSetting::from_str(s).map_err(D::Error::custom) + } +} + +impl Default for CacheSetting { + fn default() -> Self { + Self::NoCache + } +} + +/// RMenu Data-Source Plugin Configuration +#[derive(Debug, PartialEq, Deserialize)] +pub struct PluginConfig { + pub exec: Vec, + #[serde(default)] + pub cache: CacheSetting, +} + +/// Global RMenu Complete Configuration #[derive(Debug, PartialEq, Deserialize)] #[serde(default)] pub struct Config { @@ -139,7 +193,7 @@ pub struct Config { pub use_icons: bool, pub search_regex: bool, pub ignore_case: bool, - pub plugins: BTreeMap>, + pub plugins: BTreeMap, pub keybinds: KeyConfig, pub window: WindowConfig, pub terminal: Option, diff --git a/rmenu/src/gui.rs b/rmenu/src/gui.rs index 2a4d821..21fa62c 100644 --- a/rmenu/src/gui.rs +++ b/rmenu/src/gui.rs @@ -1,7 +1,5 @@ //! RMENU GUI Implementation using Dioxus #![allow(non_snake_case)] -use std::fs::read_to_string; - use dioxus::prelude::*; use keyboard_types::{Code, Modifiers}; use rmenu_plugin::Entry; diff --git a/rmenu/src/main.rs b/rmenu/src/main.rs index 0043aaf..2986497 100644 --- a/rmenu/src/main.rs +++ b/rmenu/src/main.rs @@ -5,6 +5,7 @@ use std::io::{self, prelude::*, BufReader}; use std::process::{Command, ExitStatus, Stdio}; use std::str::FromStr; +mod cache; mod config; mod exec; mod gui; @@ -142,20 +143,30 @@ impl Args { log::debug!("config: {cfg:?}"); // execute commands to get a list of entries let mut entries = vec![]; - for plugin in self.run.iter() { - log::debug!("running plugin: {plugin}"); + for name in self.run.iter() { + log::debug!("running plugin: {name}"); // retrieve plugin command arguments - let Some(args) = cfg.plugins.get(plugin) else { - return Err(RMenuError::NoSuchPlugin(plugin.to_owned())); - }; + let plugin = cfg + .plugins + .get(name) + .ok_or_else(|| RMenuError::NoSuchPlugin(name.to_owned()))?; + // attempt to read cache rather than run command + match cache::read_cache(name, plugin) { + Ok(cached) => { + entries.extend(cached); + continue; + } + Err(err) => log::error!("cache read error: {err:?}"), + } // build command - let mut cmdargs: VecDeque = args + let mut cmdargs: VecDeque = plugin + .exec .iter() .map(|arg| shellexpand::tilde(arg).to_string()) .collect(); - let Some(main) = cmdargs.pop_front() else { - return Err(RMenuError::InvalidPlugin(plugin.to_owned())); - }; + let main = cmdargs + .pop_front() + .ok_or_else(|| RMenuError::InvalidPlugin(name.to_owned()))?; let mut cmd = Command::new(main); for arg in cmdargs.iter() { cmd.arg(arg); @@ -165,7 +176,7 @@ impl Args { let stdout = proc .stdout .as_mut() - .ok_or_else(|| RMenuError::CommandError(args.clone().into(), None))?; + .ok_or_else(|| RMenuError::CommandError(plugin.exec.clone().into(), None))?; let reader = BufReader::new(stdout); // read output line by line and parse content for line in reader.lines() { @@ -176,10 +187,15 @@ impl Args { let status = proc.wait()?; if !status.success() { return Err(RMenuError::CommandError( - args.clone().into(), + plugin.exec.clone().into(), Some(status.clone()), )); } + // write cache for entries collected + match cache::write_cache(name, plugin, &entries) { + Ok(_) => {} + Err(err) => log::error!("cache write error: {err:?}"), + }; } Ok(entries) } diff --git a/rmenu/src/state.rs b/rmenu/src/state.rs index c0458aa..3075d87 100644 --- a/rmenu/src/state.rs +++ b/rmenu/src/state.rs @@ -201,13 +201,13 @@ impl<'a> AppState<'a> { } /// Automatically Increase PageCount When Nearing Bottom - pub fn scroll_down(&self) { - self.state.with_mut(|s| { - if self.app.config.page_size * s.page < self.app.entries.len() { - s.page += 1; - } - }); - } + // pub fn scroll_down(&self) { + // self.state.with_mut(|s| { + // if self.app.config.page_size * s.page < self.app.entries.len() { + // s.page += 1; + // } + // }); + // } /// Move Position To SubMenu if it Exists pub fn open_menu(&self) {