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
+11
View File
@@ -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,
}
}
+77
View File
@@ -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<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`.
@@ -124,6 +131,12 @@ impl Args {
let mut help = false;
let mut no_undo = false;
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);
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:
+3
View File
@@ -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.
+7
View File
@@ -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<AppEvent>,
undo_enabled: bool,
initial_mode: crate::mode::Mode,
demo_mode: bool,
) -> Result<Option<String>> {
let (event_tx, mut event_rx) = mpsc::channel::<AppEvent>(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