vapor_cli/
shell.rs

1use std::io::{self};
2use std::process::Command;
3use std::env;
4use std::path::Path;
5use rustyline::error::ReadlineError;
6use rustyline::{Editor, Helper};
7use rustyline::completion::{Completer, FilenameCompleter};
8use rustyline::highlight::Highlighter;
9use rustyline::hint::Hinter;
10use rustyline::validate::Validator;
11use ctrlc;
12use anyhow::Result;
13
14const BUILTIN_COMMANDS: &[&str] = &["cd", "pwd", "history", "help", "exit"];
15
16struct ShellHelper {
17    filename_completer: FilenameCompleter,
18}
19
20impl Helper for ShellHelper {}
21
22impl Completer for ShellHelper {
23    type Candidate = String;
24
25    fn complete(&self, line: &str, pos: usize, _ctx: &rustyline::Context<'_>) -> rustyline::Result<(usize, Vec<String>)> {
26        let line = &line[..pos];
27        let words: Vec<&str> = line.split_whitespace().collect();
28        
29        if words.is_empty() {
30            // Complete with built-in commands
31            return Ok((0, BUILTIN_COMMANDS.iter().map(|&s| s.to_string()).collect()));
32        }
33
34        if words.len() == 1 {
35            // First word - complete with commands
36            let prefix = words[0];
37            let candidates: Vec<String> = BUILTIN_COMMANDS
38                .iter()
39                .filter(|&&cmd| cmd.starts_with(prefix))
40                .map(|&s| s.to_string())
41                .collect();
42            
43            if !candidates.is_empty() {
44                return Ok((0, candidates));
45            }
46        }
47
48        // For other cases, use filename completion
49        if line.trim_start().starts_with("cd ") || line.trim_start().starts_with("ls ") {
50            let (start, pairs) = self.filename_completer.complete(line, pos, _ctx)?;
51            let candidates: Vec<String> = pairs.into_iter().map(|p| p.replacement).collect();
52            return Ok((start, candidates));
53        }
54
55        Ok((pos, Vec::new()))
56    }
57}
58
59impl Highlighter for ShellHelper {}
60
61impl Hinter for ShellHelper {
62    type Hint = String;
63}
64
65impl Validator for ShellHelper {}
66
67pub struct Shell {
68    history: Vec<String>,
69    editor: Editor<ShellHelper, rustyline::history::FileHistory>,
70    original_dir: std::path::PathBuf,
71    history_path: std::path::PathBuf,
72}
73
74impl Shell {
75    pub fn new() -> Self {
76        let helper = ShellHelper {
77            filename_completer: FilenameCompleter::new(),
78        };
79        
80        let mut editor = Editor::new().unwrap();
81        editor.set_helper(Some(helper));
82        
83        let original_dir = env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
84        
85        // Set up history path in home directory
86        let history_path = match env::var("HOME") {
87            Ok(home) => Path::new(&home).join(".vapor_shell_history"),
88            Err(_) => Path::new(".vapor_shell_history").to_path_buf(),
89        };
90
91        // Load history if available
92        if history_path.exists() {
93            if let Err(e) = editor.load_history(&history_path) {
94                eprintln!("Warning: Could not load shell history: {}", e);
95            }
96        }
97
98        // Set up Ctrl+C handler
99        if let Err(e) = ctrlc::set_handler(move || {
100            println!("\nUse 'exit' to return to the REPL");
101        }) {
102            eprintln!("Warning: Could not set up Ctrl+C handler: {}", e);
103        }
104
105        Shell {
106            history: Vec::new(),
107            editor,
108            original_dir,
109            history_path,
110        }
111    }
112
113    fn get_prompt(&self) -> String {
114        let cwd = env::current_dir().unwrap_or_default();
115        let home = env::var("HOME").map(std::path::PathBuf::from).unwrap_or_default();
116
117        let display_path = if cwd == home {
118            "~".to_string()
119        } else if let Ok(stripped) = cwd.strip_prefix(&home) {
120            format!("~/{}", stripped.display())
121        } else {
122            cwd.display().to_string()
123        };
124
125        format!("[vapor-shell {}]$ ", display_path)
126    }
127
128    pub fn run(&mut self) {
129        println!("Welcome to Vapor Shell! Type 'exit' to return to the REPL.");
130        println!("Type 'help' for available commands.");
131        
132        loop {
133            let prompt = self.get_prompt();
134            let readline = self.editor.readline(&prompt);
135            match readline {
136                Ok(line) => {
137                    let line = line.trim();
138                    if line.is_empty() {
139                        continue;
140                    }
141
142                    // Add to history
143                    self.history.push(line.to_string());
144                    if let Err(e) = self.editor.add_history_entry(line) {
145                        eprintln!("Warning: Could not add to history: {}", e);
146                    }
147
148                    // Handle exit command first
149                    if line == "exit" {
150                        println!("Returning to REPL...");
151                        // Save history before exiting
152                        if let Err(e) = self.editor.save_history(&self.history_path) {
153                            eprintln!("Warning: Could not save shell history: {}", e);
154                        }
155                        // Restore original directory
156                        if let Err(e) = env::set_current_dir(&self.original_dir) {
157                            eprintln!("Warning: Could not restore original directory: {}", e);
158                        }
159                        return;
160                    }
161
162                    if line == "help" {
163                        self.show_help();
164                        continue;
165                    }
166
167                    self.execute_command(line);
168                }
169                Err(ReadlineError::Interrupted) => {
170                    println!("^C");
171                    continue;
172                }
173                Err(ReadlineError::Eof) => {
174                    println!("EOF");
175                    break;
176                }
177                Err(err) => {
178                    eprintln!("Input error: {}", err);
179                    continue;
180                }
181            }
182        }
183
184        // Save history
185        if let Err(e) = self.editor.save_history(&self.history_path) {
186            eprintln!("Warning: Could not save shell history: {}", e);
187        }
188        // Restore original directory
189        if let Err(e) = env::set_current_dir(&self.original_dir) {
190            eprintln!("Warning: Could not restore original directory: {}", e);
191        }
192    }
193
194    fn execute_command(&mut self, command: &str) {
195        let parts: Vec<&str> = command.split_whitespace().collect();
196        if parts.is_empty() {
197            return;
198        }
199
200        match parts[0] {
201            "cd" => {
202                let path = if parts.len() > 1 {
203                    let p = parts[1];
204                    if p == "~" {
205                        env::var("HOME").unwrap_or_else(|_| ".".to_string())
206                    } else if p.starts_with("~/") {
207                        env::var("HOME").map(|home| format!("{}/{}", home, &p[2..])).unwrap_or_else(|_| p.to_string())
208                    } else {
209                        p.to_string()
210                    }
211                } else {
212                    env::var("HOME").unwrap_or_else(|_| ".".to_string())
213                };
214
215                if let Err(e) = env::set_current_dir(Path::new(&path)) {
216                    eprintln!("cd: {}: {}", path, e);
217                }
218            }
219            "pwd" => {
220                if let Ok(current_dir) = env::current_dir() {
221                    println!("{}", current_dir.display());
222                }
223            }
224            "history" => {
225                for (i, cmd) in self.history.iter().enumerate() {
226                    println!("{}: {}", i + 1, cmd);
227                }
228            }
229            "help" => self.show_help(),
230            _ => {
231                let status = Command::new(parts[0])
232                    .args(&parts[1..])
233                    .status();
234
235                match status {
236                    Ok(status) => {
237                        if !status.success() {
238                            if let Some(code) = status.code() {
239                                eprintln!("Command failed with exit code: {}", code);
240                            } else {
241                                eprintln!("Command terminated by signal");
242                            }
243                        }
244                    }
245                    Err(e) => {
246                        eprintln!("Error executing command: {}", e);
247                        if e.kind() == io::ErrorKind::NotFound {
248                            eprintln!("Command not found: {}", parts[0]);
249                        }
250                    }
251                }
252            }
253        }
254    }
255
256    fn show_help(&self) {
257        println!("Vapor Shell - Available Commands:");
258        println!("\nBuilt-in Commands:");
259        println!("  cd [dir]     Change directory (defaults to home if no dir specified)");
260        println!("  pwd          Print working directory");
261        println!("  history      Show command history");
262        println!("  help         Show this help message");
263        println!("  exit         Exit shell and return to REPL");
264        println!("\nSystem Commands:");
265        println!("  All standard Unix/Linux commands are available");
266        println!("  Command completion is available (press TAB)");
267        println!("  File/directory completion is available (press TAB)");
268        println!("\nFeatures:");
269        println!("  • Command history with arrow keys");
270        println!("  • Tab completion for commands and files");
271        println!("  • Command history persistence");
272        println!("  • Error handling and reporting");
273    }
274}
275
276/// Start shell mode with database context
277pub fn shell_mode(db_path: &str) -> Result<()> {
278    println!("Starting shell mode for database: {}", db_path);
279    println!("Database context available for operations.");
280    
281    let mut shell = Shell::new();
282    shell.run();
283    
284    Ok(())
285}