1use crate::commands::{execute, parse, Command};
4use crate::state::SessionState;
5use entrenar_common::{cli::styles, EntrenarError, Result};
6use rustyline::error::ReadlineError;
7use rustyline::DefaultEditor;
8use std::path::PathBuf;
9
10pub struct Repl {
12 editor: DefaultEditor,
13 state: SessionState,
14 history_path: Option<PathBuf>,
15}
16
17impl Repl {
18 pub fn new() -> Result<Self> {
20 let editor = DefaultEditor::new().map_err(|e| EntrenarError::Internal {
21 message: format!("Failed to create editor: {e}"),
22 })?;
23
24 let history_path = dirs::data_dir().map(|p| p.join("entrenar").join("shell_history"));
25
26 let mut repl = Self {
27 editor,
28 state: SessionState::new(),
29 history_path,
30 };
31
32 if let Some(ref path) = repl.history_path {
34 let _ = repl.editor.load_history(path);
35 }
36
37 Ok(repl)
38 }
39
40 pub fn with_state(state: SessionState) -> Result<Self> {
42 let mut repl = Self::new()?;
43 repl.state = state;
44 Ok(repl)
45 }
46
47 pub fn run(&mut self) -> Result<()> {
49 self.print_banner();
50
51 loop {
52 let prompt = self.format_prompt();
53
54 match self.editor.readline(&prompt) {
55 Ok(line) => {
56 let line = line.trim();
57 if line.is_empty() {
58 continue;
59 }
60
61 let _ = self.editor.add_history_entry(line);
63
64 match parse(line) {
66 Ok(cmd) => {
67 if matches!(cmd, Command::Quit) {
68 self.save_state();
69 println!("{}", styles::info("Session saved. Goodbye!"));
70 break;
71 }
72
73 if matches!(cmd, Command::Clear) {
74 print!("\x1B[2J\x1B[1;1H");
75 continue;
76 }
77
78 match execute(&cmd, &mut self.state) {
79 Ok(output) => {
80 if !output.is_empty() {
81 println!("{output}");
82 }
83 }
84 Err(e) => {
85 println!("{}", styles::error(&e.to_string()));
86 }
87 }
88 }
89 Err(e) => {
90 println!("{}", styles::error(&e.to_string()));
91 }
92 }
93 }
94 Err(ReadlineError::Interrupted) => {
95 println!("{}", styles::warning("Use 'quit' or Ctrl-D to exit"));
96 }
97 Err(ReadlineError::Eof) => {
98 self.save_state();
99 println!("\n{}", styles::info("Session saved. Goodbye!"));
100 break;
101 }
102 Err(e) => {
103 println!("{}", styles::error(&format!("Error: {e}")));
104 }
105 }
106 }
107
108 Ok(())
109 }
110
111 fn print_banner(&self) {
112 println!("{}", styles::header("Entrenar Shell v0.1.0"));
113 println!("Interactive Distillation Environment");
114 println!("Type 'help' for commands, 'quit' to exit.\n");
115 }
116
117 fn format_prompt(&self) -> String {
118 let model_count = self.state.loaded_models().len();
119 if model_count > 0 {
120 format!("entrenar ({model_count} models)> ")
121 } else {
122 "entrenar> ".to_string()
123 }
124 }
125
126 fn save_state(&mut self) {
127 if let Some(ref path) = self.history_path {
129 if let Some(parent) = path.parent() {
130 let _ = std::fs::create_dir_all(parent);
131 }
132 let _ = self.editor.save_history(path);
133 }
134
135 if self.state.preferences().auto_save_history {
137 if let Some(data_dir) = dirs::data_dir() {
138 let state_path = data_dir.join("entrenar").join("session.json");
139 if let Some(parent) = state_path.parent() {
140 let _ = std::fs::create_dir_all(parent);
141 }
142 let _ = self.state.save(&state_path);
143 }
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_repl_creation() {
154 let repl = Repl::new();
156 assert!(repl.is_ok());
157 }
158
159 #[test]
160 fn test_repl_with_state() {
161 let mut state = SessionState::new();
162 state.preferences_mut().default_batch_size = 64;
163
164 let repl = Repl::with_state(state).unwrap();
165 assert_eq!(repl.state.preferences().default_batch_size, 64);
166 }
167}