From d56680fc0d7eea8194dac373190e26d8becd7154 Mon Sep 17 00:00:00 2001 From: imgurbot12 Date: Tue, 18 Jul 2023 22:29:31 -0700 Subject: [PATCH] feat: start on formalizing configuration & app --- rmenu/Cargo.toml | 7 +- rmenu/public/default.css | 27 ++++++-- rmenu/src/config.rs | 9 ++- rmenu/src/gui.rs | 87 ++++++++++++++++++++----- rmenu/src/main.rs | 135 +++++++++++++++++++++++++++------------ rmenu/src/search.rs | 2 +- rmenu/src/state.rs | 60 ++++++++--------- 7 files changed, 230 insertions(+), 97 deletions(-) diff --git a/rmenu/Cargo.toml b/rmenu/Cargo.toml index ff167d0..12266fc 100644 --- a/rmenu/Cargo.toml +++ b/rmenu/Cargo.toml @@ -7,10 +7,13 @@ edition = "2021" [dependencies] clap = { version = "4.3.15", features = ["derive"] } -dioxus = { git = "https://github.com/DioxusLabs/dioxus", version = "0.3.2" } -dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", version = "0.3.0" } +dioxus = "0.3.2" +dioxus-desktop = "0.3.0" +dirs = "5.0.1" keyboard-types = "0.6.2" +log = "0.4.19" regex = { version = "1.9.1", features = ["pattern"] } rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" } serde = { version = "1.0.171", features = ["derive"] } serde_json = "1.0.103" +toml = "0.7.6" diff --git a/rmenu/public/default.css b/rmenu/public/default.css index 6d90509..9ca8818 100644 --- a/rmenu/public/default.css +++ b/rmenu/public/default.css @@ -8,17 +8,19 @@ input { min-width: 99%; } -div.result { +div.selected { + background-color: lightblue; +} + +/* Result CSS */ + +div.result, div.action { display: flex; align-items: center; justify-content: left; } -div.selected { - background-color: lightblue; -} - -div.result > div { +div.result > div, div.action > div { margin: 2px 5px; } @@ -42,3 +44,16 @@ div.result > div.name { div.result > div.comment { flex: 1; } + +/* Action CSS */ + +div.actions { + display: none; + padding-left: 5%; +} + +div.actions.active { + display: flex; + flex-direction: column; + justify-content: center; +} diff --git a/rmenu/src/config.rs b/rmenu/src/config.rs index b0a69ed..29420cc 100644 --- a/rmenu/src/config.rs +++ b/rmenu/src/config.rs @@ -1,15 +1,20 @@ use serde::{Deserialize, Serialize}; +use std::ffi::OsString; #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Config { - pub regex: bool, + pub css: Vec, + pub use_icons: bool, + pub search_regex: bool, pub ignore_case: bool, } impl Default for Config { fn default() -> Self { Self { - regex: true, + css: vec![], + use_icons: true, + search_regex: false, ignore_case: true, } } diff --git a/rmenu/src/gui.rs b/rmenu/src/gui.rs index 40e297d..6e67231 100644 --- a/rmenu/src/gui.rs +++ b/rmenu/src/gui.rs @@ -13,44 +13,90 @@ pub fn run(app: App) { #[derive(PartialEq, Props)] struct GEntry<'a> { - i: usize, - o: &'a Entry, - selected: bool, + index: usize, + entry: &'a Entry, + pos: usize, + subpos: usize, } fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> { - let classes = if cx.props.selected { "selected" } else { "" }; + // build css classes for result and actions (if nessesary) + let main_select = cx.props.index == cx.props.pos; + let action_select = main_select && cx.props.subpos > 0; + let action_classes = match action_select { + true => "active", + false => "", + }; + let result_classes = match main_select && !action_select { + true => "selected", + false => "", + }; + // build sub-actions if present + let actions = cx + .props + .entry + .actions + .iter() + .skip(1) + .enumerate() + .map(|(idx, action)| { + let act_class = match action_select && idx + 1 == cx.props.subpos { + true => "selected", + false => "", + }; + cx.render(rsx! { + div { + class: "action {act_class}", + div { + class: "action-name", + "{action.name}" + } + div { + class: "action-comment", + if let Some(comment) = action.comment.as_ref() { + format!("- {comment}") + } + } + } + }) + }); cx.render(rsx! { div { - id: "result-{cx.props.i}", - class: "result {classes}", + id: "result-{cx.props.index}", + class: "result {result_classes}", div { class: "icon", - if let Some(icon) = cx.props.o.icon.as_ref() { + if let Some(icon) = cx.props.entry.icon.as_ref() { cx.render(rsx! { img { src: "{icon}" } }) } } div { class: "name", - "{cx.props.o.name}" + "{cx.props.entry.name}" } div { class: "comment", - if let Some(comment) = cx.props.o.comment.as_ref() { + if let Some(comment) = cx.props.entry.comment.as_ref() { format!("- {comment}") } } } + div { + id: "result-{cx.props.index}-actions", + class: "actions {action_classes}", + actions.into_iter() + } }) } fn App(cx: Scope) -> Element { let search = use_state(cx, || "".to_string()); - let position = use_state(cx, || 0); // retrieve build results tracker let results = &cx.props.entries; - let mut tracker = PosTracker::new(position, results); + let tracker = PosTracker::new(cx, results); + let (pos, subpos) = tracker.position(); + println!("pos: {pos}, {subpos}"); // keyboard events let eval = dioxus_desktop::use_eval(cx); @@ -60,8 +106,14 @@ fn App(cx: Scope) -> Element { Code::ArrowUp => tracker.shift_up(), Code::ArrowDown => tracker.shift_down(), Code::Tab => match evt.modifiers().contains(Modifiers::SHIFT) { - true => tracker.close_menu(), - false => tracker.open_menu(), + true => { + println!("close menu"); + tracker.close_menu() + } + false => { + println!("open menu!"); + tracker.open_menu() + } }, _ => println!("key: {:?}", evt.key()), } @@ -76,9 +128,14 @@ fn App(cx: Scope) -> Element { .iter() .filter(|entry| searchfn(entry)) .enumerate() - .map(|(i, entry)| { + .map(|(index, entry)| { cx.render(rsx! { - TableEntry{ i: i, o: entry, selected: (i + 1) == active } + TableEntry{ + index: index, + entry: entry, + pos: pos, + subpos: subpos, + } }) }) .collect(); diff --git a/rmenu/src/main.rs b/rmenu/src/main.rs index f85733c..c9c8bc5 100644 --- a/rmenu/src/main.rs +++ b/rmenu/src/main.rs @@ -1,5 +1,6 @@ +use std::ffi::OsString; use std::fs::{read_to_string, File}; -use std::io::{prelude::*, BufReader, Error}; +use std::io::{prelude::*, BufReader, Error, ErrorKind}; mod config; mod gui; @@ -9,6 +10,15 @@ mod state; use clap::*; use rmenu_plugin::Entry; +/// Application State for GUI +#[derive(Debug, PartialEq)] +pub struct App { + css: String, + name: String, + entries: Vec, + config: config::Config, +} + /// Rofi Clone (Built with Rust) #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -22,10 +32,87 @@ pub struct Args { msgpack: bool, #[arg(short, long)] run: Vec, + #[arg(short, long)] + config: Option, #[arg(long)] - css: Option, + css: Vec, } +impl Args { + /// Load Config based on CLI Settings + fn config(&self) -> Result { + let path = match &self.config { + Some(path) => path.to_owned(), + None => match dirs::config_dir() { + Some(mut dir) => { + dir.push("rmenu"); + dir.push("config.toml"); + dir.into() + } + None => { + return Err(Error::new(ErrorKind::NotFound, "$HOME not found")); + } + }, + }; + log::debug!("loading config from {path:?}"); + let cfg = match read_to_string(path) { + Ok(cfg) => cfg, + Err(err) => { + log::error!("failed to load config: {err:?}"); + return Ok(config::Config::default()); + } + }; + toml::from_str(&cfg).map_err(|e| Error::new(ErrorKind::InvalidInput, format!("{e}"))) + } + + /// Load Entries From Input (Stdin by Default) + fn load_default(&self) -> Result, Error> { + let fpath = match self.input.as_str() { + "-" => "/dev/stdin", + _ => &self.input, + }; + let file = File::open(fpath)?; + let reader = BufReader::new(file); + let mut entries = vec![]; + for line in reader.lines() { + let entry = serde_json::from_str::(&line?)?; + entries.push(entry); + } + Ok(entries) + } + + /// Load Entries From Specified Sources + fn load_sources(&self, cfg: &config::Config) -> Result, Error> { + todo!() + } + + /// Load Application + pub fn parse_app() -> Result { + let args = Self::parse(); + let mut config = args.config()?; + // load css files from settings + config.css.extend(args.css.clone()); + let mut css = vec![]; + for path in config.css.iter() { + let src = read_to_string(path)?; + css.push(src); + } + // load entries from configured sources + let entries = match args.run.len() > 0 { + true => args.load_sources(&config)?, + false => args.load_default()?, + }; + // generate app object + return Ok(App { + css: css.join("\n"), + name: "rmenu".to_owned(), + entries, + config, + }); + } +} + +//TODO: add better errors with `thiserror` to add context //TODO: improve search w/ options for regex/case-insensivity/modes? //TODO: add secondary menu for sub-actions aside from the main action //TODO: improve looks and css @@ -35,45 +122,9 @@ pub struct Args { // - allow/disable icons (also available via CLI) // - custom keybindings (some available via CLI?) -/// Application State for GUI -#[derive(Debug, PartialEq)] -pub struct App { - css: String, - name: String, - entries: Vec, - config: config::Config, -} - -fn default(args: &Args) -> Result { - // read entries from specified input - let fpath = match args.input.as_str() { - "-" => "/dev/stdin", - _ => &args.input, - }; - let file = File::open(fpath)?; - let reader = BufReader::new(file); - let mut entries = vec![]; - for line in reader.lines() { - let entry = serde_json::from_str::(&line?)?; - entries.push(entry); - } - // generate app object based on configured args - let css = args - .css - .clone() - .unwrap_or("rmenu/public/default.css".to_owned()); - let args = App { - name: "default".to_string(), - css: read_to_string(css)?, - entries, - config: Default::default(), - }; - Ok(args) -} - -fn main() { - let cli = Args::parse(); - let app = default(&cli).unwrap(); - println!("{:?}", app); +fn main() -> Result<(), Error> { + // parse cli / config / application-settings + let app = Args::parse_app()?; gui::run(app); + Ok(()) } diff --git a/rmenu/src/search.rs b/rmenu/src/search.rs index ae1f1ba..ba84f27 100644 --- a/rmenu/src/search.rs +++ b/rmenu/src/search.rs @@ -31,7 +31,7 @@ macro_rules! search { /// Generate a new dynamic Search Function based on /// Configurtaion Settigns and Search-String pub fn new_searchfn(cfg: &Config, search: &str) -> Box bool> { - if cfg.regex { + if cfg.search_regex { let regex = RegexBuilder::new(search) .case_insensitive(cfg.ignore_case) .build(); diff --git a/rmenu/src/state.rs b/rmenu/src/state.rs index 86df84c..6477eaa 100644 --- a/rmenu/src/state.rs +++ b/rmenu/src/state.rs @@ -1,66 +1,68 @@ /// Application State Trackers and Utilities -use dioxus::prelude::UseState; +use dioxus::prelude::{use_state, Scope, UseState}; use rmenu_plugin::Entry; +use crate::App; + pub struct PosTracker<'a> { pos: &'a UseState, - subpos: usize, + subpos: &'a UseState, results: &'a Vec, } impl<'a> PosTracker<'a> { - pub fn new(pos: &UseState, results: &Vec) -> Self { + pub fn new(cx: Scope<'a, App>, results: &'a Vec) -> Self { + let pos = use_state(cx, || 0); + let subpos = use_state(cx, || 0); Self { pos, + subpos, results, - subpos: 0, } } /// Move X Primary Results Upwards - pub fn move_up(&mut self, x: usize) { - self.subpos = 0; + pub fn move_up(&self, x: usize) { + self.subpos.set(0); self.pos.modify(|v| if v >= &x { v - x } else { 0 }) } /// Move X Primary Results Downwards - pub fn move_down(&mut self, x: usize) { - self.subpos = 0; + pub fn move_down(&self, x: usize) { + self.subpos.set(0); self.pos - .modify(|v| std::cmp::min(v + x, self.results.len())) + .modify(|v| std::cmp::min(v + x, self.results.len() - 1)) } /// Get Current Position/SubPosition pub fn position(&self) -> (usize, usize) { - (*self.pos.get(), self.subpos) + (self.pos.get().clone(), self.subpos.get().clone()) } /// Move Position To SubMenu if it Exists - pub fn open_menu(&mut self) { - self.subpos = 1; + pub fn open_menu(&self) { + let index = *self.pos.get(); + let result = &self.results[index]; + if result.actions.len() > 0 { + self.subpos.set(1); + } } // Reset and Close SubMenu Position - pub fn close_menu(&mut self) { - self.subpos = 0; + pub fn close_menu(&self) { + self.subpos.set(0); } /// Move Up Once With Context of SubMenu - pub fn shift_up(&mut self) { - let index = *self.pos.get(); - if index == 0 { - return; - } - let result = self.results[index]; - if self.subpos > 0 { - self.subpos -= 1; + pub fn shift_up(&self) { + if self.subpos.get() > &0 { + self.subpos.modify(|v| v - 1); return; } self.move_up(1) } /// Move Down Once With Context of SubMenu - pub fn shift_down(&mut self) { + pub fn shift_down(&self) { let index = *self.pos.get(); - if index == 0 { - return self.move_down(1); - } - let result = self.results[index]; - if self.subpos > 0 && self.subpos < result.actions.len() { - self.subpos += 1; + let result = &self.results[index]; + let subpos = *self.subpos.get(); + println!("modify subpos? {} {}", subpos, result.actions.len()); + if subpos > 0 && subpos < result.actions.len() - 1 { + self.subpos.modify(|v| v + 1); return; } self.move_down(1)