4cd574b909
The input mode always started in simple; a learner who quit in advanced had to re-toggle every launch. Store the mode per-project in project.yaml (project.mode:, optional, default simple) and restore it on every open. Mode is live UI state, not schema: the worker stamps the current mode into project.yaml on every write, so a later command rewrites the live value rather than clobbering it — no db round-trip needed. The mode is persisted on unload (quit + project switch) so the mode you leave a project in is always what reopens; the `mode` command also persists immediately. A switch saves the outgoing mode, then restores the incoming project's stored mode. New --mode simple|advanced CLI flag (precedence --mode > stored > simple; combines with --resume). A teacher can ship a project that opens in advanced mode and export it to students (the mode travels in the zip). ADR-0015 Amendment 1; ADR-0003 note; help banner; requirements L1b.
122 lines
4.0 KiB
Rust
122 lines
4.0 KiB
Rust
//! 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 <value>` 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<Self> {
|
|
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<Self>, stored: Option<Self>) -> 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);
|
|
}
|
|
}
|