feat: better internal tracking, added keyboard support

This commit is contained in:
imgurbot12 2023-07-18 15:55:08 -07:00
parent 6fe171c398
commit 7b5633b82c
7 changed files with 207 additions and 32 deletions

View File

@ -7,12 +7,10 @@ edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.3.15", features = ["derive"] } 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" } rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" }
serde = { version = "1.0.171", features = ["derive"] }
serde_json = "1.0.103" 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" }

View File

@ -14,6 +14,10 @@ div.result {
justify-content: left; justify-content: left;
} }
div.selected {
background-color: lightblue;
}
div.result > div { div.result > div {
margin: 2px 5px; margin: 2px 5px;
} }

16
rmenu/src/config.rs Normal file
View File

@ -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,
}
}
}

View File

@ -1,26 +1,29 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use dioxus::prelude::*; use dioxus::prelude::*;
use keyboard_types::{Code, Modifiers};
use rmenu_plugin::Entry; use rmenu_plugin::Entry;
use crate::search::new_searchfn;
use crate::state::PosTracker;
use crate::App; use crate::App;
pub fn run(app: 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()); dioxus_desktop::launch_with_props(App, app, dioxus_desktop::Config::default());
} }
#[derive(PartialEq, Props)] #[derive(PartialEq, Props)]
struct GEntry<'a> { struct GEntry<'a> {
i: usize,
o: &'a Entry, o: &'a Entry,
selected: bool,
} }
fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> { fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
let classes = if cx.props.selected { "selected" } else { "" };
cx.render(rsx! { cx.render(rsx! {
div { div {
class: "result", id: "result-{cx.props.i}",
class: "result {classes}",
div { div {
class: "icon", class: "icon",
if let Some(icon) = cx.props.o.icon.as_ref() { 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<App>) -> Element { fn App(cx: Scope<App>) -> Element {
let search = use_state(cx, || "".to_string()); let search = use_state(cx, || "".to_string());
let position = use_state(cx, || 0);
// retrieve build results tracker
let results = &cx.props.entries; let results = &cx.props.entries;
let searchstr = search.as_str(); let mut tracker = PosTracker::new(position, results);
let results_rendered = 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<Element> = results
.iter() .iter()
.filter(|entry| { .filter(|entry| searchfn(entry))
if entry.name.contains(searchstr) { .enumerate()
return true; .map(|(i, entry)| {
} cx.render(rsx! {
if let Some(comment) = entry.comment.as_ref() { TableEntry{ i: i, o: entry, selected: (i + 1) == active }
return comment.contains(searchstr);
}
false
}) })
.map(|entry| cx.render(rsx! { TableEntry{ o: entry } })); })
.collect();
cx.render(rsx! { cx.render(rsx! {
style { "{cx.props.css}" } style { "{cx.props.css}" }
div {
onkeydown: change_evt,
input { input {
id: "search",
value: "{search}", value: "{search}",
oninput: move |evt| search.set(evt.value.clone()), oninput: move |evt| search.set(evt.value.clone()),
}
results_rendered.into_iter()
} }
results_rendered
}) })
} }

View File

@ -1,7 +1,10 @@
use std::fs::{read_to_string, File}; use std::fs::{read_to_string, File};
use std::io::{prelude::*, BufReader, Error}; use std::io::{prelude::*, BufReader, Error};
mod config;
mod gui; mod gui;
mod search;
mod state;
use clap::*; use clap::*;
use rmenu_plugin::Entry; use rmenu_plugin::Entry;
@ -24,22 +27,28 @@ pub struct Args {
} }
//TODO: improve search w/ options for regex/case-insensivity/modes? //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: 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 /// Application State for GUI
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct App { pub struct App {
css: String, css: String,
name: String, name: String,
entries: Vec<Entry>, entries: Vec<Entry>,
config: config::Config,
} }
fn default(args: &Args) -> Result<App, Error> { fn default(args: &Args) -> Result<App, Error> {
// read entries from specified input // read entries from specified input
let fpath = if args.input == "-" { let fpath = match args.input.as_str() {
"/dev/stdin" "-" => "/dev/stdin",
} else { _ => &args.input,
&args.input
}; };
let file = File::open(fpath)?; let file = File::open(fpath)?;
let reader = BufReader::new(file); let reader = BufReader::new(file);
@ -57,6 +66,7 @@ fn default(args: &Args) -> Result<App, Error> {
name: "default".to_string(), name: "default".to_string(),
css: read_to_string(css)?, css: read_to_string(css)?,
entries, entries,
config: Default::default(),
}; };
Ok(args) Ok(args)
} }

49
rmenu/src/search.rs Normal file
View File

@ -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<dyn Fn(&Entry) -> 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);
}

68
rmenu/src/state.rs Normal file
View File

@ -0,0 +1,68 @@
/// Application State Trackers and Utilities
use dioxus::prelude::UseState;
use rmenu_plugin::Entry;
pub struct PosTracker<'a> {
pos: &'a UseState<usize>,
subpos: usize,
results: &'a Vec<Entry>,
}
impl<'a> PosTracker<'a> {
pub fn new(pos: &UseState<usize>, results: &Vec<Entry>) -> 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)
}
}