use std::time::Instant;
use tokio::process::Command;
fn canonicalize_url(raw: &str) -> String {
let raw = raw.trim();
if raw.starts_with("https://https://") {
let dedup = &raw["https://".len()..];
format!("https://{}", dedup)
} else if raw.starts_with("https://") || raw.starts_with("http://") {
raw.to_string()
} else {
format!("https://{}", raw)
}
}
pub async fn run_curl(raw_url: &str, debug: bool) -> Result<(), String> {
let url = canonicalize_url(raw_url);
let mut curl_cmd = Command::new("curl");
curl_cmd
.arg("-sS") .arg("-D")
.arg("-") .arg("-w")
.arg("%{http_code}") .arg(&url);
if debug {
println!("[DEBUG] running curl command for '{}'", url);
}
let start = Instant::now();
let output = curl_cmd
.output()
.await
.map_err(|e| format!("failed to run curl: {}", e))?;
let duration = start.elapsed();
if !output.status.success() {
return Err(format!(
"\x1b[91mCurl failed with status: {}\nStderr: {}\x1b[0m",
output.status,
String::from_utf8_lossy(&output.stderr)
));
}
let all_output = output.stdout;
let all_str = String::from_utf8_lossy(&all_output);
let mut lines = all_str.lines();
let mut headers = Vec::new();
let mut body = String::new();
let mut in_headers = true;
for line in lines.by_ref() {
if in_headers && line.trim() == "" {
in_headers = false;
continue;
}
if in_headers {
headers.push(line.to_string());
} else {
body.push_str(line);
body.push('\n');
}
}
let mut status_code = None;
if !body.is_empty() && body.trim().chars().all(|c| c.is_ascii_digit()) {
status_code = Some(body.trim().to_string());
body.clear();
} else if let Some(code_line) = lines.last() {
if code_line.chars().all(|c| c.is_ascii_digit()) {
status_code = Some(code_line.to_string());
}
}
println!("\x1b[96m=== Curl to {}\x1b[0m", url);
println!("\x1b[90m--- Response Headers ---\x1b[0m");
for h in &headers {
if h.starts_with("HTTP/") {
println!("\x1b[94m{}\x1b[0m", h);
} else {
println!("\x1b[90m{}\x1b[0m", h);
}
}
if let Some(status) = &status_code {
let status_int = status.parse::<u16>().unwrap_or(0);
let color = if status_int >= 200 && status_int < 300 {
"\x1b[92m"
} else if status_int >= 400 {
"\x1b[91m"
} else {
"\x1b[93m"
};
println!(
"\x1b[90m--- Status ---\x1b[0m {}{}{}\x1b[0m",
color, status, "\x1b[0m"
);
}
println!(
"\x1b[90m--- Response Time ---\x1b[0m \x1b[95m{:.2?}\x1b[0m",
duration
);
let mut jq_cmd = Command::new("jq");
jq_cmd.arg("-C").arg(".");
if debug {
println!("[DEBUG] piping response body to jq for colored pretty-print");
}
let mut jq_child = jq_cmd
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.map_err(|e| format!("failed to spawn jq: {}", e))?;
use tokio::io::AsyncWriteExt;
if let Some(mut stdin) = jq_child.stdin.take() {
use tokio::io::AsyncWriteExt;
stdin
.write_all(body.as_bytes())
.await
.map_err(|e| format!("failed to write to jq stdin: {}", e))?;
}
let jq_output = jq_child
.wait_with_output()
.await
.map_err(|e| format!("failed to collect jq output: {}", e))?;
println!("\x1b[90m--- Body (pretty JSON) ---\x1b[0m");
print!("{}", String::from_utf8_lossy(&jq_output.stdout));
if !jq_output.status.success() {
println!("\x1b[93m[WARN] jq encountered an error (body may not be JSON)\x1b[0m");
if !body.trim().is_empty() {
println!("\x1b[90m--- Raw Body ---\x1b[0m");
print!("{}", body);
}
}
Ok(())
}