runtime: debounce the validity indicator (ADR-0027 step E)
The event loop now time-boxes `recv` while an indicator recompute is owed: every keystroke hides the indicator and arms an `INDICATOR_DEBOUNCE` (1s) window; once typing pauses that long the runtime computes `App::input_validity_verdict` and shows `[ERR]` / `[WRN]`. An idle session (nothing owed) still blocks plainly on `recv` — no wake-ups. `update()` stays pure — the debounce timer lives in the runtime; `App` only holds the resulting `input_indicator` state, which the runtime clears on a keystroke and sets when the quiet interval elapses. `App::input_validity_verdict` is tested directly (a simple-mode verdict, and silence in advanced mode / the `:` one-shot); the debounce timing itself is runtime-loop glue, covered at the integration level.
This commit is contained in:
+37
@@ -3042,4 +3042,41 @@ mod tests {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Validity-indicator verdict (ADR-0027) ----------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn input_validity_verdict_flags_a_broken_simple_command() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.input = "create table".to_string();
|
||||||
|
assert_eq!(
|
||||||
|
app.input_validity_verdict(),
|
||||||
|
Some(crate::dsl::walker::Severity::Error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn input_validity_verdict_is_none_for_clean_input() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.input = "quit".to_string();
|
||||||
|
assert_eq!(app.input_validity_verdict(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn input_validity_verdict_silent_in_advanced_mode() {
|
||||||
|
// Advanced mode is raw SQL — the DSL walker must not
|
||||||
|
// pass a verdict on it (ADR-0027 §7).
|
||||||
|
let mut app = App::new();
|
||||||
|
app.mode = Mode::Advanced;
|
||||||
|
app.input = "create table".to_string();
|
||||||
|
assert_eq!(app.input_validity_verdict(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn input_validity_verdict_silent_for_colon_one_shot() {
|
||||||
|
// A `:`-prefixed line is a one-shot advanced escape.
|
||||||
|
let mut app = App::new();
|
||||||
|
app.input = ":create table".to_string();
|
||||||
|
assert_eq!(app.input_validity_verdict(), None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-1
@@ -44,6 +44,10 @@ use crate::ui;
|
|||||||
const EVENT_CHANNEL_CAPACITY: usize = 64;
|
const EVENT_CHANNEL_CAPACITY: usize = 64;
|
||||||
const SHUTDOWN_GRACE: Duration = Duration::from_millis(100);
|
const SHUTDOWN_GRACE: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
|
/// Quiet interval before the input-validity indicator
|
||||||
|
/// reappears once typing stops (ADR-0027 §3).
|
||||||
|
const INDICATOR_DEBOUNCE: Duration = Duration::from_millis(1000);
|
||||||
|
|
||||||
/// Run the application until a `Quit` action is enacted or the
|
/// Run the application until a `Quit` action is enacted or the
|
||||||
/// terminal closes.
|
/// terminal closes.
|
||||||
pub async fn run(args: Args) -> Result<()> {
|
pub async fn run(args: Args) -> Result<()> {
|
||||||
@@ -274,7 +278,35 @@ async fn run_loop(
|
|||||||
.context("initial draw")?;
|
.context("initial draw")?;
|
||||||
|
|
||||||
info!("entering main event loop");
|
info!("entering main event loop");
|
||||||
while let Some(event) = event_rx.recv().await {
|
// ADR-0027 §3: the validity indicator is debounced — hidden
|
||||||
|
// on every keystroke, recomputed and shown once typing has
|
||||||
|
// paused for `INDICATOR_DEBOUNCE`. `indicator_pending` is
|
||||||
|
// true while a recompute is owed; only then is `recv`
|
||||||
|
// time-boxed, so an idle session still does no wake-ups.
|
||||||
|
let mut indicator_pending = false;
|
||||||
|
loop {
|
||||||
|
let event = if indicator_pending {
|
||||||
|
match tokio::time::timeout(INDICATOR_DEBOUNCE, event_rx.recv()).await {
|
||||||
|
Ok(Some(event)) => event,
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(_elapsed) => {
|
||||||
|
// Typing has been quiet for the debounce
|
||||||
|
// interval — settle the indicator.
|
||||||
|
app.input_indicator = app.input_validity_verdict();
|
||||||
|
indicator_pending = false;
|
||||||
|
terminal
|
||||||
|
.draw(|f| ui::render(&mut app, &theme, f))
|
||||||
|
.context("redraw")?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match event_rx.recv().await {
|
||||||
|
Some(event) => event,
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let is_key = matches!(event, AppEvent::Key(_));
|
||||||
let actions = app.update(event);
|
let actions = app.update(event);
|
||||||
let mut should_quit = false;
|
let mut should_quit = false;
|
||||||
for action in actions {
|
for action in actions {
|
||||||
@@ -381,6 +413,13 @@ async fn run_loop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if is_key {
|
||||||
|
// A keystroke hides the indicator and re-arms the
|
||||||
|
// debounce (ADR-0027 §3) — it reappears once typing
|
||||||
|
// pauses.
|
||||||
|
app.input_indicator = None;
|
||||||
|
indicator_pending = true;
|
||||||
|
}
|
||||||
terminal
|
terminal
|
||||||
.draw(|f| ui::render(&mut app, &theme, f))
|
.draw(|f| ui::render(&mut app, &theme, f))
|
||||||
.context("redraw")?;
|
.context("redraw")?;
|
||||||
|
|||||||
Reference in New Issue
Block a user