nexsh/
lib.rs

1use colored::*;
2use rustyline::{error::ReadlineError, DefaultEditor};
3use std::error::Error;
4
5use crate::{
6    ai_client::AIClient,
7    available_models::list_available_models,
8    command_executor::CommandExecutor,
9    config_manager::ConfigManager,
10    context_manager::ContextManager,
11    types::{CommandResult, Message, NexShConfig},
12    ui_progress::ProgressManager,
13    ui_prompt_builder::PromptBuilder,
14};
15
16// Export modules
17pub mod ai_client;
18pub mod ai_request_builder;
19pub mod available_models;
20pub mod command_executor;
21pub mod config_manager;
22pub mod context_manager;
23pub mod prompt;
24pub mod types;
25pub mod ui_progress;
26pub mod ui_prompt_builder;
27
28impl Default for NexShConfig {
29    fn default() -> Self {
30        Self {
31            api_key: String::new(),
32            history_size: 1000,
33            max_context_messages: 100,
34            model: Some("gemini-2.0-flash".to_string()),
35        }
36    }
37}
38
39pub struct NexSh {
40    config_manager: ConfigManager,
41    ai_client: AIClient,
42    command_executor: CommandExecutor,
43    context_manager: ContextManager,
44    progress_manager: ProgressManager,
45    messages: Vec<Message>,
46    editor: DefaultEditor,
47}
48
49impl NexSh {
50    pub fn new() -> Result<Self, Box<dyn Error>> {
51        let config_manager = ConfigManager::new()?;
52        let editor = config_manager.create_editor()?;
53
54        // Initialize AI client with config
55        let ai_client = AIClient::new(
56            config_manager.config.api_key.clone(),
57            config_manager.config.model.clone(),
58        );
59
60        // Initialize context manager
61        let context_manager = ContextManager::new(
62            config_manager.context_file.clone(),
63            config_manager.config.max_context_messages,
64        );
65
66        // Load existing messages from context
67        let messages = context_manager.load_context().unwrap_or_default();
68
69        Ok(Self {
70            config_manager,
71            ai_client,
72            command_executor: CommandExecutor::new(),
73            context_manager,
74            progress_manager: ProgressManager::new(),
75            messages,
76            editor,
77        })
78    }
79
80    pub fn set_model(&mut self, model: &str) -> Result<(), Box<dyn Error>> {
81        // Update config
82        self.config_manager.update_config(|config| {
83            config.model = Some(model.to_string());
84        })?;
85
86        // Update AI client
87        self.ai_client.set_model(model.to_string());
88
89        println!("โœ… Gemini model set to: {}", model.green());
90        Ok(())
91    }
92
93    pub fn initialize(&mut self) -> Result<(), Box<dyn Error>> {
94        println!("๐Ÿค– Welcome to NexSh Setup!");
95
96        // Get API key
97        let input = self
98            .editor
99            .readline("Enter your Gemini API key (leave blank to keep current if exists): ")?;
100        let api_key = input.trim();
101        if !api_key.is_empty() {
102            self.config_manager.update_config(|config| {
103                config.api_key = api_key.to_string();
104            })?;
105        }
106
107        // Get history size
108        if let Ok(input) = self.editor.readline("Enter history size (default 1000): ") {
109            if let Ok(size) = input.trim().parse() {
110                self.config_manager.update_config(|config| {
111                    config.history_size = size;
112                })?;
113            }
114        }
115
116        // Get max context messages
117        if let Ok(input) = self
118            .editor
119            .readline("Enter max context messages (default 100): ")
120        {
121            if let Ok(size) = input.trim().parse() {
122                self.config_manager.update_config(|config| {
123                    config.max_context_messages = size;
124                })?;
125            }
126        }
127
128        // Model selection
129        let models = list_available_models();
130        println!("Available Gemini models:");
131        for (i, m) in models.iter().enumerate() {
132            println!("  {}. {}", i + 1, m);
133        }
134
135        let input = self
136            .editor
137            .readline("Select Gemini model by number or name (default 1): ")?;
138        let model = input.trim();
139        let selected = if model.is_empty() {
140            models[0]
141        } else if let Ok(idx) = model.parse::<usize>() {
142            models
143                .get(idx.saturating_sub(1))
144                .copied()
145                .unwrap_or(models[0])
146        } else {
147            models
148                .iter()
149                .find(|m| m.starts_with(model))
150                .copied()
151                .unwrap_or(models[0])
152        };
153
154        self.set_model(selected)?;
155        println!("โœ… Configuration saved successfully!");
156        Ok(())
157    }
158
159    pub async fn process_command(&mut self, input: &str) -> Result<(), Box<dyn Error>> {
160        if self.config_manager.config.api_key.is_empty() {
161            self.initialize()?;
162        }
163
164        // Add user message to context
165        self.context_manager
166            .add_message(&mut self.messages, "user", input)?;
167
168        // Create progress indicator
169        let pb = self
170            .progress_manager
171            .create_spinner("Thinking...".yellow().to_string());
172
173        // Process command with AI client
174        match self
175            .ai_client
176            .process_command_request(input, &self.messages)
177            .await
178        {
179            Ok(response) => {
180                pb.finish_and_clear();
181
182                println!("{} {}", "๐Ÿค– โ†’".green(), response.message.yellow());
183
184                if response.command.is_empty() {
185                    // Add model response to context
186                    self.context_manager.add_message(
187                        &mut self.messages,
188                        "model",
189                        &response.message,
190                    )?;
191                    return Ok(());
192                }
193
194                // Add command to history
195                self.editor.add_history_entry(&response.command)?;
196
197                println!("{} {}", "Category :".green(), response.category.yellow());
198                println!("{} {}", "โ†’".blue(), response.command);
199
200                // Add model response to context
201                self.context_manager.add_message(
202                    &mut self.messages,
203                    "model",
204                    &format!(
205                        "Command: {}, message: {}",
206                        response.command, response.message
207                    ),
208                )?;
209
210                // Execute command if not dangerous or user confirms
211                if !response.dangerous || self.confirm_execution()? {
212                    match self.command_executor.execute(&response.command)? {
213                        CommandResult::Success(output) => {
214                            if !output.is_empty() {
215                                self.context_manager.add_message(
216                                    &mut self.messages,
217                                    "model",
218                                    &format!("Command output:\n{}", output),
219                                )?;
220                            }
221                        }
222                        CommandResult::Error(error) => {
223                            // Get AI explanation for the error
224                            let _pb = self
225                                .progress_manager
226                                .create_spinner("Requesting explanation ...".blue().to_string());
227                            if let Ok(explanation) = self
228                                .ai_client
229                                .get_command_explanation(&response.command, &error)
230                                .await
231                            {
232                                _pb.finish_and_clear();
233                                println!(
234                                    "{} {}",
235                                    "๐Ÿค– AI Explanation:".green(),
236                                    explanation.yellow()
237                                );
238                            }
239                        }
240                    }
241                } else {
242                    println!("Command execution cancelled.");
243                }
244            }
245            Err(e) => {
246                pb.finish_and_clear();
247                eprintln!("Failed to process command: {}", e);
248            }
249        }
250
251        Ok(())
252    }
253
254    fn confirm_execution(&mut self) -> Result<bool, Box<dyn Error>> {
255        let _input = self
256            .editor
257            .readline(&PromptBuilder::create_simple_confirmation())?;
258
259        if _input.trim().to_lowercase() == "n" {
260            return Ok(false);
261        }
262
263        let _input = self
264            .editor
265            .readline(&PromptBuilder::create_danger_confirmation())?;
266
267        Ok(_input.trim().to_lowercase() == "y")
268    }
269
270    fn clear_context(&mut self) -> Result<(), Box<dyn Error>> {
271        self.context_manager.clear_context(&mut self.messages)?;
272        println!("{}", "๐Ÿงน Conversation context cleared".green());
273        Ok(())
274    }
275
276    pub fn print_help(&self) -> Result<(), Box<dyn Error>> {
277        println!("๐Ÿค– NexSh Help:");
278        println!("  - Type 'exit' or 'quit' to exit the shell.");
279        println!("  - Type any command to execute it.");
280        println!("  - Use 'init' to set up your API key.");
281        println!("  - Use 'clear' to clear conversation context.");
282        println!("  - Type 'models' to list and select available Gemini models interactively.");
283        Ok(())
284    }
285
286    fn handle_model_selection(&mut self) -> Result<(), Box<dyn Error>> {
287        let models = list_available_models();
288        println!("Available Gemini models:");
289        for (i, m) in models.iter().enumerate() {
290            println!("  {}. {}", i + 1, m);
291        }
292
293        let input = self
294            .editor
295            .readline("Select model by number or name (Enter to cancel): ")
296            .unwrap_or_default();
297        let model = input.trim();
298
299        if !model.is_empty() {
300            let selected = if let Ok(idx) = model.parse::<usize>() {
301                models
302                    .get(idx.saturating_sub(1))
303                    .copied()
304                    .unwrap_or(models[0])
305            } else {
306                models
307                    .iter()
308                    .find(|m| m.starts_with(model))
309                    .copied()
310                    .unwrap_or(models[0])
311            };
312
313            if let Err(e) = self.set_model(selected) {
314                eprintln!("{} {}", "error:".red(), e);
315            }
316        }
317
318        Ok(())
319    }
320
321    pub async fn run(&mut self) -> Result<(), Box<dyn Error>> {
322        println!("๐Ÿค– Welcome to NexSh!");
323
324        loop {
325            let prompt = PromptBuilder::create_shell_prompt()?;
326
327            match self.editor.readline(&prompt) {
328                Ok(line) => {
329                    let input = line.trim();
330                    if input.is_empty() {
331                        continue;
332                    }
333
334                    match input {
335                        "exit" | "quit" => break,
336                        "clear" => self.clear_context()?,
337                        "init" => self.initialize()?,
338                        "help" => self.print_help()?,
339                        "models" => {
340                            self.handle_model_selection()?;
341                            continue;
342                        }
343                        _ => {
344                            if let Err(e) = self.process_command(input).await {
345                                eprintln!("{} {}", "error:".red(), e);
346                            }
347                        }
348                    }
349                }
350                Err(ReadlineError::Interrupted) => {
351                    println!("Use 'exit' to quit");
352                    continue;
353                }
354                Err(ReadlineError::Eof) => break,
355                Err(err) => {
356                    eprintln!("Error: {}", err);
357                    break;
358                }
359            }
360        }
361
362        // Save history
363        self.editor
364            .save_history(&self.config_manager.history_file)?;
365        Ok(())
366    }
367}