fix: request keyboard enhancement flags for proper Release events

Root cause of both issues: terminals don't send Release events by
default. When pressing W while holding A, terminal stops repeating A
(but doesn't send Release), so A times out and strafing breaks.
On key release, 400ms timeout means 400ms of extra movement.

Fix: PushKeyboardEnhancementFlags(REPORT_EVENT_TYPES) in terminal init.
This asks the terminal to send proper Press/Release/Repeat events for
ALL keys, not just special ones.

With Release events:
- Key release is instant (no 400ms timeout delay)
- Pressing W doesn't cancel A's held state (terminal sends Release
  only when A is actually released)
- A/D strafing works while W is held

Fallback: if terminal doesn't support enhancement flags (old xterm),
got_release flag stays false and 150ms timeout is used. Once any
Release event is received, all keys switch to Release-based mode
(infinite timeout, rely on actual Release events).

109 tests, 0 warnings, 0 failures
This commit is contained in:
Emil
2026-06-20 23:48:03 +03:00
parent 61e2cf1544
commit 6d98c09ece
2 changed files with 25 additions and 20 deletions
+19 -19
View File
@@ -44,11 +44,10 @@ pub enum HeldKey {
struct HeldState {
last_seen: Instant,
seen_repeat: bool,
got_release: bool,
}
const HOLD_TIMEOUT_PRESS: Duration = Duration::from_millis(400);
const HOLD_TIMEOUT_REPEAT: Duration = Duration::from_millis(80);
const FALLBACK_TIMEOUT: Duration = Duration::from_millis(150);
pub struct InputHandler {
pub paint_brush: MaterialBrush,
@@ -104,13 +103,6 @@ impl InputHandler {
}
}
fn mark_held(&mut self, key: HeldKey, is_repeat: bool) {
self.held.insert(key, HeldState {
last_seen: Instant::now(),
seen_repeat: is_repeat,
});
}
fn release(&mut self, key: HeldKey) {
self.held.remove(&key);
}
@@ -124,11 +116,6 @@ impl InputHandler {
self.jump_pressed = false;
let now = Instant::now();
self.held.retain(|_, state| {
let timeout = if state.seen_repeat { HOLD_TIMEOUT_REPEAT } else { HOLD_TIMEOUT_PRESS };
now.duration_since(state.last_seen) < timeout
});
let events: Vec<Event> = match &self.receiver {
Some(rx) => rx.try_iter().collect(),
None => return one_shots,
@@ -171,17 +158,30 @@ impl InputHandler {
}
if let Some(held_key) = Self::key_to_held(code) {
if is_press {
self.mark_held(held_key, false);
} else if is_repeat {
self.mark_held(held_key, true);
if is_press || is_repeat {
let prev_got_release = self.held.get(&held_key).map(|s| s.got_release).unwrap_or(false);
self.held.insert(held_key, HeldState {
last_seen: Instant::now(),
got_release: prev_got_release,
});
} else if is_release {
self.release(held_key);
for state in self.held.values_mut() {
state.got_release = true;
}
}
}
}
}
self.held.retain(|_, state| {
if state.got_release {
true
} else {
now.duration_since(state.last_seen) < FALLBACK_TIMEOUT
}
});
one_shots
}
+6 -1
View File
@@ -1,7 +1,7 @@
use std::io::{self, Write, stdout};
use crossterm::{
cursor::{Hide, MoveTo, Show},
event::{DisableMouseCapture, EnableMouseCapture},
event::{DisableMouseCapture, EnableMouseCapture, KeyboardEnhancementFlags, PushKeyboardEnhancementFlags, PopKeyboardEnhancementFlags},
execute, queue,
style::{Color, SetBackgroundColor, SetForegroundColor, ResetColor, Print},
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, size as term_size},
@@ -53,6 +53,10 @@ impl Renderer for TerminalRenderer {
stdout(),
EnterAlternateScreen,
Hide,
PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
),
EnableMouseCapture,
Clear(ClearType::All),
)?;
@@ -169,6 +173,7 @@ impl Renderer for TerminalRenderer {
stdout(),
ResetColor,
Show,
PopKeyboardEnhancementFlags,
LeaveAlternateScreen,
DisableMouseCapture,
)?;