feat: improved cli and implemented options entry override

This commit is contained in:
imgurbot12 2023-08-19 01:09:39 -07:00
parent 7e3c6c45d8
commit 1501e748d0
6 changed files with 322 additions and 115 deletions

View File

@ -12,13 +12,8 @@ path = "src/lib.rs"
[[bin]] [[bin]]
name = "rmenu-build" name = "rmenu-build"
path = "src/bin/main.rs" path = "src/bin/main.rs"
required-features = ["cli"]
[features]
default = []
cli = ["clap", "serde_json"]
[dependencies] [dependencies]
clap = { version = "4.3.22", features = ["derive"], optional = true } clap = { version = "4.3.22", features = ["derive"] }
serde = { version = "1.0.171", features = ["derive"] } serde = { version = "1.0.171", features = ["derive"] }
serde_json = { version = "1.0.105", optional = true } serde_json = "1.0.105"

View File

@ -1,6 +1,8 @@
use std::{fmt::Display, str::FromStr};
use rmenu_plugin::*; use rmenu_plugin::*;
use clap::{Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
/// Parse Action from JSON /// Parse Action from JSON
fn parse_action(action: &str) -> Result<Action, serde_json::Error> { fn parse_action(action: &str) -> Result<Action, serde_json::Error> {
@ -17,10 +19,74 @@ fn parse_action(action: &str) -> Result<Action, serde_json::Error> {
// 2. plugin/source latest merged options // 2. plugin/source latest merged options
// 3. configuration settings // 3. configuration settings
#[derive(Debug, Subcommand)] //TODO: add python library to build entries as well
enum Command {
/// Generate Complete RMenu Entry /// Valid Action Modes
Entry { #[derive(Debug, Clone)]
enum ActionMode {
Run,
Terminal,
Echo,
}
impl Display for ActionMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Run => write!(f, "run"),
Self::Terminal => write!(f, "terminal"),
Self::Echo => write!(f, "echo"),
}
}
}
impl FromStr for ActionMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"run" => Ok(Self::Run),
"terminal" => Ok(Self::Terminal),
"echo" => Ok(Self::Echo),
_ => Err(format!("Invalid Method: {s:?}")),
}
}
}
/// Arguents for Action CLI Command
#[derive(Debug, Args)]
struct ActionArgs {
/// Set Name of Action
#[arg(short, long, default_value_t=String::from("main"))]
name: String,
/// Set Comment of Action
#[arg(short, long)]
comment: Option<String>,
/// Arguments to run As Action Command
#[clap(required = true, value_delimiter = ' ')]
args: Vec<String>,
/// Action Mode
#[arg(short, long, default_value_t=ActionMode::Run)]
mode: ActionMode,
}
impl Into<Action> for ActionArgs {
fn into(self) -> Action {
let exec = self.args.join(" ");
Action {
name: self.name,
comment: self.comment,
exec: match self.mode {
ActionMode::Run => Method::Run(exec),
ActionMode::Terminal => Method::Terminal(exec),
ActionMode::Echo => Method::Echo(exec),
},
}
}
}
/// Arguments for Entry CLI Command
#[derive(Debug, Args)]
struct EntryArgs {
/// Set Name of Entry /// Set Name of Entry
#[arg(short, long, default_value_t=String::from("main"))] #[arg(short, long, default_value_t=String::from("main"))]
name: String, name: String,
@ -37,22 +103,108 @@ enum Command {
/// Alternative Image Text/HTML /// Alternative Image Text/HTML
#[arg(short = 'o', long)] #[arg(short = 'o', long)]
icon_alt: Option<String>, icon_alt: Option<String>,
}, }
impl Into<Entry> for EntryArgs {
fn into(self) -> Entry {
Entry {
name: self.name,
comment: self.comment,
actions: self.actions,
icon: self.icon,
icon_alt: self.icon_alt,
}
}
}
/// Arguments for Options CLI Command
#[derive(Debug, Args)]
struct OptionArgs {
/// Override Applicaiton Theme
#[arg(short, long)]
pub theme: Option<String>,
// search settings
/// Override Default Placeholder
#[arg(short, long)]
pub placeholder: Option<String>,
/// Override Search Restriction
#[arg(short = 'r', long)]
pub search_restrict: Option<String>,
/// Override Minimum Search Length
#[arg(short = 'm', long)]
pub search_min_length: Option<usize>,
/// Override Maximum Search Length
#[arg(short = 'M', long)]
pub search_max_length: Option<usize>,
// key settings
/// Override Execution Keybinds
#[arg(short = 'e', long)]
pub key_exec: Option<Vec<String>>,
/// Override Program-Exit Keybinds
#[arg(short = 'E', long)]
pub key_exit: Option<Vec<String>>,
/// Override Move-Next Keybinds
#[arg(short = 'n', long)]
pub key_move_next: Option<Vec<String>>,
/// Override Move-Previous Keybinds
#[arg(short = 'p', long)]
pub key_move_prev: Option<Vec<String>>,
/// Override Open-Menu Keybinds
#[arg(short = 'o', long)]
pub key_open_menu: Option<Vec<String>>,
/// Override Close-Menu Keybinds
#[arg(short = 'c', long)]
pub key_close_menu: Option<Vec<String>>,
// window settings
/// Override Window Title
#[arg(short, long)]
pub title: Option<String>,
/// Override Window Deocration Settings
#[arg(short, long)]
pub deocorate: Option<bool>,
/// Override Window Fullscreen Settings
#[arg(short, long)]
pub fullscreen: Option<bool>,
/// Override Window Width
#[arg(short = 'w', long)]
pub window_width: Option<f64>,
/// Override Window Height
#[arg(short = 'h', long)]
pub window_height: Option<f64>,
}
impl Into<Options> for OptionArgs {
fn into(self) -> Options {
Options {
theme: self.theme,
placeholder: self.placeholder,
search_restrict: self.search_restrict,
search_min_length: self.search_min_length,
search_max_length: self.search_max_length,
key_exec: self.key_exec,
key_exit: self.key_exit,
key_move_next: self.key_move_next,
key_move_prev: self.key_move_prev,
key_open_menu: self.key_open_menu,
key_close_menu: self.key_close_menu,
title: self.title,
decorate: self.deocorate,
fullscreen: self.fullscreen,
window_width: self.window_width,
window_height: self.window_height,
}
}
}
/// Valid CLI Commands and their Arguments
#[derive(Debug, Subcommand)]
enum Command {
/// Generate Complete RMenu Entry
Entry(EntryArgs),
/// Generate RMenu Entry Action Object /// Generate RMenu Entry Action Object
Action { Action(ActionArgs),
/// Set Name of Action /// Generate RMenu Options Settings
#[arg(short, long, default_value_t=String::from("main"))] Options(OptionArgs),
name: String,
/// Set Comment of Action
#[arg(short, long)]
comment: Option<String>,
/// Arguments to run As Action Command
#[clap(required = true, value_delimiter = ' ')]
args: Vec<String>,
/// Run in New Terminal Session if Active
#[arg(short, long)]
terminal: bool,
},
} }
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -67,29 +219,18 @@ struct Cli {
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
let result = match cli.command { let result = match cli.command {
Command::Entry { Command::Entry(args) => {
name, let entry: Entry = args.into();
comment, serde_json::to_string(&entry)
actions, }
icon, Command::Action(args) => {
icon_alt, let action: Action = args.into();
} => serde_json::to_string(&Entry { serde_json::to_string(&action)
name, }
comment, Command::Options(args) => {
actions, let options: Options = args.into();
icon, serde_json::to_string(&options)
icon_alt, }
}),
Command::Action {
name,
comment,
args,
terminal,
} => serde_json::to_string(&Action {
name,
exec: Method::new(args.join(" "), terminal),
comment,
}),
}; };
println!("{}", result.expect("Serialization Failed")); println!("{}", result.expect("Serialization Failed"));
} }

