forked from mirrors/rmenu
feat: default config, cleaner code, added plugin-cache system
This commit is contained in:
parent
0a6a741f58
commit
07db986da1
@ -6,6 +6,7 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bincode = "1.3.3"
|
||||||
cached = "0.44.0"
|
cached = "0.44.0"
|
||||||
clap = { version = "4.3.15", features = ["derive"] }
|
clap = { version = "4.3.15", features = ["derive"] }
|
||||||
dioxus = "0.3.2"
|
dioxus = "0.3.2"
|
||||||
@ -13,6 +14,7 @@ dioxus-desktop = "0.3.0"
|
|||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
heck = "0.4.1"
|
heck = "0.4.1"
|
||||||
keyboard-types = "0.6.2"
|
keyboard-types = "0.6.2"
|
||||||
|
lastlog = { version = "0.2.3", features = ["libc"] }
|
||||||
log = "0.4.19"
|
log = "0.4.19"
|
||||||
once_cell = "1.18.0"
|
once_cell = "1.18.0"
|
||||||
png = "0.17.9"
|
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)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct Keybind {
|
pub struct Keybind {
|
||||||
pub mods: Modifiers,
|
pub mods: Modifiers,
|
||||||
@ -73,6 +74,7 @@ impl<'de> Deserialize<'de> for Keybind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Global GUI Keybind Settings Options
|
||||||
#[derive(Debug, PartialEq, Deserialize)]
|
#[derive(Debug, PartialEq, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct KeyConfig {
|
pub struct KeyConfig {
|
||||||
@ -97,6 +99,7 @@ impl Default for KeyConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GUI Desktop Window Configuration Settings
|
||||||
#[derive(Debug, PartialEq, Deserialize)]
|
#[derive(Debug, PartialEq, Deserialize)]
|
||||||
pub struct WindowConfig {
|
pub struct WindowConfig {
|
||||||
pub title: String,
|
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)]
|
#[derive(Debug, PartialEq, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@ -139,7 +193,7 @@ pub struct Config {
|
|||||||
pub use_icons: bool,
|
pub use_icons: bool,
|
||||||
pub search_regex: bool,
|
pub search_regex: bool,
|
||||||
pub ignore_case: bool,
|
pub ignore_case: bool,
|
||||||
pub plugins: BTreeMap<String, Vec<String>>,
|
pub plugins: BTreeMap<String, PluginConfig>,
|
||||||
pub keybinds: KeyConfig,
|
pub keybinds: KeyConfig,
|
||||||
pub window: WindowConfig,
|
pub window: WindowConfig,
|
||||||
pub terminal: Option<String>,
|
pub terminal: Option<String>,
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
//! RMENU GUI Implementation using Dioxus
|
//! RMENU GUI Implementation using Dioxus
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
use std::fs::read_to_string;
|
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use keyboard_types::{Code, Modifiers};
|
use keyboard_types::{Code, Modifiers};
|
||||||
use rmenu_plugin::Entry;
|
use rmenu_plugin::Entry;
|
||||||
|
@ -5,6 +5,7 @@ use std::io::{self, prelude::*, BufReader};
|
|||||||
use std::process::{Command, ExitStatus, Stdio};
|
use std::process::{Command, ExitStatus, Stdio};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
mod cache;
|
||||||
mod config;
|
mod config;
|
||||||
mod exec;
|
mod exec;
|
||||||
mod gui;
|
mod gui;
|
||||||
@ -142,20 +143,30 @@ impl Args {
|
|||||||
log::debug!("config: {cfg:?}");
|
log::debug!("config: {cfg:?}");
|
||||||
// execute commands to get a list of entries
|
// execute commands to get a list of entries
|
||||||
let mut entries = vec![];
|
let mut entries = vec![];
|
||||||
for plugin in self.run.iter() {
|
for name in self.run.iter() {
|
||||||
log::debug!("running plugin: {plugin}");
|
log::debug!("running plugin: {name}");
|
||||||
// retrieve plugin command arguments
|
// retrieve plugin command arguments
|
||||||
let Some(args) = cfg.plugins.get(plugin) else {
|
let plugin = cfg
|
||||||
return Err(RMenuError::NoSuchPlugin(plugin.to_owned()));
|
.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
|
// build command
|
||||||
let mut cmdargs: VecDeque<String> = args
|
let mut cmdargs: VecDeque<String> = plugin
|
||||||
|
.exec
|
||||||
.iter()
|
.iter()
|
||||||
.map(|arg| shellexpand::tilde(arg).to_string())
|
.map(|arg| shellexpand::tilde(arg).to_string())
|
||||||
.collect();
|
.collect();
|
||||||
let Some(main) = cmdargs.pop_front() else {
|
let main = cmdargs
|
||||||
return Err(RMenuError::InvalidPlugin(plugin.to_owned()));
|
.pop_front()
|
||||||
};
|
.ok_or_else(|| RMenuError::InvalidPlugin(name.to_owned()))?;
|
||||||
let mut cmd = Command::new(main);
|
let mut cmd = Command::new(main);
|
||||||
for arg in cmdargs.iter() {
|
for arg in cmdargs.iter() {
|
||||||
cmd.arg(arg);
|
cmd.arg(arg);
|
||||||
@ -165,7 +176,7 @@ impl Args {
|
|||||||
let stdout = proc
|
let stdout = proc
|
||||||
.stdout
|
.stdout
|
||||||
.as_mut()
|
.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);
|
let reader = BufReader::new(stdout);
|
||||||
// read output line by line and parse content
|
// read output line by line and parse content
|
||||||
for line in reader.lines() {
|
for line in reader.lines() {
|
||||||
@ -176,10 +187,15 @@ impl Args {
|
|||||||
let status = proc.wait()?;
|
let status = proc.wait()?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
return Err(RMenuError::CommandError(
|
return Err(RMenuError::CommandError(
|
||||||
args.clone().into(),
|
plugin.exec.clone().into(),
|
||||||
Some(status.clone()),
|
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)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
@ -201,13 +201,13 @@ impl<'a> AppState<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Automatically Increase PageCount When Nearing Bottom
|
/// Automatically Increase PageCount When Nearing Bottom
|
||||||
pub fn scroll_down(&self) {
|
// pub fn scroll_down(&self) {
|
||||||
self.state.with_mut(|s| {
|
// self.state.with_mut(|s| {
|
||||||
if self.app.config.page_size * s.page < self.app.entries.len() {
|
// if self.app.config.page_size * s.page < self.app.entries.len() {
|
||||||
s.page += 1;
|
// s.page += 1;
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
/// Move Position To SubMenu if it Exists
|
/// Move Position To SubMenu if it Exists
|
||||||
pub fn open_menu(&self) {
|
pub fn open_menu(&self) {
|
||||||
|
Loading…
Reference in New Issue
Block a user