fix(setup): drain residual events and filter key kind in onboard prompts (#937) (#949)

On Windows, single keypresses during `ironclaw onboard` are registered
  twice, causing channel/tool selection to skip or toggle incorrectly.
  Two root causes:

  1. select_many() had no residual event drain, so Enter from a prior
     prompt was immediately consumed on entry, skipping the selection.

  2. Neither select_many() nor read_secret_line() filtered on
     KeyEventKind::Press, so Windows Key Release/Repeat events caused
     every keypress to fire twice (Space toggles cancel out, Enter
     triggers double-advance, arrows jump two positions).

  Extract a shared drain_pending_events() helper (replacing the inline
  drain in read_secret_line from #849), add it to select_many() entry,
  and filter both event loops to only handle KeyEventKind::Press.

  Fixes #937
[skip-regression-check]
This commit is contained in:
Reid
2026-03-11 16:46:03 -07:00
committed by GitHub
parent 94b448ffab
commit 6321bb4688
+27 -13
View File
@@ -11,13 +11,25 @@ use std::io::{self, Write};
use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
execute,
style::{Color, Print, ResetColor, SetForegroundColor},
terminal::{self, ClearType},
};
use secrecy::SecretString;
/// Drain any residual key events already queued in the terminal buffer.
///
/// On Windows, transitioning between raw mode and cooked mode (or between
/// successive raw-mode prompts) can leave stale events (e.g. the Release
/// half of an Enter keypress) in the queue. Consuming them with a
/// non-blocking poll prevents the next prompt from mis-firing.
fn drain_pending_events() {
while event::poll(std::time::Duration::ZERO).unwrap_or(false) {
let _ = event::read();
}
}
/// Display a numbered menu and get user selection.
///
/// Returns the index (0-based) of the selected option.
@@ -94,6 +106,7 @@ pub fn select_many(prompt: &str, options: &[(&str, bool)]) -> io::Result<Vec<usi
let mut cursor_pos = 0;
terminal::enable_raw_mode()?;
drain_pending_events();
execute!(stdout, cursor::Hide)?;
let result = (|| {
@@ -124,9 +137,13 @@ pub fn select_many(prompt: &str, options: &[(&str, bool)]) -> io::Result<Vec<usi
stdout.flush()?;
// Read key
// Read key — only act on Press events to avoid double-firing
// from Release/Repeat events on Windows.
if let Event::Key(KeyEvent {
code, modifiers, ..
code,
modifiers,
kind: KeyEventKind::Press,
..
}) = event::read()?
{
match code {
@@ -200,19 +217,16 @@ fn read_secret_line() -> io::Result<SecretString> {
let mut input = String::new();
let mut stdout = io::stdout();
// Drain any residual key events (e.g. Enter from a prior `read_line` prompt)
// that are already queued before we start reading. Without this, on
// Windows the leftover Enter is immediately consumed and the function
// returns an empty string before the user can type anything.
// Uses Duration::ZERO so we never block waiting for new input — only
// events already in the queue are consumed.
while event::poll(std::time::Duration::ZERO)? {
let _ = event::read()?;
}
drain_pending_events();
loop {
// Only act on Press events to avoid double-firing from
// Release/Repeat events on Windows.
if let Event::Key(KeyEvent {
code, modifiers, ..
code,
modifiers,
kind: KeyEventKind::Press,
..
}) = event::read()?
{
match code {