mod abort; mod handler; mod highlighter; mod init; mod prompt; mod validator; pub use self::abort::*; pub use self::handler::*; pub use self::init::Repl; use crate::config::SharedConfig; use crate::print_now; use anyhow::Result; use fancy_regex::Regex; use lazy_static::lazy_static; use reedline::Signal; use std::rc::Rc; pub const REPL_COMMANDS: [(&str, &str); 12] = [ (".help", "Print this help message"), (".info", "Print system-wide information"), (".edit", "Multi-line editing (CTRL+S to finish)"), (".model", "Switch LLM model"), (".role", "Use role"), (".exit role", "Leave current role"), (".session", "Start a context-aware chat session"), (".exit session", "End the current session"), (".set", "Modify the configuration parameters"), (".copy", "Copy the last reply to the clipboard"), (".read", "Import from file and submit"), (".exit", "Exit the REPL"), ]; lazy_static! { static ref COMMAND_RE: Regex = Regex::new(r"^\s*(\.\S+)\s*").unwrap(); static ref EDIT_RE: Regex = Regex::new(r"^\s*\.edit\s*").unwrap(); } impl Repl { pub fn run(&mut self, config: SharedConfig) -> Result<()> { let abort = AbortSignal::new(); let handler = ReplCmdHandler::init(config, abort.clone())?; print_now!("Welcome to aichat {}\n", env!("CARGO_PKG_VERSION")); print_now!("Type \".help\" for more information.\n"); let mut already_ctrlc = false; let handler = Rc::new(handler); loop { if abort.aborted_ctrld() { break; } if abort.aborted_ctrlc() && !already_ctrlc { already_ctrlc = true; } let sig = self.editor.read_line(&self.prompt); match sig { Ok(Signal::Success(line)) => { already_ctrlc = false; abort.reset(); match self.handle_line(&handler, &line) { Ok(quit) => { if quit { break; } } Err(err) => { let err = format!("{err:?}"); print_now!("{}\n\n", err.trim()); } } } Ok(Signal::CtrlC) => { abort.set_ctrlc(); if already_ctrlc { break; } already_ctrlc = true; print_now!("(To exit, press Ctrl+C again or Ctrl+D or type .exit)\n\n"); } Ok(Signal::CtrlD) => { abort.set_ctrld(); break; } _ => {} } } handler.handle(ReplCmd::ExitSession)?; Ok(()) } fn handle_line(&mut self, handler: &Rc, line: &str) -> Result { match parse_command(line) { Some((cmd, args)) => match cmd { ".help" => { dump_repl_help(); } ".info" => { handler.handle(ReplCmd::ViewInfo)?; } ".edit" => { if let Some(text) = args { handler.handle(ReplCmd::Submit(text.to_string()))?; } } ".model" => match args { Some(name) => handler.handle(ReplCmd::SetModel(name.to_string()))?, None => print_now!("Usage: .model \n\n"), }, ".role" => match args { Some(name) => handler.handle(ReplCmd::SetRole(name.to_string()))?, None => print_now!("Usage: .role \n\n"), }, ".session" => { handler.handle(ReplCmd::StartSession(args.map(|v| v.to_string())))?; } ".set" => { handler.handle(ReplCmd::Set(args.unwrap_or_default().to_string()))?; self.prompt.sync_config(); } ".copy" => { handler.handle(ReplCmd::Copy)?; } ".read" => match args { Some(file) => handler.handle(ReplCmd::ReadFile(file.to_string()))?, None => print_now!("Usage: .read \n\n"), }, ".exit" => match args { Some("role") => handler.handle(ReplCmd::ExitRole)?, Some("session") => handler.handle(ReplCmd::ExitSession)?, Some(_) => dump_unknown_command(), None => { return Ok(true); } }, // deprecated ".clear" => match args { Some("role") => { print_now!("Deprecated. Use '.exit role' instead.\n\n"); } Some("session") => { print_now!("Deprecated. Use '.exit session' instead.\n\n"); } _ => dump_unknown_command(), }, _ => dump_unknown_command(), }, None => { handler.handle(ReplCmd::Submit(line.to_string()))?; } } Ok(false) } } fn dump_unknown_command() { print_now!("Unknown command. Type \".help\" for more information.\n\n"); } fn dump_repl_help() { let head = REPL_COMMANDS .iter() .map(|(name, desc)| format!("{name:<24} {desc}")) .collect::>() .join("\n"); print_now!( r###"{head} Press Ctrl+C to abort readline, Ctrl+D to exit the REPL "###, ); } fn parse_command(line: &str) -> Option<(&str, Option<&str>)> { if let Ok(Some(captures)) = COMMAND_RE.captures(line) { if let Some(cmd) = captures.get(1) { let cmd = cmd.as_str(); let args = line[captures[0].len()..].trim(); let args = if args.is_empty() { None } else { Some(args) }; return Some((cmd, args)); } } None } #[cfg(test)] mod tests { use super::*; #[test] fn test_process_command_line() { assert_eq!(parse_command(" .role"), Some((".role", None))); assert_eq!(parse_command(" .role "), Some((".role", None))); assert_eq!( parse_command(" .set dry_run true"), Some((".set", Some("dry_run true"))) ); assert_eq!( parse_command(" .set dry_run true "), Some((".set", Some("dry_run true"))) ); assert_eq!( parse_command(".prompt \nabc\n"), Some((".prompt", Some("abc"))) ); assert_eq!( parse_command(".edit\r\nabc\r\n"), Some((".edit", Some("abc"))) ); } }