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