feat(cli): --demo demonstration mode flag + app plumbing (#22, ADR-0047 D1)

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.
This commit is contained in:
claude@clouddev1
2026-06-10 22:22:12 +00:00
parent e9eb1b177e
commit f879d54721
5 changed files with 107 additions and 4 deletions
+9 -4
View File
@@ -276,10 +276,15 @@ from `IndicatorDebounce`) pair — not an inconsistency.
The CLI banner (`help.cli_banner` in `en-US.yaml`) gains a `--demo` 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 line. User-facing wording obeys the house rules (no engine name, no
"DSL"): *"Demonstration mode — show on-screen badges for otherwise- "DSL"): *"Demonstration mode — show on-screen badges for otherwise-
invisible keys (Tab, Enter, …) and enable scripted step captions, for invisible keys (Tab, Enter, …), for screencasts and live teaching."*
screencasts and live teaching."* Badge labels and the `[…]` chrome are
fixed ASCII, not localised; caption content is author-supplied free The help text **deliberately mentions only the visible badges, not the
text and likewise not a catalog string. `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 ## Alternatives considered
+11
View File
@@ -368,6 +368,14 @@ pub struct App {
/// flag; the `undo` / `redo` commands then report undo is off /// flag; the `undo` / `redo` commands then report undo is off
/// rather than emitting a prepare action. /// rather than emitting a prepare action.
pub undo_enabled: bool, 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 /// The DSL → SQL teaching echo (ADR-0038) for the command currently
/// being rendered: set from the success event just before its handler /// being rendered: set from the success event just before its handler
/// runs, consumed by `note_ok_summary` (which pushes it beneath /// 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 // Undo is on by default; the runtime flips this off for
// a `--no-undo` session (ADR-0006 Amendment 1). // a `--no-undo` session (ADR-0006 Amendment 1).
undo_enabled: true, 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, pending_echo: None,
} }
} }
+77
View File
@@ -42,6 +42,13 @@ pub struct Args {
/// mode > the default (`simple`). Combines with `--resume` and /// mode > the default (`simple`). Combines with `--resume` and
/// a positional path; on collision the flag wins. /// a positional path; on collision the flag wins.
pub mode: Option<Mode>, pub mode: Option<Mode>,
/// `--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`. /// Usage banner printed by `--help`.
@@ -124,6 +131,12 @@ impl Args {
let mut help = false; let mut help = false;
let mut no_undo = false; let mut no_undo = false;
let mut mode: Option<Mode> = None; let mut mode: Option<Mode> = 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); let mut iter = iter.into_iter().map(Into::into);
while let Some(arg) = iter.next() { while let Some(arg) = iter.next() {
match arg.as_str() { match arg.as_str() {
@@ -136,6 +149,9 @@ impl Args {
"--no-undo" => { "--no-undo" => {
no_undo = true; no_undo = true;
} }
"--demo" => {
demo = true;
}
"--theme" => { "--theme" => {
let value = iter.next().ok_or(ArgsError::MissingValue("theme"))?; let value = iter.next().ok_or(ArgsError::MissingValue("theme"))?;
theme = match value.as_str() { theme = match value.as_str() {
@@ -194,10 +210,25 @@ impl Args {
help, help,
no_undo, no_undo,
mode, 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 { fn default_theme() -> Theme {
// NFR-7: support both backgrounds. For the walking skeleton we // NFR-7: support both backgrounds. For the walking skeleton we
// honour an explicit `--theme` flag and the COLORFGBG env var // 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] #[test]
fn unknown_double_dash_flag_errors_even_with_positional() { fn unknown_double_dash_flag_errors_even_with_positional() {
// Make sure the path-vs-flag distinction is robust: // Make sure the path-vs-flag distinction is robust:
+3
View File
@@ -204,6 +204,9 @@ help:
project's stored mode. Without it, the project's stored mode. Without it, the
project's last-used mode is restored project's last-used mode is restored
(default: simple). (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): App-level commands (typed inside the app, available in both modes):
quit Exit cleanly. quit Exit cleanly.
+7
View File
@@ -216,6 +216,9 @@ pub async fn run(args: Args) -> Result<()> {
let db_existed = db_path.exists(); let db_existed = db_path.exists();
// Undo is on unless `--no-undo` (ADR-0006 Amendment 1). // Undo is on unless `--no-undo` (ADR-0006 Amendment 1).
let undo_enabled = !args.no_undo; 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 = let database =
Database::open_with_persistence_and_undo(db_path.as_path(), persistence, undo_enabled) Database::open_with_persistence_and_undo(db_path.as_path(), persistence, undo_enabled)
.context("open database")?; .context("open database")?;
@@ -273,6 +276,7 @@ pub async fn run(args: Args) -> Result<()> {
initial_events, initial_events,
undo_enabled, undo_enabled,
resolved_mode, resolved_mode,
demo_mode,
) )
.await; .await;
if let Err(e) = teardown_terminal(&mut terminal) { if let Err(e) = teardown_terminal(&mut terminal) {
@@ -331,6 +335,7 @@ async fn run_loop(
initial_events: Vec<AppEvent>, initial_events: Vec<AppEvent>,
undo_enabled: bool, undo_enabled: bool,
initial_mode: crate::mode::Mode, initial_mode: crate::mode::Mode,
demo_mode: bool,
) -> Result<Option<String>> { ) -> Result<Option<String>> {
let (event_tx, mut event_rx) = mpsc::channel::<AppEvent>(EVENT_CHANNEL_CAPACITY); let (event_tx, mut event_rx) = mpsc::channel::<AppEvent>(EVENT_CHANNEL_CAPACITY);
let reader_handle = spawn_event_reader(event_tx.clone()); 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_name = Some(project_display_name);
app.project_is_temp = project_is_temp; app.project_is_temp = project_is_temp;
app.undo_enabled = undo_enabled; 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 // Start in the resolved input mode (ADR-0015 mode-restore
// amendment, issue #14): `--mode` > stored project mode > // amendment, issue #14): `--mode` > stored project mode >
// default. `Persistence` already carries the same value, so the // default. `Persistence` already carries the same value, so the