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, ®istry),
45 Command::Repl {
46 initial_language,
47 detect_language,
48 } => {
49 let language = resolve_language(initial_language, detect_language, None, ®istry)?;
50 repl::run_repl(language, registry, detect_language)
51 }
52 Command::ShowVersion => unreachable!("handled before registry bootstrap"),
53 Command::CheckToolchains => check_toolchains(®istry),
54 Command::ShowVersions { language } => show_versions(®istry, 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, ®istry, iterations),
60 Command::Watch { spec } => watch_run(spec, ®istry),
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 ®istry,
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(®istry),
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, ®istry),
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 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 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 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('&', "&")
779 .replace('<', "<")
780 .replace('>', ">")
781 .replace('"', """)
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}