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

View file

@ -3,6 +3,7 @@ use dioxus::prelude::*;
use keyboard_types::{Code, Modifiers};
use rmenu_plugin::Entry;
use crate::config::Config;
use crate::search::new_searchfn;
use crate::state::PosTracker;
use crate::App;
@ -15,6 +16,7 @@ pub fn run(app: App) {
struct GEntry<'a> {
index: usize,
entry: &'a Entry,
config: &'a Config,
pos: usize,
subpos: usize,
}
@ -64,11 +66,15 @@ fn TableEntry<'a>(cx: Scope<'a, GEntry<'a>>) -> Element<'a> {
div {
id: "result-{cx.props.index}",
class: "result {result_classes}",
div {
class: "icon",
if let Some(icon) = cx.props.entry.icon.as_ref() {
cx.render(rsx! { img { src: "{icon}" } })
}
if cx.props.config.use_icons {
cx.render(rsx! {
div {
class: "icon",
if let Some(icon) = cx.props.entry.icon.as_ref() {
cx.render(rsx! { img { src: "{icon}" } })
}
}
})
}
div {
class: "name",
@ -133,6 +139,7 @@ fn App(cx: Scope<App>) -> Element {
TableEntry{
index: index,
entry: entry,
config: &cx.props.config,
pos: pos,
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::io::{prelude::*, BufReader, Error, ErrorKind};
use std::io::{self, prelude::*, BufReader};
use std::process::{Command, ExitStatus, Stdio};
use std::str::FromStr;
mod config;
mod gui;
mod search;
mod state;
use clap::*;
use clap::Parser;
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
#[derive(Debug, PartialEq)]
@ -26,31 +71,29 @@ pub struct App {
pub struct Args {
#[arg(short, long, default_value_t=String::from("-"))]
input: String,
#[arg(short, long)]
json: bool,
#[arg(short, long)]
msgpack: bool,
#[arg(short, long, default_value_t=Format::Json)]
format: Format,
#[arg(short, long)]
run: Vec<String>,
#[arg(short, long)]
config: Option<OsString>,
config: Option<String>,
#[arg(long)]
css: Vec<OsString>,
css: Vec<String>,
}
impl Args {
/// 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 {
Some(path) => path.to_owned(),
None => match dirs::config_dir() {
Some(mut dir) => {
dir.push("rmenu");
dir.push("config.toml");
dir.into()
dir.push("config.yaml");
dir.to_string_lossy().to_string()
}
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());
}
};
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)
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() {
"-" => "/dev/stdin",
_ => &self.input,
};
let file = File::open(fpath)?;
let file = File::open(fpath).map_err(|e| RMenuError::FileError(e))?;
let reader = BufReader::new(file);
let mut entries = vec![];
for line in reader.lines() {
let entry = serde_json::from_str::<Entry>(&line?)?;
let entry = self.readentry(cfg, &line?)?;
entries.push(entry);
}
Ok(entries)
}
/// Load Entries From Specified Sources
fn load_sources(&self, cfg: &config::Config) -> Result<Vec<Entry>, Error> {
todo!()
fn load_sources(&self, cfg: &config::Config) -> Result<Vec<Entry>, RMenuError> {
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
pub fn parse_app() -> Result<App, Error> {
pub fn parse_app() -> Result<App, RMenuError> {
let args = Self::parse();
let mut config = args.config()?;
// load css files from settings
@ -100,7 +194,7 @@ impl Args {
// load entries from configured sources
let entries = match args.run.len() > 0 {
true => args.load_sources(&config)?,
false => args.load_default()?,
false => args.load_default(&config)?,
};
// generate app object
return Ok(App {
@ -112,9 +206,7 @@ impl Args {
}
}
//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 search w/ modes?
//TODO: improve looks and css
//TODO: config
@ -122,7 +214,9 @@ impl Args {
// - allow/disable icons (also 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
let app = Args::parse_app()?;
gui::run(app);