diff --git a/Cargo.toml b/Cargo.toml index a830b4d..7e2089a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" wasmtime = "11" wat = "1.0" + +# REPL line editing and persistent history +reedline = "0.10" diff --git a/src/main.rs b/src/main.rs index c4db041..f4d61ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,9 @@ use crate::dotfiles::import_dotfiles; use crate::wasm_host::{load_plugin_manifest, WasmPlugin}; use std::io::{self, Write}; +use reedline::{Reedline, Signal, DefaultPrompt, FileBackedHistory}; +use std::path::PathBuf; + fn main() { println!("Welcome to TruShell Native Engine"); @@ -31,103 +34,129 @@ fn main() { let mut terminal = terminal::Terminal::new(24, 80); - loop { - let prompt = terminal.prompt(); - print!("{prompt}"); - if let Err(e) = io::stdout().flush() { - eprintln!("Prompt flush error: {}", e); - continue; - } + // --- reedline setup --- + // Use the same ANSI-colored prompt returned by terminal.prompt() + let prompt_text = terminal.prompt(); + // DefaultPrompt::new in reedline 0.10 expects three strings: (left, indicator, multiline_indicator) + // We supply the terminal's ANSI prompt as the left section and simple indicators for the others. + let prompt = DefaultPrompt::new(prompt_text.clone(), " ".to_string(), "... ".to_string()); - let mut input = String::new(); - match io::stdin().read_line(&mut input) { - Ok(0) => break, // Ctrl+D to exit safely - Ok(_) => {} - Err(e) => { - eprintln!("Error reading input: {}", e); - continue; + // Initialize Reedline with a file-backed history if available. + let mut line_editor = if let Ok(home) = std::env::var("HOME") { + let hist_path = PathBuf::from(home).join(".trushell_history"); + match FileBackedHistory::with_file(1000, hist_path.clone()) { + Ok(history) => { + // On Unix, harden the history file permissions to 0o600. + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let _ = std::fs::set_permissions(&hist_path, std::fs::Permissions::from_mode(0o600)); + } + Reedline::with_history(Box::new(history)) } + Err(_) => Reedline::create(), } + } else { + Reedline::create() + }; + // ----------------------- - // Clean trailing newlines and whitespace completely - let trimmed_input = input.trim(); - if trimmed_input.is_empty() { - continue; - } + loop { + match line_editor.read_line(&prompt) { + Ok(Signal::Success(buffer)) => { + let trimmed_input = buffer.trim(); + if trimmed_input.is_empty() { + continue; + } - if trimmed_input == "exit" { - println!("Goodbye!"); - break; - } + if trimmed_input == "exit" { + println!("Goodbye!"); + break; + } - let parts = split_posix_words(trimmed_input); - if parts.first().map(String::as_str) == Some("cd") { - let new_dir = parts - .get(1) - .cloned() - .unwrap_or_else(|| std::env::var("HOME").unwrap_or_else(|_| ".".to_string())); - if let Err(e) = std::env::set_current_dir(new_dir.as_str()) { - eprintln!("trushell: cd: {}: {}", new_dir, e); - } - continue; - } + let parts = split_posix_words(trimmed_input); + if parts.first().map(String::as_str) == Some("cd") { + let new_dir = parts + .get(1) + .cloned() + .unwrap_or_else(|| std::env::var("HOME").unwrap_or_else(|_| ".".to_string())); + if let Err(e) = std::env::set_current_dir(new_dir.as_str()) { + eprintln!("trushell: cd: {}: {}", new_dir, e); + } + continue; + } - if parts.first().map(String::as_str) == Some("plugin") { - match parts.get(1).map(String::as_str) { - Some("run") => { - let module_path = parts.get(2).cloned(); - let input = parts.get(3).cloned().unwrap_or_default(); - if let Some(path) = module_path { - match WasmPlugin::load(&path) { - Ok(mut plugin) => match plugin.run(&input) { - Ok(logs) => { - for line in logs { - println!("plugin: {line}"); + if parts.first().map(String::as_str) == Some("plugin") { + match parts.get(1).map(String::as_str) { + Some("run") => { + let module_path = parts.get(2).cloned(); + let input = parts.get(3).cloned().unwrap_or_default(); + if let Some(path) = module_path { + match WasmPlugin::load(&path) { + Ok(mut plugin) => match plugin.run(&input) { + Ok(logs) => { + for line in logs { + println!("plugin: {line}"); + } + } + Err(err) => eprintln!("Plugin execution failed: {}", err), + }, + Err(err) => eprintln!("Failed to load plugin: {}", err), + } + } else { + eprintln!("Usage: plugin run [input]"); + } + } + Some("manifest") => { + if let Some(path) = parts.get(2) { + match load_plugin_manifest(path) { + Ok(manifest) => { + println!("Plugin: {}@{}", manifest.name, manifest.version); + println!("API version: {}", manifest.api_version); } + Err(err) => eprintln!("Failed to load plugin manifest: {}", err), } - Err(err) => eprintln!("Plugin execution failed: {}", err), - }, - Err(err) => eprintln!("Failed to load plugin: {}", err), + } else { + eprintln!("Usage: plugin manifest "); + } + } + _ => { + eprintln!("Unknown plugin command"); } - } else { - eprintln!("Usage: plugin run [input]"); } + continue; } - Some("manifest") => { - if let Some(path) = parts.get(2) { - match load_plugin_manifest(path) { - Ok(manifest) => { - println!("Plugin: {}@{}", manifest.name, manifest.version); - println!("API version: {}", manifest.api_version); - println!("Capabilities: {:?}", manifest.capabilities); - } - Err(err) => eprintln!("Failed to load manifest: {}", err), + + // Try to parse as TruShell AST -> execution (existing code path) + match parser::parse_line(trimmed_input) { + Ok(ast) => { + if let Some((cmd, args)) = probable_cli_from_ast(&ast) { + execute_system_command(&cmd, &args); + } else { + // Placeholder: existing executor should handle AST execution here. + // For now we print the parsed AST (retain current behavior). + println!("Parsed AST: {:#?}", ast); } - } else { - eprintln!("Usage: plugin manifest "); } - } - _ => { - eprintln!("Usage: plugin ..."); + Err(_) => { + // Fallback to running as a system command if parsing fails + execute_system_command_from_input(trimmed_input); + } } } - continue; - } - - match parser::parse_line(trimmed_input) { - Ok(ast) => { - // If the parsed AST looks like a CLI invocation that was - // accidentally parsed as subtraction (e.g. `ls -la` -> `ls - la`), - // fall back to executing the system command. - if let Some((cmd, args)) = probable_cli_from_ast(&ast) { - job_control::spawn_and_wait(&cmd, &args); - } else { - println!("Parsed AST: {:#?}", ast); - } + Ok(Signal::CtrlC) => { + // interrupt — print a newline and continue + println!(); + continue; + } + Ok(Signal::CtrlD) => { + // EOF — exit cleanly + println!(); + break; } Err(err) => { - eprintln!("Parse error: {}", err); - job_control::spawn_and_wait(&parts[0], &parts[1..].to_vec()); + eprintln!("Input error: {}", err); + break; } } } @@ -241,7 +270,6 @@ fn execute_system_command(cmd: &str, args: &[String]) { cmd }; - // Deprecated: routed to job_control::spawn_and_wait in main job_control::spawn_and_wait(command_name, args); }