use std::env;
use std::fs;
use std::io::{self, BufRead, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use colored::Colorize;
fn find_project_root() -> Option<PathBuf> {
let mut current = std::env::current_dir().ok()?;
loop {
if current.join("oxidite.toml").exists() || current.join("Cargo.toml").exists() {
return Some(current);
}
if !current.pop() {
return None;
}
}
}
fn load_dotenv() {
if env::var("OXIDITE_SKIP_DOTENV").is_err() {
if let Some(root) = find_project_root() {
let env_path = root.join(".env");
if env_path.exists() {
let _ = dotenv::from_path(&env_path);
return;
}
}
let _ = dotenv::dotenv();
}
}
pub fn run_tinker() -> Result<(), Box<dyn std::error::Error>> {
load_dotenv();
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('-', "_");
let has_lib = Path::new("src/lib.rs").exists();
fs::create_dir_all("src/bin")?;
let tinker_path = Path::new("src/bin/_tinker.rs");
println!("Oxidite Tinker v2.3.3");
if has_lib {
println!("Library crate detected: using `{crate_ident}::*` imports.");
} else {
println!("Binary-only crate detected: only std/oxidite prelude available.");
}
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.\n");
if has_lib {
println!("Your project has a library crate (src/lib.rs).");
println!("All public items from your crate are pre-imported.\n");
} else {
println!("Your project is a binary-only crate (no src/lib.rs).");
println!("Only the Oxidite prelude and standard items are available.\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 = if has_lib {
format!(
r#"// Auto-generated by `oxidite tinker`. Do not commit.
use {crate_ident}::*;
#[tokio::main]
async fn main() {{
let __result = {{ {snippet} }};
println!("{{:?}}", __result);
}}
"#,
snippet = trimmed,
)
} else {
format!(
r#"// Auto-generated by `oxidite tinker`. Do not commit.
use oxidite::prelude::*;
#[tokio::main]
async fn main() {{
let __result = {{ {snippet} }};
println!("{{:?}}", __result);
}}
"#,
snippet = trimmed,
)
};
fs::write(tinker_path, &code)?;
print!("{}", " ⚙️ Evaluating...".green());
io::stdout().flush()?;
let output = Command::new("cargo")
.args(["run", "--bin", "_tinker", "--quiet"])
.envs(std::env::vars())
.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);
}
}