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