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

@@ -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::<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
if args.len() < 3 {
eprintln!("Usage: lux compile <file.lux> [-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 <file.lux> Run a file (interpreter)");
println!(" lux compile <file.lux> Compile to native binary");
println!(" lux compile <f> -o app Compile to binary named 'app'");
println!(" lux compile <f> --run Compile and execute");
println!(" lux compile <f> --emit-c Output C code instead of binary");
println!(" lux compile <f> --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 <file.lux> Watch and re-run on changes");
println!(" lux debug <file.lux> Start interactive debugger");
println!(" lux init [name] Initialize a new project");
println!(" lux pkg <command> Package manager (install, add, remove, list, update)");
println!(" lux registry Start package registry server");
println!(" -s, --storage <dir> Storage directory (default: ./lux-registry)");
println!(" -b, --bind <addr> 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, "<file.lux>"));
println!(" {} {} {} Compile to native binary", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<file.lux>"));
println!(" {} {} {} {} Compile with output name", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<f>"), c(colors::YELLOW, "-o app"));
println!(" {} {} {} {} Compile and execute", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<f>"), c(colors::YELLOW, "--run"));
println!(" {} {} {} {} Output C code", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<f>"), c(colors::YELLOW, "--emit-c"));
println!(" {} {} {} {} Compile to JavaScript", bc(colors::CYAN, "lux"), bc(colors::CYAN, "compile"), c(colors::YELLOW, "<f>"), 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, "<file.lux>"));
println!(" {} {} {} Start interactive debugger",
bc(colors::CYAN, "lux"), bc(colors::CYAN, "debug"), c(colors::YELLOW, "<file.lux>"));
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]) {
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