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 return Ok((0, BUILTIN_COMMANDS.iter().map(|&s| s.to_string()).collect()));
32 }
33
34 if words.len() == 1 {
35 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 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 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 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 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 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 if line == "exit" {
150 println!("Returning to REPL...");
151 if let Err(e) = self.editor.save_history(&self.history_path) {
153 eprintln!("Warning: Could not save shell history: {}", e);
154 }
155 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 if let Err(e) = self.editor.save_history(&self.history_path) {
186 eprintln!("Warning: Could not save shell history: {}", e);
187 }
188 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
276pub 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}