1use crate::config;
17use anyhow::{Context, Result};
18use ctrlc;
19use rustyline::completion::{Completer, FilenameCompleter};
20use rustyline::error::ReadlineError;
21use rustyline::highlight::Highlighter;
22use rustyline::hint::Hinter;
23use rustyline::validate::Validator;
24use rustyline::{Editor, Helper};
25use std::env;
26use std::io::{self};
27use std::path::Path;
28use std::process::Command;
29
30#[derive(Debug, PartialEq, Eq)]
34pub enum ShellAction {
35 Exit,
36 SwitchToRepl,
37}
38
39const BUILTIN_COMMANDS: &[&str] = &["cd", "pwd", "history", "help", "exit", ".vrepl", ".dbinfo"];
40
41struct ShellHelper {
42 filename_completer: FilenameCompleter,
43}
44
45impl Helper for ShellHelper {}
46
47impl Completer for ShellHelper {
48 type Candidate = String;
49
50 fn complete(
51 &self,
52 line: &str,
53 pos: usize,
54 _ctx: &rustyline::Context<'_>,
55 ) -> rustyline::Result<(usize, Vec<String>)> {
56 let line = &line[..pos];
57 let words: Vec<&str> = line.split_whitespace().collect();
58
59 if words.is_empty() {
60 return Ok((0, BUILTIN_COMMANDS.iter().map(|&s| s.to_string()).collect()));
62 }
63
64 if words.len() == 1 {
65 let prefix = words[0];
67 let candidates: Vec<String> = BUILTIN_COMMANDS
68 .iter()
69 .filter(|&&cmd| cmd.starts_with(prefix))
70 .map(|&s| s.to_string())
71 .collect();
72
73 if !candidates.is_empty() {
74 return Ok((0, candidates));
75 }
76 }
77
78 if line.trim_start().starts_with("cd ") || line.trim_start().starts_with("ls ") {
80 let (start, pairs) = self.filename_completer.complete(line, pos, _ctx)?;
81 let candidates: Vec<String> = pairs.into_iter().map(|p| p.replacement).collect();
82 return Ok((start, candidates));
83 }
84
85 Ok((pos, Vec::new()))
86 }
87}
88
89impl Highlighter for ShellHelper {}
90
91impl Hinter for ShellHelper {
92 type Hint = String;
93}
94
95impl Validator for ShellHelper {}
96
97pub struct Shell {
103 editor: Editor<ShellHelper, rustyline::history::FileHistory>,
104 original_dir: std::path::PathBuf,
105 history_path: std::path::PathBuf,
106 db_path: String, }
108
109impl Shell {
110 pub fn new(db_path: &str) -> Result<Self> {
123 let helper = ShellHelper {
124 filename_completer: FilenameCompleter::new(),
125 };
126
127 let mut editor = Editor::new().unwrap();
128 editor.set_helper(Some(helper));
129
130 let original_dir = env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
131
132 let history_path = config::get_shell_history_path()?;
134
135 if history_path.exists() {
137 if let Err(e) = editor.load_history(&history_path) {
138 eprintln!("Warning: Could not load shell history: {}", e);
139 }
140 }
141
142 if let Err(e) = ctrlc::set_handler(move || {
144 println!("\nUse 'exit' to return to the REPL");
145 }) {
146 eprintln!("Warning: Could not set up Ctrl+C handler: {}", e);
147 }
148
149 Ok(Shell {
150 editor,
151 original_dir,
152 history_path,
153 db_path: db_path.to_string(),
154 })
155 }
156
157 fn get_prompt(&self) -> String {
158 let cwd = env::current_dir().unwrap_or_default();
159 let home = env::var("HOME")
160 .map(std::path::PathBuf::from)
161 .unwrap_or_default();
162
163 let display_path = if cwd == home {
164 "~".to_string()
165 } else if let Ok(stripped) = cwd.strip_prefix(&home) {
166 format!("~/{}", stripped.display())
167 } else {
168 cwd.display().to_string()
169 };
170
171 format!("[vapor-shell {}]$ ", display_path)
172 }
173
174 pub fn run(&mut self) -> ShellAction {
185 println!("Welcome to Vapor Shell! Type 'exit' to return to the REPL.");
186 println!("Type 'help' for available commands.");
187
188 loop {
189 let prompt = self.get_prompt();
190 let readline = self.editor.readline(&prompt);
191 match readline {
192 Ok(line) => {
193 let line = line.trim();
194 if line.is_empty() {
195 continue;
196 }
197
198 if let Err(e) = self.editor.add_history_entry(line) {
199 eprintln!("Warning: Could not add to history: {}", e);
200 }
201
202 if line == "exit" {
203 return ShellAction::Exit;
204 }
205
206 if line == ".vrepl" {
207 return ShellAction::SwitchToRepl;
208 }
209
210 if line == ".dbinfo" {
211 println!("Connected to database: {}", self.db_path);
212 continue;
213 }
214
215 if line == "help" {
216 self.show_help();
217 continue;
218 }
219
220 self.execute_command(line);
221 }
222 Err(ReadlineError::Interrupted) => {
223 println!("^C");
224 continue;
225 }
226 Err(ReadlineError::Eof) => {
227 return ShellAction::Exit; }
229 Err(err) => {
230 eprintln!("Input error: {}", err);
231 continue;
232 }
233 }
234 }
235 }
236
237 fn execute_command(&mut self, command: &str) {
238 let parts: Vec<&str> = command.split_whitespace().collect();
239 if parts.is_empty() {
240 return;
241 }
242
243 match parts[0] {
244 "cd" => {
245 let path = if parts.len() > 1 {
246 let p = parts[1];
247 if p == "~" {
248 env::var("HOME").unwrap_or_else(|_| ".".to_string())
249 } else if p.starts_with("~/") {
250 env::var("HOME")
251 .map(|home| format!("{}/{}", home, &p[2..]))
252 .unwrap_or_else(|_| p.to_string())
253 } else {
254 p.to_string()
255 }
256 } else {
257 env::var("HOME").unwrap_or_else(|_| ".".to_string())
258 };
259
260 if let Err(e) = env::set_current_dir(Path::new(&path)) {
261 eprintln!("cd: {}: {}", path, e);
262 }
263 }
264 "pwd" => {
265 if let Ok(current_dir) = env::current_dir() {
266 println!("{}", current_dir.display());
267 }
268 }
269 "history" => {
270 for (i, entry) in self.editor.history().iter().enumerate() {
271 println!("{}: {}", i + 1, entry);
272 }
273 }
274 _ => {
275 let status = Command::new(parts[0]).args(&parts[1..]).status();
276
277 match status {
278 Ok(status) => {
279 if !status.success() {
280 if let Some(code) = status.code() {
281 eprintln!("Command failed with exit code: {}", code);
282 } else {
283 eprintln!("Command terminated by signal");
284 }
285 }
286 }
287 Err(e) => {
288 eprintln!("Error executing command: {}", e);
289 if e.kind() == io::ErrorKind::NotFound {
290 eprintln!("Command not found: {}", parts[0]);
291 }
292 }
293 }
294 }
295 }
296 }
297
298 fn show_help(&self) {
299 println!("Vapor Shell - Available Commands:");
300 println!(" .vrepl - Switch back to the SQL REPL");
301 println!(" .dbinfo - Show information about the connected database");
302 println!(" cd <dir> - Change directory");
303 println!(" ls [dir] - List directory contents");
304 println!(" pwd - Print working directory");
305 println!(" history - Show command history");
306 println!(" help - Show this help message");
307 println!(" exit - Exit the shell and return to the REPL");
308 }
309
310 fn save_history(&mut self) -> Result<()> {
311 self.editor
312 .save_history(&self.history_path)
313 .context("Failed to save shell history")
314 }
315}
316
317pub fn shell_mode(db_path: &str) -> Result<ShellAction> {
331 println!("Starting shell mode for database: {}", db_path);
332
333 let mut shell = Shell::new(db_path)?;
334 let action = shell.run();
335
336 if let Err(e) = shell.save_history() {
338 eprintln!("Warning: Could not save shell history: {}", e);
339 }
340
341 if let Err(e) = env::set_current_dir(&shell.original_dir) {
343 eprintln!("Warning: Could not restore original directory: {}", e);
344 }
345
346 Ok(action)
347}