mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 23:10:11 +00:00
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:
+27
-13
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user