use gut::prelude::*;
use std::path::{Path, PathBuf};
mod helper;
use rustyline::{history::FileHistory, Editor};
type MyEditor<R> = Editor<helper::MyHelper<R>, FileHistory>;
pub struct Interpreter<A> {
prompt: String,
history_file: Option<PathBuf>,
action: A,
}
impl<A: Actionable> Interpreter<A> {
fn continue_interpret_line(&mut self, line: &str) -> bool {
if let Some(mut args) = shlex::split(line) {
assert!(args.len() >= 1);
args.insert(0, self.prompt.to_owned());
match A::try_parse_from(&args) {
Ok(x) => match self.action.act_on(&x) {
Ok(exit) => {
if exit {
return false;
}
}
Err(e) => {
eprintln!("{:?}", e);
}
},
Err(e) => println!("{:}", e),
}
true
} else {
eprintln!("Invalid quoting: {line:?}");
false
}
}
fn continue_read_eval_print<R: HelpfulCommand>(&mut self, editor: &mut MyEditor<R>) -> bool {
match editor.readline(&self.prompt) {
Err(rustyline::error::ReadlineError::Eof) => false,
Ok(line) => {
let line = line.trim();
if !line.is_empty() {
let _ = editor.add_history_entry(line);
self.continue_interpret_line(&line)
} else {
true
}
}
Err(e) => {
eprintln!("{}", e);
false
}
}
}
}
fn create_readline_editor<R: HelpfulCommand>() -> Result<Editor<helper::MyHelper<R>, FileHistory>> {
use rustyline::{ColorMode, CompletionType, Config};
let config = Config::builder()
.color_mode(ColorMode::Enabled)
.completion_type(CompletionType::Fuzzy)
.history_ignore_dups(true)?
.history_ignore_space(true)
.max_history_size(1000)?
.build();
let mut rl = Editor::with_config(config)?;
let h = self::helper::MyHelper::new();
rl.set_helper(Some(h));
Ok(rl)
}
impl<A: Actionable> Interpreter<A> {
fn load_history<R: HelpfulCommand>(&mut self, editor: &mut MyEditor<R>) -> Result<()> {
if let Some(h) = self.history_file.as_ref() {
editor.load_history(h).context("no history")?;
}
Ok(())
}
fn save_history<R: HelpfulCommand>(&mut self, editor: &mut MyEditor<R>) -> Result<()> {
if let Some(h) = self.history_file.as_ref() {
editor.save_history(h).context("write history file")?;
}
Ok(())
}
}
impl<A: Actionable> Interpreter<A> {
pub fn interpret_script(&mut self, script: &str) -> Result<()> {
let lines = script.lines().filter(|s| !s.trim().is_empty());
for line in lines {
debug!("Execute: {:?}", line);
if !self.continue_interpret_line(&line) {
break;
}
}
Ok(())
}
pub fn interpret_script_file(&mut self, script_file: &Path) -> Result<()> {
let s = gut::fs::read_file(script_file)?;
self.interpret_script(&s)?;
Ok(())
}
}
pub trait Actionable {
type Command: clap::Parser;
fn act_on(&mut self, cmd: &Self::Command) -> Result<bool>;
fn try_parse_from<I, T>(iter: I) -> Result<Self::Command>
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
use clap::Parser;
let r = Self::Command::try_parse_from(iter)?;
Ok(r)
}
}
pub trait HelpfulCommand {
fn get_subcommands() -> Vec<String>;
fn suitable_for_path_complete(line: &str, pos: usize) -> bool;
}
impl<T: clap::CommandFactory> HelpfulCommand for T {
fn get_subcommands() -> Vec<String> {
let app = Self::command();
app.get_subcommands().map(|s| s.get_name().into()).collect()
}
fn suitable_for_path_complete(line: &str, pos: usize) -> bool {
line[..pos]
.chars()
.last()
.map(|x| std::path::is_separator(x))
.unwrap_or(false)
}
}
impl<A: Actionable> Interpreter<A> {
#[track_caller]
pub fn new(action: A) -> Self {
Self {
prompt: "> ".to_string(),
history_file: None,
action,
}
}
}
impl<A: Actionable> Interpreter<A> {
pub fn with_history_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
let p = path.into();
self.history_file = Some(p);
self
}
pub fn with_prompt(mut self, s: &str) -> Self {
self.prompt = s.into();
self
}
pub fn run(&mut self) -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
println!("This is the interactive parser, version {}.", version);
println!("Enter \"help\" or \"?\" for a list of commands.");
println!("Press Ctrl-D or enter \"quit\" or \"q\" to exit.");
println!("");
let mut editor = create_readline_editor::<A::Command>()?;
let _ = self.load_history(&mut editor);
while self.continue_read_eval_print(&mut editor) {
debug!("excuted one loop");
}
self.save_history(&mut editor)?;
Ok(())
}
}