entracte_lib/platform.rs
1//! Authoritative platform detection for the renderer.
2//!
3//! Frontend code used to lean on `navigator.userAgent`, which lies on
4//! Linux WebViews that masquerade as Mac/Safari for compatibility.
5//! Rust knows the host OS for certain via `std::env::consts::OS`, so
6//! this module exposes it as a Tauri command and the renderer caches
7//! the result through its `usePlatform()` hook.
8
9use serde::Serialize;
10
11/// Return the host platform string as known to Rust:
12/// `"macos"`, `"linux"`, `"windows"`, etc. — the value of
13/// `std::env::consts::OS`. The renderer normalises this through
14/// `normalisePlatform` in `lib/platform.ts`.
15#[tauri::command]
16pub fn get_platform() -> &'static str {
17 std::env::consts::OS
18}
19
20/// Return the host CPU architecture as the value of
21/// `std::env::consts::ARCH` (`"aarch64"`, `"x86_64"`, …). Compiled into
22/// the binary, so it reflects the build target the user is running —
23/// exactly the build the Download button should offer them. The browser
24/// can't see this reliably (WKWebView masks Apple Silicon as Intel), so
25/// the renderer asks Rust and normalises the answer through
26/// `normaliseArch` in `lib/platform.ts`.
27#[tauri::command]
28pub fn get_arch() -> &'static str {
29 std::env::consts::ARCH
30}
31
32/// Normalise an optional OS locale into a BCP-47 tag the renderer can hand
33/// to `Intl`. Falls back to `"en-US"` when the OS reports nothing usable.
34/// Pure so the fallback is unit-testable without the platform locale source.
35fn locale_or_default(raw: Option<String>) -> String {
36 raw.map(|s| s.trim().to_string())
37 .filter(|s| !s.is_empty())
38 .unwrap_or_else(|| "en-US".to_string())
39}
40
41/// Return the OS's preferred locale as a BCP-47 tag (e.g. `"en-GB"`,
42/// `"nb-NO"`). The renderer formats the pause-picker date with `Intl` using
43/// this. The WebView's own locale is unreliable — a non-localised app falls
44/// back to en-US even when the OS region differs — so it's read natively.
45#[tauri::command]
46pub fn get_locale() -> String {
47 locale_or_default(sys_locale::get_locale())
48}
49
50/// Behavioural capability flags the renderer branches on, so each
51/// platform-gated feature is decided once here rather than re-derived
52/// from the raw platform string in every component.
53#[derive(Serialize)]
54pub struct PlatformCapabilities {
55 /// Whether the OS exposes a Do-Not-Disturb / Focus state the app
56 /// can read (macOS, Windows, GNOME/KDE on Linux).
57 pub supports_dnd_read: bool,
58 /// Whether media pause during breaks can target individual players
59 /// precisely (Linux via MPRIS) rather than firing a best-effort
60 /// play/pause media key (macOS, Windows).
61 pub media_pause_granular: bool,
62 /// Whether the installer is unsigned and the OS will show a
63 /// reputation warning (Windows SmartScreen) on update.
64 pub installer_unsigned_warning: bool,
65 /// Whether fullscreen-video detection (the "pause during fullscreen
66 /// video" setting) is reliable on this host. True on macOS, Windows
67 /// and X11 Linux, where the app enumerates on-screen windows to
68 /// confirm a real fullscreen window. False on Linux Wayland, where
69 /// no portable window enumeration exists and detection degrades to
70 /// an assertion-only signal that fires on any media keeping the
71 /// display awake — see `video.rs`.
72 pub video_pause_reliable: bool,
73}
74
75/// Derive the capability flags for a given target-OS string and whether
76/// the session is Wayland. Kept pure and parameterised so every branch
77/// is unit-testable on a single host; the `#[tauri::command]` wrapper
78/// feeds it the real target and session type.
79fn capabilities_for(os: &str, wayland: bool) -> PlatformCapabilities {
80 PlatformCapabilities {
81 supports_dnd_read: matches!(os, "macos" | "windows" | "linux"),
82 media_pause_granular: os == "linux",
83 installer_unsigned_warning: os == "windows",
84 video_pause_reliable: matches!(os, "macos" | "windows") || (os == "linux" && !wayland),
85 }
86}
87
88/// Whether the given OS + session env signals a Wayland session. Off
89/// Linux the Wayland env vars are irrelevant to video detection, so this
90/// is always false there. On Linux it mirrors the probe in `video.rs` /
91/// `overlay.rs`: `XDG_SESSION_TYPE=wayland` or a `WAYLAND_DISPLAY` set.
92/// Kept pure (OS + env passed in) so every branch — including the
93/// off-Linux short-circuit — is unit-testable on the single Linux
94/// coverage runner.
95fn wayland_session_from_env(os: &str, session_type: Option<&str>, wayland_display: bool) -> bool {
96 if os != "linux" {
97 return false;
98 }
99 session_type.is_some_and(|s| s.eq_ignore_ascii_case("wayland")) || wayland_display
100}
101
102/// Whether the current session is Wayland. Thin shim that reads the host
103/// OS and session env vars and defers to [`wayland_session_from_env`].
104fn is_wayland_session() -> bool {
105 wayland_session_from_env(
106 std::env::consts::OS,
107 std::env::var("XDG_SESSION_TYPE").ok().as_deref(),
108 std::env::var("WAYLAND_DISPLAY").is_ok(),
109 )
110}
111
112/// Return the host's platform capability flags, derived from the
113/// compile-time target OS and the runtime session type. The renderer
114/// caches the result through its `usePlatformCapabilities()` hook.
115#[tauri::command]
116pub fn get_platform_capabilities() -> PlatformCapabilities {
117 capabilities_for(std::env::consts::OS, is_wayland_session())
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn locale_or_default_falls_back_when_missing_or_blank() {
126 assert_eq!(locale_or_default(None), "en-US");
127 assert_eq!(locale_or_default(Some("".into())), "en-US");
128 assert_eq!(locale_or_default(Some(" ".into())), "en-US");
129 }
130
131 #[test]
132 fn locale_or_default_passes_through_and_trims_a_real_tag() {
133 assert_eq!(locale_or_default(Some("nb-NO".into())), "nb-NO");
134 assert_eq!(locale_or_default(Some(" en-GB ".into())), "en-GB");
135 }
136
137 #[test]
138 fn get_locale_returns_a_nonempty_tag() {
139 // Whatever the host reports, the command never yields an empty string.
140 assert!(!get_locale().is_empty());
141 }
142
143 #[test]
144 fn get_platform_returns_a_known_value() {
145 let p = get_platform();
146 // The crate's CI matrix covers all three; reject anything we don't
147 // expect so a new target gets a deliberate decision rather than a
148 // surprise string slipping through to the renderer.
149 assert!(
150 matches!(p, "macos" | "linux" | "windows"),
151 "unexpected std::env::consts::OS = {p:?}",
152 );
153 }
154
155 #[test]
156 fn get_arch_returns_a_known_value() {
157 let a = get_arch();
158 // The CI matrix builds x86_64 and aarch64; reject anything else so a
159 // new target arch gets a deliberate mapping in the renderer rather
160 // than silently falling through to the "other" download bucket.
161 assert!(
162 matches!(a, "x86_64" | "aarch64"),
163 "unexpected std::env::consts::ARCH = {a:?}",
164 );
165 }
166
167 #[test]
168 fn macos_capabilities() {
169 let c = capabilities_for("macos", false);
170 assert!(c.supports_dnd_read);
171 assert!(!c.media_pause_granular);
172 assert!(!c.installer_unsigned_warning);
173 assert!(c.video_pause_reliable);
174 }
175
176 #[test]
177 fn windows_capabilities() {
178 let c = capabilities_for("windows", false);
179 assert!(c.supports_dnd_read);
180 assert!(!c.media_pause_granular);
181 assert!(c.installer_unsigned_warning);
182 assert!(c.video_pause_reliable);
183 }
184
185 #[test]
186 fn linux_x11_capabilities() {
187 let c = capabilities_for("linux", false);
188 assert!(c.supports_dnd_read);
189 assert!(c.media_pause_granular);
190 assert!(!c.installer_unsigned_warning);
191 assert!(c.video_pause_reliable);
192 }
193
194 #[test]
195 fn linux_wayland_video_pause_is_unreliable() {
196 // Wayland can't enumerate windows, so fullscreen-video detection
197 // degrades to assertion-only — the renderer must warn there.
198 let c = capabilities_for("linux", true);
199 assert!(c.supports_dnd_read);
200 assert!(c.media_pause_granular);
201 assert!(!c.installer_unsigned_warning);
202 assert!(!c.video_pause_reliable);
203 }
204
205 #[test]
206 fn wayland_flag_only_affects_video_pause_on_linux() {
207 // A stray Wayland signal off Linux (shouldn't happen, but the
208 // flag is a plain bool) must not flip video_pause_reliable.
209 assert!(capabilities_for("macos", true).video_pause_reliable);
210 assert!(capabilities_for("windows", true).video_pause_reliable);
211 }
212
213 #[test]
214 fn unknown_platform_gets_conservative_capabilities() {
215 // Anything outside the supported targets gets every flag off so
216 // the renderer hides platform-specific copy rather than showing
217 // a claim we can't back.
218 let c = capabilities_for("freebsd", false);
219 assert!(!c.supports_dnd_read);
220 assert!(!c.media_pause_granular);
221 assert!(!c.installer_unsigned_warning);
222 assert!(!c.video_pause_reliable);
223 }
224
225 #[test]
226 fn wayland_session_from_env_detects_session_type() {
227 assert!(wayland_session_from_env("linux", Some("wayland"), false));
228 assert!(wayland_session_from_env("linux", Some("Wayland"), false));
229 assert!(!wayland_session_from_env("linux", Some("x11"), false));
230 assert!(!wayland_session_from_env("linux", None, false));
231 }
232
233 #[test]
234 fn wayland_session_from_env_detects_wayland_display() {
235 assert!(wayland_session_from_env("linux", None, true));
236 assert!(wayland_session_from_env("linux", Some("x11"), true));
237 }
238
239 #[test]
240 fn wayland_session_from_env_is_false_off_linux() {
241 // Off Linux the Wayland env vars don't apply, even if inherited.
242 assert!(!wayland_session_from_env("macos", Some("wayland"), true));
243 assert!(!wayland_session_from_env("windows", Some("wayland"), true));
244 assert!(!wayland_session_from_env("freebsd", None, true));
245 }
246
247 #[test]
248 fn command_matches_host_target() {
249 let c = get_platform_capabilities();
250 let expected = capabilities_for(std::env::consts::OS, is_wayland_session());
251 assert_eq!(c.supports_dnd_read, expected.supports_dnd_read);
252 assert_eq!(c.media_pause_granular, expected.media_pause_granular);
253 assert_eq!(
254 c.installer_unsigned_warning,
255 expected.installer_unsigned_warning
256 );
257 assert_eq!(c.video_pause_reliable, expected.video_pause_reliable);
258 }
259}