pi/core/platform/
open_browser.rs1use std::process::{Command, Stdio};
13use std::sync::OnceLock;
14
15#[derive(Copy, Clone, Debug, Eq, PartialEq)]
19pub enum BrowserPlatform {
20 Darwin,
22 Windows,
24 Unix,
26}
27
28impl BrowserPlatform {
29 #[must_use]
31 pub fn host() -> Self {
32 #[cfg(target_os = "macos")]
33 {
34 Self::Darwin
35 }
36 #[cfg(target_os = "windows")]
37 {
38 Self::Windows
39 }
40 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
41 {
42 Self::Unix
43 }
44 }
45}
46
47#[must_use]
53pub fn open_browser_command(platform: BrowserPlatform, target: &str) -> (String, Vec<String>) {
54 match platform {
55 BrowserPlatform::Darwin => ("open".to_owned(), vec![target.to_owned()]),
56 BrowserPlatform::Windows => (
57 "rundll32".to_owned(),
58 vec!["url.dll,FileProtocolHandler".to_owned(), target.to_owned()],
59 ),
60 BrowserPlatform::Unix => ("xdg-open".to_owned(), vec![target.to_owned()]),
61 }
62}
63
64pub trait SpawnSink: Send + Sync {
71 fn on_error(&self, command: &str, error: std::io::Error);
73}
74
75#[derive(Debug, Default)]
77pub struct DefaultSpawnSink;
78
79impl SpawnSink for DefaultSpawnSink {
80 fn on_error(&self, _command: &str, _error: std::io::Error) {}
81}
82
83#[derive(Debug, Default)]
85pub struct RecordingSpawnSink {
86 pub error: std::sync::Mutex<Option<(String, String)>>,
88}
89
90impl SpawnSink for RecordingSpawnSink {
91 fn on_error(&self, command: &str, error: std::io::Error) {
92 let mut guard = match self.error.lock() {
93 Ok(guard) => guard,
94 Err(poisoned) => poisoned.into_inner(),
95 };
96 if guard.is_none() {
97 *guard = Some((command.to_owned(), error.to_string()));
98 }
99 }
100}
101
102static SPAWN_SINK: OnceLock<Box<dyn SpawnSink>> = OnceLock::new();
103
104pub fn install_spawn_sink(sink: Box<dyn SpawnSink>) {
108 let _ = SPAWN_SINK.set(sink);
109}
110
111fn report_error(command: &str, error: std::io::Error) {
112 let sink = SPAWN_SINK.get_or_init(|| Box::<DefaultSpawnSink>::default());
113 sink.on_error(command, error);
114}
115
116pub fn open_browser(target: &str) {
123 open_browser_with(BrowserPlatform::host(), target);
124}
125
126pub fn open_browser_with(platform: BrowserPlatform, target: &str) {
131 let (command, args) = open_browser_command(platform, target);
132 let spawn_result = Command::new(&command)
133 .args(&args)
134 .stdin(Stdio::null())
135 .stdout(Stdio::null())
136 .stderr(Stdio::null())
137 .spawn();
138 match spawn_result {
139 Ok(mut child) => {
140 child.try_wait().ok();
144 }
145 Err(error) => report_error(&command, error),
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn darwin_argv_is_open_target() {
155 let (cmd, args) = open_browser_command(BrowserPlatform::Darwin, "https://pi.dev");
156 assert_eq!(cmd, "open");
157 assert_eq!(args, vec!["https://pi.dev"]);
158 }
159
160 #[test]
161 fn windows_argv_is_rundll32_fileprotocohandler_target() {
162 let (cmd, args) =
163 open_browser_command(BrowserPlatform::Windows, "https://pi.dev/session/#abc");
164 assert_eq!(cmd, "rundll32");
165 assert_eq!(
166 args,
167 vec!["url.dll,FileProtocolHandler", "https://pi.dev/session/#abc"]
168 );
169 }
170
171 #[test]
172 fn unix_argv_is_xdg_open_target() {
173 let (cmd, args) = open_browser_command(BrowserPlatform::Unix, "https://pi.dev");
174 assert_eq!(cmd, "xdg-open");
175 assert_eq!(args, vec!["https://pi.dev"]);
176 }
177
178 #[test]
179 fn host_platform_matches_cfg() {
180 #[cfg(target_os = "macos")]
181 assert_eq!(BrowserPlatform::host(), BrowserPlatform::Darwin);
182 #[cfg(target_os = "windows")]
183 assert_eq!(BrowserPlatform::host(), BrowserPlatform::Windows);
184 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
185 assert_eq!(BrowserPlatform::host(), BrowserPlatform::Unix);
186 }
187}