From f879d54721d08ff5b510d2aa84a7704e3d2776f2 Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Wed, 10 Jun 2026 22:22:12 +0000 Subject: [PATCH] feat(cli): --demo demonstration mode flag + app plumbing (#22, ADR-0047 D1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `--demo` (and the RDBMS_PLAYGROUND_DEMO env fallback) to enter demonstration mode, threaded onto App.demo_mode through run_loop — mirrors the --no-undo plumbing. Off by default, zero footprint when off. The --help line advertises only the visible keystroke badges; the Ctrl+] caption trigger is kept low-profile (ADR-0047 D6 updated). Phase A of ADR-0047; behaviour (badges/captions) lands in B and C. --- docs/adr/0047-demonstration-overlay-layer.md | 13 +++- src/app.rs | 11 +++ src/cli.rs | 77 ++++++++++++++++++++ src/friendly/strings/en-US.yaml | 3 + src/runtime.rs | 7 ++ 5 files changed, 107 insertions(+), 4 deletions(-) diff --git a/docs/adr/0047-demonstration-overlay-layer.md b/docs/adr/0047-demonstration-overlay-layer.md index 9d4e78f..754bb03 100644 --- a/docs/adr/0047-demonstration-overlay-layer.md +++ b/docs/adr/0047-demonstration-overlay-layer.md @@ -276,10 +276,15 @@ from `IndicatorDebounce`) pair — not an inconsistency. The CLI banner (`help.cli_banner` in `en-US.yaml`) gains a `--demo` line. User-facing wording obeys the house rules (no engine name, no "DSL"): *"Demonstration mode — show on-screen badges for otherwise- -invisible keys (Tab, Enter, …) and enable scripted step captions, for -screencasts and live teaching."* Badge labels and the `[…]` chrome are -fixed ASCII, not localised; caption content is author-supplied free -text and likewise not a catalog string. +invisible keys (Tab, Enter, …), for screencasts and live teaching."* + +The help text **deliberately mentions only the visible badges, not the +`Ctrl+]` step-caption mechanism** (user decision): the caption trigger +stays low-profile, true to #22's "secret trigger" framing — a cast +author or lesson script knows it; a casual `--help` reader is not +pointed at it. Badge labels and the `[…]` chrome are fixed ASCII, not +localised; caption content is author-supplied free text and likewise +not a catalog string. ## Alternatives considered diff --git a/src/app.rs b/src/app.rs index 6747172..fd1f2eb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -368,6 +368,14 @@ pub struct App { /// flag; the `undo` / `redo` commands then report undo is off /// rather than emitting a prepare action. pub undo_enabled: bool, + /// Whether **demonstration mode** is active this session (ADR-0047, + /// issue #22). `true` under `--demo` / `RDBMS_PLAYGROUND_DEMO`. When + /// off (the default) none of the demo key handling or overlay + /// rendering runs — zero footprint. When on, otherwise-invisible + /// keys raise a transient badge (set by the runtime, see + /// `demo_badge`) and `Ctrl+]` drives the stealth step-caption + /// buffer (`demo_caption` / `demo_capturing`). + pub demo_mode: bool, /// The DSL → SQL teaching echo (ADR-0038) for the command currently /// being rendered: set from the success event just before its handler /// runs, consumed by `note_ok_summary` (which pushes it beneath @@ -512,6 +520,9 @@ impl App { // Undo is on by default; the runtime flips this off for // a `--no-undo` session (ADR-0006 Amendment 1). undo_enabled: true, + // Demo mode is off by default; the runtime flips it on for + // a `--demo` session (ADR-0047). + demo_mode: false, pending_echo: None, } } diff --git a/src/cli.rs b/src/cli.rs index 29c4311..57f9a23 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -42,6 +42,13 @@ pub struct Args { /// mode > the default (`simple`). Combines with `--resume` and /// a positional path; on collision the flag wins. pub mode: Option, + /// `--demo` (or `RDBMS_PLAYGROUND_DEMO` set truthy): enter + /// **demonstration mode** (ADR-0047, issue #22). Off by default, + /// zero footprint when off. When on, the app shows transient + /// on-screen badges for otherwise-invisible keys (Tab, Enter, …) + /// and enables the `Ctrl+]` stealth step-caption buffer — for + /// screencasts and live teaching. The flag wins over the env var. + pub demo: bool, } /// Usage banner printed by `--help`. @@ -124,6 +131,12 @@ impl Args { let mut help = false; let mut no_undo = false; let mut mode: Option = None; + // Demonstration mode (ADR-0047): the env var is the default, + // the `--demo` flag overrides it to on. Mirrors the + // env-then-flag layering used for the log file above. + let mut demo = env::var("RDBMS_PLAYGROUND_DEMO") + .ok() + .is_some_and(|v| demo_value_is_truthy(&v)); let mut iter = iter.into_iter().map(Into::into); while let Some(arg) = iter.next() { match arg.as_str() { @@ -136,6 +149,9 @@ impl Args { "--no-undo" => { no_undo = true; } + "--demo" => { + demo = true; + } "--theme" => { let value = iter.next().ok_or(ArgsError::MissingValue("theme"))?; theme = match value.as_str() { @@ -194,10 +210,25 @@ impl Args { help, no_undo, mode, + demo, }) } } +/// Whether a `RDBMS_PLAYGROUND_DEMO` value enables demo mode. +/// +/// Truthy for any value except the conventional "off" set +/// (`0`/`false`/`no`/`off`, case-insensitively, and the empty +/// string). So `RDBMS_PLAYGROUND_DEMO=1` and `=true` enable, while +/// `=0` / `=false` explicitly disable — letting a value of `0` turn +/// it off even if something upstream exported the variable. +fn demo_value_is_truthy(value: &str) -> bool { + !matches!( + value.trim().to_ascii_lowercase().as_str(), + "" | "0" | "false" | "no" | "off" + ) +} + fn default_theme() -> Theme { // NFR-7: support both backgrounds. For the walking skeleton we // honour an explicit `--theme` flag and the COLORFGBG env var @@ -391,6 +422,52 @@ mod tests { ); } + // ---- ADR-0047 (issue #22): --demo demonstration mode ---- + + #[test] + fn demo_flag_parses() { + let args = Args::parse(["--demo"]).unwrap(); + assert!(args.demo); + } + + #[test] + fn demo_defaults_off() { + // Absent `--demo` (and absent env var in the test runner), + // demo mode is off — zero footprint for real users. + let args = Args::parse(std::iter::empty::<&str>()).unwrap(); + assert!(!args.demo, "demo is off unless --demo or the env var is given"); + } + + #[test] + fn demo_flag_coexists_with_positional_path() { + let args = Args::parse(["--demo", "/home/me/MyProject"]).unwrap(); + assert!(args.demo); + assert_eq!( + args.project_path.as_deref(), + Some(std::path::Path::new("/home/me/MyProject")) + ); + } + + #[test] + fn demo_flag_combines_with_resume_and_mode() { + let args = Args::parse(["--resume", "--demo", "--mode", "advanced"]).unwrap(); + assert!(args.demo); + assert!(args.resume); + assert_eq!(args.mode, Some(Mode::Advanced)); + } + + #[test] + fn demo_env_value_truthiness() { + // Enabling values. + for v in ["1", "true", "TRUE", "yes", "on", "anything", " 1 "] { + assert!(demo_value_is_truthy(v), "{v:?} should enable demo mode"); + } + // Disabling values. + for v in ["", " ", "0", "false", "False", "no", "off", "OFF"] { + assert!(!demo_value_is_truthy(v), "{v:?} should not enable demo mode"); + } + } + #[test] fn unknown_double_dash_flag_errors_even_with_positional() { // Make sure the path-vs-flag distinction is robust: diff --git a/src/friendly/strings/en-US.yaml b/src/friendly/strings/en-US.yaml index c38402b..88778ee 100644 --- a/src/friendly/strings/en-US.yaml +++ b/src/friendly/strings/en-US.yaml @@ -204,6 +204,9 @@ help: project's stored mode. Without it, the project's last-used mode is restored (default: simple). + --demo Demonstration mode: show on-screen badges + for otherwise-invisible keys (Tab, Enter, + ...) — for screencasts and live teaching. App-level commands (typed inside the app, available in both modes): quit Exit cleanly. diff --git a/src/runtime.rs b/src/runtime.rs index 65d5f62..ee1b75b 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -216,6 +216,9 @@ pub async fn run(args: Args) -> Result<()> { let db_existed = db_path.exists(); // Undo is on unless `--no-undo` (ADR-0006 Amendment 1). let undo_enabled = !args.no_undo; + // Demonstration mode under `--demo` / `RDBMS_PLAYGROUND_DEMO` + // (ADR-0047). Off by default; threaded onto the `App` in run_loop. + let demo_mode = args.demo; let database = Database::open_with_persistence_and_undo(db_path.as_path(), persistence, undo_enabled) .context("open database")?; @@ -273,6 +276,7 @@ pub async fn run(args: Args) -> Result<()> { initial_events, undo_enabled, resolved_mode, + demo_mode, ) .await; if let Err(e) = teardown_terminal(&mut terminal) { @@ -331,6 +335,7 @@ async fn run_loop( initial_events: Vec, undo_enabled: bool, initial_mode: crate::mode::Mode, + demo_mode: bool, ) -> Result> { let (event_tx, mut event_rx) = mpsc::channel::(EVENT_CHANNEL_CAPACITY); let reader_handle = spawn_event_reader(event_tx.clone()); @@ -339,6 +344,8 @@ async fn run_loop( app.project_name = Some(project_display_name); app.project_is_temp = project_is_temp; app.undo_enabled = undo_enabled; + // ADR-0047: enable the demo overlays for this session under `--demo`. + app.demo_mode = demo_mode; // Start in the resolved input mode (ADR-0015 mode-restore // amendment, issue #14): `--mode` > stored project mode > // default. `Persistence` already carries the same value, so the