View File

@ -83,29 +83,52 @@ impl Entry {
/// Additional Plugin Option Overrides /// Additional Plugin Option Overrides
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename = "options")] #[serde(default, tag = "type", rename = "options")]
#[serde(default)]
pub struct Options { pub struct Options {
// base settings // base settings
theme: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<String>,
// search settings // search settings
placeholder: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
search_restrict: Option<String>, pub placeholder: Option<String>,
search_min_length: Option<usize>, #[serde(skip_serializing_if = "Option::is_none")]
search_max_length: Option<usize>, pub search_restrict: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_min_length: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_max_length: Option<usize>,
// key settings // key settings
key_exec: Option<Vec<String>>, #[serde(skip_serializing_if = "Option::is_none")]
key_exit: Option<Vec<String>>, pub key_exec: Option<Vec<String>>,
key_move_next: Option<Vec<String>>, #[serde(skip_serializing_if = "Option::is_none")]
key_move_prev: Option<Vec<String>>, pub key_exit: Option<Vec<String>>,
key_open_menu: Option<Vec<String>>, #[serde(skip_serializing_if = "Option::is_none")]
key_close_menu: Option<Vec<String>>, pub key_move_next: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_move_prev: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_open_menu: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_close_menu: Option<Vec<String>>,
// window settings // window settings
title: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
deocorate: Option<bool>, pub title: Option<String>,
fullscreen: Option<bool>, #[serde(skip_serializing_if = "Option::is_none")]
window_width: Option<usize>, pub decorate: Option<bool>,
window_height: Option<usize>, #[serde(skip_serializing_if = "Option::is_none")]
pub fullscreen: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub window_width: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub window_height: Option<f64>,
}
/// Valid RMenu Plugin Messages
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Message {
Entry(Entry),
Options(Options),
} }
/// Retrieve EXE of Self /// Retrieve EXE of Self

