Skip to main content

run/
app.rs

1use std::fs;
2use std::io::{self, Write};
3use std::path::Path;
4use std::sync::mpsc;
5use std::time::SystemTime;
6
7use anyhow::{Context, Result};
8
9use crate::cli::{AliasAction, CacheAction, Command, ExecutionSpec, InputSource};
10use crate::engine::{
11    ExecutionPayload, LanguageRegistry, build_install_command, default_language,
12    detect_language_for_source, ensure_known_language, perf_reset, perf_snapshot,
13};
14use crate::language::LanguageSpec;
15use crate::output;
16use crate::repl;
17use crate::version;
18
19pub fn run(command: Command) -> Result<i32> {
20    match command {
21        Command::ShowVersion => {
22            println!("{}", version::describe());
23            Ok(0)
24        }
25        Command::PerfReport => {
26            print_perf_report();
27            Ok(0)
28        }
29        Command::PerfReset => {
30            perf_reset();
31            eprintln!("\x1b[2m[perf] counters reset\x1b[0m");
32            Ok(0)
33        }
34        Command::Cache { action } => cache_command(action),
35        Command::Alias { action } => alias_command(action),
36        other => run_with_registry(other),
37    }
38}
39
40fn run_with_registry(command: Command) -> Result<i32> {
41    let registry = LanguageRegistry::bootstrap();
42
43    match command {
44        Command::Execute(spec) => execute_once(spec, &registry),
45        Command::Repl {
46            initial_language,
47            detect_language,
48        } => {
49            let language = resolve_language(initial_language, detect_language, None, &registry)?;
50            repl::run_repl(language, registry, detect_language)
51        }
52        Command::ShowVersion => unreachable!("handled before registry bootstrap"),
53        Command::CheckToolchains => check_toolchains(&registry),
54        Command::ShowVersions { language } => show_versions(&registry, language),
55        Command::Install { language, package } => {
56            let lang = language.unwrap_or_else(|| LanguageSpec::new(default_language()));
57            install_package(&lang, &package)
58        }
59        Command::Bench { spec, iterations } => bench_run(spec, &registry, iterations),
60        Command::Watch { spec } => watch_run(spec, &registry),
61        Command::WatchFile {
62            path,
63            language,
64            args,
65        } => watch_run(
66            ExecutionSpec {
67                language,
68                source: InputSource::File(path),
69                detect_language: true,
70                args,
71                json: false,
72            },
73            &registry,
74        ),
75        Command::Format { path } => format_file(&path),
76        Command::Snippet {
77            language,
78            name,
79            list,
80        } => snippet_command(language, name, list),
81        Command::Doctor => doctor(&registry),
82        Command::Cache { .. } => unreachable!("handled before registry bootstrap"),
83        Command::Alias { .. } => unreachable!("handled before registry bootstrap"),
84        Command::Share { path, port } => share_file(&path, port, &registry),
85        Command::PerfReport | Command::PerfReset => {
86            unreachable!("handled before registry bootstrap")
87        }
88    }
89}
90
91fn print_perf_report() {
92    let rows = perf_snapshot();
93    if rows.is_empty() {
94        println!("perf_counter,count");
95        return;
96    }
97    println!("perf_counter,count");
98    for (key, value) in rows {
99        println!("{key},{value}");
100    }
101}
102
103fn check_toolchains(registry: &LanguageRegistry) -> Result<i32> {
104    println!("Checking language toolchains...\n");
105
106    let mut available = 0u32;
107    let mut missing = 0u32;
108
109    let mut languages: Vec<_> = registry.known_languages();
110    languages.sort();
111
112    for lang_id in &languages {
113        let spec = LanguageSpec::new(lang_id.to_string());
114        if let Some(engine) = registry.resolve(&spec) {
115            let status = match engine.validate() {
116                Ok(()) => {
117                    available += 1;
118                    "\x1b[32m OK \x1b[0m"
119                }
120                Err(_) => {
121                    missing += 1;
122                    "\x1b[31mMISS\x1b[0m"
123                }
124            };
125            println!("  [{status}] {:<14} {}", engine.display_name(), lang_id);
126        }
127    }
128
129    println!();
130    println!(
131        "  {} available, {} missing, {} total",
132        available,
133        missing,
134        available + missing
135    );
136
137    if missing > 0 {
138        println!("\n  Tip: Install missing toolchains to enable those languages.");
139    }
140
141    Ok(0)
142}
143
144fn show_versions(registry: &LanguageRegistry, language: Option<LanguageSpec>) -> Result<i32> {
145    println!("Language toolchain versions...\n");
146
147    let mut available = 0u32;
148    let mut missing = 0u32;
149
150    let mut languages: Vec<String> = if let Some(lang) = language {
151        vec![lang.canonical_id().to_string()]
152    } else {
153        registry
154            .known_languages()
155            .into_iter()
156            .map(|value| value.to_string())
157            .collect()
158    };
159    languages.sort();
160
161    for lang_id in &languages {
162        let spec = LanguageSpec::new(lang_id.to_string());
163        if let Some(engine) = registry.resolve(&spec) {
164            match engine.toolchain_version() {
165                Ok(Some(version)) => {
166                    available += 1;
167                    println!(
168                        "  [\x1b[32m OK \x1b[0m] {:<14} {} - {}",
169                        engine.display_name(),
170                        lang_id,
171                        version
172                    );
173                }
174                Ok(None) => {
175                    available += 1;
176                    println!(
177                        "  [\x1b[33m ?? \x1b[0m] {:<14} {} - unknown",
178                        engine.display_name(),
179                        lang_id
180                    );
181                }
182                Err(_) => {
183                    missing += 1;
184                    println!(
185                        "  [\x1b[31mMISS\x1b[0m] {:<14} {}",
186                        engine.display_name(),
187                        lang_id
188                    );
189                }
190            }
191        }
192    }
193
194    println!();
195    println!(
196        "  {} available, {} missing, {} total",
197        available,
198        missing,
199        available + missing
200    );
201
202    if missing > 0 {
203        println!("\n  Tip: Install missing toolchains to enable those languages.");
204    }
205
206    Ok(0)
207}
208
209fn execute_once(spec: ExecutionSpec, registry: &LanguageRegistry) -> Result<i32> {
210    let payload = ExecutionPayload::from_input_source(&spec.source, &spec.args)
211        .context("failed to materialize execution payload")?;
212    let language = resolve_language(
213        spec.language,
214        spec.detect_language,
215        Some(&payload),
216        registry,
217    )?;
218
219    let engine = registry
220        .resolve(&language)
221        .context("failed to resolve language engine")?;
222
223    if let Err(e) = engine.validate() {
224        let display = engine.display_name();
225        let id = engine.id();
226        eprintln!(
227            "Warning: {display} ({id}) toolchain not found: {e:#}\n\
228             Install the required toolchain and ensure it is on your PATH."
229        );
230        return Err(e.context(format!("{display} is not available")));
231    }
232
233    let outcome = match engine.execute(&payload) {
234        Ok(outcome) => outcome,
235        Err(err) if err.to_string().contains("Execution timed out") => {
236            eprintln!(
237                "[run] Execution timed out after {}s",
238                crate::runtime::timeout_secs()
239            );
240            return Ok(124);
241        }
242        Err(err) => return Err(err),
243    };
244
245    if spec.json {
246        let exit_code = outcome
247            .exit_code
248            .unwrap_or(if outcome.success() { 0 } else { 1 });
249        let version = engine
250            .toolchain_version()
251            .ok()
252            .flatten()
253            .unwrap_or_default();
254        let envelope = serde_json::json!({
255            "language": engine.id(),
256            "stdout": outcome.stdout,
257            "stderr": outcome.stderr,
258            "exit_code": exit_code,
259            "duration_ms": outcome.duration.as_millis(),
260            "toolchain_version": version,
261        });
262        println!("{}", serde_json::to_string(&envelope)?);
263        return Ok(exit_code);
264    }
265
266    if !outcome.stdout.is_empty() {
267        print!("{}", outcome.stdout);
268        io::stdout().flush().ok();
269    }
270    if !outcome.stderr.is_empty() {
271        let formatted =
272            output::format_stderr(engine.display_name(), &outcome.stderr, outcome.success());
273        eprint!("{formatted}");
274        io::stderr().flush().ok();
275    }
276
277    // Show timing on stderr if requested or if execution was slow (>1s)
278    let show_timing = crate::runtime::timing_enabled();
279    if show_timing || outcome.duration.as_millis() > 1000 {
280        eprintln!(
281            "\x1b[2m[{} {}ms]\x1b[0m",
282            engine.display_name(),
283            outcome.duration.as_millis()
284        );
285    }
286
287    if std::env::var("RUN_PERF_REPORT").is_ok_and(|v| v == "1" || v == "true") {
288        eprintln!("\x1b[2m[perf]\x1b[0m");
289        for (key, value) in perf_snapshot() {
290            eprintln!("\x1b[2m  {key}={value}\x1b[0m");
291        }
292    }
293
294    Ok(outcome
295        .exit_code
296        .unwrap_or(if outcome.success() { 0 } else { 1 }))
297}
298
299fn install_package(language: &LanguageSpec, package: &str) -> Result<i32> {
300    let lang_id = language.canonical_id();
301    let override_key = format!("RUN_INSTALL_COMMAND_{}", lang_id.to_ascii_uppercase());
302    let override_value = std::env::var(&override_key).ok();
303
304    let Some(mut cmd) = build_install_command(lang_id, package) else {
305        if override_value.is_some() {
306            eprintln!(
307                "\x1b[31mError:\x1b[0m {override_key} is set but could not be parsed.\n\
308                 Provide a valid command, e.g. {override_key}=\"uv pip install {{package}}\""
309            );
310            return Ok(1);
311        }
312        eprintln!(
313            "\x1b[31mError:\x1b[0m No package manager available for '{lang_id}'.\n\
314             This language doesn't have a standard CLI package manager.\n\
315             Tip: You can override with {override_key}=\"<cmd> {{package}}\"",
316        );
317        return Ok(1);
318    };
319
320    eprintln!("\x1b[36m[run]\x1b[0m Installing '{package}' for {lang_id}...");
321
322    let result = cmd
323        .stdin(std::process::Stdio::inherit())
324        .stdout(std::process::Stdio::inherit())
325        .stderr(std::process::Stdio::inherit())
326        .status();
327
328    match result {
329        Ok(status) if status.success() => {
330            eprintln!("\x1b[32m[run]\x1b[0m Successfully installed '{package}' for {lang_id}");
331            Ok(0)
332        }
333        Ok(status) => {
334            eprintln!("\x1b[31m[run]\x1b[0m Failed to install '{package}' for {lang_id}");
335            Ok(status.code().unwrap_or(1))
336        }
337        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
338            let program = cmd.get_program().to_string_lossy();
339            eprintln!("\x1b[31m[run]\x1b[0m Package manager not found: {program}");
340            eprintln!("Tip: install it or set {override_key}=\"<cmd> {{package}}\"");
341            Ok(1)
342        }
343        Err(err) => {
344            Err(err).with_context(|| format!("failed to run package manager for {lang_id}"))
345        }
346    }
347}
348
349fn cache_command(action: CacheAction) -> Result<i32> {
350    match action {
351        CacheAction::Stats => {
352            let stats = crate::cache::stats()?;
353            println!("Cache: {}", crate::cache::root_dir().display());
354            println!("entries: {}", stats.entries);
355            println!("bytes: {}", stats.total_bytes);
356            for (lang, count) in stats.by_language {
357                println!("{lang}: {count}");
358            }
359            Ok(0)
360        }
361        CacheAction::Clear => {
362            crate::cache::clear()?;
363            println!("[run] cache cleared");
364            Ok(0)
365        }
366        CacheAction::ClearLang(lang) => {
367            crate::cache::clear_lang(&lang)?;
368            println!("[run] cache cleared for {lang}");
369            Ok(0)
370        }
371    }
372}
373
374fn snippet_command(language: LanguageSpec, name: Option<String>, list: bool) -> Result<i32> {
375    let language = language.canonical_id().to_string();
376    let names = crate::templates::names_for_language(&language);
377    if list {
378        if names.is_empty() {
379            eprintln!("[run] No snippets available for {language}");
380            return Ok(2);
381        }
382        for name in names {
383            println!("{name}");
384        }
385        return Ok(0);
386    }
387
388    let Some(name) = name else {
389        eprintln!(
390            "[run] snippet requires a template name. Available: {}",
391            names.join(", ")
392        );
393        return Ok(2);
394    };
395
396    if let Some(template) = crate::templates::find(&language, &name) {
397        print!("{}", template.source);
398        io::stdout().flush().ok();
399        Ok(0)
400    } else {
401        eprintln!(
402            "[run] Unknown snippet '{name}' for {language}. Available: {}",
403            names.join(", ")
404        );
405        Ok(2)
406    }
407}
408
409fn alias_command(action: AliasAction) -> Result<i32> {
410    match action {
411        AliasAction::List => {
412            println!("{:<16} Language", "Alias");
413            println!("────────────────────────");
414            for (alias, language) in crate::language::known_language_aliases() {
415                println!("{alias:<16} {language}");
416            }
417            Ok(0)
418        }
419    }
420}
421
422fn format_file(path: &Path) -> Result<i32> {
423    if !path.is_file() {
424        eprintln!("[run] File not found: {}", path.display());
425        return Ok(1);
426    }
427
428    let Some(lang) = language_from_path(path) else {
429        eprintln!("[run] No formatter available for unknown");
430        return Ok(2);
431    };
432    let candidates: &[(&str, &[&str])] = match lang {
433        "python" => &[("black", &[]), ("autopep8", &["-i"])],
434        "javascript" | "typescript" => &[("prettier", &["--write"])],
435        "rust" => &[("rustfmt", &[])],
436        "go" => &[("gofmt", &["-w"])],
437        "c" | "cpp" => &[("clang-format", &["-i"])],
438        "java" => &[("google-java-format", &["-i"])],
439        _ => &[],
440    };
441    if candidates.is_empty() {
442        eprintln!("[run] No formatter available for {lang}");
443        return Ok(2);
444    }
445
446    for (program, args) in candidates {
447        let Ok(binary) = which::which(program) else {
448            continue;
449        };
450        let status = std::process::Command::new(binary)
451            .args(*args)
452            .arg(path)
453            .status()
454            .with_context(|| format!("failed to run formatter {program}"))?;
455        return Ok(if status.success() {
456            0
457        } else {
458            eprintln!("[run] formatter {program} failed");
459            1
460        });
461    }
462
463    eprintln!("[run] Formatter not found for {lang}");
464    Ok(2)
465}
466
467fn doctor(registry: &LanguageRegistry) -> Result<i32> {
468    println!(
469        "{:<12} {:<16} {:<24} Status",
470        "Language", "Toolchain", "Version"
471    );
472    println!("────────────────────────────────────────────────────────────");
473    let mut missing = 0;
474    let mut languages = registry.known_languages();
475    languages.sort();
476    for lang in languages {
477        let spec = LanguageSpec::new(lang.clone());
478        if let Some(engine) = registry.resolve(&spec) {
479            let toolchain = toolchain_name(engine.id());
480            match engine.validate() {
481                Ok(()) => {
482                    let version = engine
483                        .toolchain_version()
484                        .ok()
485                        .flatten()
486                        .unwrap_or_else(|| "unknown".to_string());
487                    let status = if version == "unknown" {
488                        "⚠ Unknown"
489                    } else {
490                        "✓ OK"
491                    };
492                    println!(
493                        "{:<12} {:<16} {:<24} {}",
494                        engine.display_name(),
495                        toolchain,
496                        version.lines().next().unwrap_or("unknown"),
497                        status
498                    );
499                }
500                Err(_) => {
501                    missing += 1;
502                    println!(
503                        "{:<12} {:<16} {:<24} ✗ MISSING",
504                        engine.display_name(),
505                        toolchain,
506                        "✗ Not found"
507                    );
508                }
509            }
510        }
511    }
512    Ok(if missing == 0 { 0 } else { 1 })
513}
514
515fn share_file(path: &Path, port: Option<u16>, registry: &LanguageRegistry) -> Result<i32> {
516    if !path.is_file() {
517        eprintln!("[run] File not found: {}", path.display());
518        return Ok(1);
519    }
520    let address = format!("127.0.0.1:{}", port.unwrap_or(0));
521    let server = tiny_http::Server::http(&address)
522        .map_err(|err| anyhow::anyhow!("failed to start share server: {err}"))?;
523    let url = format!("http://{}", server.server_addr());
524    println!("Sharing at {url} (Ctrl-C to stop)");
525
526    let lang = language_from_path(path).unwrap_or("text");
527    let spec = ExecutionSpec {
528        language: (lang != "text").then(|| LanguageSpec::new(lang.to_string())),
529        source: InputSource::File(path.to_path_buf()),
530        detect_language: true,
531        args: Vec::new(),
532        json: false,
533    };
534    let output = execute_capture(spec, registry).unwrap_or_default();
535    for request in server.incoming_requests() {
536        let route = request.url().to_string();
537        if route == "/raw" {
538            let text = fs::read_to_string(path).unwrap_or_default();
539            let _ = request.respond(tiny_http::Response::from_string(text));
540            continue;
541        }
542        let body = render_share_html(path, lang, &output);
543        let response = tiny_http::Response::from_string(body).with_header(
544            tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..])
545                .unwrap(),
546        );
547        let _ = request.respond(response);
548    }
549    Ok(0)
550}
551
552fn bench_run(spec: ExecutionSpec, registry: &LanguageRegistry, iterations: u32) -> Result<i32> {
553    let payload = ExecutionPayload::from_input_source(&spec.source, &spec.args)
554        .context("failed to materialize execution payload")?;
555    let language = resolve_language(
556        spec.language,
557        spec.detect_language,
558        Some(&payload),
559        registry,
560    )?;
561
562    let engine = registry
563        .resolve(&language)
564        .context("failed to resolve language engine")?;
565
566    engine
567        .validate()
568        .with_context(|| format!("{} is not available", engine.display_name()))?;
569
570    eprintln!(
571        "\x1b[1mBenchmark:\x1b[0m {} — {} iteration{}",
572        engine.display_name(),
573        iterations,
574        if iterations == 1 { "" } else { "s" }
575    );
576
577    // Warmup run (not counted)
578    let warmup = engine.execute(&payload)?;
579    if !warmup.success() {
580        eprintln!("\x1b[31mError:\x1b[0m Code failed during warmup run");
581        if !warmup.stderr.is_empty() {
582            eprint!("{}", warmup.stderr);
583        }
584        return Ok(1);
585    }
586    eprintln!("\x1b[2m  warmup: {}ms\x1b[0m", warmup.duration.as_millis());
587
588    let mut times: Vec<f64> = Vec::with_capacity(iterations as usize);
589
590    for i in 0..iterations {
591        let outcome = engine.execute(&payload)?;
592        let ms = outcome.duration.as_secs_f64() * 1000.0;
593        times.push(ms);
594
595        if i < 3 || i == iterations - 1 || (i + 1) % 10 == 0 {
596            eprintln!("\x1b[2m  run {}: {:.2}ms\x1b[0m", i + 1, ms);
597        }
598    }
599
600    times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
601    let total: f64 = times.iter().sum();
602    let avg = total / times.len() as f64;
603    let min = times.first().copied().unwrap_or(0.0);
604    let max = times.last().copied().unwrap_or(0.0);
605    let median = if times.len().is_multiple_of(2) && times.len() >= 2 {
606        (times[times.len() / 2 - 1] + times[times.len() / 2]) / 2.0
607    } else {
608        times[times.len() / 2]
609    };
610
611    // Standard deviation
612    let variance: f64 = times.iter().map(|t| (t - avg).powi(2)).sum::<f64>() / times.len() as f64;
613    let stddev = variance.sqrt();
614
615    eprintln!();
616    eprintln!("\x1b[1mResults ({} runs):\x1b[0m", iterations);
617    eprintln!("  min:    \x1b[32m{:.2}ms\x1b[0m", min);
618    eprintln!("  max:    \x1b[33m{:.2}ms\x1b[0m", max);
619    eprintln!("  avg:    \x1b[36m{:.2}ms\x1b[0m", avg);
620    eprintln!("  median: \x1b[36m{:.2}ms\x1b[0m", median);
621    eprintln!("  stddev: {:.2}ms", stddev);
622
623    if !warmup.stdout.is_empty() {
624        print!("{}", warmup.stdout);
625        io::stdout().flush().ok();
626    }
627
628    Ok(0)
629}
630
631fn watch_run(spec: ExecutionSpec, registry: &LanguageRegistry) -> Result<i32> {
632    use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
633
634    let file_path = match &spec.source {
635        InputSource::File(p) => p.clone(),
636        _ => anyhow::bail!("--watch requires a file path (use -f or pass a file as argument)"),
637    };
638
639    if !file_path.exists() {
640        anyhow::bail!("File not found: {}", file_path.display());
641    }
642
643    let payload = ExecutionPayload::from_input_source(&spec.source, &spec.args)
644        .context("failed to materialize execution payload")?;
645    let language = resolve_language(
646        spec.language.clone(),
647        spec.detect_language,
648        Some(&payload),
649        registry,
650    )?;
651
652    let engine = registry
653        .resolve(&language)
654        .context("failed to resolve language engine")?;
655
656    engine
657        .validate()
658        .with_context(|| format!("{} is not available", engine.display_name()))?;
659
660    println!(
661        "[run watch] watching {} ({}) — Ctrl-C to stop",
662        file_path.display(),
663        engine.display_name()
664    );
665
666    let mut run_count = 0u32;
667
668    run_count += 1;
669    print!("\x1b[2J\x1b[H");
670    println!("[run watch] run #{run_count}");
671    run_file_once(&file_path, engine, &spec.args);
672
673    let (tx, rx) = mpsc::channel();
674    let mut watcher = RecommendedWatcher::new(
675        move |res| {
676            let _ = tx.send(res);
677        },
678        Config::default(),
679    )?;
680    watcher.watch(&file_path, RecursiveMode::NonRecursive)?;
681
682    loop {
683        match rx.recv() {
684            Ok(Ok(_event)) => {
685                while rx
686                    .recv_timeout(std::time::Duration::from_millis(150))
687                    .is_ok()
688                {}
689                run_count += 1;
690                print!("\x1b[2J\x1b[H");
691                let now = SystemTime::now()
692                    .duration_since(SystemTime::UNIX_EPOCH)
693                    .map(|duration| duration.as_secs())
694                    .unwrap_or(0);
695                println!("[run watch] run #{run_count} at {now}");
696                run_file_once(&file_path, engine, &spec.args);
697            }
698            Ok(Err(err)) => eprintln!("[run] watch error: {err}"),
699            Err(err) => anyhow::bail!("[run] watch channel closed: {err}"),
700        }
701    }
702}
703
704fn run_file_once(file_path: &Path, engine: &dyn crate::engine::LanguageEngine, args: &[String]) {
705    let payload = ExecutionPayload::File {
706        path: file_path.to_path_buf(),
707        args: args.to_vec(),
708    };
709    match engine.execute(&payload) {
710        Ok(outcome) => {
711            if !outcome.stdout.is_empty() {
712                print!("{}", outcome.stdout);
713                io::stdout().flush().ok();
714            }
715            if !outcome.stderr.is_empty() {
716                eprint!("\x1b[31m{}\x1b[0m", outcome.stderr);
717                io::stderr().flush().ok();
718            }
719            let ms = outcome.duration.as_millis();
720            let status = if outcome.success() {
721                "\x1b[32mOK\x1b[0m"
722            } else {
723                "\x1b[31mFAIL\x1b[0m"
724            };
725            eprintln!("\x1b[2m[{status} {ms}ms]\x1b[0m");
726        }
727        Err(e) => {
728            eprintln!("\x1b[31mError:\x1b[0m {e:#}");
729        }
730    }
731}
732
733fn execute_capture(spec: ExecutionSpec, registry: &LanguageRegistry) -> Result<String> {
734    let payload = ExecutionPayload::from_input_source(&spec.source, &spec.args)
735        .context("failed to materialize execution payload")?;
736    let language = resolve_language(
737        spec.language,
738        spec.detect_language,
739        Some(&payload),
740        registry,
741    )?;
742    let engine = registry
743        .resolve(&language)
744        .context("failed to resolve language engine")?;
745    let outcome = engine.execute(&payload)?;
746    let mut output = outcome.stdout;
747    output.push_str(&outcome.stderr);
748    Ok(output)
749}
750
751fn render_share_html(path: &Path, language: &str, output: &str) -> String {
752    let code = fs::read_to_string(path).unwrap_or_default();
753    let syntax_set = syntect::parsing::SyntaxSet::load_defaults_newlines();
754    let theme_set = syntect::highlighting::ThemeSet::load_defaults();
755    let syntax = syntax_set
756        .find_syntax_by_extension(path.extension().and_then(|ext| ext.to_str()).unwrap_or(""))
757        .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
758    let rendered_code = theme_set
759        .themes
760        .get("base16-ocean.dark")
761        .and_then(|theme| {
762            syntect::html::highlighted_html_for_string(&code, &syntax_set, syntax, theme).ok()
763        })
764        .unwrap_or_else(|| format!("<pre>{}</pre>", html_escape(&code)));
765    format!(
766        "<!doctype html><meta charset=\"utf-8\"><title>{}</title>\
767         <style>body{{font-family:system-ui;margin:2rem;background:#111;color:#eee}}pre{{padding:1rem;overflow:auto;background:#1b1b1b}}.out{{white-space:pre-wrap}}</style>\
768         <h1>{}</h1><p>Language: {}</p>{}<h2>Last output</h2><pre class=\"out\">{}</pre>",
769        html_escape(&path.display().to_string()),
770        html_escape(&path.display().to_string()),
771        html_escape(language),
772        rendered_code,
773        html_escape(output)
774    )
775}
776
777fn html_escape(text: &str) -> String {
778    text.replace('&', "&amp;")
779        .replace('<', "&lt;")
780        .replace('>', "&gt;")
781        .replace('"', "&quot;")
782}
783
784fn language_from_path(path: &Path) -> Option<&'static str> {
785    let ext = path.extension()?.to_str()?.to_ascii_lowercase();
786    match ext.as_str() {
787        "py" | "pyw" => Some("python"),
788        "js" | "jsx" | "mjs" | "cjs" => Some("javascript"),
789        "ts" | "tsx" => Some("typescript"),
790        "rs" => Some("rust"),
791        "go" => Some("go"),
792        "c" | "h" => Some("c"),
793        "cc" | "cpp" | "cxx" | "hpp" | "hxx" => Some("cpp"),
794        "java" => Some("java"),
795        "rb" => Some("ruby"),
796        "sh" | "bash" | "zsh" => Some("bash"),
797        _ => None,
798    }
799}
800
801fn toolchain_name(language: &str) -> &'static str {
802    match language {
803        "python" => "python3",
804        "javascript" => "node",
805        "typescript" => "deno",
806        "rust" => "rustc",
807        "go" => "go",
808        "c" => "cc",
809        "cpp" => "c++",
810        "java" => "javac/java",
811        "kotlin" => "kotlinc",
812        "csharp" => "dotnet",
813        "bash" => "bash",
814        "ruby" => "ruby",
815        "lua" => "lua",
816        "php" => "php",
817        "r" => "Rscript",
818        "dart" => "dart",
819        "swift" => "swift",
820        "perl" => "perl",
821        "julia" => "julia",
822        "haskell" => "runghc",
823        "elixir" => "elixir",
824        "crystal" => "crystal",
825        "zig" => "zig",
826        "nim" => "nim",
827        "groovy" => "groovy",
828        _ => "unknown",
829    }
830}
831
832fn resolve_language(
833    explicit: Option<LanguageSpec>,
834    allow_detect: bool,
835    payload: Option<&ExecutionPayload>,
836    registry: &LanguageRegistry,
837) -> Result<LanguageSpec> {
838    if let Some(spec) = explicit {
839        ensure_known_language(&spec, registry)?;
840        return Ok(spec);
841    }
842
843    if allow_detect
844        && let Some(payload) = payload
845        && let Some(detected) = detect_language_for_source(payload, registry)
846    {
847        return Ok(detected);
848    }
849
850    let default = LanguageSpec::new(default_language());
851    ensure_known_language(&default, registry)?;
852    Ok(default)
853}