feat: CLI UX overhaul with colored output, timing, shorthands, and fuzzy suggestions

Add polished CLI output across all commands: colored help text, green/red
pass/fail indicators (✓/✗), elapsed timing on compile/check/test/fmt,
command shorthands (c/t/f/s/k), fuzzy "did you mean?" on typos, and
smart port-in-use suggestions for serve. Respects NO_COLOR/TERM=dumb.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 06:52:36 -05:00
parent bc60f1c8f1
commit 8c90d5a8dc
2 changed files with 380 additions and 112 deletions

View File

@@ -224,10 +224,31 @@ pub mod colors {
pub const BOLD: &str = "\x1b[1m"; pub const BOLD: &str = "\x1b[1m";
pub const DIM: &str = "\x1b[2m"; pub const DIM: &str = "\x1b[2m";
pub const RED: &str = "\x1b[31m"; pub const RED: &str = "\x1b[31m";
pub const GREEN: &str = "\x1b[32m";
pub const YELLOW: &str = "\x1b[33m"; pub const YELLOW: &str = "\x1b[33m";
pub const BLUE: &str = "\x1b[34m"; pub const BLUE: &str = "\x1b[34m";
pub const MAGENTA: &str = "\x1b[35m";
pub const CYAN: &str = "\x1b[36m"; pub const CYAN: &str = "\x1b[36m";
pub const WHITE: &str = "\x1b[37m"; 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 /// Severity level for diagnostics

View File

@@ -19,7 +19,7 @@ mod symbol_table;
mod typechecker; mod typechecker;
mod types; mod types;
use diagnostics::render; use diagnostics::{render, c, bc, colors};
use interpreter::Interpreter; use interpreter::Interpreter;
use parser::Parser; use parser::Parser;
use rustyline::completion::{Completer, Pair}; use rustyline::completion::{Completer, Pair};
@@ -98,13 +98,13 @@ fn main() {
print_help(); print_help();
} }
"--version" | "-v" => { "--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 (auto-discovers if no file specified)
format_files(&args[2..]); format_files(&args[2..]);
} }
"test" => { "test" | "t" => {
// Run tests // Run tests
run_tests(&args[2..]); run_tests(&args[2..]);
} }
@@ -120,7 +120,7 @@ fn main() {
// Initialize a new project // Initialize a new project
init_project(args.get(2).map(|s| s.as_str())); init_project(args.get(2).map(|s| s.as_str()));
} }
"check" => { "check" | "k" => {
// Type check files (auto-discovers if no file specified) // Type check files (auto-discovers if no file specified)
check_files(&args[2..]); check_files(&args[2..]);
} }
@@ -158,7 +158,22 @@ fn main() {
std::process::exit(1); 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::<u16>().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 // Compile to native binary or JavaScript
if args.len() < 3 { if args.len() < 3 {
eprintln!("Usage: lux compile <file.lux> [-o binary]"); eprintln!("Usage: lux compile <file.lux> [-o binary]");
@@ -189,9 +204,23 @@ fn main() {
// Generate API documentation // Generate API documentation
generate_docs(&args[2..]); generate_docs(&args[2..]);
} }
path => { cmd => {
// Run a file // Check if it looks like a command typo
run_file(path); 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 { } else {
@@ -201,37 +230,57 @@ fn main() {
} }
fn print_help() { 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!();
println!("Usage:"); println!("{}", bc("", "Usage:"));
println!(" lux Start the REPL"); println!();
println!(" lux <file.lux> Run a file (interpreter)"); println!(" {} Start the REPL", bc(colors::CYAN, "lux"));
println!(" lux compile <file.lux> Compile to native binary"); println!(" {} {} Run a file (interpreter)", bc(colors::CYAN, "lux"), c(colors::YELLOW, "<file.lux>"));
println!(" lux compile <f> -o app Compile to binary named 'app'"); println!(" {} {} {} Compile to native binary", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<file.lux>"));
println!(" lux compile <f> --run Compile and execute"); println!(" {} {} {} {} Compile with output name", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<f>"), c(colors::YELLOW, "-o app"));
println!(" lux compile <f> --emit-c Output C code instead of binary"); println!(" {} {} {} {} Compile and execute", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<f>"), c(colors::YELLOW, "--run"));
println!(" lux compile <f> --target js Compile to JavaScript"); println!(" {} {} {} {} Output C code", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<f>"), c(colors::YELLOW, "--emit-c"));
println!(" lux fmt [file] [--check] Format files (auto-discovers if no file given)"); println!(" {} {} {} {} Compile to JavaScript", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<f>"), c(colors::YELLOW, "--target js"));
println!(" lux check [file] Type check files (auto-discovers if no file given)"); println!(" {} {} {} Format files {}",
println!(" lux test [pattern] Run tests (optional pattern filter)"); bc(colors::CYAN, "lux"), bc(colors::CYAN, "fmt"), c(colors::YELLOW, "[file] [--check]"),
println!(" lux watch <file.lux> Watch and re-run on changes"); c(colors::DIM, "(alias: f)"));
println!(" lux debug <file.lux> Start interactive debugger"); println!(" {} {} {} Type check files {}",
println!(" lux init [name] Initialize a new project"); bc(colors::CYAN, "lux"), bc(colors::CYAN, "check"), c(colors::YELLOW, "[file]"),
println!(" lux pkg <command> Package manager (install, add, remove, list, update)"); c(colors::DIM, "(alias: k)"));
println!(" lux registry Start package registry server"); println!(" {} {} {} Run tests {}",
println!(" -s, --storage <dir> Storage directory (default: ./lux-registry)"); bc(colors::CYAN, "lux"), bc(colors::CYAN, "test"), c(colors::YELLOW, "[pattern]"),
println!(" -b, --bind <addr> Bind address (default: 127.0.0.1:8080)"); c(colors::DIM, "(alias: t)"));
println!(" lux doc [file] [-o dir] Generate API documentation (HTML)"); println!(" {} {} {} Watch and re-run on changes",
println!(" --json Output as JSON"); bc(colors::CYAN, "lux"), bc(colors::CYAN, "watch"), c(colors::YELLOW, "<file.lux>"));
println!(" lux --lsp Start LSP server (for IDE integration)"); println!(" {} {} {} Start interactive debugger",
println!(" lux --help Show this help"); bc(colors::CYAN, "lux"), bc(colors::CYAN, "debug"), c(colors::YELLOW, "<file.lux>"));
println!(" lux --version Show version"); 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, "<command>"),
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]) { fn format_files(args: &[String]) {
use formatter::{format, FormatConfig}; use formatter::{format, FormatConfig};
use std::path::Path; use std::path::Path;
use std::time::Instant;
let start = Instant::now();
let check_only = args.iter().any(|a| a == "--check"); let check_only = args.iter().any(|a| a == "--check");
let pattern = args.iter().find(|a| !a.starts_with('-')).map(|s| s.as_str()); 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() { if files_to_format.is_empty() {
println!("No .lux files found."); println!("No .lux files found.");
println!("Looking in: ., src/, examples/, tests/"); println!("{}", c(colors::DIM, "Looking in: ., src/, examples/, tests/"));
return; return;
} }
@@ -287,7 +336,7 @@ fn format_files(args: &[String]) {
let source = match std::fs::read_to_string(file_path) { let source = match std::fs::read_to_string(file_path) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
eprintln!("{}: ERROR - {}", path, e); eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e);
error_count += 1; error_count += 1;
continue; continue;
} }
@@ -296,7 +345,7 @@ fn format_files(args: &[String]) {
let formatted = match format(&source, &config) { let formatted = match format(&source, &config) {
Ok(f) => f, Ok(f) => f,
Err(e) => { Err(e) => {
eprintln!("{}: FORMAT ERROR - {}", path, e); eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e);
error_count += 1; error_count += 1;
continue; continue;
} }
@@ -310,43 +359,57 @@ fn format_files(args: &[String]) {
} }
} else if source != formatted { } else if source != formatted {
if let Err(e) = std::fs::write(file_path, &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; error_count += 1;
continue; continue;
} }
println!("Formatted {}", path); println!(" {} {}", c(colors::CYAN, "Formatted"), path);
formatted_count += 1; formatted_count += 1;
} else { } else {
unchanged_count += 1; unchanged_count += 1;
} }
} }
let elapsed = start.elapsed();
let time_str = c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64()));
println!(); println!();
if check_only { if check_only {
if !would_reformat.is_empty() { if !would_reformat.is_empty() {
println!("Files that would be reformatted:"); println!("Files that would be reformatted:");
for path in &would_reformat { for path in &would_reformat {
println!(" {}", path); println!(" {}", c(colors::YELLOW, path));
} }
println!(); 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); std::process::exit(1);
} else { } 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 { } else if error_count > 0 {
println!("{} formatted, {} unchanged, {} errors", formatted_count, unchanged_count, error_count); 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)); }
if error_count > 0 { parts.push(format!("{} errors", error_count));
println!("{} {} {}", c(colors::RED, "\u{2717}"), parts.join(", "), time_str);
std::process::exit(1); 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]) { fn check_files(args: &[String]) {
use modules::ModuleLoader; use modules::ModuleLoader;
use std::path::Path; use std::path::Path;
use std::time::Instant;
let start = Instant::now();
let pattern = args.first().map(|s| s.as_str()); let pattern = args.first().map(|s| s.as_str());
// Collect files to check // Collect files to check
@@ -382,7 +445,7 @@ fn check_files(args: &[String]) {
if files_to_check.is_empty() { if files_to_check.is_empty() {
println!("No .lux files found."); println!("No .lux files found.");
println!("Looking in: ., src/, examples/, tests/"); println!("{}", c(colors::DIM, "Looking in: ., src/, examples/, tests/"));
return; return;
} }
@@ -397,7 +460,7 @@ fn check_files(args: &[String]) {
let source = match std::fs::read_to_string(file_path) { let source = match std::fs::read_to_string(file_path) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
eprintln!("{}: ERROR - {}", path, e); eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e);
failed += 1; failed += 1;
continue; continue;
} }
@@ -411,7 +474,7 @@ fn check_files(args: &[String]) {
let program = match loader.load_source(&source, Some(file_path)) { let program = match loader.load_source(&source, Some(file_path)) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
eprintln!("{}: MODULE ERROR - {}", path, e); eprintln!("{} {}: {}", c(colors::RED, "\u{2717}"), path, e);
failed += 1; failed += 1;
continue; continue;
} }
@@ -419,23 +482,29 @@ fn check_files(args: &[String]) {
let mut checker = TypeChecker::new(); let mut checker = TypeChecker::new();
if let Err(errors) = checker.check_program_with_modules(&program, &loader) { 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 { for error in errors {
let diagnostic = error.to_diagnostic(); let diagnostic = error.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(&path))); eprint!("{}", render(&diagnostic, &source, Some(&path)));
} }
failed += 1; failed += 1;
} else { } else {
println!("{}: OK", path); println!("{} {}", c(colors::GREEN, "\u{2713}"), path);
passed += 1; passed += 1;
} }
} }
println!(); let elapsed = start.elapsed();
println!("Checked {} files: {} passed, {} failed", passed + failed, passed, failed); let time_str = c(colors::DIM, &format!("in {:.2}s", elapsed.as_secs_f64()));
println!();
if failed > 0 { if failed > 0 {
println!("{} {} passed, {} failed {}",
c(colors::RED, "\u{2717}"), passed, failed, time_str);
std::process::exit(1); 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 modules::ModuleLoader;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
use std::time::Instant;
let start = Instant::now();
let file_path = Path::new(path); let file_path = Path::new(path);
let source = match std::fs::read_to_string(file_path) { let source = match std::fs::read_to_string(file_path) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
eprintln!("Error reading file '{}': {}", path, e); eprintln!("{} Error reading file '{}': {}", c(colors::RED, "error:"), path, e);
std::process::exit(1); 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)) { let program = match loader.load_source(&source, Some(file_path)) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
eprintln!("Module error: {}", e); eprintln!("{} {}", c(colors::RED, "error:"), e);
std::process::exit(1); 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) { let c_code = match backend.generate(&program) {
Ok(code) => code, Ok(code) => code,
Err(e) => { Err(e) => {
eprintln!("C codegen error: {}", e); eprintln!("{} C codegen: {}", c(colors::RED, "error:"), e);
eprintln!(); eprintln!();
eprintln!("Note: The C backend supports functions, closures, ADTs,"); eprintln!("{}", c(colors::DIM, "Note: The C backend supports functions, closures, ADTs,"));
eprintln!("pattern matching, lists, and Console.print."); eprintln!("{}", c(colors::DIM, "pattern matching, lists, and Console.print."));
eprintln!(); eprintln!("{}", c(colors::DIM, "Not yet supported: other effects, some advanced features."));
eprintln!("Not yet supported: other effects, some advanced features.");
std::process::exit(1); 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 emit_c {
if let Some(out_path) = output_path { if let Some(out_path) = output_path {
if let Err(e) = std::fs::write(out_path, &c_code) { 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); 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 { } else {
println!("{}", c_code); 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) { 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); 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 { match compile_result {
Ok(output) => { Ok(output) => {
if !output.status.success() { if !output.status.success() {
eprintln!("C compilation failed:"); eprintln!("{} C compilation failed:", c(colors::RED, "error:"));
eprintln!("{}", String::from_utf8_lossy(&output.stderr)); eprintln!("{}", String::from_utf8_lossy(&output.stderr));
std::process::exit(1); std::process::exit(1);
} }
} }
Err(e) => { Err(e) => {
eprintln!("Failed to run C compiler '{}': {}", cc, e); eprintln!("{} Failed to run C compiler '{}': {}", c(colors::RED, "error:"), cc, e);
eprintln!("Make sure gcc or clang is installed, or set CC environment variable."); eprintln!("{}", c(colors::DIM, "Make sure gcc or clang is installed, or set CC environment variable."));
std::process::exit(1); std::process::exit(1);
} }
} }
// Keep temp file for debugging let elapsed = start.elapsed();
eprintln!("C file: {}", temp_c.display());
// let _ = std::fs::remove_file(&temp_c);
if run_after { if run_after {
// Run the compiled binary // 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)); std::process::exit(status.code().unwrap_or(1));
} }
Err(e) => { Err(e) => {
eprintln!("Failed to run compiled binary: {}", e); eprintln!("{} Failed to run compiled binary: {}", c(colors::RED, "error:"), e);
std::process::exit(1); std::process::exit(1);
} }
} }
} else { } else {
// Just print where the binary is eprintln!("{} Compiled to {} {}",
eprintln!("Compiled to {}", output_bin.display()); 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 modules::ModuleLoader;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
use std::time::Instant;
let start = Instant::now();
let file_path = Path::new(path); let file_path = Path::new(path);
let source = match std::fs::read_to_string(file_path) { let source = match std::fs::read_to_string(file_path) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
eprintln!("Error reading file '{}': {}", path, e); eprintln!("{} Error reading file '{}': {}", c(colors::RED, "error:"), path, e);
std::process::exit(1); 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)) { let program = match loader.load_source(&source, Some(file_path)) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
eprintln!("Module error: {}", e); eprintln!("{} {}", c(colors::RED, "error:"), e);
std::process::exit(1); 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) { let js_code = match backend.generate(&program) {
Ok(code) => code, Ok(code) => code,
Err(e) => { Err(e) => {
eprintln!("JS codegen error: {}", e); eprintln!("{} JS codegen: {}", c(colors::RED, "error:"), e);
std::process::exit(1); 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 // Write the JavaScript file
if let Err(e) = std::fs::write(&output_js, &js_code) { 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); std::process::exit(1);
} }
let elapsed = start.elapsed();
if run_after { if run_after {
// Run with Node.js // Run with Node.js
let node_result = Command::new("node") 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)); std::process::exit(status.code().unwrap_or(1));
} }
Err(e) => { Err(e) => {
eprintln!("Failed to run with Node.js: {}", e); eprintln!("{} Failed to run with Node.js: {}", c(colors::RED, "error:"), e);
eprintln!("Make sure Node.js is installed."); eprintln!("{}", c(colors::DIM, "Make sure Node.js is installed."));
std::process::exit(1); std::process::exit(1);
} }
} }
} else { } 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]) { fn run_tests(args: &[String]) {
use std::path::Path; use std::path::Path;
use std::fs; use std::fs;
use std::time::Instant;
let start = Instant::now();
// Find test files // Find test files
let pattern = args.first().map(|s| s.as_str()); let pattern = args.first().map(|s| s.as_str());
@@ -707,12 +790,12 @@ fn run_tests(args: &[String]) {
if test_files.is_empty() { if test_files.is_empty() {
println!("No test files found."); println!("No test files found.");
println!("Test files should be named test_*.lux or *_test.lux"); println!("{}", c(colors::DIM, "Test files should be named test_*.lux or *_test.lux"));
println!("Or contain functions named test_*"); println!("{}", c(colors::DIM, "Or contain functions named test_*"));
return; return;
} }
println!("Running tests...\n"); println!("{}\n", bc("", "Running tests..."));
let mut total_passed = 0; let mut total_passed = 0;
let mut total_failed = 0; let mut total_failed = 0;
@@ -725,7 +808,7 @@ fn run_tests(args: &[String]) {
let source = match fs::read_to_string(test_file) { let source = match fs::read_to_string(test_file) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
println!(" {} ... ERROR: {}", path_str, e); println!(" {} {} {}", c(colors::RED, "\u{2717}"), path_str, c(colors::RED, &e.to_string()));
total_failed += 1; total_failed += 1;
continue; continue;
} }
@@ -734,7 +817,7 @@ fn run_tests(args: &[String]) {
let program = match Parser::parse_source(&source) { let program = match Parser::parse_source(&source) {
Ok(p) => p, Ok(p) => p,
Err(e) => { 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; total_failed += 1;
continue; continue;
} }
@@ -743,7 +826,7 @@ fn run_tests(args: &[String]) {
// Type check // Type check
let mut checker = typechecker::TypeChecker::new(); let mut checker = typechecker::TypeChecker::new();
if let Err(errors) = checker.check_program(&program) { 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 { for err in errors {
eprintln!(" {}", err); eprintln!(" {}", err);
} }
@@ -774,14 +857,15 @@ fn run_tests(args: &[String]) {
Ok(_) => { Ok(_) => {
let results = interp.get_test_results(); let results = interp.get_test_results();
if results.failed == 0 && results.passed == 0 { if results.failed == 0 && results.passed == 0 {
// No Test assertions, just check it runs println!(" {} {} {}", c(colors::GREEN, "\u{2713}"), path_str, c(colors::DIM, "(no assertions)"));
println!(" {} ... OK (no assertions)", path_str);
total_passed += 1; total_passed += 1;
} else if results.failed == 0 { } 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; total_passed += results.passed;
} else { } 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_passed += results.passed;
total_failed += results.failed; total_failed += results.failed;
for failure in &results.failures { for failure in &results.failures {
@@ -790,13 +874,13 @@ fn run_tests(args: &[String]) {
} }
} }
Err(e) => { 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; total_failed += 1;
} }
} }
} else { } else {
// Run individual test functions // Run individual test functions
println!(" {}:", path_str); println!(" {}:", c(colors::DIM, &path_str));
for test_name in &test_funcs { for test_name in &test_funcs {
let mut interp = Interpreter::new(); let mut interp = Interpreter::new();
@@ -805,7 +889,7 @@ fn run_tests(args: &[String]) {
// First run the file to define all functions // First run the file to define all functions
if let Err(e) = interp.run(&program) { 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; total_failed += 1;
continue; continue;
} }
@@ -815,7 +899,7 @@ fn run_tests(args: &[String]) {
let call_program = match Parser::parse_source(&call_source) { let call_program = match Parser::parse_source(&call_source) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
println!(" {} ... ERROR: {}", test_name, e); println!(" {} {} {}", c(colors::RED, "\u{2717}"), test_name, c(colors::RED, &e.to_string()));
total_failed += 1; total_failed += 1;
continue; continue;
} }
@@ -825,10 +909,10 @@ fn run_tests(args: &[String]) {
Ok(_) => { Ok(_) => {
let results = interp.get_test_results(); let results = interp.get_test_results();
if results.failed == 0 { if results.failed == 0 {
println!(" {} ... OK", test_name); println!(" {} {}", c(colors::GREEN, "\u{2713}"), test_name);
total_passed += 1; total_passed += 1;
} else { } else {
println!(" {} ... FAILED", test_name); println!(" {} {}", c(colors::RED, "\u{2717}"), test_name);
total_failed += 1; total_failed += 1;
for failure in &results.failures { for failure in &results.failures {
all_failures.push((path_str.clone(), test_name.clone(), failure.clone())); all_failures.push((path_str.clone(), test_name.clone(), failure.clone()));
@@ -836,7 +920,7 @@ fn run_tests(args: &[String]) {
} }
} }
Err(e) => { Err(e) => {
println!(" {} ... ERROR: {}", test_name, e); println!(" {} {} {}", c(colors::RED, "\u{2717}"), test_name, c(colors::RED, &e.to_string()));
total_failed += 1; total_failed += 1;
} }
} }
@@ -846,28 +930,34 @@ fn run_tests(args: &[String]) {
// Print failure details // Print failure details
if !all_failures.is_empty() { if !all_failures.is_empty() {
println!("\n--- Failures ---\n"); println!("\n{}\n", bc(colors::RED, "--- Failures ---"));
for (file, test, failure) in &all_failures { for (file, test, failure) in &all_failures {
if test.is_empty() { if test.is_empty() {
println!("{}:", file); println!("{}:", bc("", file));
} else { } else {
println!("{} - {}:", file, test); println!("{} - {}:", bc("", file), bc("", test));
} }
println!(" {}", failure.message); println!(" {}", failure.message);
if let Some(expected) = &failure.expected { if let Some(expected) = &failure.expected {
println!(" Expected: {}", expected); println!(" {} {}", c(colors::CYAN, "Expected:"), expected);
} }
if let Some(actual) = &failure.actual { if let Some(actual) = &failure.actual {
println!(" Actual: {}", actual); println!(" {} {}", c(colors::RED, "Actual: "), actual);
} }
println!(); 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 { if total_failed > 0 {
println!("{} {} passed, {} failed {}",
c(colors::RED, "\u{2717}"), total_passed, total_failed, time_str);
std::process::exit(1); 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]) { fn handle_pkg_command(args: &[String]) {
use package::{PackageManager, DependencySource}; use package::{PackageManager, DependencySource};
use std::path::PathBuf; use std::path::PathBuf;
@@ -1429,19 +1676,19 @@ let output = run main() with {}
"#; "#;
fs::write(project_dir.join(".gitignore"), gitignore_content).unwrap(); 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!();
println!("Project structure:"); println!("{}", bc("", "Project structure:"));
println!(" {}/", project_name); println!(" {}", bc(colors::CYAN, &format!("{}/", project_name)));
println!(" ├── lux.toml"); println!(" {} {}", c(colors::DIM, "\u{251c}\u{2500}\u{2500}"), "lux.toml");
println!(" ├── src/"); println!(" {} {}", c(colors::DIM, "\u{251c}\u{2500}\u{2500}"), bc(colors::CYAN, "src/"));
println!(" │ └── main.lux"); println!(" {} {} {}", c(colors::DIM, "\u{2502}"), c(colors::DIM, " \u{2514}\u{2500}\u{2500}"), "main.lux");
println!(" └── tests/"); println!(" {} {}", c(colors::DIM, "\u{2514}\u{2500}\u{2500}"), bc(colors::CYAN, "tests/"));
println!(" └── test_example.lux"); println!(" {} {}", c(colors::DIM, "\u{2514}\u{2500}\u{2500}"), "test_example.lux");
println!(); println!();
println!("To get started:"); println!("To get started:");
println!(" cd {}", project_name); println!(" {} {}", c(colors::DIM, "$"), bc(colors::CYAN, &format!("cd {}", project_name)));
println!(" lux src/main.lux"); println!(" {} {}", c(colors::DIM, "$"), bc(colors::CYAN, "lux src/main.lux"));
} }
/// Generate API documentation for Lux source files /// Generate API documentation for Lux source files