1use crate::config::{paths, EditMode, HistoryConfig, TerminalConfig};
7use anyhow::{Context, Result};
8use crossterm::{execute, terminal};
9use reedline::{
10 default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
11 ColumnarMenu, Emacs, FileBackedHistory, KeyCode, KeyModifiers, Keybindings, MenuBuilder,
12 Reedline, ReedlineEvent, ReedlineMenu, Signal, Vi,
13};
14use std::io;
15use std::path::PathBuf;
16
17use crate::repl::completer::OxurCompleter;
18use crate::repl::oxur_prompt::OxurPrompt;
19use crate::repl::pager;
20use crate::repl::sexp_highlighter::SExpHighlighter;
21use crate::repl::sexp_validator::SExpValidator;
22
23fn format_version(version_str: &str) -> String {
28 let parts: Vec<&str> = version_str.split_whitespace().collect();
31 if parts.len() > 1 {
32 parts[1..].join(" ")
33 } else {
34 version_str.to_string()
35 }
36}
37
38fn visible_width(s: &str) -> usize {
42 let mut width = 0;
45 let mut chars = s.chars().peekable();
46
47 while let Some(ch) = chars.next() {
48 if ch == '\x1b' {
49 if chars.peek() == Some(&'[') {
51 chars.next(); for esc_ch in chars.by_ref() {
54 if esc_ch == 'm' {
55 break;
56 }
57 }
58 }
59 } else {
60 width += 1;
62 }
63 }
64
65 width
66}
67
68fn substitute_placeholder_in_line(line: &str, placeholder: &str, value: &str) -> String {
73 if !line.contains(placeholder) {
74 return line.to_string();
75 }
76
77 let original_visible_width = visible_width(line);
79
80 let result = line.replace(placeholder, value);
82
83 let new_visible_width = visible_width(&result);
85
86 if new_visible_width == original_visible_width {
87 return result;
89 }
90
91 let border_pos = result
94 .rfind('\x1b')
95 .or_else(|| result.rfind('║'))
96 .or_else(|| result.rfind('│'))
97 .or_else(|| result.rfind('|'))
98 .or_else(|| result.rfind(']'));
99
100 let (before_border, border_and_after) = if let Some(pos) = border_pos {
101 (&result[..pos], &result[pos..])
102 } else {
103 (&result[..], "")
105 };
106
107 if new_visible_width < original_visible_width {
108 let spaces_needed = original_visible_width - new_visible_width;
110 format!("{}{}{}", before_border, " ".repeat(spaces_needed), border_and_after)
111 } else {
112 let spaces_to_remove = new_visible_width - original_visible_width;
114
115 let trimmed = before_border.trim_end();
117 let trailing_space_count = before_border.len() - trimmed.len();
118
119 if trailing_space_count >= spaces_to_remove {
120 let keep_len = trimmed.len() + (trailing_space_count - spaces_to_remove);
122 format!("{}{}", &before_border[..keep_len], border_and_after)
123 } else {
124 format!("{}{}", trimmed, border_and_after)
126 }
127 }
128}
129
130fn substitute_banner_versions(
139 banner: &str,
140 metadata: &oxur_repl::metadata::SystemMetadata,
141) -> String {
142 banner
143 .lines()
144 .map(|line| {
145 let line = substitute_placeholder_in_line(line, "N.N.N", &metadata.oxur_version);
146 let line = substitute_placeholder_in_line(
147 &line,
148 "M.M.M",
149 &format_version(&metadata.rust_version),
150 );
151 substitute_placeholder_in_line(&line, "L.L.L", &format_version(&metadata.cargo_version))
152 })
153 .collect::<Vec<_>>()
154 .join("\n")
155}
156
157fn add_completion_keybinding(keybindings: &mut Keybindings) {
159 keybindings.add_binding(
160 KeyModifiers::NONE,
161 KeyCode::Tab,
162 ReedlineEvent::UntilFound(vec![
163 ReedlineEvent::Menu("completion_menu".to_string()),
164 ReedlineEvent::MenuNext,
165 ]),
166 );
167}
168
169pub struct ReplTerminal {
171 editor: Reedline,
172 #[allow(dead_code)] history_path: PathBuf,
174 terminal_config: TerminalConfig,
175}
176
177impl ReplTerminal {
178 pub fn with_config(
189 terminal_config: TerminalConfig,
190 history_config: HistoryConfig,
191 ) -> Result<Self> {
192 let edit_mode: Box<dyn reedline::EditMode> = match terminal_config.edit_mode {
194 EditMode::Emacs => {
195 let mut keybindings = default_emacs_keybindings();
196 add_completion_keybinding(&mut keybindings);
197 Box::new(Emacs::new(keybindings))
198 }
199 EditMode::Vi => {
200 let mut insert_keybindings = default_vi_insert_keybindings();
201 let mut normal_keybindings = default_vi_normal_keybindings();
202 add_completion_keybinding(&mut insert_keybindings);
203 add_completion_keybinding(&mut normal_keybindings);
204 Box::new(Vi::new(insert_keybindings, normal_keybindings))
205 }
206 };
207
208 let history_path = history_config.path.unwrap_or_else(paths::default_history_path);
210
211 let history_path_for_backend = if history_config.enabled {
215 history_path.clone()
216 } else {
217 std::env::temp_dir().join("oxur-repl-temp-history")
219 };
220
221 let history = Box::new(
222 FileBackedHistory::with_file(
223 history_config.max_size.unwrap_or(10000),
224 history_path_for_backend,
225 )
226 .context("Failed to create history backend")?,
227 );
228
229 let completion_menu = ColumnarMenu::default()
231 .with_name("completion_menu")
232 .with_columns(4)
233 .with_column_width(Some(20))
234 .with_column_padding(2);
235
236 let editor = Reedline::create()
238 .with_history(history)
239 .with_edit_mode(edit_mode)
240 .with_highlighter(Box::new(SExpHighlighter::new(terminal_config.color_enabled)))
241 .with_validator(Box::new(SExpValidator::new()))
242 .with_completer(Box::new(OxurCompleter::new()))
243 .with_menu(ReedlineMenu::EngineCompleter(Box::new(completion_menu)));
244
245 Ok(Self { editor, history_path, terminal_config })
246 }
247
248 pub fn read_line(&mut self, prompt: &str) -> Result<Option<String>> {
255 let oxur_prompt = OxurPrompt::new(
256 prompt.to_string(),
257 self.terminal_config.formatted_continuation_prompt(),
258 );
259
260 match self.editor.read_line(&oxur_prompt) {
261 Ok(Signal::Success(line)) => Ok(Some(line)),
262 Ok(Signal::CtrlC) => Ok(None),
263 Ok(Signal::CtrlD) => Err(anyhow::anyhow!("EOF")),
264 Err(e) => Err(anyhow::anyhow!("Input error: {}", e)),
265 }
266 }
267
268 pub fn read_line_default(&mut self) -> Result<Option<String>> {
270 let prompt = self.prompt();
271 self.read_line(&prompt)
272 }
273
274 pub fn prompt(&self) -> String {
276 self.terminal_config.formatted_prompt()
277 }
278
279 #[allow(dead_code)]
281 pub fn continuation_prompt(&self) -> String {
282 self.terminal_config.formatted_continuation_prompt()
283 }
284
285 pub fn save_history(&mut self) -> Result<()> {
290 Ok(())
292 }
293
294 #[allow(dead_code)]
296 pub fn color_enabled(&self) -> bool {
297 self.terminal_config.color_enabled
298 }
299
300 pub fn print_error(&self, msg: &str) {
302 if self.terminal_config.color_enabled {
303 eprintln!("\x1b[31mError:\x1b[0m {}", msg);
304 } else {
305 eprintln!("Error: {}", msg);
306 }
307 }
308
309 pub fn print_result(&self, value: &str) {
311 if self.terminal_config.color_enabled {
312 println!("\x1b[36m{}\x1b[0m", value);
313 } else {
314 println!("{}", value);
315 }
316 }
317
318 pub fn print_output(&self, output: &str) {
320 print!("{}", output);
321 }
322
323 pub fn print_help(&self, content: &str) {
327 if let Err(e) = pager::page_text(content) {
328 eprintln!("Warning: Pager failed ({}), printing directly", e);
330 println!("{}", content);
331 }
332 }
333
334 pub fn print_banner(&self, metadata: &oxur_repl::metadata::SystemMetadata) {
336 if let Some(ref banner) = self.terminal_config.banner {
337 let banner_with_versions = substitute_banner_versions(banner, metadata);
339 println!("{}", banner_with_versions);
340 } else {
341 if self.terminal_config.color_enabled {
343 println!(
344 "\x1b[1mOxur REPL\x1b[0m v{} | \x1b[90mRust: {} | Cargo: {}\x1b[0m",
345 metadata.oxur_version,
346 format_version(&metadata.rust_version),
347 format_version(&metadata.cargo_version)
348 );
349 } else {
350 println!(
351 "Oxur REPL v{} | Rust: {} | Cargo: {}",
352 metadata.oxur_version,
353 format_version(&metadata.rust_version),
354 format_version(&metadata.cargo_version)
355 );
356 }
357 println!("Type (help) for assistance, Ctrl-D to exit.");
358 }
359 println!();
360 }
361
362 pub fn print_goodbye(&self) {
364 println!();
365 if self.terminal_config.color_enabled {
366 println!("\x1b[33mGoodbye!\x1b[0m");
367 } else {
368 println!("Goodbye!");
369 }
370 }
371
372 pub fn clear_screen(&self) -> Result<()> {
374 execute!(io::stdout(), terminal::Clear(terminal::ClearType::All))?;
375 execute!(io::stdout(), crossterm::cursor::MoveTo(0, 0))?;
376 Ok(())
377 }
378
379 pub fn config(&self) -> &TerminalConfig {
381 &self.terminal_config
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_default_history_path() {
391 let path = paths::default_history_path();
392 assert!(path.ends_with("repl_history"));
393 }
394
395 #[test]
396 fn test_terminal_config_prompt() {
397 let config = TerminalConfig::builder().prompt("test> ").color(false).build();
398 assert_eq!(config.formatted_prompt(), "test> ");
399 }
400
401 #[test]
402 #[serial_test::serial]
403 fn test_terminal_config_colored_prompt() {
404 colored::control::set_override(true);
406
407 let config = TerminalConfig::builder().prompt("test> ").color(true).build();
409 let test_prompt = config.formatted_prompt();
410 assert_ne!(test_prompt, "test> ");
411 assert!(test_prompt.contains("\x1b["));
412 assert!(test_prompt.contains("test> "));
413
414 let oxur_config = TerminalConfig::builder().prompt("oxur> ").color(true).build();
416 let oxur_prompt = oxur_config.formatted_prompt();
417 assert_ne!(oxur_prompt, "oxur> ");
419 assert!(oxur_prompt.contains("\x1b["));
421 assert!(oxur_prompt.contains("o"));
423 assert!(oxur_prompt.contains("x"));
424 assert!(oxur_prompt.contains("u"));
425 assert!(oxur_prompt.contains("r"));
426
427 colored::control::unset_override();
429 }
430
431 #[test]
432 fn test_continuation_prompt() {
433 let config = TerminalConfig::builder().continuation_prompt("... ").color(false).build();
434 assert_eq!(config.formatted_continuation_prompt(), "... ");
435 }
436
437 #[test]
438 fn test_custom_banner() {
439 let config = TerminalConfig::builder().banner("Custom Welcome!").build();
440 assert_eq!(config.banner, Some("Custom Welcome!".to_string()));
441 }
442
443 #[test]
444 fn test_format_version_rustc() {
445 let version = "rustc 1.75.0 (82e1608df 2023-12-21)";
446 assert_eq!(format_version(version), "1.75.0 (82e1608df 2023-12-21)");
447 }
448
449 #[test]
450 fn test_format_version_cargo() {
451 let version = "cargo 1.75.0 (1d8b05cdd 2023-11-20)";
452 assert_eq!(format_version(version), "1.75.0 (1d8b05cdd 2023-11-20)");
453 }
454
455 #[test]
456 fn test_format_version_unknown() {
457 let version = "unknown";
458 assert_eq!(format_version(version), "unknown");
459 }
460
461 #[test]
462 fn test_visible_width_plain_text() {
463 assert_eq!(visible_width("Hello"), 5);
464 assert_eq!(visible_width(""), 0);
465 assert_eq!(visible_width("Test 123"), 8);
466 }
467
468 #[test]
469 fn test_visible_width_with_ansi_codes() {
470 assert_eq!(visible_width("\x1b[31mRed\x1b[0m"), 3); assert_eq!(visible_width("\x1b[1;32mGreen\x1b[0m"), 5); assert_eq!(visible_width("\x1b[38;2;255;0;0mRed\x1b[0m"), 3); assert_eq!(visible_width("\x1b[38;2;138;59;13m║\x1b[0m"), 1); }
478
479 #[test]
480 fn test_visible_width_complex_banner_line() {
481 let line = "\x1b[38;2;138;59;13m║\x1b[0m text \x1b[38;2;138;59;13m║\x1b[0m";
483 assert_eq!(visible_width(line), 8);
485 }
486
487 #[test]
488 fn test_substitute_placeholder_in_line_no_placeholder() {
489 let line = "This is a test line";
490 let result = substitute_placeholder_in_line(line, "N.N.N", "1.2.3");
491 assert_eq!(result, line);
492 }
493
494 #[test]
495 fn test_substitute_placeholder_in_line_simple() {
496 let line = "Version: N.N.N ║";
497 let result = substitute_placeholder_in_line(line, "N.N.N", "1.2.3");
498 assert_eq!(visible_width(&result), visible_width(line));
499 assert!(result.contains("1.2.3"));
500 assert!(!result.contains("N.N.N"));
501 }
502
503 #[test]
504 fn test_substitute_placeholder_in_line_shorter_value() {
505 let line = "oxur: N.N.N ║";
506 let result = substitute_placeholder_in_line(line, "N.N.N", "1.0");
507 assert_eq!(visible_width(&result), visible_width(line));
509 assert!(result.contains("1.0"));
510 }
511
512 #[test]
513 fn test_substitute_placeholder_in_line_longer_value() {
514 let line = "oxur: N.N.N ║";
515 let result = substitute_placeholder_in_line(line, "N.N.N", "1.0.0-beta");
516 assert_eq!(visible_width(&result), visible_width(line));
518 assert!(result.contains("1.0.0-beta"));
519 }
520
521 #[test]
522 fn test_substitute_placeholder_in_line_with_ansi() {
523 let line = "\x1b[32moxur: N.N.N\x1b[37m \x1b[38;2;138;59;13m║\x1b[0m";
524 let result = substitute_placeholder_in_line(line, "N.N.N", "1.2.3");
525 assert_eq!(visible_width(&result), visible_width(line));
527 assert!(result.contains("1.2.3"));
528 assert!(!result.contains("N.N.N"));
529 }
530
531 #[test]
532 fn test_substitute_banner_versions() {
533 let banner = "oxur: N.N.N\nrustc: M.M.M\ncargo: L.L.L";
534 let metadata = oxur_repl::metadata::SystemMetadata {
535 oxur_version: "0.1.0".to_string(),
536 rust_version: "rustc 1.75.0 (82e1608df 2023-12-21)".to_string(),
537 cargo_version: "cargo 1.75.0 (1d8b05cdd 2023-11-20)".to_string(),
538 os_name: "Test".to_string(),
539 os_version: "1.0".to_string(),
540 arch: "x86_64".to_string(),
541 hostname: "test".to_string(),
542 pid: 1234,
543 cwd: std::path::PathBuf::from("/test"),
544 started_at: std::time::SystemTime::now(),
545 };
546
547 let result = substitute_banner_versions(banner, &metadata);
548 assert!(result.contains("oxur: 0.1.0"));
549 assert!(result.contains("rustc: 1.75.0 (82e1608df 2023-12-21)"));
550 assert!(result.contains("cargo: 1.75.0 (1d8b05cdd 2023-11-20)"));
551 assert!(!result.contains("N.N.N"));
552 assert!(!result.contains("M.M.M"));
553 assert!(!result.contains("L.L.L"));
554 }
555
556 #[test]
557 fn test_substitute_banner_versions_preserves_width() {
558 let banner = "║ oxur: N.N.N ║\n║ rustc: M.M.M ║\n║ cargo: L.L.L ║";
560 let metadata = oxur_repl::metadata::SystemMetadata {
561 oxur_version: "0.2.0".to_string(),
562 rust_version: "rustc 1.76.0".to_string(),
563 cargo_version: "cargo 1.76.0".to_string(),
564 os_name: "Test".to_string(),
565 os_version: "1.0".to_string(),
566 arch: "x86_64".to_string(),
567 hostname: "test".to_string(),
568 pid: 1234,
569 cwd: std::path::PathBuf::from("/test"),
570 started_at: std::time::SystemTime::now(),
571 };
572
573 let result = substitute_banner_versions(banner, &metadata);
574 let original_lines: Vec<&str> = banner.lines().collect();
575 let result_lines: Vec<&str> = result.lines().collect();
576
577 assert_eq!(original_lines.len(), result_lines.len());
579 for (orig, res) in original_lines.iter().zip(result_lines.iter()) {
580 assert_eq!(
581 visible_width(orig),
582 visible_width(res),
583 "Line width mismatch:\nOriginal: {}\nResult: {}",
584 orig,
585 res
586 );
587 }
588 }
589
590 #[test]
591 fn test_substitute_banner_versions_with_real_banner() {
592 let config = crate::config::TerminalConfig::default();
594 let banner = config.banner.expect("Default banner should exist");
595
596 let metadata = oxur_repl::metadata::SystemMetadata {
597 oxur_version: "0.2.0".to_string(),
598 rust_version: "rustc 1.76.0 (07dca489a 2024-02-04)".to_string(),
599 cargo_version: "cargo 1.76.0 (c84b36747 2024-01-18)".to_string(),
600 os_name: "Test".to_string(),
601 os_version: "1.0".to_string(),
602 arch: "x86_64".to_string(),
603 hostname: "test".to_string(),
604 pid: 1234,
605 cwd: std::path::PathBuf::from("/test"),
606 started_at: std::time::SystemTime::now(),
607 };
608
609 let result = substitute_banner_versions(&banner, &metadata);
610 let original_lines: Vec<&str> = banner.lines().collect();
611 let result_lines: Vec<&str> = result.lines().collect();
612
613 assert_eq!(original_lines.len(), result_lines.len());
615
616 for (i, (orig, res)) in original_lines.iter().zip(result_lines.iter()).enumerate() {
618 let orig_width = visible_width(orig);
619 let res_width = visible_width(res);
620 assert_eq!(
621 orig_width,
622 res_width,
623 "Line {} width mismatch (orig={}, res={}):\nOriginal: {}\nResult: {}",
624 i + 1,
625 orig_width,
626 res_width,
627 orig,
628 res
629 );
630 }
631
632 assert!(result.contains("0.2.0"));
634 assert!(result.contains("1.76.0"));
635 assert!(!result.contains("N.N.N"));
636 assert!(!result.contains("M.M.M"));
637 assert!(!result.contains("L.L.L"));
638 }
639
640 #[test]
643 fn test_format_version_empty() {
644 let version = "";
645 assert_eq!(format_version(version), "");
646 }
647
648 #[test]
649 fn test_format_version_single_word() {
650 let version = "1.75.0";
651 assert_eq!(format_version(version), "1.75.0");
652 }
653
654 #[test]
655 fn test_format_version_many_parts() {
656 let version = "tool 1.0.0 extra info here";
657 assert_eq!(format_version(version), "1.0.0 extra info here");
658 }
659
660 #[test]
661 fn test_substitute_banner_no_placeholders() {
662 let banner = "Welcome to the REPL!";
663 let metadata = oxur_repl::metadata::SystemMetadata {
664 oxur_version: "0.1.0".to_string(),
665 rust_version: "rustc 1.75.0".to_string(),
666 cargo_version: "cargo 1.75.0".to_string(),
667 os_name: "Test".to_string(),
668 os_version: "1.0".to_string(),
669 arch: "x86_64".to_string(),
670 hostname: "test".to_string(),
671 pid: 1234,
672 cwd: std::path::PathBuf::from("/test"),
673 started_at: std::time::SystemTime::now(),
674 };
675
676 let result = substitute_banner_versions(banner, &metadata);
677 assert_eq!(result, "Welcome to the REPL!");
678 }
679
680 #[test]
681 fn test_substitute_banner_partial_placeholders() {
682 let banner = "Oxur N.N.N only";
683 let metadata = oxur_repl::metadata::SystemMetadata {
684 oxur_version: "0.2.0".to_string(),
685 rust_version: "rustc 1.76.0".to_string(),
686 cargo_version: "cargo 1.76.0".to_string(),
687 os_name: "Test".to_string(),
688 os_version: "1.0".to_string(),
689 arch: "x86_64".to_string(),
690 hostname: "test".to_string(),
691 pid: 1234,
692 cwd: std::path::PathBuf::from("/test"),
693 started_at: std::time::SystemTime::now(),
694 };
695
696 let result = substitute_banner_versions(banner, &metadata);
697 assert_eq!(result, "Oxur 0.2.0 only");
698 }
699
700 #[test]
701 fn test_add_completion_keybinding() {
702 let mut keybindings = default_emacs_keybindings();
703 add_completion_keybinding(&mut keybindings);
705 }
706
707 #[test]
708 fn test_add_completion_keybinding_vi_insert() {
709 let mut keybindings = default_vi_insert_keybindings();
710 add_completion_keybinding(&mut keybindings);
711 }
712
713 #[test]
714 fn test_add_completion_keybinding_vi_normal() {
715 let mut keybindings = default_vi_normal_keybindings();
716 add_completion_keybinding(&mut keybindings);
717 }
718
719 #[test]
720 fn test_terminal_config_default_banner() {
721 let config = TerminalConfig::default();
722 assert!(config.banner.is_some());
724 }
725
726 #[test]
727 fn test_terminal_config_color_disabled() {
728 let config = TerminalConfig::builder().color(false).build();
729 assert!(!config.color_enabled);
730 }
731
732 #[test]
733 fn test_terminal_config_color_enabled() {
734 let config = TerminalConfig::builder().color(true).build();
735 assert!(config.color_enabled);
736 }
737
738 #[test]
739 fn test_terminal_config_edit_mode_emacs() {
740 let config = TerminalConfig::builder().edit_mode(EditMode::Emacs).build();
741 assert!(matches!(config.edit_mode, EditMode::Emacs));
742 }
743
744 #[test]
745 fn test_terminal_config_edit_mode_vi() {
746 let config = TerminalConfig::builder().edit_mode(EditMode::Vi).build();
747 assert!(matches!(config.edit_mode, EditMode::Vi));
748 }
749
750 #[test]
751 fn test_history_config_default() {
752 let config = HistoryConfig::default();
753 assert!(config.enabled);
754 assert!(config.path.is_none());
755 assert_eq!(config.max_size, Some(10000));
757 }
758
759 #[test]
760 fn test_history_config_disabled() {
761 let config = HistoryConfig { enabled: false, path: None, max_size: None };
762 assert!(!config.enabled);
763 }
764
765 #[test]
766 fn test_history_config_custom_path() {
767 let path = PathBuf::from("/custom/history");
768 let config = HistoryConfig { enabled: true, path: Some(path.clone()), max_size: None };
769 assert_eq!(config.path, Some(path));
770 }
771
772 #[test]
773 fn test_history_config_custom_max_size() {
774 let config = HistoryConfig { enabled: true, path: None, max_size: Some(5000) };
775 assert_eq!(config.max_size, Some(5000));
776 }
777
778 #[test]
782 #[serial_test::serial]
783 fn test_repl_terminal_with_config_emacs() {
784 let terminal_config =
786 TerminalConfig::builder().edit_mode(EditMode::Emacs).color(false).build();
787 let history_config = HistoryConfig { enabled: false, path: None, max_size: Some(100) };
788
789 let result = ReplTerminal::with_config(terminal_config, history_config);
790 assert!(result.is_ok());
791 let terminal = result.unwrap();
792 assert!(!terminal.config().color_enabled);
793 }
794
795 #[test]
796 #[serial_test::serial]
797 fn test_repl_terminal_with_config_vi() {
798 let terminal_config =
800 TerminalConfig::builder().edit_mode(EditMode::Vi).color(false).build();
801 let history_config = HistoryConfig { enabled: false, path: None, max_size: Some(100) };
802
803 let result = ReplTerminal::with_config(terminal_config, history_config);
804 assert!(result.is_ok());
805 }
806
807 #[test]
808 #[serial_test::serial]
809 fn test_repl_terminal_with_history_enabled() {
810 let terminal_config = TerminalConfig::builder().color(false).build();
812 let temp_dir = std::env::temp_dir();
813 let history_path = temp_dir.join("test-oxur-history");
814 let history_config =
815 HistoryConfig { enabled: true, path: Some(history_path.clone()), max_size: Some(500) };
816
817 let result = ReplTerminal::with_config(terminal_config, history_config);
818 assert!(result.is_ok());
819
820 let _ = std::fs::remove_file(history_path);
822 }
823
824 #[test]
825 #[serial_test::serial]
826 fn test_repl_terminal_with_history_disabled() {
827 let terminal_config = TerminalConfig::builder().color(false).build();
829 let history_config = HistoryConfig { enabled: false, path: None, max_size: None };
830
831 let result = ReplTerminal::with_config(terminal_config, history_config);
832 assert!(result.is_ok());
833 }
834
835 #[test]
836 #[serial_test::serial]
837 fn test_repl_terminal_config_accessor() {
838 let terminal_config = TerminalConfig::builder()
839 .prompt("test> ")
840 .continuation_prompt("..> ")
841 .color(false)
842 .build();
843 let history_config = HistoryConfig::default();
844
845 let terminal = ReplTerminal::with_config(terminal_config.clone(), history_config).unwrap();
846
847 let config = terminal.config();
849 assert_eq!(config.prompt, "test> ");
850 assert_eq!(config.continuation_prompt, "..> ");
851 assert!(!config.color_enabled);
852 }
853
854 #[test]
855 #[serial_test::serial]
856 fn test_repl_terminal_prompt() {
857 let terminal_config = TerminalConfig::builder().prompt("custom> ").color(false).build();
858 let history_config = HistoryConfig::default();
859
860 let terminal = ReplTerminal::with_config(terminal_config, history_config).unwrap();
861
862 let prompt = terminal.prompt();
864 assert_eq!(prompt, "custom> ");
865 }
866
867 #[test]
868 #[serial_test::serial]
869 fn test_repl_terminal_continuation_prompt() {
870 let terminal_config =
871 TerminalConfig::builder().continuation_prompt(">>> ").color(false).build();
872 let history_config = HistoryConfig::default();
873
874 let terminal = ReplTerminal::with_config(terminal_config, history_config).unwrap();
875
876 let cont_prompt = terminal.continuation_prompt();
878 assert_eq!(cont_prompt, ">>> ");
879 }
880
881 #[test]
882 #[serial_test::serial]
883 fn test_repl_terminal_color_enabled() {
884 let terminal_config = TerminalConfig::builder().color(true).build();
885 let history_config = HistoryConfig::default();
886
887 let terminal = ReplTerminal::with_config(terminal_config, history_config).unwrap();
888
889 assert!(terminal.color_enabled());
891 }
892
893 #[test]
894 #[serial_test::serial]
895 fn test_repl_terminal_color_disabled() {
896 let terminal_config = TerminalConfig::builder().color(false).build();
897 let history_config = HistoryConfig::default();
898
899 let terminal = ReplTerminal::with_config(terminal_config, history_config).unwrap();
900
901 assert!(!terminal.color_enabled());
902 }
903
904 #[test]
905 #[serial_test::serial]
906 fn test_repl_terminal_save_history() {
907 let terminal_config = TerminalConfig::builder().color(false).build();
908 let history_config = HistoryConfig::default();
909
910 let mut terminal = ReplTerminal::with_config(terminal_config, history_config).unwrap();
911
912 let result = terminal.save_history();
914 assert!(result.is_ok());
915 }
916}