feat: start on formalizing configuration & app

This commit is contained in:
imgurbot12 2023-07-18 22:29:31 -07:00
parent 9c03c4bf1e
commit d56680fc0d
7 changed files with 230 additions and 97 deletions

View File

@ -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"

View File

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

View File

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

View File

@ -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<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();

View File

@ -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(())
}

View File

@ -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();

View File

@ -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)