diff --git a/rmenu/Cargo.toml b/rmenu/Cargo.toml index 8c3d0fc..ff167d0 100644 --- a/rmenu/Cargo.toml +++ b/rmenu/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rmenu" +name = "rmenu" version = "0.0.0" edition = "2021" @@ -7,12 +7,10 @@ edition = "2021" [dependencies] clap = { version = "4.3.15", features = ["derive"] } -dioxus = "0.3.2" +dioxus = { git = "https://github.com/DioxusLabs/dioxus", version = "0.3.2" } +dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus", version = "0.3.0" } +keyboard-types = "0.6.2" +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" - -[target.'cfg(any(unix, windows))'.dependencies] -dioxus-desktop = { version = "0.3.0" } - -[target.'cfg(target_family = "wasm")'.dependencies] -dioxus-web = { version = "0.3.1" } diff --git a/rmenu/public/default.css b/rmenu/public/default.css index 48f3270..6d90509 100644 --- a/rmenu/public/default.css +++ b/rmenu/public/default.css @@ -14,6 +14,10 @@ div.result { justify-content: left; } +div.selected { + background-color: lightblue; +} + div.result > div { margin: 2px 5px; } diff --git a/rmenu/src/config.rs b/rmenu/src/config.rs new file mode 100644 index 0000000..b0a69ed --- /dev/null +++ b/rmenu/src/config.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Config { + pub regex: bool, + pub ignore_case: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + regex: true, + ignore_case: true, + } + } +} diff --git a/rmenu/src/gui.rs b/rmenu/src/gui.rs index 9844481..40e297d 100644 --- a/rmenu/src/gui.rs +++ b/rmenu/src/gui.rs @@ -1,26 +1,29 @@ #![allow(non_snake_case)] use dioxus::prelude::*; +use keyboard_types::{Code, Modifiers}; use rmenu_plugin::Entry; +use crate::search::new_searchfn; +use crate::state::PosTracker; use crate::App; pub fn run(app: App) { - #[cfg(target_family = "wasm")] - dioxus_web::launch(App, app, dioxus_web::Config::default()); - - #[cfg(any(windows, unix))] dioxus_desktop::launch_with_props(App, app, dioxus_desktop::Config::default()); } #[derive(PartialEq, Props)] struct GEntry<'a> { + i: usize, o: &'a Entry, + selected: bool, } fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> { + let classes = if cx.props.selected { "selected" } else { "" }; cx.render(rsx! { div { - class: "result", + id: "result-{cx.props.i}", + class: "result {classes}", div { class: "icon", if let Some(icon) = cx.props.o.icon.as_ref() { @@ -43,27 +46,54 @@ fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> { 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 searchstr = search.as_str(); - let results_rendered = results + let mut tracker = PosTracker::new(position, results); + + // keyboard events + let eval = dioxus_desktop::use_eval(cx); + let change_evt = move |evt: KeyboardEvent| { + match evt.code() { + // modify position + 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(), + }, + _ => println!("key: {:?}", evt.key()), + } + // always set focus back on input + let js = "document.getElementById(`search`).focus()"; + eval(js.to_owned()); + }; + + // pre-render results into elements + let searchfn = new_searchfn(&cx.props.config, &search); + let results_rendered: Vec = results .iter() - .filter(|entry| { - if entry.name.contains(searchstr) { - return true; - } - if let Some(comment) = entry.comment.as_ref() { - return comment.contains(searchstr); - } - false + .filter(|entry| searchfn(entry)) + .enumerate() + .map(|(i, entry)| { + cx.render(rsx! { + TableEntry{ i: i, o: entry, selected: (i + 1) == active } + }) }) - .map(|entry| cx.render(rsx! { TableEntry{ o: entry } })); + .collect(); cx.render(rsx! { style { "{cx.props.css}" } - input { - value: "{search}", - oninput: move |evt| search.set(evt.value.clone()), + div { + onkeydown: change_evt, + input { + id: "search", + value: "{search}", + oninput: move |evt| search.set(evt.value.clone()), + + } + results_rendered.into_iter() } - results_rendered }) } diff --git a/rmenu/src/main.rs b/rmenu/src/main.rs index 1708deb..f85733c 100644 --- a/rmenu/src/main.rs +++ b/rmenu/src/main.rs @@ -1,7 +1,10 @@ use std::fs::{read_to_string, File}; use std::io::{prelude::*, BufReader, Error}; +mod config; mod gui; +mod search; +mod state; use clap::*; use rmenu_plugin::Entry; @@ -24,22 +27,28 @@ pub struct Args { } //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 +//TODO: config +// - default and cli accessable modules (instead of piped in) +// - 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 = if args.input == "-" { - "/dev/stdin" - } else { - &args.input + let fpath = match args.input.as_str() { + "-" => "/dev/stdin", + _ => &args.input, }; let file = File::open(fpath)?; let reader = BufReader::new(file); @@ -57,6 +66,7 @@ fn default(args: &Args) -> Result { name: "default".to_string(), css: read_to_string(css)?, entries, + config: Default::default(), }; Ok(args) } diff --git a/rmenu/src/search.rs b/rmenu/src/search.rs new file mode 100644 index 0000000..ae1f1ba --- /dev/null +++ b/rmenu/src/search.rs @@ -0,0 +1,49 @@ +use regex::RegexBuilder; +use rmenu_plugin::Entry; + +use crate::config::Config; + +macro_rules! search { + ($search:expr) => { + Box::new(move |entry: &Entry| { + if entry.name.contains($search) { + return true; + } + if let Some(comment) = entry.comment.as_ref() { + return comment.contains($search); + } + false + }) + }; + ($search:expr,$mod:ident) => { + Box::new(move |entry: &Entry| { + if entry.name.$mod().contains($search) { + return true; + } + if let Some(comment) = entry.comment.as_ref() { + return comment.$mod().contains($search); + } + false + }) + }; +} + +/// 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 { + let regex = RegexBuilder::new(search) + .case_insensitive(cfg.ignore_case) + .build(); + return match regex { + Ok(rgx) => search!(&rgx), + Err(_) => Box::new(|_| false), + }; + } + if cfg.ignore_case { + let matchstr = search.to_lowercase(); + return search!(&matchstr, to_lowercase); + } + let matchstr = search.to_owned(); + return search!(&matchstr); +} diff --git a/rmenu/src/state.rs b/rmenu/src/state.rs new file mode 100644 index 0000000..86df84c --- /dev/null +++ b/rmenu/src/state.rs @@ -0,0 +1,68 @@ +/// Application State Trackers and Utilities +use dioxus::prelude::UseState; +use rmenu_plugin::Entry; + +pub struct PosTracker<'a> { + pos: &'a UseState, + subpos: usize, + results: &'a Vec, +} + +impl<'a> PosTracker<'a> { + pub fn new(pos: &UseState, results: &Vec) -> Self { + Self { + pos, + results, + subpos: 0, + } + } + /// Move X Primary Results Upwards + pub fn move_up(&mut self, x: usize) { + self.subpos = 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; + self.pos + .modify(|v| std::cmp::min(v + x, self.results.len())) + } + /// Get Current Position/SubPosition + pub fn position(&self) -> (usize, usize) { + (*self.pos.get(), self.subpos) + } + /// Move Position To SubMenu if it Exists + pub fn open_menu(&mut self) { + self.subpos = 1; + } + // Reset and Close SubMenu Position + pub fn close_menu(&mut self) { + self.subpos = 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; + return; + } + self.move_up(1) + } + /// Move Down Once With Context of SubMenu + pub fn shift_down(&mut 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; + return; + } + self.move_down(1) + } +}