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:
+40
-1
@@ -44,6 +44,10 @@ use crate::ui;
|
||||
const EVENT_CHANNEL_CAPACITY: usize = 64;
|
||||
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
|
||||
/// terminal closes.
|
||||
pub async fn run(args: Args) -> Result<()> {
|
||||
@@ -274,7 +278,35 @@ async fn run_loop(
|
||||
.context("initial draw")?;
|
||||
|
||||
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 mut should_quit = false;
|
||||
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
|
||||
.draw(|f| ui::render(&mut app, &theme, f))
|
||||
.context("redraw")?;
|
||||
|
||||
Reference in New Issue
Block a user