//! The onboarding mode: which preset (Basic / Advanced / Power User, whatever the app defines) the //! owner started from, and whether the first-run screen was answered. A mode is a starting point: //! the presets themselves are the app's (its feature table) and are applied by the shell through //! `features_set`; native code only remembers the choice (`modes.json`), so the screen shows once. //! //! `existing` is decided natively, once, on the first start of a version that has modes: an app //! data folder that already held settings belongs to someone who set the app up before. They keep //! exactly what they have, and the screen they see can be dismissed without choosing. use serde::{Deserialize, Serialize}; pub const MODES_FILE: &str = "modes.json"; const MAX_MODE_ID: usize = 32; #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct ModeState { /// The mode chosen last; `None` = never chosen (the app's defaults, or an earlier setup). pub mode: Option, /// The first-run screen was answered or dismissed. pub onboarded: bool, /// The app was set up before modes existed. pub existing: bool, } /// A mode id: lowercase letters, digits and `-`, starting with a letter, at most 32 characters. pub fn valid_mode(id: &str) -> bool { !id.is_empty() && id.len() <= MAX_MODE_ID && id.as_bytes()[0].is_ascii_lowercase() && id.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-') } impl ModeState { /// The state on the first start: nothing chosen, the screen still to be shown. pub fn first_run(existing: bool) -> Self { Self { mode: None, onboarded: false, existing } } pub fn validated(self) -> Result { if self.mode.as_deref().is_some_and(|mode| !valid_mode(mode)) { return Err("VALIDATION: invalid mode".into()); } Ok(self) } /// What the shell may change: the chosen mode and that the screen was answered. `existing` /// stays what native code decided. pub fn chosen(&self, mode: Option, onboarded: bool) -> Result { Self { mode, onboarded, existing: self.existing }.validated() } } #[cfg(test)] mod tests { use super::*; #[test] fn mode_ids_are_short_plain_words() { for good in ["basic", "advanced", "power-user", "m2"] { assert!(valid_mode(good), "{good}"); } for bad in ["", "Basic", "2fast", "a b", "a/b", "../x", &"a".repeat(33)] { assert!(!valid_mode(bad), "{bad}"); } } #[test] fn the_shell_chooses_a_mode_but_never_what_kind_of_install_this_is() { let fresh = ModeState::first_run(false); assert_eq!(fresh, ModeState { mode: None, onboarded: false, existing: false }); let existing = ModeState::first_run(true); let dismissed = existing.chosen(None, true).unwrap(); assert_eq!(dismissed, ModeState { mode: None, onboarded: true, existing: true }, "dismissed without choosing: nothing else changes"); let chosen = dismissed.chosen(Some("power".into()), true).unwrap(); assert_eq!(chosen.mode.as_deref(), Some("power")); assert!(chosen.existing); assert!(existing.chosen(Some("