use anyhow::Result;
use log::{debug, error, info, warn};
use rustyline::error::ReadlineError;
use rustyline::Editor;
use std::{
marker::PhantomData,
path::{Path, PathBuf},
};
use crate::commands::ReplCommandProcessor;
const DEFAULT_HISTORY_FILE_NAME: &str = ".repl_history";
#[cfg(test)]
mod tests;
#[cfg(not(feature = "async"))]
macro_rules! get_specific_processing_call {
($self:ident, $cli:expr) => {
$self.command_processor.process_command($cli)?;
};
}
#[cfg(feature = "async")]
macro_rules! get_specific_processing_call {
($self:ident, $cli:expr) => {
$self.command_processor.process_command($cli).await?
};
}
macro_rules! process_block {
( $self:ident ) => {
{
loop {
let readline = $self.editor.readline(&$self.prompt);
match readline {
Ok(line) => {
let parts: Vec<&str> = line.split(' ').collect();
let mut command = String::new();
if let Some(head) = parts.first() {
command = String::from(*head);
}
match command.to_lowercase().as_ref() {
"" => {} maybe_quit if $self.command_processor.is_quit(maybe_quit) => break, _ => {
$self.editor.add_history_entry(line.as_str());
let mut cmd_parts: Vec<&str> = vec!["repl-interface"];
cmd_parts.extend(line.split(' ').collect::<Vec<_>>().iter().copied());
match C::try_parse_from(cmd_parts.into_iter()) {
Ok(cli) => {
get_specific_processing_call!($self, cli);
}
Err(clap_err) => match clap::Error::kind(&clap_err) {
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
println!("{}", clap_err);
}
_ => {
warn!(
"Invalid command (type 'help' for the help menu\r\n{}",
clap_err
);
}
},
}
}
}
}
Err(ReadlineError::Interrupted) => break, Err(ReadlineError::Eof) => break, Err(err) => {
error!("Error: {:?}", err);
break;
}
}
}
$self.close_history();
Ok(())
}
};
}
#[derive(Debug)]
pub struct Repl<C>
where
C: clap::Parser,
{
editor: Editor<()>,
history: Option<PathBuf>,
command_processor: Box<dyn ReplCommandProcessor<C>>,
prompt: String,
_command_type: PhantomData<C>,
}
impl<C> Repl<C>
where
C: clap::Parser,
{
fn get_history_file_path(history_file_name: Option<String>) -> Option<PathBuf> {
if let Some(history_file) = &history_file_name {
let path = Path::new(history_file);
match (
path.is_file(),
path.is_dir(),
path.is_absolute(),
path.exists(),
path.extension(),
path.components(),
) {
(true, _, _, _, _, _) | (_, _, true, true, Some(_), _) => {
Some(path.to_path_buf())
}
(_, true, _, _, _, _) => {
let mut full_path = path.to_path_buf();
full_path.push(DEFAULT_HISTORY_FILE_NAME);
Some(full_path)
}
(_, _, _, _, Some(_), components) if components.clone().count() == 1 => {
dirs::home_dir().map(|mut home_dir| {
home_dir.push(history_file);
home_dir
})
}
_ => None,
}
} else {
debug!("REPL history disabled as no history file provided");
None
}
}
fn get_editor(history: &Option<PathBuf>) -> Result<Editor<()>> {
let mut rl = Editor::<()>::new();
if let Some(history_file) = history {
match rl.load_history(history_file.as_os_str()) {
Ok(_) => info!("REPL command history file loaded"),
Err(err) => warn!("Failed to load REPL command history {}", err),
}
}
Ok(rl)
}
fn close_history(&mut self) {
if let Some(history_path) = &self.history {
match self.editor.save_history(history_path.as_os_str()) {
Ok(_) => info!("REPL command history updated"),
Err(err) => warn!("Failed to safe REPL command history with error '{}'", err),
}
}
}
pub fn new(
command_processor: Box<dyn crate::commands::ReplCommandProcessor<C>>,
history_file: Option<String>,
prompt: Option<String>,
) -> Result<Self> {
let history_path = Self::get_history_file_path(history_file);
let editor = Self::get_editor(&history_path)?;
Ok(Self {
editor,
history: history_path,
command_processor,
prompt: prompt.unwrap_or_else(|| "$ ".to_string()),
_command_type: PhantomData,
})
}
#[cfg(feature = "async")]
pub async fn process(&mut self) -> Result<()> {
process_block!(self)
}
#[cfg(not(feature = "async"))]
pub fn process(&mut self) -> Result<()> {
process_block!(self)
}
}