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]]
name = "rmenu-build"
path = "src/bin/main.rs"
required-features = ["cli"]
[features]
default = []
cli = ["clap", "serde_json"]
[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_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 clap::{Parser, Subcommand};
use clap::{Args, Parser, Subcommand};
/// Parse Action from JSON
fn parse_action(action: &str) -> Result<Action, serde_json::Error> {
@ -17,42 +19,192 @@ fn parse_action(action: &str) -> Result<Action, serde_json::Error> {
// 2. plugin/source latest merged options
// 3. configuration settings
//TODO: add python library to build entries as well
/// Valid Action Modes
#[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
#[arg(short, long, default_value_t=String::from("main"))]
name: String,
/// Set Comment of Entry
#[arg(short, long)]
comment: Option<String>,
/// Precomposed Action JSON Objects
#[arg(short, long, value_parser=parse_action)]
#[clap(required = true)]
actions: Vec<Action>,
/// Icon Image Path
#[arg(short, long)]
icon: Option<String>,
/// Alternative Image Text/HTML
#[arg(short = 'o', long)]
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 {
/// Set Name of Entry
#[arg(short, long, default_value_t=String::from("main"))]
name: String,
/// Set Comment of Entry
#[arg(short, long)]
comment: Option<String>,
/// Precomposed Action JSON Objects
#[arg(short, long, value_parser=parse_action)]
#[clap(required = true)]
actions: Vec<Action>,
/// Icon Image Path
#[arg(short, long)]
icon: Option<String>,
/// Alternative Image Text/HTML
#[arg(short = 'o', long)]
icon_alt: Option<String>,
},
Entry(EntryArgs),
/// Generate RMenu Entry Action Object
Action {
/// 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>,
/// Run in New Terminal Session if Active
#[arg(short, long)]
terminal: bool,
},
Action(ActionArgs),
/// Generate RMenu Options Settings
Options(OptionArgs),
}
#[derive(Debug, Parser)]
@ -67,29 +219,18 @@ struct Cli {
fn main() {
let cli = Cli::parse();
let result = match cli.command {
Command::Entry {
name,
comment,
actions,
icon,
icon_alt,
} => serde_json::to_string(&Entry {
name,
comment,
actions,
icon,
icon_alt,
}),
Command::Action {
name,
comment,
args,
terminal,
} => serde_json::to_string(&Action {
name,
exec: Method::new(args.join(" "), terminal),
comment,
}),
Command::Entry(args) => {
let entry: Entry = args.into();
serde_json::to_string(&entry)
}
Command::Action(args) => {
let action: Action = args.into();
serde_json::to_string(&action)
}
Command::Options(args) => {
let options: Options = args.into();
serde_json::to_string(&options)
}
};
println!("{}", result.expect("Serialization Failed"));
}

View File

@ -83,29 +83,52 @@ impl Entry {
/// Additional Plugin Option Overrides
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename = "options")]
#[serde(default)]
#[serde(default, tag = "type", rename = "options")]
pub struct Options {
// base settings
theme: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<String>,
// search settings
placeholder: Option<String>,
search_restrict: Option<String>,
search_min_length: Option<usize>,
search_max_length: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
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_exec: Option<Vec<String>>,
key_exit: Option<Vec<String>>,
key_move_next: Option<Vec<String>>,
key_move_prev: Option<Vec<String>>,
key_open_menu: Option<Vec<String>>,
key_close_menu: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_exec: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_exit: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
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
title: Option<String>,
deocorate: Option<bool>,
fullscreen: Option<bool>,
window_width: Option<usize>,
window_height: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decorate: Option<bool>,
#[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

View File

@ -5,7 +5,7 @@ use std::str::FromStr;
use std::{fmt::Display, fs::read_to_string};
use clap::Parser;
use rmenu_plugin::Entry;
use rmenu_plugin::{Entry, Message};
use thiserror::Error;
use crate::config::{Config, Keybind};
@ -161,6 +161,8 @@ pub enum RMenuError {
NoSuchPlugin(String),
#[error("Invalid Plugin Specified")]
InvalidPlugin(String),
#[error("Invalid Keybind Definition")]
InvalidKeybind(String),
#[error("Command Runtime Exception")]
CommandError(Option<ExitStatus>),
#[error("Invalid JSON Entry Object")]
@ -168,7 +170,6 @@ pub enum RMenuError {
}
pub type Result<T> = std::result::Result<T, RMenuError>;
type MaybeEntry = Result<Entry>;
macro_rules! cli_replace {
($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 {
/// Load Configuration File and Update w/ Argument Overrides
/// Load Configuration File
pub fn get_config(&self) -> Result<Config> {
// read configuration
let path = self
@ -193,13 +208,18 @@ impl Args {
.map(|v| v.as_str())
.unwrap_or(DEFAULT_CONFIG);
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),
Err(err) => {
log::error!("Failed to Load Config: {err:?}");
Ok(Config::default())
}
}?;
Ok(config)
}
/// Update Configuration w/ CLI Specified Settings
pub fn update_config(&self, mut config: Config) -> Config {
// override basic settings
config.terminal = self.terminal.clone().or_else(|| config.terminal);
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.always_top, self.always_top, true);
cli_replace!(config.window.fullscreen, self.fullscreen);
Ok(config)
config
}
/// Load CSS or Default
@ -258,20 +278,50 @@ impl Args {
String::new()
}
/// Read Entries Contained within the Given Reader
fn read_entries<T: Read>(&self, reader: BufReader<T>) -> impl Iterator<Item = MaybeEntry> {
let format = self.format.clone();
reader
.lines()
.filter_map(|l| l.ok())
.map(move |l| match format {
Format::Json => serde_json::from_str(&l).map_err(|e| RMenuError::InvalidJson(e)),
Format::DMenu => Ok(Entry::echo(l.trim(), None)),
})
fn read_entries<T: Read>(
&mut self,
r: BufReader<T>,
v: &mut Vec<Entry>,
c: &mut Config,
) -> Result<()> {
for line in r.lines().filter_map(|l| l.ok()) {
match &self.format {
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
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
let input = if input == "-" { "/dev/stdin" } else { input };
let fpath = shellexpand::tilde(input).to_string();
@ -280,24 +330,23 @@ impl Args {
let file = File::open(fpath)?;
let reader = BufReader::new(file);
let mut entries = vec![];
for entry in self.read_entries(reader) {
entries.push(entry?);
}
self.read_entries(reader, &mut entries, config)?;
Ok(entries)
}
/// 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![];
for name in self.run.iter() {
for name in self.run.clone().into_iter() {
// retrieve plugin configuration
log::info!("running plugin: {name:?}");
let plugin = config
.plugins
.get(name)
.get(&name)
.cloned()
.ok_or_else(|| RMenuError::NoSuchPlugin(name.to_owned()))?;
// 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:?}"),
Ok(cached) => {
entries.extend(cached);
@ -323,9 +372,7 @@ impl Args {
.as_mut()
.ok_or_else(|| RMenuError::CommandError(None))?;
let reader = BufReader::new(stdout);
for entry in self.read_entries(reader) {
entries.push(entry?);
}
self.read_entries(reader, &mut entries, config)?;
let status = command.wait()?;
if !status.success() {
return Err(RMenuError::CommandError(Some(status)));
@ -334,7 +381,7 @@ impl Args {
if config.search.placeholder.is_none() {
config.search.placeholder = plugin.placeholder.clone();
}
match crate::cache::write_cache(name, plugin, &entries) {
match crate::cache::write_cache(&name, &plugin, &entries) {
Ok(_) => {}
Err(err) => log::error!("cache write error: {err:?}"),
}
@ -343,7 +390,7 @@ impl Args {
}
/// 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
let mut input = self.input.clone();
let mut entries = vec![];
@ -352,7 +399,7 @@ impl Args {
}
// load entries
if let Some(input) = input {
entries.extend(self.load_input(&input)?);
entries.extend(self.load_input(&input, config)?);
}
entries.extend(self.load_plugins(config)?);
Ok(entries)

View File

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

View File

@ -42,13 +42,14 @@ fn main() -> cli::Result<()> {
env_logger::init();
// 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 entries = cli.get_entries(&mut config)?;
let css = cli.get_css();
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
&& entries
.iter()