use std::fs;
use std::io::{self, BufRead, Write};
use std::path::Path;
use std::process::Command;
use colored::Colorize;
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.1");
println!("Type Rust expressions to evaluate them in your project's context.");
println!("Type `help` or `?` for a guide, and `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;
}
if trimmed == "help" || trimmed == "?" || trimmed == "--help" {
println!("\n๐ Oxidite Tinker Help Guide:");
println!("-----------------------------");
println!("Oxidite Tinker evaluates Rust expressions inside your project's context.");
println!("Every line you type is compiled with all of your project's models and");
println!("dependencies pre-imported.\n");
println!("Commands:");
println!(" help, ? Show this guide");
println!(" clear Clear the terminal screen");
println!(" exit, quit Exit the tinker session\n");
println!("Examples:");
println!(" 1 + 2");
println!(" Config::load()");
println!(" Project::all(&db).await\n");
continue;
}
if trimmed == "clear" {
print!("\x1B[2J\x1B[1H");
io::stdout().flush()?;
continue;
}
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)?;
print!("{}", " โ๏ธ Evaluating...".green());
io::stdout().flush()?;
let output = Command::new("cargo")
.args(["run", "--bin", "_tinker", "--quiet"])
.output();
print!("\r\x1B[K");
io::stdout().flush()?;
match output {
Ok(out) => {
if !out.status.success() {
println!("{}", " โ Evaluation failed:".red().bold());
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.red());
}
} else {
if !out.stdout.is_empty() {
io::stdout().write_all(&out.stdout)?;
}
}
}
Err(e) => {
eprintln!("{}", format!("Failed to run snippet: {}", e).red());
}
}
}
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);
}
}