View File

@ -5,7 +5,7 @@ use std::str::FromStr;
use std::{fmt::Display, fs::read_to_string}; use std::{fmt::Display, fs::read_to_string};
use clap::Parser; use clap::Parser;
use rmenu_plugin::Entry; use rmenu_plugin::{Entry, Message};
use thiserror::Error; use thiserror::Error;
use crate::config::{Config, Keybind}; use crate::config::{Config, Keybind};
@ -161,6 +161,8 @@ pub enum RMenuError {
NoSuchPlugin(String), NoSuchPlugin(String),
#[error("Invalid Plugin Specified")] #[error("Invalid Plugin Specified")]
InvalidPlugin(String), InvalidPlugin(String),
#[error("Invalid Keybind Definition")]
InvalidKeybind(String),
#[error("Command Runtime Exception")] #[error("Command Runtime Exception")]
CommandError(Option<ExitStatus>), CommandError(Option<ExitStatus>),
#[error("Invalid JSON Entry Object")] #[error("Invalid JSON Entry Object")]
@ -168,7 +170,6 @@ pub enum RMenuError {
} }
pub type Result<T> = std::result::Result<T, RMenuError>; pub type Result<T> = std::result::Result<T, RMenuError>;
type MaybeEntry = Result<Entry>;
macro_rules! cli_replace { macro_rules! cli_replace {
($key:expr, $repl:expr) => { ($key:expr, $repl:expr) => {
@ -183,8 +184,22 @@ macro_rules! cli_replace {
}; };
} }
macro_rules! cli_keybind {
($key:expr, $repl:expr) => {
if let Some(bind_strings) = $repl.as_ref() {
let mut keybinds = vec![];
for bind_str in bind_strings.iter() {
let bind =
Keybind::from_str(bind_str).map_err(|e| RMenuError::InvalidKeybind(e))?;
keybinds.push(bind);
}
$key = keybinds;
}
};
}
impl Args { impl Args {
/// Load Configuration File and Update w/ Argument Overrides /// Load Configuration File
pub fn get_config(&self) -> Result<Config> { pub fn get_config(&self) -> Result<Config> {
// read configuration // read configuration
let path = self let path = self
@ -193,13 +208,18 @@ impl Args {
.map(|v| v.as_str()) .map(|v| v.as_str())
.unwrap_or(DEFAULT_CONFIG); .unwrap_or(DEFAULT_CONFIG);
let path = shellexpand::tilde(path).to_string(); let path = shellexpand::tilde(path).to_string();
let mut config: Config = match read_to_string(path) { let config: Config = match read_to_string(path) {
Ok(content) => serde_yaml::from_str(&content), Ok(content) => serde_yaml::from_str(&content),
Err(err) => { Err(err) => {
log::error!("Failed to Load Config: {err:?}"); log::error!("Failed to Load Config: {err:?}");
Ok(Config::default()) Ok(Config::default())
} }
}?; }?;
Ok(config)
}
/// Update Configuration w/ CLI Specified Settings
pub fn update_config(&self, mut config: Config) -> Config {
// override basic settings // override basic settings
config.terminal = self.terminal.clone().or_else(|| config.terminal); config.terminal = self.terminal.clone().or_else(|| config.terminal);
config.page_size = self.page_size.unwrap_or(config.page_size); config.page_size = self.page_size.unwrap_or(config.page_size);
@ -231,7 +251,7 @@ impl Args {
cli_replace!(config.window.transparent, self.transparent, true); cli_replace!(config.window.transparent, self.transparent, true);
cli_replace!(config.window.always_top, self.always_top, true); cli_replace!(config.window.always_top, self.always_top, true);
cli_replace!(config.window.fullscreen, self.fullscreen); cli_replace!(config.window.fullscreen, self.fullscreen);
Ok(config) config
} }
/// Load CSS or Default /// Load CSS or Default
@ -258,20 +278,50 @@ impl Args {
String::new() String::new()
} }
/// Read Entries Contained within the Given Reader fn read_entries<T: Read>(
fn read_entries<T: Read>(&self, reader: BufReader<T>) -> impl Iterator<Item = MaybeEntry> { &mut self,
let format = self.format.clone(); r: BufReader<T>,
reader v: &mut Vec<Entry>,
.lines() c: &mut Config,
.filter_map(|l| l.ok()) ) -> Result<()> {
.map(move |l| match format { for line in r.lines().filter_map(|l| l.ok()) {
Format::Json => serde_json::from_str(&l).map_err(|e| RMenuError::InvalidJson(e)), match &self.format {
Format::DMenu => Ok(Entry::echo(l.trim(), None)), Format::DMenu => v.push(Entry::echo(line.trim(), None)),
}) Format::Json => {
let msg: Message = serde_json::from_str(&line)?;
match msg {
Message::Entry(entry) => v.push(entry),
Message::Options(options) => {
// base settings
self.theme = self.theme.clone().or(options.theme);
// search settings
cli_replace!(c.search.placeholder, options.placeholder);
cli_replace!(c.search.restrict, options.search_restrict);
cli_replace!(c.search.min_length, options.search_min_length);
cli_replace!(c.search.max_length, options.search_max_length);
// keybind settings
cli_keybind!(c.keybinds.exec, options.key_exec);
cli_keybind!(c.keybinds.exec, options.key_exec);
cli_keybind!(c.keybinds.exit, options.key_exit);
cli_keybind!(c.keybinds.move_next, options.key_move_next);
cli_keybind!(c.keybinds.move_prev, options.key_move_prev);
cli_keybind!(c.keybinds.open_menu, options.key_open_menu);
cli_keybind!(c.keybinds.close_menu, options.key_close_menu);
// window settings
cli_replace!(c.window.title, options.title, true);
cli_replace!(c.window.decorate, options.decorate, true);
cli_replace!(c.window.size.width, options.window_width, true);
cli_replace!(c.window.size.height, options.window_height, true);
}
}
}
}
}
Ok(())
} }
/// Read Entries from a Configured Input /// Read Entries from a Configured Input
fn load_input(&self, input: &str) -> Result<Vec<Entry>> { fn load_input(&mut self, input: &str, config: &mut Config) -> Result<Vec<Entry>> {
// retrieve input file // retrieve input file
let input = if input == "-" { "/dev/stdin" } else { input }; let input = if input == "-" { "/dev/stdin" } else { input };
let fpath = shellexpand::tilde(input).to_string(); let fpath = shellexpand::tilde(input).to_string();
@ -280,24 +330,23 @@ impl Args {
let file = File::open(fpath)?; let file = File::open(fpath)?;
let reader = BufReader::new(file); let reader = BufReader::new(file);
let mut entries = vec![]; let mut entries = vec![];
for entry in self.read_entries(reader) { self.read_entries(reader, &mut entries, config)?;
entries.push(entry?);
}
Ok(entries) Ok(entries)
} }
/// Read Entries from a Plugin Source /// Read Entries from a Plugin Source
fn load_plugins(&self, config: &mut Config) -> Result<Vec<Entry>> { fn load_plugins(&mut self, config: &mut Config) -> Result<Vec<Entry>> {
let mut entries = vec![]; let mut entries = vec![];
for name in self.run.iter() { for name in self.run.clone().into_iter() {
// retrieve plugin configuration // retrieve plugin configuration
log::info!("running plugin: {name:?}"); log::info!("running plugin: {name:?}");
let plugin = config let plugin = config
.plugins .plugins
.get(name) .get(&name)
.cloned()
.ok_or_else(|| RMenuError::NoSuchPlugin(name.to_owned()))?; .ok_or_else(|| RMenuError::NoSuchPlugin(name.to_owned()))?;
// read cache when available // read cache when available
match crate::cache::read_cache(name, plugin) { match crate::cache::read_cache(&name, &plugin) {
Err(err) => log::error!("cache read failed: {err:?}"), Err(err) => log::error!("cache read failed: {err:?}"),
Ok(cached) => { Ok(cached) => {
entries.extend(cached); entries.extend(cached);
@ -323,9 +372,7 @@ impl Args {
.as_mut() .as_mut()
.ok_or_else(|| RMenuError::CommandError(None))?; .ok_or_else(|| RMenuError::CommandError(None))?;
let reader = BufReader::new(stdout); let reader = BufReader::new(stdout);
for entry in self.read_entries(reader) { self.read_entries(reader, &mut entries, config)?;
entries.push(entry?);
}
let status = command.wait()?; let status = command.wait()?;
if !status.success() { if !status.success() {
return Err(RMenuError::CommandError(Some(status))); return Err(RMenuError::CommandError(Some(status)));
@ -334,7 +381,7 @@ impl Args {
if config.search.placeholder.is_none() { if config.search.placeholder.is_none() {
config.search.placeholder = plugin.placeholder.clone(); config.search.placeholder = plugin.placeholder.clone();
} }
match crate::cache::write_cache(name, plugin, &entries) { match crate::cache::write_cache(&name, &plugin, &entries) {
Ok(_) => {} Ok(_) => {}
Err(err) => log::error!("cache write error: {err:?}"), Err(err) => log::error!("cache write error: {err:?}"),
} }
@ -343,7 +390,7 @@ impl Args {
} }
/// Load Entries from Enabled/Configured Entry-Sources /// Load Entries from Enabled/Configured Entry-Sources
pub fn get_entries(&self, config: &mut Config) -> Result<Vec<Entry>> { pub fn get_entries(&mut self, config: &mut Config) -> Result<Vec<Entry>> {
// configure default source if none are given // configure default source if none are given
let mut input = self.input.clone(); let mut input = self.input.clone();
let mut entries = vec![]; let mut entries = vec![];
@ -352,7 +399,7 @@ impl Args {
} }
// load entries // load entries
if let Some(input) = input { if let Some(input) = input {
entries.extend(self.load_input(&input)?); entries.extend(self.load_input(&input, config)?);
} }
entries.extend(self.load_plugins(config)?); entries.extend(self.load_plugins(config)?);
Ok(entries) Ok(entries)

View File

@ -148,7 +148,7 @@ impl Default for WindowConfig {
} }
/// Cache Settings for Configured RMenu Plugins /// Cache Settings for Configured RMenu Plugins
#[derive(Debug, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum CacheSetting { pub enum CacheSetting {
NoCache, NoCache,
Never, Never,
@ -190,7 +190,7 @@ impl Default for CacheSetting {
} }
/// RMenu Data-Source Plugin Configuration /// RMenu Data-Source Plugin Configuration
#[derive(Debug, PartialEq, Deserialize)] #[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct PluginConfig { pub struct PluginConfig {
pub exec: Vec<String>, pub exec: Vec<String>,
#[serde(default)] #[serde(default)]

View File

@ -42,13 +42,14 @@ fn main() -> cli::Result<()> {
env_logger::init(); env_logger::init();
// parse cli and retrieve values for app // parse cli and retrieve values for app
let cli = cli::Args::parse(); let mut cli = cli::Args::parse();
let mut config = cli.get_config()?; let mut config = cli.get_config()?;
let entries = cli.get_entries(&mut config)?;
let css = cli.get_css(); let css = cli.get_css();
let theme = cli.get_theme(); let theme = cli.get_theme();
let entries = cli.get_entries(&mut config)?;
// update config based on entries // update config based on cli-settings and entries
config = cli.update_config(config);
config.use_icons = config.use_icons config.use_icons = config.use_icons
&& entries && entries
.iter() .iter()