use std::fs;
use std::io::{self, BufRead, Write};
use std::path::Path;
use std::process::Command;
pub fn run_tinker() -> Result<(), Box<dyn std::error::Error>> {
if !Path::new("Cargo.toml").exists() {
return Err("Not inside a Cargo project. Run this command from your project root.".into());
}
let cargo_toml = fs::read_to_string("Cargo.toml")?;
let crate_name = extract_crate_name(&cargo_toml)
.ok_or("Could not determine crate name from Cargo.toml")?;
let crate_ident = crate_name.replace('-', "_");
fs::create_dir_all("src/bin")?;
let tinker_path = Path::new("src/bin/_tinker.rs");
println!("๐งช Oxidite Tinker v2.2.0");
println!("Type Rust expressions to evaluate them in your project's context.");
println!("Use `exit` or Ctrl-D to quit.\n");
let stdin = io::stdin();
let mut lines = stdin.lock().lines();
loop {
print!("oxidite> ");
io::stdout().flush()?;
let line = match lines.next() {
Some(Ok(l)) => l,
_ => break, };
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed == "exit" || trimmed == "quit" {
break;
}
let code = format!(
r#"// Auto-generated by `oxidite tinker`. Do not commit.
use {crate_ident}::*;
#[tokio::main]
async fn main() {{
let __result = {{ {snippet} }};
println!("{{:?}}", __result);
}}
"#,
crate_ident = crate_ident,
snippet = trimmed,
);
fs::write(tinker_path, &code)?;
let output = Command::new("cargo")
.args(["run", "--bin", "_tinker", "--quiet"])
.output();
match output {
Ok(out) => {
if !out.stdout.is_empty() {
io::stdout().write_all(&out.stdout)?;
}
if !out.stderr.is_empty() {
let stderr = String::from_utf8_lossy(&out.stderr);
for line in stderr.lines() {
if line.trim_start().starts_with("Compiling")
|| line.trim_start().starts_with("Finished")
|| line.trim_start().starts_with("Running")
{
continue;
}
eprintln!("{}", line);
}
}
}
Err(e) => {
eprintln!("Failed to run snippet: {}", e);
}
}
}
if tinker_path.exists() {
let _ = fs::remove_file(tinker_path);
}
println!("\n๐ Goodbye!");
Ok(())
}
fn extract_crate_name(toml_content: &str) -> Option<String> {
let mut in_package = false;
for line in toml_content.lines() {
let trimmed = line.trim();
if trimmed == "[package]" {
in_package = true;
continue;
}
if trimmed.starts_with('[') {
in_package = false;
continue;
}
if in_package {
if let Some(rest) = trimmed.strip_prefix("name") {
let rest = rest.trim_start();
if let Some(rest) = rest.strip_prefix('=') {
let rest = rest.trim();
let rest = rest.trim_matches('"');
return Some(rest.to_string());
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::extract_crate_name;
#[test]
fn extracts_name_from_cargo_toml() {
let toml = r#"
[package]
name = "my-cool-app"
version = "0.1.0"
"#;
assert_eq!(extract_crate_name(toml), Some("my-cool-app".to_string()));
}
#[test]
fn returns_none_for_missing_name() {
assert_eq!(extract_crate_name("[dependencies]"), None);
}
}