mirror of
https://github.com/imgurbot12/rmenu.git
synced 2024-11-10 11:33:48 +01:00
feat: better internal tracking, added keyboard support
This commit is contained in:
parent
6fe171c398
commit
7b5633b82c
@ -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" }
|
|
||||||
|
@ -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
16
rmenu/src/config.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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
49
rmenu/src/search.rs
Normal 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
68
rmenu/src/state.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user