1use crate::error::{RazError, RazResult};
4use std::process::Command;
5
6pub fn open_browser(url: &str, browser: Option<&str>) -> RazResult<()> {
8 let result = if let Some(browser_cmd) = browser {
9 open_with_browser(url, browser_cmd)
11 } else {
12 open_default_browser(url)
14 };
15
16 result.map_err(|e| RazError::execution(format!("Failed to open browser: {e}")))
17}
18
19fn 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 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
61fn open_with_browser(url: &str, browser: &str) -> Result<(), std::io::Error> {
63 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 _ => browser,
122 };
123
124 Command::new(browser_cmd).arg(url).spawn()?;
125 Ok(())
126}
127
128pub fn extract_server_url(output: &str) -> Option<String> {
130 let patterns = [
132 r"(?i)serving\s+at\s+(https?://[^\s]+)",
134 r"(?i)listening\s+on\s+(https?://[^\s]+)",
136 r"(?i)server\s+running\s+on\s+(https?://[^\s]+)",
138 r"(?i)available\s+at\s+(https?://[^\s]+)",
140 r"(?i)Listening\s+on\s+(https?://[^\s]+)",
142 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 if url_str.contains('*') {
153 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}