forked from mirrors/rmenu
feat: impl thiserr, use yaml for config, remove osstring
This commit is contained in:
parent
d56680fc0d
commit
9b8d626c4d
4 changed files with 137 additions and 33 deletions
|
@ -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"
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue