use super::command::{Command, CommandResult};
use anyhow::Result;
use colored::Colorize;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ReplPhase {
#[default]
Ready,
Continuation {
buffer: String,
},
Executing {
command: Command,
},
DisplayingResults {
output: String,
},
Error {
message: String,
recoverable: bool,
},
Exiting,
}
impl ReplPhase {
pub fn is_terminal(&self) -> bool {
matches!(
self,
Self::Exiting
| Self::Error {
recoverable: false,
..
}
)
}
pub fn is_recoverable(&self) -> bool {
!matches!(
self,
Self::Error {
recoverable: false,
..
}
)
}
pub fn status_indicator(&self) -> String {
match self {
Self::Ready => "🔵".to_string(),
Self::Continuation { .. } => "🟡".to_string(),
Self::Executing { .. } => "🟢".to_string(),
Self::DisplayingResults { .. } => "✓".green().to_string(),
Self::Error {
recoverable: true, ..
} => "âš ".yellow().to_string(),
Self::Error {
recoverable: false, ..
} => "✗".red().to_string(),
Self::Exiting => "👋".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub enum ReplEvent {
LineSubmitted {
line: String,
},
CommandParsed {
command: Command,
},
CommandExecuted {
result: CommandResult,
},
Interrupted,
Eof,
ParseError {
message: String,
},
ExecutionError {
message: String,
recoverable: bool,
},
ContinuationNeeded {
buffer: String,
},
ResultsReady {
output: String,
},
}
#[derive(Debug)]
pub struct Transition {
pub new_phase: ReplPhase,
pub output: Option<String>,
pub follow_up: Option<ReplEvent>,
}
impl Transition {
pub fn to(phase: ReplPhase) -> Self {
Self {
new_phase: phase,
output: None,
follow_up: None,
}
}
pub fn to_with_output(phase: ReplPhase, output: String) -> Self {
Self {
new_phase: phase,
output: Some(output),
follow_up: None,
}
}
pub fn to_with_follow_up(phase: ReplPhase, follow_up: ReplEvent) -> Self {
Self {
new_phase: phase,
output: None,
follow_up: Some(follow_up),
}
}
pub fn to_with_both(phase: ReplPhase, output: String, follow_up: ReplEvent) -> Self {
Self {
new_phase: phase,
output: Some(output),
follow_up: Some(follow_up),
}
}
}
pub struct ReplStateMachine {
phase: ReplPhase,
}
impl ReplStateMachine {
pub fn new() -> Self {
Self {
phase: ReplPhase::Ready,
}
}
pub fn phase(&self) -> &ReplPhase {
&self.phase
}
pub fn is_terminal(&self) -> bool {
self.phase.is_terminal()
}
pub fn process_event(&mut self, event: ReplEvent) -> Result<Transition> {
let transition = match (&self.phase, &event) {
(ReplPhase::Ready, ReplEvent::LineSubmitted { line }) => {
if line.is_empty() {
Transition::to(ReplPhase::Ready)
} else if line.ends_with('\\') {
let buffer = line.trim_end_matches('\\').to_string();
Transition::to(ReplPhase::Continuation { buffer })
} else {
match Command::parse(line) {
Ok(command) => Transition::to_with_follow_up(
ReplPhase::Executing {
command: command.clone(),
},
ReplEvent::CommandParsed { command },
),
Err(e) => Transition::to_with_output(
ReplPhase::Ready,
format!("{}: {}", "Parse error".red().bold(), e),
),
}
}
}
(ReplPhase::Ready, ReplEvent::Interrupted) => Transition::to_with_output(
ReplPhase::Ready,
"^C (Use 'exit' or Ctrl+D to quit)".yellow().to_string(),
),
(ReplPhase::Ready, ReplEvent::Eof) => {
Transition::to_with_output(ReplPhase::Exiting, "Goodbye!".green().to_string())
}
(ReplPhase::Continuation { buffer }, ReplEvent::LineSubmitted { line }) => {
let mut new_buffer = buffer.clone();
new_buffer.push(' ');
new_buffer.push_str(line);
if line.ends_with('\\') {
let trimmed = new_buffer.trim_end_matches('\\').to_string();
Transition::to(ReplPhase::Continuation { buffer: trimmed })
} else {
match Command::parse(&new_buffer) {
Ok(command) => Transition::to_with_follow_up(
ReplPhase::Executing {
command: command.clone(),
},
ReplEvent::CommandParsed { command },
),
Err(e) => Transition::to_with_output(
ReplPhase::Ready,
format!("{}: {}", "Parse error".red().bold(), e),
),
}
}
}
(ReplPhase::Continuation { .. }, ReplEvent::Interrupted) => Transition::to_with_output(
ReplPhase::Ready,
"Continuation cancelled".yellow().to_string(),
),
(ReplPhase::Executing { .. }, ReplEvent::CommandExecuted { result }) => match result {
CommandResult::Continue(output) => {
if output.is_empty() {
Transition::to(ReplPhase::Ready)
} else {
Transition::to_with_output(ReplPhase::Ready, output.clone())
}
}
CommandResult::Exit => {
Transition::to_with_output(ReplPhase::Exiting, "Goodbye!".green().to_string())
}
CommandResult::Silent => Transition::to(ReplPhase::Ready),
},
(
ReplPhase::Executing { .. },
ReplEvent::ExecutionError {
message,
recoverable,
},
) => {
if *recoverable {
Transition::to_with_output(
ReplPhase::Ready,
format!("{}: {}", "Error".red().bold(), message),
)
} else {
Transition::to(ReplPhase::Error {
message: message.clone(),
recoverable: false,
})
}
}
(
ReplPhase::Error {
recoverable: true, ..
},
ReplEvent::LineSubmitted { .. },
) => {
Transition::to(ReplPhase::Ready)
}
(ReplPhase::Exiting, _) => {
Transition::to(ReplPhase::Exiting)
}
(current, event) => {
eprintln!(
"{}: Unexpected event {:?} in phase {:?}",
"Warning".yellow(),
event,
current
);
Transition::to(ReplPhase::Ready)
}
};
self.phase = transition.new_phase.clone();
Ok(transition)
}
pub fn reset(&mut self) {
self.phase = ReplPhase::Ready;
}
}
impl Default for ReplStateMachine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ready_to_executing() {
let mut sm = ReplStateMachine::new();
assert!(matches!(sm.phase(), ReplPhase::Ready));
let result = sm.process_event(ReplEvent::LineSubmitted {
line: "help".to_string(),
});
assert!(result.is_ok());
assert!(matches!(sm.phase(), ReplPhase::Executing { .. }));
}
#[test]
fn test_continuation() {
let mut sm = ReplStateMachine::new();
let result = sm.process_event(ReplEvent::LineSubmitted {
line: "query test\\".to_string(),
});
assert!(result.is_ok());
assert!(matches!(sm.phase(), ReplPhase::Continuation { .. }));
}
#[test]
fn test_interrupt_recovery() {
let mut sm = ReplStateMachine::new();
let result = sm.process_event(ReplEvent::Interrupted);
assert!(result.is_ok());
assert!(matches!(sm.phase(), ReplPhase::Ready));
}
#[test]
fn test_eof_exits() {
let mut sm = ReplStateMachine::new();
let result = sm.process_event(ReplEvent::Eof);
assert!(result.is_ok());
assert!(matches!(sm.phase(), ReplPhase::Exiting));
assert!(sm.is_terminal());
}
}