diff --git a/src/diagnostics.rs b/src/diagnostics.rs index f6578bf..b080581 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -224,10 +224,31 @@ pub mod colors { pub const BOLD: &str = "\x1b[1m"; pub const DIM: &str = "\x1b[2m"; pub const RED: &str = "\x1b[31m"; + pub const GREEN: &str = "\x1b[32m"; pub const YELLOW: &str = "\x1b[33m"; pub const BLUE: &str = "\x1b[34m"; + pub const MAGENTA: &str = "\x1b[35m"; pub const CYAN: &str = "\x1b[36m"; pub const WHITE: &str = "\x1b[37m"; + pub const GRAY: &str = "\x1b[90m"; +} + +/// Apply color to text, respecting NO_COLOR / TERM=dumb +pub fn c(color: &str, text: &str) -> String { + if supports_color() { + format!("{}{}{}", color, text, colors::RESET) + } else { + text.to_string() + } +} + +/// Apply bold + color to text +pub fn bc(color: &str, text: &str) -> String { + if supports_color() { + format!("{}{}{}{}", colors::BOLD, color, text, colors::RESET) + } else { + text.to_string() + } } /// Severity level for diagnostics diff --git a/src/main.rs b/src/main.rs index c236fad..e02a889 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ mod symbol_table; mod typechecker; mod types; -use diagnostics::render; +use diagnostics::{render, c, bc, colors}; use interpreter::Interpreter; use parser::Parser; use rustyline::completion::{Completer, Pair}; @@ -98,13 +98,13 @@ fn main() { print_help(); } "--version" | "-v" => { - println!("Lux {}", VERSION); + println!("{}", bc(colors::GREEN, &format!("Lux {}", VERSION))); } - "fmt" => { + "fmt" | "f" => { // Format files (auto-discovers if no file specified) format_files(&args[2..]); } - "test" => { + "test" | "t" => { // Run tests run_tests(&args[2..]); } @@ -120,7 +120,7 @@ fn main() { // Initialize a new project init_project(args.get(2).map(|s| s.as_str())); } - "check" => { + "check" | "k" => { // Type check files (auto-discovers if no file specified) check_files(&args[2..]); } @@ -158,7 +158,22 @@ fn main() { std::process::exit(1); } } - "compile" => { + "serve" | "s" => { + // Static file server (like python -m http.server) + let port = args.iter() + .position(|a| a == "--port" || a == "-p") + .and_then(|i| args.get(i + 1)) + .and_then(|s| s.parse::().ok()) + .unwrap_or(8080); + + let dir = args.get(2) + .filter(|a| !a.starts_with('-')) + .map(|s| s.as_str()) + .unwrap_or("."); + + serve_static_files(dir, port); + } + "compile" | "c" => { // Compile to native binary or JavaScript if args.len() < 3 { eprintln!("Usage: lux compile [-o binary]"); @@ -189,9 +204,23 @@ fn main() { // Generate API documentation generate_docs(&args[2..]); } - path => { - // Run a file - run_file(path); + cmd => { + // Check if it looks like a command typo + if !std::path::Path::new(cmd).exists() && !cmd.starts_with('-') && !cmd.contains('.') && !cmd.contains('/') { + let known_commands = vec![ + "fmt", "test", "watch", "init", "check", "debug", + "pkg", "registry", "serve", "compile", "doc", + ]; + let suggestions = diagnostics::find_similar_names(cmd, known_commands.into_iter(), 2); + if !suggestions.is_empty() { + if let Some(hint) = diagnostics::format_did_you_mean(&suggestions) { + eprintln!("{} Unknown command '{}'. {}", c(colors::YELLOW, "warning:"), cmd, c(colors::YELLOW, &hint)); + std::process::exit(1); + } + } + } + // Run as file + run_file(cmd); } } } else { @@ -201,37 +230,57 @@ fn main() { } fn print_help() { - println!("Lux {} - A functional language with first-class effects", VERSION); + println!("{}", bc(colors::GREEN, &format!("Lux {}", VERSION))); + println!("{}", c(colors::DIM, "A functional language with first-class effects")); println!(); - println!("Usage:"); - println!(" lux Start the REPL"); - println!(" lux Run a file (interpreter)"); - println!(" lux compile Compile to native binary"); - println!(" lux compile -o app Compile to binary named 'app'"); - println!(" lux compile --run Compile and execute"); - println!(" lux compile --emit-c Output C code instead of binary"); - println!(" lux compile --target js Compile to JavaScript"); - println!(" lux fmt [file] [--check] Format files (auto-discovers if no file given)"); - println!(" lux check [file] Type check files (auto-discovers if no file given)"); - println!(" lux test [pattern] Run tests (optional pattern filter)"); - println!(" lux watch Watch and re-run on changes"); - println!(" lux debug Start interactive debugger"); - println!(" lux init [name] Initialize a new project"); - println!(" lux pkg Package manager (install, add, remove, list, update)"); - println!(" lux registry Start package registry server"); - println!(" -s, --storage Storage directory (default: ./lux-registry)"); - println!(" -b, --bind Bind address (default: 127.0.0.1:8080)"); - println!(" lux doc [file] [-o dir] Generate API documentation (HTML)"); - println!(" --json Output as JSON"); - println!(" lux --lsp Start LSP server (for IDE integration)"); - println!(" lux --help Show this help"); - println!(" lux --version Show version"); + println!("{}", bc("", "Usage:")); + println!(); + println!(" {} Start the REPL", bc(colors::CYAN, "lux")); + println!(" {} {} Run a file (interpreter)", bc(colors::CYAN, "lux"), c(colors::YELLOW, "")); + println!(" {} {} {} Compile to native binary", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "")); + println!(" {} {} {} {} Compile with output name", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, ""), c(colors::YELLOW, "-o app")); + println!(" {} {} {} {} Compile and execute", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, ""), c(colors::YELLOW, "--run")); + println!(" {} {} {} {} Output C code", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, ""), c(colors::YELLOW, "--emit-c")); + println!(" {} {} {} {} Compile to JavaScript", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, ""), c(colors::YELLOW, "--target js")); + println!(" {} {} {} Format files {}", + bc(colors::CYAN, "lux"), bc(colors::CYAN, "fmt"), c(colors::YELLOW, "[file] [--check]"), + c(colors::DIM, "(alias: f)")); + println!(" {} {} {} Type check files {}", + bc(colors::CYAN, "lux"), bc(colors::CYAN, "check"), c(colors::YELLOW, "[file]"), + c(colors::DIM, "(alias: k)")); + println!(" {} {} {} Run tests {}", + bc(colors::CYAN, "lux"), bc(colors::CYAN, "test"), c(colors::YELLOW, "[pattern]"), + c(colors::DIM, "(alias: t)")); + println!(" {} {} {} Watch and re-run on changes", + bc(colors::CYAN, "lux"), bc(colors::CYAN, "watch"), c(colors::YELLOW, "")); + println!(" {} {} {} Start interactive debugger", + bc(colors::CYAN, "lux"), bc(colors::CYAN, "debug"), c(colors::YELLOW, "")); + println!(" {} {} {} Initialize a new project", + bc(colors::CYAN, "lux"), bc(colors::CYAN, "init"), c(colors::YELLOW, "[name]")); + println!(" {} {} {} Package manager {}", + bc(colors::CYAN, "lux"), bc(colors::CYAN, "pkg"), c(colors::YELLOW, ""), + c(colors::DIM, "(install, add, remove, list, update)")); + println!(" {} {} Start package registry server", + bc(colors::CYAN, "lux"), bc(colors::CYAN, "registry")); + println!(" {} {} {} Start static file server {}", + bc(colors::CYAN, "lux"), bc(colors::CYAN, "serve"), c(colors::YELLOW, "[dir]"), + c(colors::DIM, "(alias: s)")); + println!(" {} {} {} Generate API documentation", + bc(colors::CYAN, "lux"), bc(colors::CYAN, "doc"), c(colors::YELLOW, "[file] [-o dir]")); + println!(" {} {} Start LSP server", + bc(colors::CYAN, "lux"), c(colors::YELLOW, "--lsp")); + println!(" {} {} Show this help", + bc(colors::CYAN, "lux"), c(colors::YELLOW, "--help")); + println!(" {} {} Show version", + bc(colors::CYAN, "lux"), c(colors::YELLOW, "--version")); } fn format_files(args: &[String]) { use formatter::{format, FormatConfig}; use std::path::Path; + use std::time::Instant; + let start = Instant::now(); let check_only = args.iter().any(|a| a == "--check"); let pattern = args.iter().find(|a| !a.starts_with('-')).map(|s| s.as_str()); @@ -268,7 +317,7 @@ fn format_files(args: &[String]) { if files_to_format.is_empty() { println!("No .lux files found."); - println!("Looking in: ., src/, examples/, tests/"); + println!("{}", c(colors::DIM, "Looking in: ., src/, examples/, tests/")); return; } @@ -287,7 +336,7 @@ fn format_files(args: &[String]) { let source = match std::fs::read_to_string(file_path) { Ok(s) => s, Err(e) => { - eprintln!("{}: ERROR - {}", path, e); + eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e); error_count += 1; continue; } @@ -296,7 +345,7 @@ fn format_files(args: &[String]) { let formatted = match format(&source, &config) { Ok(f) => f, Err(e) => { - eprintln!("{}: FORMAT ERROR - {}", path, e); + eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e); error_count += 1; continue; } @@ -310,43 +359,57 @@ fn format_files(args: &[String]) { } } else if source != formatted { if let Err(e) = std::fs::write(file_path, &formatted) { - eprintln!("{}: WRITE ERROR - {}", path, e); + eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e); error_count += 1; continue; } - println!("Formatted {}", path); + println!(" {} {}", c(colors::CYAN, "Formatted"), path); formatted_count += 1; } else { unchanged_count += 1; } } + let elapsed = start.elapsed(); + let time_str = c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64())); + println!(); if check_only { if !would_reformat.is_empty() { println!("Files that would be reformatted:"); for path in &would_reformat { - println!(" {}", path); + println!(" {}", c(colors::YELLOW, path)); } println!(); - println!("{} would be reformatted, {} already formatted", would_reformat.len(), unchanged_count); + println!("{} {} would be reformatted, {} already formatted {}", + c(colors::YELLOW, "\u{2717}"), would_reformat.len(), unchanged_count, time_str); std::process::exit(1); } else { - println!("All {} files are correctly formatted", unchanged_count); + println!("{} All {} files are correctly formatted {}", + c(colors::GREEN, "\u{2713}"), unchanged_count, time_str); } - } else { - println!("{} formatted, {} unchanged, {} errors", formatted_count, unchanged_count, error_count); - } - - if error_count > 0 { + } else if error_count > 0 { + let mut parts = Vec::new(); + if formatted_count > 0 { parts.push(format!("{} formatted", formatted_count)); } + if unchanged_count > 0 { parts.push(format!("{} unchanged", unchanged_count)); } + parts.push(format!("{} errors", error_count)); + println!("{} {} {}", c(colors::RED, "\u{2717}"), parts.join(", "), time_str); std::process::exit(1); + } else if formatted_count > 0 { + println!("{} {} formatted, {} unchanged {}", + c(colors::GREEN, "\u{2713}"), formatted_count, unchanged_count, time_str); + } else { + println!("{} All {} files already formatted {}", + c(colors::GREEN, "\u{2713}"), unchanged_count, time_str); } } fn check_files(args: &[String]) { use modules::ModuleLoader; use std::path::Path; + use std::time::Instant; + let start = Instant::now(); let pattern = args.first().map(|s| s.as_str()); // Collect files to check @@ -382,7 +445,7 @@ fn check_files(args: &[String]) { if files_to_check.is_empty() { println!("No .lux files found."); - println!("Looking in: ., src/, examples/, tests/"); + println!("{}", c(colors::DIM, "Looking in: ., src/, examples/, tests/")); return; } @@ -397,7 +460,7 @@ fn check_files(args: &[String]) { let source = match std::fs::read_to_string(file_path) { Ok(s) => s, Err(e) => { - eprintln!("{}: ERROR - {}", path, e); + eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e); failed += 1; continue; } @@ -411,7 +474,7 @@ fn check_files(args: &[String]) { let program = match loader.load_source(&source, Some(file_path)) { Ok(p) => p, Err(e) => { - eprintln!("{}: MODULE ERROR - {}", path, e); + eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e); failed += 1; continue; } @@ -419,23 +482,29 @@ fn check_files(args: &[String]) { let mut checker = TypeChecker::new(); if let Err(errors) = checker.check_program_with_modules(&program, &loader) { - eprintln!("{}: FAILED", path); + eprintln!("{} {}", c(colors::RED, "\u{2717}"), path); for error in errors { let diagnostic = error.to_diagnostic(); eprint!("{}", render(&diagnostic, &source, Some(&path))); } failed += 1; } else { - println!("{}: OK", path); + println!("{} {}", c(colors::GREEN, "\u{2713}"), path); passed += 1; } } - println!(); - println!("Checked {} files: {} passed, {} failed", passed + failed, passed, failed); + let elapsed = start.elapsed(); + let time_str = c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64())); + println!(); if failed > 0 { + println!("{} {} passed, {} failed {}", + c(colors::RED, "\u{2717}"), passed, failed, time_str); std::process::exit(1); + } else { + println!("{} {} passed {}", + c(colors::GREEN, "\u{2713}"), passed, time_str); } } @@ -459,12 +528,14 @@ fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c: use modules::ModuleLoader; use std::path::Path; use std::process::Command; + use std::time::Instant; + let start = Instant::now(); let file_path = Path::new(path); let source = match std::fs::read_to_string(file_path) { Ok(s) => s, Err(e) => { - eprintln!("Error reading file '{}': {}", path, e); + eprintln!("{} Error reading file '{}': {}", c(colors::RED, "error:"), path, e); std::process::exit(1); } }; @@ -478,7 +549,7 @@ fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c: let program = match loader.load_source(&source, Some(file_path)) { Ok(p) => p, Err(e) => { - eprintln!("Module error: {}", e); + eprintln!("{} {}", c(colors::RED, "error:"), e); std::process::exit(1); } }; @@ -498,12 +569,11 @@ fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c: let c_code = match backend.generate(&program) { Ok(code) => code, Err(e) => { - eprintln!("C codegen error: {}", e); + eprintln!("{} C codegen: {}", c(colors::RED, "error:"), e); eprintln!(); - eprintln!("Note: The C backend supports functions, closures, ADTs,"); - eprintln!("pattern matching, lists, and Console.print."); - eprintln!(); - eprintln!("Not yet supported: other effects, some advanced features."); + eprintln!("{}", c(colors::DIM, "Note: The C backend supports functions, closures, ADTs,")); + eprintln!("{}", c(colors::DIM, "pattern matching, lists, and Console.print.")); + eprintln!("{}", c(colors::DIM, "Not yet supported: other effects, some advanced features.")); std::process::exit(1); } }; @@ -512,10 +582,13 @@ fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c: if emit_c { if let Some(out_path) = output_path { if let Err(e) = std::fs::write(out_path, &c_code) { - eprintln!("Error writing file '{}': {}", out_path, e); + eprintln!("{} writing '{}': {}", c(colors::RED, "error:"), out_path, e); std::process::exit(1); } - eprintln!("Wrote C code to {}", out_path); + let elapsed = start.elapsed(); + eprintln!("{} Emitted C to {} {}", + c(colors::GREEN, "\u{2713}"), bc(colors::CYAN, out_path), + c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64()))); } else { println!("{}", c_code); } @@ -537,7 +610,7 @@ fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c: }; if let Err(e) = std::fs::write(&temp_c, &c_code) { - eprintln!("Error writing temp file: {}", e); + eprintln!("{} writing temp file: {}", c(colors::RED, "error:"), e); std::process::exit(1); } @@ -553,21 +626,19 @@ fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c: match compile_result { Ok(output) => { if !output.status.success() { - eprintln!("C compilation failed:"); + eprintln!("{} C compilation failed:", c(colors::RED, "error:")); eprintln!("{}", String::from_utf8_lossy(&output.stderr)); std::process::exit(1); } } Err(e) => { - eprintln!("Failed to run C compiler '{}': {}", cc, e); - eprintln!("Make sure gcc or clang is installed, or set CC environment variable."); + eprintln!("{} Failed to run C compiler '{}': {}", c(colors::RED, "error:"), cc, e); + eprintln!("{}", c(colors::DIM, "Make sure gcc or clang is installed, or set CC environment variable.")); std::process::exit(1); } } - // Keep temp file for debugging - eprintln!("C file: {}", temp_c.display()); - // let _ = std::fs::remove_file(&temp_c); + let elapsed = start.elapsed(); if run_after { // Run the compiled binary @@ -577,13 +648,15 @@ fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c: std::process::exit(status.code().unwrap_or(1)); } Err(e) => { - eprintln!("Failed to run compiled binary: {}", e); + eprintln!("{} Failed to run compiled binary: {}", c(colors::RED, "error:"), e); std::process::exit(1); } } } else { - // Just print where the binary is - eprintln!("Compiled to {}", output_bin.display()); + eprintln!("{} Compiled to {} {}", + c(colors::GREEN, "\u{2713}"), + bc(colors::CYAN, &output_bin.display().to_string()), + c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64()))); } } @@ -592,12 +665,14 @@ fn compile_to_js(path: &str, output_path: Option<&str>, run_after: bool) { use modules::ModuleLoader; use std::path::Path; use std::process::Command; + use std::time::Instant; + let start = Instant::now(); let file_path = Path::new(path); let source = match std::fs::read_to_string(file_path) { Ok(s) => s, Err(e) => { - eprintln!("Error reading file '{}': {}", path, e); + eprintln!("{} Error reading file '{}': {}", c(colors::RED, "error:"), path, e); std::process::exit(1); } }; @@ -611,7 +686,7 @@ fn compile_to_js(path: &str, output_path: Option<&str>, run_after: bool) { let program = match loader.load_source(&source, Some(file_path)) { Ok(p) => p, Err(e) => { - eprintln!("Module error: {}", e); + eprintln!("{} {}", c(colors::RED, "error:"), e); std::process::exit(1); } }; @@ -631,7 +706,7 @@ fn compile_to_js(path: &str, output_path: Option<&str>, run_after: bool) { let js_code = match backend.generate(&program) { Ok(code) => code, Err(e) => { - eprintln!("JS codegen error: {}", e); + eprintln!("{} JS codegen: {}", c(colors::RED, "error:"), e); std::process::exit(1); } }; @@ -649,10 +724,12 @@ fn compile_to_js(path: &str, output_path: Option<&str>, run_after: bool) { // Write the JavaScript file if let Err(e) = std::fs::write(&output_js, &js_code) { - eprintln!("Error writing file '{}': {}", output_js.display(), e); + eprintln!("{} writing '{}': {}", c(colors::RED, "error:"), output_js.display(), e); std::process::exit(1); } + let elapsed = start.elapsed(); + if run_after { // Run with Node.js let node_result = Command::new("node") @@ -664,19 +741,25 @@ fn compile_to_js(path: &str, output_path: Option<&str>, run_after: bool) { std::process::exit(status.code().unwrap_or(1)); } Err(e) => { - eprintln!("Failed to run with Node.js: {}", e); - eprintln!("Make sure Node.js is installed."); + eprintln!("{} Failed to run with Node.js: {}", c(colors::RED, "error:"), e); + eprintln!("{}", c(colors::DIM, "Make sure Node.js is installed.")); std::process::exit(1); } } } else { - eprintln!("Compiled to {}", output_js.display()); + eprintln!("{} Compiled to {} {}", + c(colors::GREEN, "\u{2713}"), + bc(colors::CYAN, &output_js.display().to_string()), + c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64()))); } } fn run_tests(args: &[String]) { use std::path::Path; use std::fs; + use std::time::Instant; + + let start = Instant::now(); // Find test files let pattern = args.first().map(|s| s.as_str()); @@ -707,12 +790,12 @@ fn run_tests(args: &[String]) { if test_files.is_empty() { println!("No test files found."); - println!("Test files should be named test_*.lux or *_test.lux"); - println!("Or contain functions named test_*"); + println!("{}", c(colors::DIM, "Test files should be named test_*.lux or *_test.lux")); + println!("{}", c(colors::DIM, "Or contain functions named test_*")); return; } - println!("Running tests...\n"); + println!("{}\n", bc("", "Running tests...")); let mut total_passed = 0; let mut total_failed = 0; @@ -725,7 +808,7 @@ fn run_tests(args: &[String]) { let source = match fs::read_to_string(test_file) { Ok(s) => s, Err(e) => { - println!(" {} ... ERROR: {}", path_str, e); + println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, &e.to_string())); total_failed += 1; continue; } @@ -734,7 +817,7 @@ fn run_tests(args: &[String]) { let program = match Parser::parse_source(&source) { Ok(p) => p, Err(e) => { - println!(" {} ... PARSE ERROR: {}", path_str, e); + println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, &format!("parse error: {}", e))); total_failed += 1; continue; } @@ -743,7 +826,7 @@ fn run_tests(args: &[String]) { // Type check let mut checker = typechecker::TypeChecker::new(); if let Err(errors) = checker.check_program(&program) { - println!(" {} ... TYPE ERROR", path_str); + println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, "type error")); for err in errors { eprintln!(" {}", err); } @@ -774,14 +857,15 @@ fn run_tests(args: &[String]) { Ok(_) => { let results = interp.get_test_results(); if results.failed == 0 && results.passed == 0 { - // No Test assertions, just check it runs - println!(" {} ... OK (no assertions)", path_str); + println!(" {} {} {}", c(colors::GREEN, "\u{2713}"), path_str, c(colors::DIM, "(no assertions)")); total_passed += 1; } else if results.failed == 0 { - println!(" {} ... OK ({} assertions)", path_str, results.passed); + println!(" {} {} {}", c(colors::GREEN, "\u{2713}"), path_str, + c(colors::DIM, &format!("({} assertions)", results.passed))); total_passed += results.passed; } else { - println!(" {} ... FAILED ({} passed, {} failed)", path_str, results.passed, results.failed); + println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, + c(colors::RED, &format!("{} passed, {} failed", results.passed, results.failed))); total_passed += results.passed; total_failed += results.failed; for failure in &results.failures { @@ -790,13 +874,13 @@ fn run_tests(args: &[String]) { } } Err(e) => { - println!(" {} ... RUNTIME ERROR: {}", path_str, e); + println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, &format!("runtime error: {}", e))); total_failed += 1; } } } else { // Run individual test functions - println!(" {}:", path_str); + println!(" {}:", c(colors::DIM, &path_str)); for test_name in &test_funcs { let mut interp = Interpreter::new(); @@ -805,7 +889,7 @@ fn run_tests(args: &[String]) { // First run the file to define all functions if let Err(e) = interp.run(&program) { - println!(" {} ... ERROR: {}", test_name, e); + println!(" {} {} {}", c(colors::RED, "\u{2717}"), test_name, c(colors::RED, &e.to_string())); total_failed += 1; continue; } @@ -815,7 +899,7 @@ fn run_tests(args: &[String]) { let call_program = match Parser::parse_source(&call_source) { Ok(p) => p, Err(e) => { - println!(" {} ... ERROR: {}", test_name, e); + println!(" {} {} {}", c(colors::RED, "\u{2717}"), test_name, c(colors::RED, &e.to_string())); total_failed += 1; continue; } @@ -825,10 +909,10 @@ fn run_tests(args: &[String]) { Ok(_) => { let results = interp.get_test_results(); if results.failed == 0 { - println!(" {} ... OK", test_name); + println!(" {} {}", c(colors::GREEN, "\u{2713}"), test_name); total_passed += 1; } else { - println!(" {} ... FAILED", test_name); + println!(" {} {}", c(colors::RED, "\u{2717}"), test_name); total_failed += 1; for failure in &results.failures { all_failures.push((path_str.clone(), test_name.clone(), failure.clone())); @@ -836,7 +920,7 @@ fn run_tests(args: &[String]) { } } Err(e) => { - println!(" {} ... ERROR: {}", test_name, e); + println!(" {} {} {}", c(colors::RED, "\u{2717}"), test_name, c(colors::RED, &e.to_string())); total_failed += 1; } } @@ -846,28 +930,34 @@ fn run_tests(args: &[String]) { // Print failure details if !all_failures.is_empty() { - println!("\n--- Failures ---\n"); + println!("\n{}\n", bc(colors::RED, "--- Failures ---")); for (file, test, failure) in &all_failures { if test.is_empty() { - println!("{}:", file); + println!("{}:", bc("", file)); } else { - println!("{} - {}:", file, test); + println!("{} - {}:", bc("", file), bc("", test)); } println!(" {}", failure.message); if let Some(expected) = &failure.expected { - println!(" Expected: {}", expected); + println!(" {} {}", c(colors::CYAN, "Expected:"), expected); } if let Some(actual) = &failure.actual { - println!(" Actual: {}", actual); + println!(" {} {}", c(colors::RED, "Actual: "), actual); } println!(); } } - println!("Results: {} passed, {} failed", total_passed, total_failed); + let elapsed = start.elapsed(); + let time_str = c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64())); if total_failed > 0 { + println!("{} {} passed, {} failed {}", + c(colors::RED, "\u{2717}"), total_passed, total_failed, time_str); std::process::exit(1); + } else { + println!("{} {} passed {}", + c(colors::GREEN, "\u{2713}"), total_passed, time_str); } } @@ -973,6 +1063,163 @@ fn watch_file(path: &str) { } } +fn serve_static_files(dir: &str, port: u16) { + use std::io::{Write, BufRead, BufReader}; + use std::net::TcpListener; + use std::path::Path; + + let root = Path::new(dir).canonicalize().unwrap_or_else(|_| { + eprintln!("{} Directory not found: {}", c(colors::RED, "error:"), dir); + std::process::exit(1); + }); + + let listener = match TcpListener::bind(format!("0.0.0.0:{}", port)) { + Ok(l) => l, + Err(e) => { + if e.kind() == std::io::ErrorKind::AddrInUse { + eprintln!("{} Port {} is already in use", c(colors::RED, "error:"), port); + // Try to find an available port + for try_port in (port + 1)..=(port + 3) { + if TcpListener::bind(format!("0.0.0.0:{}", try_port)).is_ok() { + eprintln!("{}", c(colors::CYAN, &format!(" Try: lux serve --port {}", try_port))); + break; + } + } + } else { + eprintln!("{} Could not bind to port {}: {}", c(colors::RED, "error:"), port, e); + } + std::process::exit(1); + } + }; + + println!("{}", bc("", "Lux static file server")); + println!(" {} {}", c(colors::DIM, "Serving:"), root.display()); + println!(" {} {}", c(colors::DIM, "URL:"), bc(colors::CYAN, &format!("http://localhost:{}", port))); + println!(); + println!("{}", c(colors::DIM, "Press Ctrl+C to stop")); + println!(); + + for stream in listener.incoming() { + let Ok(mut stream) = stream else { continue }; + + // Read the request line + let mut reader = BufReader::new(&stream); + let mut request_line = String::new(); + if reader.read_line(&mut request_line).is_err() { + continue; + } + + // Parse the request + let parts: Vec<&str> = request_line.split_whitespace().collect(); + if parts.len() < 2 { + continue; + } + + let method = parts[0]; + let path = parts[1]; + + // Log the request (status is logged after response is determined) + let log_method = bc("", method); + let log_path = path.to_string(); + + // Only handle GET requests + if method != "GET" { + let response = "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 18\r\n\r\nMethod Not Allowed"; + let _ = stream.write_all(response.as_bytes()); + continue; + } + + // Resolve the file path + let mut file_path = root.clone(); + let request_path = if path == "/" { "/index.html" } else { path }; + + // Prevent directory traversal + for component in request_path.trim_start_matches('/').split('/') { + if component == ".." { + let response = "HTTP/1.1 403 Forbidden\r\nContent-Length: 9\r\n\r\nForbidden"; + let _ = stream.write_all(response.as_bytes()); + continue; + } + file_path.push(component); + } + + // Try to serve the file + let (status, content_type, body) = if file_path.is_file() { + match std::fs::read(&file_path) { + Ok(content) => { + let mime = get_mime_type(&file_path); + ("200 OK", mime, content) + } + Err(_) => { + ("500 Internal Server Error", "text/plain", b"Error reading file".to_vec()) + } + } + } else if file_path.is_dir() { + // Try index.html + let index_path = file_path.join("index.html"); + if index_path.is_file() { + match std::fs::read(&index_path) { + Ok(content) => ("200 OK", "text/html", content), + Err(_) => ("500 Internal Server Error", "text/plain", b"Error reading file".to_vec()), + } + } else { + ("404 Not Found", "text/plain", b"Not Found".to_vec()) + } + } else { + // Try with .html extension + let html_path = file_path.with_extension("html"); + if html_path.is_file() { + match std::fs::read(&html_path) { + Ok(content) => ("200 OK", "text/html", content), + Err(_) => ("500 Internal Server Error", "text/plain", b"Error reading file".to_vec()), + } + } else { + ("404 Not Found", "text/plain", b"Not Found".to_vec()) + } + }; + + // Log the request with colored status + let status_color = if status.starts_with("200") { + colors::GREEN + } else if status.starts_with("404") { + colors::YELLOW + } else { + colors::RED + }; + let status_code = status.split_whitespace().next().unwrap_or(status); + println!(" {} {} {}", log_method, c(status_color, status_code), log_path); + + // Send response + let response = format!( + "HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + status, content_type, body.len() + ); + let _ = stream.write_all(response.as_bytes()); + let _ = stream.write_all(&body); + } +} + +fn get_mime_type(path: &std::path::Path) -> &'static str { + match path.extension().and_then(|e| e.to_str()) { + Some("html") | Some("htm") => "text/html", + Some("css") => "text/css", + Some("js") => "application/javascript", + Some("json") => "application/json", + Some("png") => "image/png", + Some("jpg") | Some("jpeg") => "image/jpeg", + Some("gif") => "image/gif", + Some("svg") => "image/svg+xml", + Some("ico") => "image/x-icon", + Some("woff") => "font/woff", + Some("woff2") => "font/woff2", + Some("ttf") => "font/ttf", + Some("txt") => "text/plain", + Some("xml") => "application/xml", + Some("pdf") => "application/pdf", + _ => "application/octet-stream", + } +} + fn handle_pkg_command(args: &[String]) { use package::{PackageManager, DependencySource}; use std::path::PathBuf; @@ -1429,19 +1676,19 @@ let output = run main() with {} "#; fs::write(project_dir.join(".gitignore"), gitignore_content).unwrap(); - println!("Created new Lux project: {}", project_name); + println!("{} Created new Lux project: {}", c(colors::GREEN, "\u{2713}"), bc(colors::GREEN, project_name)); println!(); - println!("Project structure:"); - println!(" {}/", project_name); - println!(" ├── lux.toml"); - println!(" ├── src/"); - println!(" │ └── main.lux"); - println!(" └── tests/"); - println!(" └── test_example.lux"); + println!("{}", bc("", "Project structure:")); + println!(" {}", bc(colors::CYAN, &format!("{}/", project_name))); + println!(" {} {}", c(colors::DIM, "\u{251c}\u{2500}\u{2500}"), "lux.toml"); + println!(" {} {}", c(colors::DIM, "\u{251c}\u{2500}\u{2500}"), bc(colors::CYAN, "src/")); + println!(" {} {} {}", c(colors::DIM, "\u{2502}"), c(colors::DIM, " \u{2514}\u{2500}\u{2500}"), "main.lux"); + println!(" {} {}", c(colors::DIM, "\u{2514}\u{2500}\u{2500}"), bc(colors::CYAN, "tests/")); + println!(" {} {}", c(colors::DIM, "\u{2514}\u{2500}\u{2500}"), "test_example.lux"); println!(); println!("To get started:"); - println!(" cd {}", project_name); - println!(" lux src/main.lux"); + println!(" {} {}", c(colors::DIM, "$"), bc(colors::CYAN, &format!("cd {}", project_name))); + println!(" {} {}", c(colors::DIM, "$"), bc(colors::CYAN, "lux src/main.lux")); } /// Generate API documentation for Lux source files