mirror of
https://github.com/imgurbot12/rmenu.git
synced 2024-11-10 11:33:48 +01:00
feat: start on formalizing configuration & app
This commit is contained in:
parent
9c03c4bf1e
commit
d56680fc0d
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<OsString>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
id: "result-{cx.props.i}",
|
||||
class: "result {classes}",
|
||||
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.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<App>) -> 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<App>) -> 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<App>) -> 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();
|
||||
|
@ -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<Entry>,
|
||||
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<String>,
|
||||
#[arg(short, long)]
|
||||
config: Option<OsString>,
|
||||
#[arg(long)]
|
||||
css: Option<String>,
|
||||
css: Vec<OsString>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
/// Load Config based on CLI Settings
|
||||
fn config(&self) -> Result<config::Config, Error> {
|
||||
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<Vec<Entry>, 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::<Entry>(&line?)?;
|
||||
entries.push(entry);
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Load Entries From Specified Sources
|
||||
fn load_sources(&self, cfg: &config::Config) -> Result<Vec<Entry>, Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Load Application
|
||||
pub fn parse_app() -> Result<App, Error> {
|
||||
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<Entry>,
|
||||
config: config::Config,
|
||||
}
|
||||
|
||||
fn default(args: &Args) -> Result<App, Error> {
|
||||
// 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::<Entry>(&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(())
|
||||
}
|
||||
|
@ -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<dyn Fn(&Entry) -> bool> {
|
||||
if cfg.regex {
|
||||
if cfg.search_regex {
|
||||
let regex = RegexBuilder::new(search)
|
||||
.case_insensitive(cfg.ignore_case)
|
||||
.build();
|
||||
|
@ -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<usize>,
|
||||
subpos: usize,
|
||||
subpos: &'a UseState<usize>,
|
||||
results: &'a Vec<Entry>,
|
||||
}
|
||||
|
||||
impl<'a> PosTracker<'a> {
|
||||
pub fn new(pos: &UseState<usize>, results: &Vec<Entry>) -> Self {
|
||||
pub fn new(cx: Scope<'a, App>, results: &'a Vec<Entry>) -> 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)
|
||||
|
Loading…
Reference in New Issue
Block a user