use std::process::{Command, Stdio};
pub fn open_url(raw: &str) -> Result<(), String> {
let url = normalize_url(raw).ok_or_else(|| "empty link".to_string())?;
let mut cmd = platform_command(&url);
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map(|_| ())
.map_err(|e| format!("could not open {url}: {e}"))
}
pub fn normalize_url(raw: &str) -> Option<String> {
let url = raw.trim();
if url.is_empty() {
return None;
}
let lower = url.to_ascii_lowercase();
if lower.starts_with("http://")
|| lower.starts_with("https://")
|| lower.starts_with("mailto:")
|| lower.starts_with("file:")
|| lower.contains("://")
{
return Some(url.to_string());
}
Some(format!("https://{url}"))
}
fn platform_command(url: &str) -> Command {
if cfg!(target_os = "macos") {
let mut c = Command::new("open");
c.args(["--", url]);
c
} else if cfg!(target_os = "windows") {
let mut c = Command::new("rundll32");
c.args(["url.dll,FileProtocolHandler", url]);
c
} else {
let mut c = Command::new("xdg-open");
c.arg(url);
c
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keeps_an_existing_scheme() {
assert_eq!(
normalize_url("https://x.ai/foo"),
Some("https://x.ai/foo".into())
);
assert_eq!(normalize_url("mailto:a@b.c"), Some("mailto:a@b.c".into()));
}
#[test]
fn adds_https_to_a_bare_host() {
assert_eq!(
normalize_url("example.com/path"),
Some("https://example.com/path".into())
);
}
#[test]
fn rejects_blank() {
assert_eq!(normalize_url(" "), None);
}
}