//! Input mode for the command field. //! //! See ADR-0003 for the design. The two modes determine how the //! input field interprets a submitted line. The `:` one-shot //! escape from simple to advanced is handled at submission time //! in `app::App::submit`, not as additional state here. use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum Mode { /// The teaching DSL only — the app's startup mode (ADR-0003) /// and the walker's default view: SQL-only grammar is gated /// out (ADR-0030 §2). #[default] Simple, Advanced, } impl Mode { pub const fn label(self) -> &'static str { match self { Self::Simple => "SIMPLE", Self::Advanced => "ADVANCED", } } /// The lowercase keyword form used wherever the mode is /// written or read as plain text — the `--mode` CLI flag, the /// `mode ` command, and the `project.yaml` `mode:` /// field (ADR-0015 mode-restore amendment, issue #14). Kept /// distinct from `label()` (the uppercase UI banner form) so /// the on-disk / CLI vocabulary is stable and case-consistent. pub const fn keyword(self) -> &'static str { match self { Self::Simple => "simple", Self::Advanced => "advanced", } } /// Parse a mode keyword, case-insensitively. `None` for any /// other string. The single source of truth for "simple" / /// "advanced" text recognition across the CLI flag, the /// `mode` command, and the `project.yaml` reader. #[must_use] pub fn from_keyword(s: &str) -> Option { match s.trim().to_ascii_lowercase().as_str() { "simple" => Some(Self::Simple), "advanced" => Some(Self::Advanced), _ => None, } } /// Resolve the startup input mode (ADR-0015 mode-restore /// amendment, issue #14). Precedence: the `--mode` CLI override /// (`flag`) wins; otherwise the project's stored mode /// (`stored`); otherwise the default (`Simple`). `stored` is /// `None` for a project with no recorded preference. #[must_use] pub fn resolve_startup(flag: Option, stored: Option) -> Self { flag.or(stored).unwrap_or_default() } } impl fmt::Display for Mode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } #[cfg(test)] mod tests { use super::Mode; #[test] fn keyword_round_trips_through_from_keyword() { for mode in [Mode::Simple, Mode::Advanced] { assert_eq!(Mode::from_keyword(mode.keyword()), Some(mode)); } } #[test] fn from_keyword_is_case_insensitive_and_trims() { assert_eq!(Mode::from_keyword(" Advanced "), Some(Mode::Advanced)); assert_eq!(Mode::from_keyword("SIMPLE"), Some(Mode::Simple)); } #[test] fn from_keyword_rejects_unknown() { assert_eq!(Mode::from_keyword("expert"), None); assert_eq!(Mode::from_keyword(""), None); } #[test] fn keyword_is_lowercase_distinct_from_label() { // `keyword()` is the on-disk/CLI form; `label()` is the // uppercase UI banner. They must not be conflated. assert_eq!(Mode::Advanced.keyword(), "advanced"); assert_eq!(Mode::Advanced.label(), "ADVANCED"); } #[test] fn resolve_startup_applies_flag_then_stored_then_default() { // Flag wins over everything. assert_eq!( Mode::resolve_startup(Some(Mode::Advanced), Some(Mode::Simple)), Mode::Advanced, ); assert_eq!( Mode::resolve_startup(Some(Mode::Simple), Some(Mode::Advanced)), Mode::Simple, ); // No flag → stored mode. assert_eq!( Mode::resolve_startup(None, Some(Mode::Advanced)), Mode::Advanced, ); // No flag, no stored preference → default (Simple). assert_eq!(Mode::resolve_startup(None, None), Mode::Simple); } }