feat: impl thiserr, use yaml for config, remove osstring

This commit is contained in:
imgurbot12 2023-07-20 16:59:27 -07:00
parent d56680fc0d
commit 9b8d626c4d
4 changed files with 137 additions and 33 deletions

View file

@ -16,4 +16,5 @@ 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 = { version = "1.0.171", features = ["derive"] }
serde_json = "1.0.103" serde_json = "1.0.103"
toml = "0.7.6" serde_yaml = "0.9.24"
thiserror = "1.0.43"

View file

@ -1,12 +1,13 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::ffi::OsString; use std::collections::{BTreeMap, VecDeque};
#[derive(Debug, PartialEq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub css: Vec<OsString>, pub css: Vec<String>,
pub use_icons: bool, pub use_icons: bool,
pub search_regex: bool, pub search_regex: bool,
pub ignore_case: bool, pub ignore_case: bool,
pub plugins: BTreeMap<String, VecDeque<String>>,
} }
impl Default for Config { impl Default for Config {
@ -16,6 +17,7 @@ impl Default for Config {
use_icons: true, use_icons: true,
search_regex: false, search_regex: false,
ignore_case: true, ignore_case: true,
plugins: Default::default(),
} }
} }
} }

View file

@ -3,6 +3,7 @@ use dioxus::prelude::*;
use keyboard_types::{Code, Modifiers}; use keyboard_types::{Code, Modifiers};
use rmenu_plugin::Entry; use rmenu_plugin::Entry;
use crate::config::Config;
use crate::search::new_searchfn; use crate::search::new_searchfn;
use crate::state::PosTracker; use crate::state::PosTracker;
use crate::App; use crate::App;
@ -15,6 +16,7 @@ pub fn run(app: App) {
struct GEntry<'a> { struct GEntry<'a> {
index: usize, index: usize,
entry: &'a Entry, entry: &'a Entry,
config: &'a Config,
pos: usize, pos: usize,
subpos: usize, subpos: usize,
} }
@ -64,11 +66,15 @@ fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
div { div {
id: "result-{cx.props.index}", id: "result-{cx.props.index}",
class: "result {result_classes}", class: "result {result_classes}",
div { if cx.props.config.use_icons {
class: "icon", cx.render(rsx! {
if let Some(icon) = cx.props.entry.icon.as_ref() { div {
cx.render(rsx! { img { src: "{icon}" } }) class: "icon",
} if let Some(icon) = cx.props.entry.icon.as_ref() {
cx.render(rsx! { img { src: "{icon}" } })
}
}
})
} }
div { div {
class: "name", class: "name",
@ -133,6 +139,7 @@ fn App(cx: Scope<App>) -> Element {
TableEntry{ TableEntry{
index: index, index: index,
entry: entry, entry: entry,
config: &cx.props.config,
pos: pos, pos: pos,
subpos: subpos, subpos: subpos,
} }

View file

@ -1,14 +1,59 @@
use std::ffi::OsString; use std::fmt::Display;
use std::fs::{read_to_string, File}; use std::fs::{read_to_string, File};
use std::io::{prelude::*, BufReader, Error, ErrorKind}; use std::io::{self, prelude::*, BufReader};
use std::process::{Command, ExitStatus, Stdio};
use std::str::FromStr;
mod config; mod config;
mod gui; mod gui;
mod search; mod search;
mod state; mod state;
use clap::*; use clap::Parser;
use rmenu_plugin::Entry; use rmenu_plugin::Entry;
use thiserror::Error;
#[derive(Debug, Clone)]
pub enum Format {
Json,
MsgPack,
}
impl Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&format!("{self:?}").to_lowercase())
}
}
impl FromStr for Format {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"json" => Ok(Format::Json),
"msgpack" => Ok(Format::MsgPack),
_ => Err("No Such Format".to_owned()),
}
}
}
#[derive(Error, Debug)]
pub enum RMenuError {
#[error("$HOME not found")]
HomeNotFound,
#[error("Invalid Config")]
InvalidConfig(#[from] serde_yaml::Error),
#[error("File Error")]
FileError(#[from] io::Error),
#[error("No Such Plugin")]
NoSuchPlugin(String),
#[error("Invalid Plugin Specified")]
InvalidPlugin(String),
#[error("Command Runtime Exception")]
CommandError(Vec<String>, Option<ExitStatus>),
#[error("Invalid JSON Entry Object")]
InvalidJson(#[from] serde_json::Error),
}
/// Application State for GUI /// Application State for GUI
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -26,31 +71,29 @@ pub struct App {
pub struct Args { pub struct Args {
#[arg(short, long, default_value_t=String::from("-"))] #[arg(short, long, default_value_t=String::from("-"))]
input: String, input: String,
#[arg(short, long)] #[arg(short, long, default_value_t=Format::Json)]
json: bool, format: Format,
#[arg(short, long)]
msgpack: bool,
#[arg(short, long)] #[arg(short, long)]
run: Vec<String>, run: Vec<String>,
#[arg(short, long)] #[arg(short, long)]
config: Option<OsString>, config: Option<String>,
#[arg(long)] #[arg(long)]
css: Vec<OsString>, css: Vec<String>,
} }
impl Args { impl Args {
/// Load Config based on CLI Settings /// Load Config based on CLI Settings
fn config(&self) -> Result<config::Config, Error> { fn config(&self) -> Result<config::Config, RMenuError> {
let path = match &self.config { let path = match &self.config {
Some(path) => path.to_owned(), Some(path) => path.to_owned(),
None => match dirs::config_dir() { None => match dirs::config_dir() {
Some(mut dir) => { Some(mut dir) => {
dir.push("rmenu"); dir.push("rmenu");
dir.push("config.toml"); dir.push("config.yaml");
dir.into() dir.to_string_lossy().to_string()
} }
None => { None => {
return Err(Error::new(ErrorKind::NotFound, "$HOME not found")); return Err(RMenuError::HomeNotFound);
} }
}, },
}; };
@ -62,32 +105,83 @@ impl Args {
return Ok(config::Config::default()); return Ok(config::Config::default());
} }
}; };
toml::from_str(&cfg).map_err(|e| Error::new(ErrorKind::InvalidInput, format!("{e}"))) serde_yaml::from_str(&cfg).map_err(|e| RMenuError::InvalidConfig(e))
}
/// Read single entry from incoming line object
fn readentry(&self, cfg: &config::Config, line: &str) -> Result<Entry, RMenuError> {
let mut entry = match self.format {
Format::Json => serde_json::from_str::<Entry>(line)?,
Format::MsgPack => todo!(),
};
if !cfg.use_icons {
entry.icon = None;
}
Ok(entry)
} }
/// Load Entries From Input (Stdin by Default) /// Load Entries From Input (Stdin by Default)
fn load_default(&self) -> Result<Vec<Entry>, Error> { fn load_default(&self, cfg: &config::Config) -> Result<Vec<Entry>, RMenuError> {
let fpath = match self.input.as_str() { let fpath = match self.input.as_str() {
"-" => "/dev/stdin", "-" => "/dev/stdin",
_ => &self.input, _ => &self.input,
}; };
let file = File::open(fpath)?; let file = File::open(fpath).map_err(|e| RMenuError::FileError(e))?;
let reader = BufReader::new(file); let reader = BufReader::new(file);
let mut entries = vec![]; let mut entries = vec![];
for line in reader.lines() { for line in reader.lines() {
let entry = serde_json::from_str::<Entry>(&line?)?; let entry = self.readentry(cfg, &line?)?;
entries.push(entry); entries.push(entry);
} }
Ok(entries) Ok(entries)
} }
/// Load Entries From Specified Sources /// Load Entries From Specified Sources
fn load_sources(&self, cfg: &config::Config) -> Result<Vec<Entry>, Error> { fn load_sources(&self, cfg: &config::Config) -> Result<Vec<Entry>, RMenuError> {
todo!() println!("{cfg:?}");
// execute commands to get a list of entries
let mut entries = vec![];
for plugin in self.run.iter() {
log::debug!("running plugin: {plugin}");
// retrieve plugin command arguments
let Some(args) = cfg.plugins.get(plugin) else {
return Err(RMenuError::NoSuchPlugin(plugin.to_owned()));
};
// build command
let mut cmdargs = args.clone();
let Some(main) = cmdargs.pop_front() else {
return Err(RMenuError::InvalidPlugin(plugin.to_owned()));
};
let mut cmd = Command::new(main);
for arg in cmdargs.iter() {
cmd.arg(arg);
}
// spawn command
let mut proc = cmd.stdout(Stdio::piped()).spawn()?;
let stdout = proc
.stdout
.as_mut()
.ok_or_else(|| RMenuError::CommandError(args.clone().into(), None))?;
let reader = BufReader::new(stdout);
// read output line by line and parse content
for line in reader.lines() {
let entry = self.readentry(cfg, &line?)?;
entries.push(entry);
}
// check status of command on exit
let status = proc.wait()?;
if !status.success() {
return Err(RMenuError::CommandError(
args.clone().into(),
Some(status.clone()),
));
}
}
Ok(entries)
} }
/// Load Application /// Load Application
pub fn parse_app() -> Result<App, Error> { pub fn parse_app() -> Result<App, RMenuError> {
let args = Self::parse(); let args = Self::parse();
let mut config = args.config()?; let mut config = args.config()?;
// load css files from settings // load css files from settings
@ -100,7 +194,7 @@ impl Args {
// load entries from configured sources // load entries from configured sources
let entries = match args.run.len() > 0 { let entries = match args.run.len() > 0 {
true => args.load_sources(&config)?, true => args.load_sources(&config)?,
false => args.load_default()?, false => args.load_default(&config)?,
}; };
// generate app object // generate app object
return Ok(App { return Ok(App {
@ -112,9 +206,7 @@ impl Args {
} }
} }
//TODO: add better errors with `thiserror` to add context //TODO: improve search w/ 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 //TODO: config
@ -122,7 +214,9 @@ impl Args {
// - allow/disable icons (also available via CLI) // - allow/disable icons (also available via CLI)
// - custom keybindings (some available via CLI?) // - custom keybindings (some available via CLI?)
fn main() -> Result<(), Error> { //TODO: add exit key (Esc by default?) - part of keybindings
fn main() -> Result<(), RMenuError> {
// parse cli / config / application-settings // parse cli / config / application-settings
let app = Args::parse_app()?; let app = Args::parse_app()?;
gui::run(app); gui::run(app);