raz_core/
browser.rs

1//! Cross-platform browser launching utilities
2
3use crate::error::{RazError, RazResult};
4use std::process::Command;
5
6/// Opens a URL in the default browser or a specified browser
7pub fn open_browser(url: &str, browser: Option<&str>) -> RazResult<()> {
8    let result = if let Some(browser_cmd) = browser {
9        // Use specified browser
10        open_with_browser(url, browser_cmd)
11    } else {
12        // Use default browser
13        open_default_browser(url)
14    };
15
16    result.map_err(|e| RazError::execution(format!("Failed to open browser: {e}")))
17}
18
19/// Opens URL in the system's default browser
20fn open_default_browser(url: &str) -> Result<(), std::io::Error> {
21    #[cfg(target_os = "macos")]
22    {
23        Command::new("open").arg(url).spawn()?;
24        Ok(())
25    }
26
27    #[cfg(target_os = "windows")]
28    {
29        Command::new("cmd").args(&["/C", "start", url]).spawn()?;
30        Ok(())
31    }
32
33    #[cfg(target_os = "linux")]
34    {
35        // Try common Linux browsers in order
36        if Command::new("xdg-open").arg(url).spawn().is_ok() {
37            return Ok(());
38        }
39        if Command::new("gnome-open").arg(url).spawn().is_ok() {
40            return Ok(());
41        }
42        if Command::new("kde-open").arg(url).spawn().is_ok() {
43            return Ok(());
44        }
45
46        Err(std::io::Error::new(
47            std::io::ErrorKind::NotFound,
48            "Could not find a suitable browser launcher",
49        ))
50    }
51
52    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
53    {
54        Err(std::io::Error::new(
55            std::io::ErrorKind::Unsupported,
56            "Browser launching not supported on this platform",
57        ))
58    }
59}
60
61/// Opens URL with a specific browser
62fn open_with_browser(url: &str, browser: &str) -> Result<(), std::io::Error> {
63    // Handle common browser aliases
64    let browser_cmd = match browser.to_lowercase().as_str() {
65        "chrome" | "google-chrome" => {
66            #[cfg(target_os = "macos")]
67            {
68                "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
69            }
70            #[cfg(target_os = "windows")]
71            {
72                "chrome.exe"
73            }
74            #[cfg(target_os = "linux")]
75            {
76                "google-chrome"
77            }
78        }
79        "firefox" => {
80            #[cfg(target_os = "macos")]
81            {
82                "/Applications/Firefox.app/Contents/MacOS/firefox"
83            }
84            #[cfg(target_os = "windows")]
85            {
86                "firefox.exe"
87            }
88            #[cfg(target_os = "linux")]
89            {
90                "firefox"
91            }
92        }
93        "safari" => {
94            #[cfg(target_os = "macos")]
95            {
96                "/Applications/Safari.app/Contents/MacOS/Safari"
97            }
98            #[cfg(not(target_os = "macos"))]
99            {
100                return Err(std::io::Error::new(
101                    std::io::ErrorKind::NotFound,
102                    "Safari is only available on macOS",
103                ));
104            }
105        }
106        "edge" | "microsoft-edge" => {
107            #[cfg(target_os = "macos")]
108            {
109                "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
110            }
111            #[cfg(target_os = "windows")]
112            {
113                "msedge.exe"
114            }
115            #[cfg(target_os = "linux")]
116            {
117                "microsoft-edge"
118            }
119        }
120        // If not a known alias, use as-is (could be full path or command in PATH)
121        _ => browser,
122    };
123
124    Command::new(browser_cmd).arg(url).spawn()?;
125    Ok(())
126}
127
128/// Extract URL from server output
129pub fn extract_server_url(output: &str) -> Option<String> {
130    // Common patterns for server URLs
131    let patterns = [
132        // "Serving at http://..."
133        r"(?i)serving\s+at\s+(https?://[^\s]+)",
134        // "listening on http://..."
135        r"(?i)listening\s+on\s+(https?://[^\s]+)",
136        // "Server running on http://..."
137        r"(?i)server\s+running\s+on\s+(https?://[^\s]+)",
138        // "Available at http://..."
139        r"(?i)available\s+at\s+(https?://[^\s]+)",
140        // Leptos pattern: "Listening on http://127.0.0.1:3000"
141        r"(?i)Listening\s+on\s+(https?://[^\s]+)",
142        // Just find any http(s) URL
143        r"(https?://(?:localhost|127\.0\.0\.1|\*+):[0-9]+)",
144    ];
145
146    for pattern in patterns {
147        if let Ok(re) = regex::Regex::new(pattern) {
148            if let Some(captures) = re.captures(output) {
149                if let Some(url) = captures.get(1) {
150                    let mut url_str = url.as_str().to_string();
151                    // Replace asterisks pattern with localhost using simple string replacement
152                    if url_str.contains('*') {
153                        // Replace consecutive asterisks with localhost
154                        url_str = url_str
155                            .chars()
156                            .collect::<String>()
157                            .replace("*", "")
158                            .replace("://:", "://localhost:");
159                        if !url_str.contains("localhost") {
160                            url_str = url_str.replace("://", "://localhost");
161                        }
162                    }
163                    return Some(url_str);
164                }
165            }
166        }
167    }
168
169    None
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_extract_server_url() {
178        assert_eq!(
179            extract_server_url("Serving at http://localhost:3000"),
180            Some("http://localhost:3000".to_string())
181        );
182
183        assert_eq!(
184            extract_server_url("listening on http://127.0.0.1:8080"),
185            Some("http://127.0.0.1:8080".to_string())
186        );
187
188        assert_eq!(
189            extract_server_url("Serving at http://*********:3000"),
190            Some("http://localhost:3000".to_string())
191        );
192
193        assert_eq!(extract_server_url("no url here"), None);
194    }
195}