mirror of
https://github.com/imgurbot12/rmenu.git
synced 2025-01-12 23:36:29 +01:00
feat: default config, cleaner code, added plugin-cache system
This commit is contained in:
parent
0a6a741f58
commit
07db986da1
7 changed files with 200 additions and 21 deletions
|
@ -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
19
rmenu/public/config.yaml
Normal 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
90
rmenu/src/cache.rs
Normal 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(())
|
||||
}
|
|
@ -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>,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue