1use std::process::Command;
9
10pub fn open_command_for(os: &str, url: &str) -> (String, Vec<String>) {
20 match os {
21 "macos" => ("open".to_string(), vec![url.to_string()]),
22 "windows" => (
23 "cmd".to_string(),
24 vec![
25 "/C".to_string(),
26 "start".to_string(),
27 String::new(),
28 url.to_string(),
29 ],
30 ),
31 _ => ("xdg-open".to_string(), vec![url.to_string()]),
32 }
33}
34
35pub fn open_url_via(spawn: fn(&mut Command) -> std::io::Result<()>, url: &str) -> bool {
42 let (program, args) = open_command_for(std::env::consts::OS, url);
43 let mut cmd = Command::new(program);
44 cmd.args(args);
45 match spawn(&mut cmd) {
46 Ok(()) => true,
47 Err(e) => {
48 tracing::warn!(error = %e, "Failed to launch browser");
49 false
50 }
51 }
52}
53
54pub fn spawn_detached(cmd: &mut Command) -> std::io::Result<()> {
59 cmd.stdin(std::process::Stdio::null())
60 .stdout(std::process::Stdio::null())
61 .stderr(std::process::Stdio::null())
62 .spawn()
63 .map(|_child| ())
64}
65
66pub fn open_url(url: &str) -> bool {
68 open_url_via(spawn_detached, url)
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn macos_uses_open() {
77 let (program, args) = open_command_for("macos", "https://x.example");
78 assert_eq!(program, "open");
79 assert_eq!(args, vec!["https://x.example"]);
80 }
81
82 #[test]
83 fn windows_uses_start_with_a_placeholder_title() {
84 let (program, args) = open_command_for("windows", "https://x.example");
85 assert_eq!(program, "cmd");
86 assert_eq!(args, vec!["/C", "start", "", "https://x.example"]);
89 }
90
91 #[test]
92 fn other_unixes_fall_back_to_xdg_open() {
93 let (program, args) = open_command_for("linux", "https://x.example");
94 assert_eq!(program, "xdg-open");
95 assert_eq!(args, vec!["https://x.example"]);
96
97 assert_eq!(open_command_for("dragonfly", "https://x").0, "xdg-open");
99 }
100
101 #[test]
102 fn open_url_via_reports_spawn_success() {
103 fn ok(_: &mut Command) -> std::io::Result<()> {
104 Ok(())
105 }
106 assert!(open_url_via(ok, "https://x.example"));
107 }
108
109 #[test]
115 fn open_url_via_reports_spawn_failure() {
116 fn boom(_: &mut Command) -> std::io::Result<()> {
117 Err(std::io::Error::other("no browser"))
118 }
119 with_tracing(|| assert!(!open_url_via(boom, "https://x.example")));
120 }
121
122 struct AlwaysOnSubscriber;
124
125 impl tracing::Subscriber for AlwaysOnSubscriber {
126 fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
127 true
128 }
129 fn register_callsite(
130 &self,
131 _metadata: &'static tracing::Metadata<'static>,
132 ) -> tracing::subscriber::Interest {
133 tracing::subscriber::Interest::always()
134 }
135 fn new_span(&self, _span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
136 tracing::span::Id::from_u64(1)
137 }
138 fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {}
139 fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {}
140 fn event(&self, event: &tracing::Event<'_>) {
141 assert!(self.enabled(event.metadata()));
144 }
145 fn enter(&self, _span: &tracing::span::Id) {}
146 fn exit(&self, _span: &tracing::span::Id) {}
147 fn max_level_hint(&self) -> Option<tracing::metadata::LevelFilter> {
148 Some(tracing::metadata::LevelFilter::TRACE)
149 }
150 }
151
152 #[test]
155 fn always_on_subscriber_span_methods_are_all_no_ops() {
156 with_tracing(|| {
157 let span = tracing::info_span!("test-span", field = tracing::field::Empty);
158 span.record("field", 1);
159 let other = tracing::info_span!("other-span");
160 span.follows_from(&other);
161 let _enter = span.enter();
162 tracing::info!(parent: &span, "inside span");
163 });
164 }
165
166 fn with_tracing<T>(f: impl FnOnce() -> T) -> T {
168 static INSTALLED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
169 INSTALLED.get_or_init(|| {
170 let _ = tracing::subscriber::set_global_default(AlwaysOnSubscriber);
174 tracing::callsite::rebuild_interest_cache();
175 });
176 f()
177 }
178
179 #[test]
180 fn spawn_detached_launches_a_real_process() {
181 let mut cmd = Command::new("true");
185 let _ = spawn_detached(&mut cmd);
186 }
187
188 #[test]
189 fn spawn_detached_errors_on_a_missing_program() {
190 let mut cmd = Command::new("/nonexistent/browser/launcher");
191 assert!(spawn_detached(&mut cmd).is_err());
192 }
193
194 #[test]
195 fn open_url_runs_the_real_launcher_without_opening_a_browser() {
196 let _ = open_url("leviath-open-url-test-target");
204 }
205}