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
418            let custom = crate::language::known_custom_language_aliases();
419            if !custom.is_empty() {
420                println!();
421                println!(
422                    "Custom aliases ({})",
423                    crate::aliases::config_path().display()
424                );
425                println!("{:<16} Language", "Alias");
426                println!("────────────────────────");
427                for (alias, language) in custom {
428                    println!("{alias:<16} {language}");
429                }
430            }
431            Ok(0)
432        }
433        AliasAction::Add { alias, language } => {
434            let registry = LanguageRegistry::bootstrap();
435            crate::aliases::add_alias(&alias, &language, &registry)?;
436            Ok(0)
437        }
438        AliasAction::Remove { alias } => {
439            crate::aliases::remove_alias(&alias)?;
440            Ok(0)
441        }
442    }
443}
444
445fn format_file(path: &Path) -> Result<i32> {
446    if !path.is_file() {
447        eprintln!("[run] File not found: {}", path.display());
448        return Ok(1);
449    }
450
451    let Some(lang) = language_from_path(path) else {
452        eprintln!("[run] No formatter available for unknown");
453        return Ok(2);
454    };
455    let candidates: &[(&str, &[&str])] = match lang {
456        "python" => &[("black", &[]), ("autopep8", &["-i"])],
457        "javascript" | "typescript" => &[("prettier", &["--write"])],
458        "rust" => &[("rustfmt", &[])],
459        "go" => &[("gofmt", &["-w"])],
460        "c" | "cpp" => &[("clang-format", &["-i"])],
461        "java" => &[("google-java-format", &["-i"])],
462        _ => &[],
463    };
464    if candidates.is_empty() {
465        eprintln!("[run] No formatter available for {lang}");
466        return Ok(2);
467    }
468
469    for (program, args) in candidates {
470        let Ok(binary) = which::which(program) else {
471            continue;
472        };
473        let status = std::process::Command::new(binary)
474            .args(*args)
475            .arg(path)
476            .status()
477            .with_context(|| format!("failed to run formatter {program}"))?;
478        return Ok(if status.success() {
479            0
480        } else {
481            eprintln!("[run] formatter {program} failed");
482            1
483        });
484    }
485
486    eprintln!("[run] Formatter not found for {lang}");
487    Ok(2)
488}
489
490fn doctor(registry: &LanguageRegistry) -> Result<i32> {
491    println!(
492        "{:<12} {:<16} {:<24} Status",
493        "Language", "Toolchain", "Version"
494    );
495    println!("────────────────────────────────────────────────────────────");
496    let mut missing = 0;
497    let mut languages = registry.known_languages();
498    languages.sort();
499    for lang in languages {
500        let spec = LanguageSpec::new(lang.clone());
501        if let Some(engine) = registry.resolve(&spec) {
502            let toolchain = toolchain_name(engine.id());
503            match engine.validate() {
504                Ok(()) => {
505                    let version = engine
506                        .toolchain_version()
507                        .ok()
508                        .flatten()
509                        .unwrap_or_else(|| "unknown".to_string());
510                    let status = if version == "unknown" {
511                        "⚠ Unknown"
512                    } else {
513                        "✓ OK"
514                    };
515                    println!(
516                        "{:<12} {:<16} {:<24} {}",
517                        engine.display_name(),
518                        toolchain,
519                        version.lines().next().unwrap_or("unknown"),
520                        status
521                    );
522                }
523                Err(_) => {
524                    missing += 1;
525                    println!(
526                        "{:<12} {:<16} {:<24} ✗ MISSING",
527                        engine.display_name(),
528                        toolchain,
529                        "✗ Not found"
530                    );
531                }
532            }
533        }
534    }
535    Ok(if missing == 0 { 0 } else { 1 })
536}
537
538fn share_file(path: &Path, port: Option<u16>, registry: &LanguageRegistry) -> Result<i32> {
539    if !path.is_file() {
540        eprintln!("[run] File not found: {}", path.display());
541        return Ok(1);
542    }
543    let address = format!("127.0.0.1:{}", port.unwrap_or(0));
544    let server = tiny_http::Server::http(&address)
545        .map_err(|err| anyhow::anyhow!("failed to start share server: {err}"))?;
546    let url = format!("http://{}", server.server_addr());
547    println!("Sharing at {url} (Ctrl-C to stop)");
548
549    let lang = language_from_path(path).unwrap_or("text");
550    let spec = ExecutionSpec {
551        language: (lang != "text").then(|| LanguageSpec::new(lang.to_string())),
552        source: InputSource::File(path.to_path_buf()),
553        detect_language: true,
554        args: Vec::new(),
555        json: false,
556    };
557    let output = execute_capture(spec, registry).unwrap_or_default();
558    for request in server.incoming_requests() {
559        let route = request.url().to_string();
560        if route == "/raw" {
561            let text = fs::read_to_string(path).unwrap_or_default();
562            let _ = request.respond(tiny_http::Response::from_string(text));
563            continue;
564        }
565        let body = render_share_html(path, lang, &output);
566        let response = tiny_http::Response::from_string(body).with_header(
567            tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..])
568                .unwrap(),
569        );
570        let _ = request.respond(response);
571    }
572    Ok(0)
573}
574
575fn bench_run(spec: ExecutionSpec, registry: &LanguageRegistry, iterations: u32) -> Result<i32> {
576    let payload = ExecutionPayload::from_input_source(&spec.source, &spec.args)
577        .context("failed to materialize execution payload")?;
578    let language = resolve_language(
579        spec.language,
580        spec.detect_language,
581        Some(&payload),
582        registry,
583    )?;
584
585    let engine = registry
586        .resolve(&language)
587        .context("failed to resolve language engine")?;
588
589    engine
590        .validate()
591        .with_context(|| format!("{} is not available", engine.display_name()))?;
592
593    eprintln!(
594        "\x1b[1mBenchmark:\x1b[0m {} — {} iteration{}",
595        engine.display_name(),
596        iterations,
597        if iterations == 1 { "" } else { "s" }
598    );
599
600    // Warmup run (not counted)
601    let warmup = engine.execute(&payload)?;
602    if !warmup.success() {
603        eprintln!("\x1b[31mError:\x1b[0m Code failed during warmup run");
604        if !warmup.stderr.is_empty() {
605            eprint!("{}", warmup.stderr);
606        }
607        return Ok(1);
608    }
609    eprintln!("\x1b[2m  warmup: {}ms\x1b[0m", warmup.duration.as_millis());
610
611    let mut times: Vec<f64> = Vec::with_capacity(iterations as usize);
612
613    for i in 0..iterations {
614        let outcome = engine.execute(&payload)?;
615        let ms = outcome.duration.as_secs_f64() * 1000.0;
616        times.push(ms);
617
618        if i < 3 || i == iterations - 1 || (i + 1) % 10 == 0 {
619            eprintln!("\x1b[2m  run {}: {:.2}ms\x1b[0m", i + 1, ms);
620        }
621    }
622
623    times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
624    let total: f64 = times.iter().sum();
625    let avg = total / times.len() as f64;
626    let min = times.first().copied().unwrap_or(0.0);
627    let max = times.last().copied().unwrap_or(0.0);
628    let median = if times.len().is_multiple_of(2) && times.len() >= 2 {
629        (times[times.len() / 2 - 1] + times[times.len() / 2]) / 2.0
630    } else {
631        times[times.len() / 2]
632    };
633
634    // Standard deviation
635    let variance: f64 = times.iter().map(|t| (t - avg).powi(2)).sum::<f64>() / times.len() as f64;
636    let stddev = variance.sqrt();
637
638    eprintln!();
639    eprintln!("\x1b[1mResults ({} runs):\x1b[0m", iterations);
640    eprintln!("  min:    \x1b[32m{:.2}ms\x1b[0m", min);
641    eprintln!("  max:    \x1b[33m{:.2}ms\x1b[0m", max);
642    eprintln!("  avg:    \x1b[36m{:.2}ms\x1b[0m", avg);
643    eprintln!("  median: \x1b[36m{:.2}ms\x1b[0m", median);
644    eprintln!("  stddev: {:.2}ms", stddev);
645
646    if !warmup.stdout.is_empty() {
647        print!("{}", warmup.stdout);
648        io::stdout().flush().ok();
649    }
650
651    Ok(0)
652}
653
654fn watch_run(spec: ExecutionSpec, registry: &LanguageRegistry) -> Result<i32> {
655    use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
656
657    let file_path = match &spec.source {
658        InputSource::File(p) => p.clone(),
659        _ => anyhow::bail!("--watch requires a file path (use -f or pass a file as argument)"),
660    };
661
662    if !file_path.exists() {
663        anyhow::bail!("File not found: {}", file_path.display());
664    }
665
666    let payload = ExecutionPayload::from_input_source(&spec.source, &spec.args)
667        .context("failed to materialize execution payload")?;
668    let language = resolve_language(
669        spec.language.clone(),
670        spec.detect_language,
671        Some(&payload),
672        registry,
673    )?;
674
675    let engine = registry
676        .resolve(&language)
677        .context("failed to resolve language engine")?;
678
679    engine
680        .validate()
681        .with_context(|| format!("{} is not available", engine.display_name()))?;
682
683    println!(
684        "[run watch] watching {} ({}) — Ctrl-C to stop",
685        file_path.display(),
686        engine.display_name()
687    );
688
689    let mut run_count = 0u32;
690
691    run_count += 1;
692    print!("\x1b[2J\x1b[H");
693    println!("[run watch] run #{run_count}");
694    run_file_once(&file_path, engine, &spec.args);
695
696    let (tx, rx) = mpsc::channel();
697    let mut watcher = RecommendedWatcher::new(
698        move |res| {
699            let _ = tx.send(res);
700        },
701        Config::default(),
702    )?;
703    watcher.watch(&file_path, RecursiveMode::NonRecursive)?;
704
705    loop {
706        match rx.recv() {
707            Ok(Ok(_event)) => {
708                while rx
709                    .recv_timeout(std::time::Duration::from_millis(150))
710                    .is_ok()
711                {}
712                run_count += 1;
713                print!("\x1b[2J\x1b[H");
714                let now = SystemTime::now()
715                    .duration_since(SystemTime::UNIX_EPOCH)
716                    .map(|duration| duration.as_secs())
717                    .unwrap_or(0);
718                println!("[run watch] run #{run_count} at {now}");
719                run_file_once(&file_path, engine, &spec.args);
720            }
721            Ok(Err(err)) => eprintln!("[run] watch error: {err}"),
722            Err(err) => anyhow::bail!("[run] watch channel closed: {err}"),
723        }
724    }
725}
726
727fn run_file_once(file_path: &Path, engine: &dyn crate::engine::LanguageEngine, args: &[String]) {
728    let payload = ExecutionPayload::File {
729        path: file_path.to_path_buf(),
730        args: args.to_vec(),
731    };
732    match engine.execute(&payload) {
733        Ok(outcome) => {
734            if !outcome.stdout.is_empty() {
735                print!("{}", outcome.stdout);
736                io::stdout().flush().ok();
737            }
738            if !outcome.stderr.is_empty() {
739                eprint!("\x1b[31m{}\x1b[0m", outcome.stderr);
740                io::stderr().flush().ok();
741            }
742            let ms = outcome.duration.as_millis();
743            let status = if outcome.success() {
744                "\x1b[32mOK\x1b[0m"
745            } else {
746                "\x1b[31mFAIL\x1b[0m"
747            };
748            eprintln!("\x1b[2m[{status} {ms}ms]\x1b[0m");
749        }
750        Err(e) => {
751            eprintln!("\x1b[31mError:\x1b[0m {e:#}");
752        }
753    }
754}
755
756fn execute_capture(spec: ExecutionSpec, registry: &LanguageRegistry) -> Result<String> {
757    let payload = ExecutionPayload::from_input_source(&spec.source, &spec.args)
758        .context("failed to materialize execution payload")?;
759    let language = resolve_language(
760        spec.language,
761        spec.detect_language,
762        Some(&payload),
763        registry,
764    )?;
765    let engine = registry
766        .resolve(&language)
767        .context("failed to resolve language engine")?;
768    let outcome = engine.execute(&payload)?;
769    let mut output = outcome.stdout;
770    output.push_str(&outcome.stderr);
771    Ok(output)
772}
773
774fn render_share_html(path: &Path, language: &str, output: &str) -> String {
775    let code = fs::read_to_string(path).unwrap_or_default();
776    let syntax_set = syntect::parsing::SyntaxSet::load_defaults_newlines();
777    let theme_set = syntect::highlighting::ThemeSet::load_defaults();
778    let syntax = syntax_set
779        .find_syntax_by_extension(path.extension().and_then(|ext| ext.to_str()).unwrap_or(""))
780        .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
781    let rendered_code = theme_set
782        .themes
783        .get("base16-ocean.dark")
784        .and_then(|theme| {
785            syntect::html::highlighted_html_for_string(&code, &syntax_set, syntax, theme).ok()
786        })
787        .unwrap_or_else(|| format!("<pre>{}</pre>", html_escape(&code)));
788    format!(
789        "<!doctype html><meta charset=\"utf-8\"><title>{}</title>\
790         <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>\
791         <h1>{}</h1><p>Language: {}</p>{}<h2>Last output</h2><pre class=\"out\">{}</pre>",
792        html_escape(&path.display().to_string()),
793        html_escape(&path.display().to_string()),
794        html_escape(language),
795        rendered_code,
796        html_escape(output)
797    )
798}
799
800fn html_escape(text: &str) -> String {
801    text.replace('&', "&amp;")
802        .replace('<', "&lt;")
803        .replace('>', "&gt;")
804        .replace('"', "&quot;")
805}
806
807fn language_from_path(path: &Path) -> Option<&'static str> {
808    let ext = path.extension()?.to_str()?;
809    crate::engine::language_from_extension(&ext.to_ascii_lowercase())
810}
811
812fn toolchain_name(language: &str) -> &'static str {
813    match language {
814        "python" => "python3",
815        "javascript" => "node",
816        "typescript" => "deno",
817        "rust" => "rustc",
818        "go" => "go",
819        "c" => "cc",
820        "cpp" => "c++",
821        "java" => "javac/java",
822        "kotlin" => "kotlinc",
823        "csharp" => "dotnet",
824        "bash" => "bash",
825        "ruby" => "ruby",
826        "lua" => "lua",
827        "php" => "php",
828        "r" => "Rscript",
829        "dart" => "dart",
830        "swift" => "swift",
831        "perl" => "perl",
832        "julia" => "julia",
833        "haskell" => "runghc",
834        "elixir" => "elixir",
835        "crystal" => "crystal",
836        "zig" => "zig",
837        "nim" => "nim",
838        "groovy" => "groovy",
839        _ => "unknown",
840    }
841}
842
843fn resolve_language(
844    explicit: Option<LanguageSpec>,
845    allow_detect: bool,
846    payload: Option<&ExecutionPayload>,
847    registry: &LanguageRegistry,
848) -> Result<LanguageSpec> {
849    if let Some(spec) = explicit {
850        ensure_known_language(&spec, registry)?;
851        return Ok(spec);
852    }
853
854    if allow_detect
855        && let Some(payload) = payload
856        && let Some(detected) = detect_language_for_source(payload, registry)
857    {
858        return Ok(detected);
859    }
860
861    let default = LanguageSpec::new(default_language());
862    ensure_known_language(&default, registry)?;
863    Ok(default)
864}