Adaptive auto-continue input lock overlay
This commit is contained in:
59
src/app.rs
59
src/app.rs
@@ -1089,6 +1089,7 @@ impl App {
|
||||
// Adaptive mode auto-continues unless milestone popups must be shown first.
|
||||
if self.drill_mode == DrillMode::Adaptive && self.milestone_queue.is_empty() {
|
||||
self.start_drill();
|
||||
self.arm_post_drill_input_lock();
|
||||
} else {
|
||||
self.screen = AppScreen::DrillResult;
|
||||
}
|
||||
@@ -2303,4 +2304,62 @@ mod tests {
|
||||
app.adaptive_word_history.len()
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper: make the current drill look "completed" so finish_drill() processes it.
|
||||
fn complete_current_drill(app: &mut App) {
|
||||
if let Some(ref mut drill) = app.drill {
|
||||
let now = Instant::now();
|
||||
drill.started_at = Some(now - Duration::from_millis(500));
|
||||
drill.finished_at = Some(now);
|
||||
drill.cursor = drill.target.len();
|
||||
// Fill input so DrillResult::from_drill doesn't panic on length mismatches
|
||||
drill.input = vec![crate::session::input::CharStatus::Correct; drill.target.len()];
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adaptive_auto_continue_arms_input_lock() {
|
||||
let mut app = App::new();
|
||||
assert_eq!(app.drill_mode, DrillMode::Adaptive);
|
||||
assert_eq!(app.screen, AppScreen::Drill);
|
||||
assert!(app.drill.is_some());
|
||||
|
||||
// Make sure no milestones are queued
|
||||
app.milestone_queue.clear();
|
||||
|
||||
complete_current_drill(&mut app);
|
||||
app.finish_drill();
|
||||
|
||||
// Auto-continue should have started a new drill and armed the lock
|
||||
assert_eq!(app.screen, AppScreen::Drill);
|
||||
assert!(
|
||||
app.post_drill_input_lock_remaining_ms().is_some(),
|
||||
"Input lock should be armed after adaptive auto-continue"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adaptive_does_not_auto_continue_with_milestones() {
|
||||
let mut app = App::new();
|
||||
assert_eq!(app.drill_mode, DrillMode::Adaptive);
|
||||
|
||||
// Push a milestone before finishing the drill
|
||||
app.milestone_queue.push_back(KeyMilestonePopup {
|
||||
kind: MilestoneKind::Unlock,
|
||||
keys: vec!['a'],
|
||||
finger_info: vec![('a', "left pinky".to_string())],
|
||||
message: "Test milestone",
|
||||
});
|
||||
|
||||
complete_current_drill(&mut app);
|
||||
app.finish_drill();
|
||||
|
||||
// Should go to DrillResult (not auto-continue) since milestones are queued
|
||||
assert_eq!(app.screen, AppScreen::DrillResult);
|
||||
// Lock IS armed via the existing milestone path
|
||||
assert!(
|
||||
app.post_drill_input_lock_remaining_ms().is_some(),
|
||||
"Input lock should be armed for milestone path"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
73
src/main.rs
73
src/main.rs
@@ -267,16 +267,19 @@ fn handle_key(app: &mut App, key: KeyEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Briefly block all input right after a drill completes to avoid accidental
|
||||
// popup dismissal or continuation from trailing keystrokes.
|
||||
if app.post_drill_input_lock_remaining_ms().is_some()
|
||||
&& (!app.milestone_queue.is_empty() || app.screen == AppScreen::DrillResult)
|
||||
{
|
||||
// Ctrl+C always quits, even during input lock.
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
|
||||
app.should_quit = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
|
||||
app.should_quit = true;
|
||||
// Briefly block all input right after a drill completes to avoid accidental
|
||||
// popup dismissal or continuation from trailing keystrokes.
|
||||
if app.post_drill_input_lock_remaining_ms().is_some()
|
||||
&& (!app.milestone_queue.is_empty()
|
||||
|| app.screen == AppScreen::DrillResult
|
||||
|| app.screen == AppScreen::Drill)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1316,6 +1319,27 @@ fn render_drill(frame: &mut ratatui::Frame, app: &App) {
|
||||
Style::default().fg(colors.text_pending()),
|
||||
)));
|
||||
frame.render_widget(footer, app_layout.footer);
|
||||
|
||||
// Show a brief countdown overlay while the post-drill input lock is active.
|
||||
if let Some(ms) = app.post_drill_input_lock_remaining_ms() {
|
||||
let msg = format!("Keys re-enabled in {}ms", ms);
|
||||
let width = msg.len() as u16 + 4; // border + padding
|
||||
let height = 3;
|
||||
let x = area.x + area.width.saturating_sub(width) / 2;
|
||||
let y = area.y + area.height.saturating_sub(height) / 2;
|
||||
let overlay_area = Rect::new(x, y, width.min(area.width), height.min(area.height));
|
||||
|
||||
frame.render_widget(ratatui::widgets::Clear, overlay_area);
|
||||
let block = Block::bordered()
|
||||
.border_style(Style::default().fg(colors.accent()))
|
||||
.style(Style::default().bg(colors.bg()));
|
||||
let inner = block.inner(overlay_area);
|
||||
frame.render_widget(block, overlay_area);
|
||||
frame.render_widget(
|
||||
Paragraph::new(msg).style(Style::default().fg(colors.text_pending())),
|
||||
inner,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2312,6 +2336,41 @@ mod review_tests {
|
||||
"ni should be confirmed (samples >= 20, streak >= required)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drill_screen_input_lock_blocks_normal_keys() {
|
||||
let mut app = test_app();
|
||||
app.screen = AppScreen::Drill;
|
||||
app.drill = Some(crate::session::drill::DrillState::new("abc"));
|
||||
app.post_drill_input_lock_until =
|
||||
Some(Instant::now() + std::time::Duration::from_millis(500));
|
||||
|
||||
let before_cursor = app.drill.as_ref().unwrap().cursor;
|
||||
handle_key(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE),
|
||||
);
|
||||
let after_cursor = app.drill.as_ref().unwrap().cursor;
|
||||
|
||||
assert_eq!(before_cursor, after_cursor, "Key should be blocked during input lock on Drill screen");
|
||||
assert_eq!(app.screen, AppScreen::Drill);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_c_passes_through_input_lock() {
|
||||
let mut app = test_app();
|
||||
app.screen = AppScreen::Drill;
|
||||
app.drill = Some(crate::session::drill::DrillState::new("abc"));
|
||||
app.post_drill_input_lock_until =
|
||||
Some(Instant::now() + std::time::Duration::from_millis(500));
|
||||
|
||||
handle_key(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
|
||||
);
|
||||
|
||||
assert!(app.should_quit, "Ctrl+C should set should_quit even during input lock");
|
||||
}
|
||||
}
|
||||
|
||||
fn render_result(frame: &mut ratatui::Frame, app: &App) {
|
||||
|
||||
Reference in New Issue
Block a user