Skip to main content

logicaffeine_web/ui/
platform.rs

1//! The visitor's system, read from what their browser says, so the front page shows the install that
2//! runs on it.
3
4use crate::ui::pages::landing::content::Install;
5
6/// What the browser says about the machine it runs on.
7pub struct Seen<'a> {
8    /// `navigator.userAgentData.platform`, where the browser offers it (Chromium).
9    pub ua_platform: Option<&'a str>,
10    /// `navigator.platform`.
11    pub platform: &'a str,
12    pub user_agent: &'a str,
13    pub max_touch_points: u32,
14}
15
16/// The systems the front page tells apart.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum System {
19    MacOs,
20    Linux,
21    Windows,
22    ChromeOs,
23    /// A phone or a tablet.
24    Phone,
25    Unknown,
26}
27
28impl System {
29    pub const ALL: &'static [System] = &[System::MacOs, System::Linux, System::Windows, System::ChromeOs, System::Phone, System::Unknown];
30
31    /// The name the fixture, the inline script and `<html data-system>` use.
32    pub fn name(self) -> &'static str {
33        match self {
34            System::MacOs => "macos",
35            System::Linux => "linux",
36            System::Windows => "windows",
37            System::ChromeOs => "chromeos",
38            System::Phone => "phone",
39            System::Unknown => "unknown",
40        }
41    }
42}
43
44/// The system a browser runs on. Phones and tablets first: Android says Linux, and an iPad asking for
45/// the desktop site says it is a Mac with a touch screen. [`SYSTEM_OF_JS`] is this function in
46/// JavaScript, for the prerendered page.
47pub fn system_of(seen: &Seen) -> System {
48    let ua = seen.user_agent;
49    let platform = seen.platform;
50    let ua_platform = seen.ua_platform.unwrap_or("");
51    let says = |word: &str| ua.contains(word);
52    let starts = |prefix: &str| platform.starts_with(prefix);
53    if says("iPhone")
54        || says("iPad")
55        || says("iPod")
56        || says("Android")
57        || ua_platform == "Android"
58        || ua_platform == "iOS"
59        || starts("iPhone")
60        || starts("iPad")
61        || starts("iPod")
62        || (platform == "MacIntel" && seen.max_touch_points > 1)
63    {
64        System::Phone
65    } else if says("CrOS") || ua_platform == "Chrome OS" || ua_platform == "ChromeOS" {
66        System::ChromeOs
67    } else if ua_platform == "Windows" || starts("Win") || says("Windows") {
68        System::Windows
69    } else if ua_platform == "macOS" || starts("Mac") || says("Macintosh") {
70        System::MacOs
71    } else if ua_platform == "Linux" || starts("Linux") || says("X11") || says("Linux") {
72        System::Linux
73    } else {
74        System::Unknown
75    }
76}
77
78/// The install that runs on `system`: PowerShell on Windows, the shell script everywhere else. A
79/// phone's reader pastes into an agent on a Mac or a Linux machine, or into a cloud agent, which runs
80/// in a Linux sandbox.
81pub fn install_for(system: System) -> Install {
82    match system {
83        System::Windows => Install::Windows,
84        System::MacOs | System::Linux | System::ChromeOs | System::Phone | System::Unknown => Install::Unix,
85    }
86}
87
88/// Where the page remembers the install a visitor chose with the switch, in their browser alone.
89pub const REMEMBERED: &str = "logos-install";
90
91/// [`system_of`] in JavaScript: `systemOf(userAgent, platform, uaPlatform, maxTouchPoints)` returns
92/// [`System::name`]. `landing_page::the_os_script_reads_the_system_as_the_app_does` holds the two to
93/// the same answers for every visitor of the fixture.
94pub const SYSTEM_OF_JS: &str = "function systemOf(ua, platform, uaPlatform, touch) { \
95var says = function (w) { return ua.indexOf(w) !== -1; }; \
96var starts = function (p) { return platform.indexOf(p) === 0; }; \
97if (says('iPhone') || says('iPad') || says('iPod') || says('Android') || uaPlatform === 'Android' || uaPlatform === 'iOS' || starts('iPhone') || starts('iPad') || starts('iPod') || (platform === 'MacIntel' && touch > 1)) return 'phone'; \
98if (says('CrOS') || uaPlatform === 'Chrome OS' || uaPlatform === 'ChromeOS') return 'chromeos'; \
99if (uaPlatform === 'Windows' || starts('Win') || says('Windows')) return 'windows'; \
100if (uaPlatform === 'macOS' || starts('Mac') || says('Macintosh')) return 'macos'; \
101if (uaPlatform === 'Linux' || starts('Linux') || says('X11') || says('Linux')) return 'linux'; \
102return 'unknown'; }";
103
104/// The script the prerendered front page runs before its hero paints: `<html>` gets the visitor's
105/// system (`data-system`) and the install to show (`data-os`), the one they chose last time if they
106/// chose one, so the page shows the right prompt with no flash and no jump.
107pub fn os_script() -> String {
108    format!(
109        "(function () {{ {SYSTEM_OF_JS} var n = navigator; \
110var system = systemOf(n.userAgent || '', n.platform || '', (n.userAgentData && n.userAgentData.platform) || '', n.maxTouchPoints || 0); \
111var chosen = null; try {{ chosen = localStorage.getItem('{REMEMBERED}'); }} catch (e) {{}} \
112var root = document.documentElement; root.setAttribute('data-system', system); \
113root.setAttribute('data-os', chosen === 'windows' || chosen === 'unix' ? chosen : (system === 'windows' ? 'windows' : 'unix')); }})();"
114    )
115}
116
117/// The install the page shows: what `<html data-os>` says, else what the app reads of the browser and
118/// the visitor's last choice, marking `<html>` with it as the inline script would have. Off the
119/// browser, the shell install.
120pub fn shown_install() -> Install {
121    #[cfg(target_arch = "wasm32")]
122    {
123        let window = web_sys::window();
124        let Some(root) = window.as_ref().and_then(|w| w.document()).and_then(|d| d.document_element()) else {
125            return Install::Unix;
126        };
127        if let Some(install) = root.get_attribute("data-os").as_deref().and_then(Install::from_name) {
128            return install;
129        }
130        let navigator = window.as_ref().map(|w| w.navigator());
131        let read = |of: &wasm_bindgen::JsValue, name: &str| js_sys::Reflect::get(of, &name.into()).ok().and_then(|v| v.as_string());
132        let user_agent = navigator.as_ref().and_then(|n| read(n, "userAgent")).unwrap_or_default();
133        let platform = navigator.as_ref().and_then(|n| read(n, "platform")).unwrap_or_default();
134        let ua_platform = navigator
135            .as_ref()
136            .and_then(|n| js_sys::Reflect::get(n, &"userAgentData".into()).ok())
137            .filter(|data| data.is_object())
138            .and_then(|data| read(&data, "platform"));
139        let max_touch_points = navigator.as_ref().map_or(0, |n| n.max_touch_points().max(0) as u32);
140        let system = system_of(&Seen { ua_platform: ua_platform.as_deref(), platform: &platform, user_agent: &user_agent, max_touch_points });
141        let chosen = window.as_ref().and_then(|w| w.local_storage().ok().flatten()).and_then(|s| s.get_item(REMEMBERED).ok().flatten());
142        let install = chosen.as_deref().and_then(Install::from_name).unwrap_or(install_for(system));
143        let _ = root.set_attribute("data-system", system.name());
144        let _ = root.set_attribute("data-os", install.name());
145        install
146    }
147    #[cfg(not(target_arch = "wasm32"))]
148    Install::Unix
149}
150
151/// Show `install`'s prompt, and remember the choice in this browser when it lets the page.
152pub fn choose_install(install: Install) {
153    #[cfg(target_arch = "wasm32")]
154    {
155        let window = web_sys::window();
156        if let Some(root) = window.as_ref().and_then(|w| w.document()).and_then(|d| d.document_element()) {
157            let _ = root.set_attribute("data-os", install.name());
158        }
159        if let Some(storage) = window.as_ref().and_then(|w| w.local_storage().ok().flatten()) {
160            let _ = storage.set_item(REMEMBERED, install.name());
161        }
162    }
163    #[cfg(not(target_arch = "wasm32"))]
164    let _ = install;
165}
166
167#[cfg(test)]
168#[path = "../../../../crates/logicaffeine_tests/unit/logicaffeine_web/ui/platform/tests.rs"]
169mod tests;