use crate::repl::help::HelpSystem;
use crate::repl::terminal::ReplTerminal;
use anyhow::Result;
use async_trait::async_trait;
use oxur_repl::protocol::{
MessageId, Operation, OperationResult, ReplMode, Request, Response, SessionId,
};
use std::sync::atomic::{AtomicU64, Ordering};
#[async_trait]
pub trait ReplClientAdapter: Send {
async fn send_eval(&mut self, request: Request) -> Result<()>;
async fn recv_response(&mut self) -> Result<Response>;
async fn close(&mut self) -> Result<()>;
async fn handle_special_command(
&mut self,
_input: &str,
_color_enabled: bool,
) -> Option<String> {
None
}
fn record_usage(&mut self, _command_type: oxur_repl::metrics::CommandType) {}
async fn create_session(&mut self, _name: Option<String>) -> Result<SessionId> {
Err(anyhow::anyhow!("Session creation not supported in this mode"))
}
async fn switch_session(&mut self, _session_id: SessionId) -> Result<()> {
Err(anyhow::anyhow!("Session switching not supported in this mode"))
}
fn current_session(&self) -> &SessionId;
async fn close_session(&mut self, _session_id: Option<SessionId>) -> Result<()> {
Err(anyhow::anyhow!("Session closing not supported in this mode"))
}
}
pub struct ReplRunner {
terminal: ReplTerminal,
session_id: SessionId,
msg_counter: AtomicU64,
metadata: Option<oxur_repl::metadata::SystemMetadata>,
}
impl ReplRunner {
pub fn new(terminal: ReplTerminal, session_id: SessionId) -> Self {
Self { terminal, session_id, msg_counter: AtomicU64::new(1), metadata: None }
}
fn next_message_id(&self) -> MessageId {
MessageId::new(self.msg_counter.fetch_add(1, Ordering::SeqCst))
}
pub fn terminal(&self) -> &ReplTerminal {
&self.terminal
}
pub fn print_banner(&mut self, metadata: &oxur_repl::metadata::SystemMetadata) {
self.metadata = Some(metadata.clone());
self.terminal.print_banner(metadata);
}
pub async fn run<C: ReplClientAdapter>(&mut self, client: &mut C) -> Result<()> {
loop {
let line = match self.terminal.read_line_default() {
Ok(Some(line)) => line,
Ok(None) => {
println!();
continue;
}
Err(e) => {
if e.to_string().contains("EOF") {
break;
}
self.terminal.print_error(&format!("Input error: {}", e));
break;
}
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if Self::is_quit_command(trimmed) {
break;
}
let color_enabled = self.terminal.config().color_enabled;
if let Some(help_output) = parse_help_command(trimmed, color_enabled) {
client.record_usage(oxur_repl::metrics::CommandType::Help);
self.terminal.print_help(&help_output);
continue;
}
if trimmed == "(clear)" {
client.record_usage(oxur_repl::metrics::CommandType::Clear);
if let Err(e) = self.terminal.clear_screen() {
self.terminal.print_error(&format!("Failed to clear screen: {}", e));
}
continue;
}
if trimmed == "(banner)" {
client.record_usage(oxur_repl::metrics::CommandType::Banner);
if let Some(ref metadata) = self.metadata {
self.terminal.print_banner(metadata);
} else {
self.terminal.print_error("No metadata available");
}
continue;
}
if let Some(output) = self.handle_session_command(trimmed, client).await {
self.terminal.print_help(&output);
continue;
}
if let Some(output) = client.handle_special_command(trimmed, color_enabled).await {
self.terminal.print_help(&output);
continue;
}
client.record_usage(oxur_repl::metrics::CommandType::Eval);
let eval_req = Request {
id: self.next_message_id(),
session_id: self.session_id.clone(),
operation: Operation::Eval { code: trimmed.to_string(), mode: ReplMode::Lisp },
};
if let Err(e) = client.send_eval(eval_req).await {
self.terminal.print_error(&format!("Failed to send request: {}", e));
continue;
}
let response = match client.recv_response().await {
Ok(r) => r,
Err(e) => {
self.terminal.print_error(&format!("Failed to receive response: {}", e));
continue;
}
};
self.display_result(&response.result);
}
Ok(())
}
pub async fn finish<C: ReplClientAdapter>(&mut self, client: &mut C) -> Result<()> {
if let Err(e) = self.terminal.save_history() {
eprintln!("Warning: Failed to save command history: {}", e);
}
self.terminal.print_goodbye();
let close_req = Request {
id: self.next_message_id(),
session_id: self.session_id.clone(),
operation: Operation::Close,
};
let _ = client.send_eval(close_req).await;
let _ = client.recv_response().await;
let _ = client.close().await;
Ok(())
}
async fn handle_session_command<C: ReplClientAdapter>(
&mut self,
input: &str,
client: &mut C,
) -> Option<String> {
if input == "(current-session)" {
let session_id = client.current_session();
return Some(format!("Current session: {}", session_id));
}
if input == "(new-session)" {
match client.create_session(None).await {
Ok(new_id) => {
self.session_id = new_id.clone();
return Some(format!("Created and switched to new session: {}", new_id));
}
Err(e) => return Some(format!("Failed to create session: {}", e)),
}
}
if input.starts_with("(new-session ") && input.ends_with(')') {
let name_part = &input[13..input.len() - 1].trim();
let name = if name_part.starts_with('"') && name_part.ends_with('"') {
&name_part[1..name_part.len() - 1]
} else {
name_part
};
match client.create_session(Some(name.to_string())).await {
Ok(new_id) => {
self.session_id = new_id.clone();
return Some(format!(
"Created and switched to new session: {} ({})",
name, new_id
));
}
Err(e) => return Some(format!("Failed to create session: {}", e)),
}
}
if input.starts_with("(switch-session ") && input.ends_with(')') {
let id_part = &input[16..input.len() - 1].trim();
let session_id = SessionId::new(*id_part);
match client.switch_session(session_id.clone()).await {
Ok(()) => {
self.session_id = session_id.clone();
return Some(format!("Switched to session: {}", session_id));
}
Err(e) => return Some(format!("Failed to switch session: {}", e)),
}
}
if input == "(close-session)" {
match client.close_session(None).await {
Ok(()) => return Some("Closed current session".to_string()),
Err(e) => return Some(format!("Failed to close session: {}", e)),
}
}
if input.starts_with("(close-session ") && input.ends_with(')') {
let id_part = &input[15..input.len() - 1].trim();
let session_id = SessionId::new(*id_part);
match client.close_session(Some(session_id.clone())).await {
Ok(()) => return Some(format!("Closed session: {}", session_id)),
Err(e) => return Some(format!("Failed to close session: {}", e)),
}
}
None
}
fn is_quit_command(input: &str) -> bool {
matches!(input, "(quit)" | "(q)" | "(exit)")
}
fn display_result(&self, result: &OperationResult) {
match result {
OperationResult::Success { value, stdout, stderr, .. } => {
if let Some(out) = stdout {
if !out.is_empty() {
self.terminal.print_output(out);
}
}
if let Some(val) = value {
if !val.is_empty() {
self.terminal.print_result(val);
}
}
if let Some(err) = stderr {
if !err.is_empty() {
eprintln!("{}", err);
}
}
}
OperationResult::Error { error, stdout, stderr } => {
if let Some(out) = stdout {
if !out.is_empty() {
self.terminal.print_output(out);
}
}
self.terminal.print_error(&error.message);
if let Some(err) = stderr {
if !err.is_empty() {
eprintln!("{}", err);
}
}
}
OperationResult::Sessions { .. } | OperationResult::HistoryEntries { .. } => {
}
_ => {
}
}
}
}
pub fn parse_help_command(input: &str, color_enabled: bool) -> Option<String> {
let help_system = HelpSystem::new(color_enabled);
if input == "(help)" {
return Some(help_system.show_overview());
}
if input.starts_with("(help ") && input.ends_with(')') {
let topic = &input[6..input.len() - 1].trim();
return help_system.show_topic(topic).or_else(|| {
Some(format!("Unknown help topic: {}. Try (help) for available topics.", topic))
});
}
None
}