diff --git a/rmenu-plugin/Cargo.toml b/rmenu-plugin/Cargo.toml index 8f180fc..fce0de4 100644 --- a/rmenu-plugin/Cargo.toml +++ b/rmenu-plugin/Cargo.toml @@ -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" diff --git a/rmenu-plugin/src/bin/main.rs b/rmenu-plugin/src/bin/main.rs index 7322f59..9a0fa31 100644 --- a/rmenu-plugin/src/bin/main.rs +++ b/rmenu-plugin/src/bin/main.rs @@ -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 { @@ -17,42 +19,192 @@ fn parse_action(action: &str) -> Result { // 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 { + 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, + /// Arguments to run As Action Command + #[clap(required = true, value_delimiter = ' ')] + args: Vec, + /// Action Mode + #[arg(short, long, default_value_t=ActionMode::Run)] + mode: ActionMode, +} + +impl Into 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, + /// Precomposed Action JSON Objects + #[arg(short, long, value_parser=parse_action)] + #[clap(required = true)] + actions: Vec, + /// Icon Image Path + #[arg(short, long)] + icon: Option, + /// Alternative Image Text/HTML + #[arg(short = 'o', long)] + icon_alt: Option, +} + +impl Into 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, + // search settings + /// Override Default Placeholder + #[arg(short, long)] + pub placeholder: Option, + /// Override Search Restriction + #[arg(short = 'r', long)] + pub search_restrict: Option, + /// Override Minimum Search Length + #[arg(short = 'm', long)] + pub search_min_length: Option, + /// Override Maximum Search Length + #[arg(short = 'M', long)] + pub search_max_length: Option, + // key settings + /// Override Execution Keybinds + #[arg(short = 'e', long)] + pub key_exec: Option>, + /// Override Program-Exit Keybinds + #[arg(short = 'E', long)] + pub key_exit: Option>, + /// Override Move-Next Keybinds + #[arg(short = 'n', long)] + pub key_move_next: Option>, + /// Override Move-Previous Keybinds + #[arg(short = 'p', long)] + pub key_move_prev: Option>, + /// Override Open-Menu Keybinds + #[arg(short = 'o', long)] + pub key_open_menu: Option>, + /// Override Close-Menu Keybinds + #[arg(short = 'c', long)] + pub key_close_menu: Option>, + // window settings + /// Override Window Title + #[arg(short, long)] + pub title: Option, + /// Override Window Deocration Settings + #[arg(short, long)] + pub deocorate: Option, + /// Override Window Fullscreen Settings + #[arg(short, long)] + pub fullscreen: Option, + /// Override Window Width + #[arg(short = 'w', long)] + pub window_width: Option, + /// Override Window Height + #[arg(short = 'h', long)] + pub window_height: Option, +} + +impl Into 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, - /// Precomposed Action JSON Objects - #[arg(short, long, value_parser=parse_action)] - #[clap(required = true)] - actions: Vec, - /// Icon Image Path - #[arg(short, long)] - icon: Option, - /// Alternative Image Text/HTML - #[arg(short = 'o', long)] - icon_alt: Option, - }, + 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, - /// Arguments to run As Action Command - #[clap(required = true, value_delimiter = ' ')] - args: Vec, - /// 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")); } diff --git a/rmenu-plugin/src/lib.rs b/rmenu-plugin/src/lib.rs index c7516f3..a5ddde0 100644 --- a/rmenu-plugin/src/lib.rs +++ b/rmenu-plugin/src/lib.rs @@ -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, + #[serde(skip_serializing_if = "Option::is_none")] + pub theme: Option, // search settings - placeholder: Option, - search_restrict: Option, - search_min_length: Option, - search_max_length: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub placeholder: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub search_restrict: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub search_min_length: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub search_max_length: Option, // key settings - key_exec: Option>, - key_exit: Option>, - key_move_next: Option>, - key_move_prev: Option>, - key_open_menu: Option>, - key_close_menu: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub key_exec: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub key_exit: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub key_move_next: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub key_move_prev: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub key_open_menu: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub key_close_menu: Option>, // window settings - title: Option, - deocorate: Option, - fullscreen: Option, - window_width: Option, - window_height: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub decorate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub fullscreen: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub window_width: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub window_height: Option, +} + +/// 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 diff --git a/rmenu/src/cli.rs b/rmenu/src/cli.rs index 202893f..37e6d19 100644 --- a/rmenu/src/cli.rs +++ b/rmenu/src/cli.rs @@ -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), #[error("Invalid JSON Entry Object")] @@ -168,7 +170,6 @@ pub enum RMenuError { } pub type Result = std::result::Result; -type MaybeEntry = Result; 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 { // 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(&self, reader: BufReader) -> impl Iterator { - 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( + &mut self, + r: BufReader, + v: &mut Vec, + 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> { + fn load_input(&mut self, input: &str, config: &mut Config) -> Result> { // 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> { + fn load_plugins(&mut self, config: &mut Config) -> Result> { 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> { + pub fn get_entries(&mut self, config: &mut Config) -> Result> { // 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) diff --git a/rmenu/src/config.rs b/rmenu/src/config.rs index c83c27f..1f69263 100644 --- a/rmenu/src/config.rs +++ b/rmenu/src/config.rs @@ -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, #[serde(default)] diff --git a/rmenu/src/main.rs b/rmenu/src/main.rs index 121eaed..67c7ec0 100644 --- a/rmenu/src/main.rs +++ b/rmenu/src/main.rs @@ -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()