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
16pub 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 let ai_client = AIClient::new(
56 config_manager.config.api_key.clone(),
57 config_manager.config.model.clone(),
58 );
59
60 let context_manager = ContextManager::new(
62 config_manager.context_file.clone(),
63 config_manager.config.max_context_messages,
64 );
65
66 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 self.config_manager.update_config(|config| {
83 config.model = Some(model.to_string());
84 })?;
85
86 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 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 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 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 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 self.context_manager
166 .add_message(&mut self.messages, "user", input)?;
167
168 let pb = self
170 .progress_manager
171 .create_spinner("Thinking...".yellow().to_string());
172
173 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 self.context_manager.add_message(
187 &mut self.messages,
188 "model",
189 &response.message,
190 )?;
191 return Ok(());
192 }
193
194 self.editor.add_history_entry(&response.command)?;
196
197 println!("{} {}", "Category :".green(), response.category.yellow());
198 println!("{} {}", "โ".blue(), response.command);
199
200 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 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 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 self.editor
364 .save_history(&self.config_manager.history_file)?;
365 Ok(())
366 }
367}