1use crate::repl::help::HelpSystem;
7use crate::repl::terminal::ReplTerminal;
8use anyhow::Result;
9use async_trait::async_trait;
10use oxur_repl::protocol::{
11 MessageId, Operation, OperationResult, ReplMode, Request, Response, SessionId,
12};
13use std::sync::atomic::{AtomicU64, Ordering};
14
15#[async_trait]
21pub trait ReplClientAdapter: Send {
22 async fn send_eval(&mut self, request: Request) -> Result<()>;
24
25 async fn recv_response(&mut self) -> Result<Response>;
27
28 async fn close(&mut self) -> Result<()>;
30
31 async fn handle_special_command(
38 &mut self,
39 _input: &str,
40 _color_enabled: bool,
41 ) -> Option<String> {
42 None
43 }
44
45 fn record_usage(&mut self, _command_type: oxur_repl::metrics::CommandType) {}
50
51 async fn create_session(&mut self, _name: Option<String>) -> Result<SessionId> {
56 Err(anyhow::anyhow!("Session creation not supported in this mode"))
57 }
58
59 async fn switch_session(&mut self, _session_id: SessionId) -> Result<()> {
64 Err(anyhow::anyhow!("Session switching not supported in this mode"))
65 }
66
67 fn current_session(&self) -> &SessionId;
71
72 async fn close_session(&mut self, _session_id: Option<SessionId>) -> Result<()> {
77 Err(anyhow::anyhow!("Session closing not supported in this mode"))
78 }
79}
80
81pub struct ReplRunner {
86 terminal: ReplTerminal,
87 session_id: SessionId,
88 msg_counter: AtomicU64,
89 metadata: Option<oxur_repl::metadata::SystemMetadata>,
90}
91
92impl ReplRunner {
93 pub fn new(terminal: ReplTerminal, session_id: SessionId) -> Self {
95 Self { terminal, session_id, msg_counter: AtomicU64::new(1), metadata: None }
96 }
97
98 fn next_message_id(&self) -> MessageId {
100 MessageId::new(self.msg_counter.fetch_add(1, Ordering::SeqCst))
101 }
102
103 pub fn terminal(&self) -> &ReplTerminal {
105 &self.terminal
106 }
107
108 pub fn print_banner(&mut self, metadata: &oxur_repl::metadata::SystemMetadata) {
110 self.metadata = Some(metadata.clone());
111 self.terminal.print_banner(metadata);
112 }
113
114 pub async fn run<C: ReplClientAdapter>(&mut self, client: &mut C) -> Result<()> {
120 loop {
121 let line = match self.terminal.read_line_default() {
123 Ok(Some(line)) => line,
124 Ok(None) => {
125 println!();
127 continue;
128 }
129 Err(e) => {
130 if e.to_string().contains("EOF") {
132 break;
133 }
134 self.terminal.print_error(&format!("Input error: {}", e));
135 break;
136 }
137 };
138
139 let trimmed = line.trim();
141 if trimmed.is_empty() {
142 continue;
143 }
144
145 if Self::is_quit_command(trimmed) {
147 break;
148 }
149
150 let color_enabled = self.terminal.config().color_enabled;
152 if let Some(help_output) = parse_help_command(trimmed, color_enabled) {
153 client.record_usage(oxur_repl::metrics::CommandType::Help);
154 self.terminal.print_help(&help_output);
155 continue;
156 }
157
158 if trimmed == "(clear)" {
160 client.record_usage(oxur_repl::metrics::CommandType::Clear);
161 if let Err(e) = self.terminal.clear_screen() {
162 self.terminal.print_error(&format!("Failed to clear screen: {}", e));
163 }
164 continue;
165 }
166
167 if trimmed == "(banner)" {
169 client.record_usage(oxur_repl::metrics::CommandType::Banner);
170 if let Some(ref metadata) = self.metadata {
171 self.terminal.print_banner(metadata);
172 } else {
173 self.terminal.print_error("No metadata available");
174 }
175 continue;
176 }
177
178 if let Some(output) = self.handle_session_command(trimmed, client).await {
180 self.terminal.print_help(&output);
181 continue;
182 }
183
184 if let Some(output) = client.handle_special_command(trimmed, color_enabled).await {
186 self.terminal.print_help(&output);
187 continue;
188 }
189
190 client.record_usage(oxur_repl::metrics::CommandType::Eval);
192
193 let eval_req = Request {
195 id: self.next_message_id(),
196 session_id: self.session_id.clone(),
197 operation: Operation::Eval { code: trimmed.to_string(), mode: ReplMode::Lisp },
198 };
199
200 if let Err(e) = client.send_eval(eval_req).await {
202 self.terminal.print_error(&format!("Failed to send request: {}", e));
203 continue;
204 }
205
206 let response = match client.recv_response().await {
208 Ok(r) => r,
209 Err(e) => {
210 self.terminal.print_error(&format!("Failed to receive response: {}", e));
211 continue;
212 }
213 };
214
215 self.display_result(&response.result);
217 }
218
219 Ok(())
220 }
221
222 pub async fn finish<C: ReplClientAdapter>(&mut self, client: &mut C) -> Result<()> {
226 if let Err(e) = self.terminal.save_history() {
228 eprintln!("Warning: Failed to save command history: {}", e);
229 }
230
231 self.terminal.print_goodbye();
232
233 let close_req = Request {
235 id: self.next_message_id(),
236 session_id: self.session_id.clone(),
237 operation: Operation::Close,
238 };
239
240 let _ = client.send_eval(close_req).await;
241 let _ = client.recv_response().await;
242 let _ = client.close().await;
243
244 Ok(())
245 }
246
247 async fn handle_session_command<C: ReplClientAdapter>(
251 &mut self,
252 input: &str,
253 client: &mut C,
254 ) -> Option<String> {
255 if input == "(current-session)" {
257 let session_id = client.current_session();
258 return Some(format!("Current session: {}", session_id));
259 }
260
261 if input == "(new-session)" {
263 match client.create_session(None).await {
264 Ok(new_id) => {
265 self.session_id = new_id.clone();
266 return Some(format!("Created and switched to new session: {}", new_id));
267 }
268 Err(e) => return Some(format!("Failed to create session: {}", e)),
269 }
270 }
271
272 if input.starts_with("(new-session ") && input.ends_with(')') {
274 let name_part = &input[13..input.len() - 1].trim();
275 let name = if name_part.starts_with('"') && name_part.ends_with('"') {
277 &name_part[1..name_part.len() - 1]
278 } else {
279 name_part
280 };
281
282 match client.create_session(Some(name.to_string())).await {
283 Ok(new_id) => {
284 self.session_id = new_id.clone();
285 return Some(format!(
286 "Created and switched to new session: {} ({})",
287 name, new_id
288 ));
289 }
290 Err(e) => return Some(format!("Failed to create session: {}", e)),
291 }
292 }
293
294 if input.starts_with("(switch-session ") && input.ends_with(')') {
296 let id_part = &input[16..input.len() - 1].trim();
297 let session_id = SessionId::new(*id_part);
298
299 match client.switch_session(session_id.clone()).await {
300 Ok(()) => {
301 self.session_id = session_id.clone();
302 return Some(format!("Switched to session: {}", session_id));
303 }
304 Err(e) => return Some(format!("Failed to switch session: {}", e)),
305 }
306 }
307
308 if input == "(close-session)" {
310 match client.close_session(None).await {
311 Ok(()) => return Some("Closed current session".to_string()),
312 Err(e) => return Some(format!("Failed to close session: {}", e)),
313 }
314 }
315
316 if input.starts_with("(close-session ") && input.ends_with(')') {
318 let id_part = &input[15..input.len() - 1].trim();
319 let session_id = SessionId::new(*id_part);
320
321 match client.close_session(Some(session_id.clone())).await {
322 Ok(()) => return Some(format!("Closed session: {}", session_id)),
323 Err(e) => return Some(format!("Failed to close session: {}", e)),
324 }
325 }
326
327 None
328 }
329
330 fn is_quit_command(input: &str) -> bool {
332 matches!(input, "(quit)" | "(q)" | "(exit)")
333 }
334
335 fn display_result(&self, result: &OperationResult) {
337 match result {
338 OperationResult::Success { value, stdout, stderr, .. } => {
339 if let Some(out) = stdout {
341 if !out.is_empty() {
342 self.terminal.print_output(out);
343 }
344 }
345
346 if let Some(val) = value {
348 if !val.is_empty() {
349 self.terminal.print_result(val);
350 }
351 }
352
353 if let Some(err) = stderr {
355 if !err.is_empty() {
356 eprintln!("{}", err);
357 }
358 }
359 }
360 OperationResult::Error { error, stdout, stderr } => {
361 if let Some(out) = stdout {
363 if !out.is_empty() {
364 self.terminal.print_output(out);
365 }
366 }
367
368 self.terminal.print_error(&error.message);
370
371 if let Some(err) = stderr {
373 if !err.is_empty() {
374 eprintln!("{}", err);
375 }
376 }
377 }
378 OperationResult::Sessions { .. } | OperationResult::HistoryEntries { .. } => {
379 }
381 _ => {
382 }
384 }
385 }
386}
387
388pub fn parse_help_command(input: &str, color_enabled: bool) -> Option<String> {
396 let help_system = HelpSystem::new(color_enabled);
397
398 if input == "(help)" {
399 return Some(help_system.show_overview());
400 }
401
402 if input.starts_with("(help ") && input.ends_with(')') {
404 let topic = &input[6..input.len() - 1].trim();
405 return help_system.show_topic(topic).or_else(|| {
406 Some(format!("Unknown help topic: {}. Try (help) for available topics.", topic))
407 });
408 }
409
410 None
411}