You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
aichat/src/repl/mod.rs

203 lines
6.5 KiB
Rust

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] = [
(".info", "Print system-wide information"),
(".set", "Modify the configuration temporarily"),
(".model", "Choose a model"),
(".role", "Select a role"),
(".clear role", "Clear the currently selected role"),
(".session", "Start a session"),
(".clear session", "End current session"),
(".copy", "Copy the last output to the clipboard"),
(".read", "Read the contents of a file and submit"),
(".edit", "Multi-line editing (CTRL+S to finish)"),
(".help", "Print this help message"),
(".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::EndSession)?;
Ok(())
}
fn handle_line(&mut self, handler: &Rc<ReplCmdHandler>, line: &str) -> Result<bool> {
match parse_command(line) {
Some((cmd, args)) => match cmd {
".exit" => {
return Ok(true);
}
".help" => {
dump_repl_help();
}
".clear" => match args {
Some("role") => handler.handle(ReplCmd::ClearRole)?,
Some("session") => handler.handle(ReplCmd::EndSession)?,
_ => dump_unknown_command(),
},
".model" => match args {
Some(name) => handler.handle(ReplCmd::SetModel(name.to_string()))?,
None => print_now!("Usage: .model <name>\n\n"),
},
".role" => match args {
Some(name) => handler.handle(ReplCmd::SetRole(name.to_string()))?,
None => print_now!("Usage: .role <name>\n\n"),
},
".info" => {
handler.handle(ReplCmd::ViewInfo)?;
}
".set" => {
handler.handle(ReplCmd::UpdateConfig(args.unwrap_or_default().to_string()))?;
self.prompt.sync_config();
}
".session" => {
handler.handle(ReplCmd::StartSession(args.map(|v| v.to_string())))?;
}
".copy" => {
handler.handle(ReplCmd::Copy)?;
}
".read" => match args {
Some(file) => handler.handle(ReplCmd::ReadFile(file.to_string()))?,
None => print_now!("Usage: .read <file name>\n\n"),
},
".edit" => {
if let Some(text) = args {
handler.handle(ReplCmd::Submit(text.to_string()))?;
}
}
_ => 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::<Vec<String>>()
.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")))
);
}
}