feat: default config, cleaner code, added plugin-cache system

This commit is contained in:
imgurbot12 2023-08-07 15:18:28 -07:00
parent 0a6a741f58
commit 07db986da1
7 changed files with 200 additions and 21 deletions

View File

@ -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"

19
rmenu/public/config.yaml Normal file
View File

@ -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"]

90
rmenu/src/cache.rs Normal file
View File

@ -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<PathBuf> =
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<Vec<Entry>, 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<Entry> = bincode::deserialize(&data)?;
Ok(results)
}
/// Write Results to Cache (if Allowed)
pub fn write_cache(name: &str, cfg: &PluginConfig, entries: &Vec<Entry>) -> 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(())
}

View File

@ -18,6 +18,7 @@ fn mod_from_str(s: &str) -> Option<Modifiers> {
}
}
/// 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<Self, Self::Err> {
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<D>(deserializer: D) -> Result<Self, D::Error>
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<String>,
#[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<String, Vec<String>>,
pub plugins: BTreeMap<String, PluginConfig>,
pub keybinds: KeyConfig,
pub window: WindowConfig,
pub terminal: Option<String>,

View File

@ -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;

View File

@ -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<String> = args
let mut cmdargs: VecDeque<String> = 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)
}

View File

@ -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) {