feat: persist & restore per-project input mode (#14)
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.
This commit is contained in:
+88
@@ -24,6 +24,42 @@ impl Mode {
|
||||
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 {
|
||||
@@ -31,3 +67,55 @@ impl fmt::Display for Mode {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user