Skip to content

[bug] Windows: keys fire on both press and release, making the TUI unusable #212

Description

@eabase

Summary

On Windows, every keystroke in the interactive view TUI is handled twice — once on key press and once on key release — because the event loop never inspects KeyEventKind. For toggle bindings (most visibly h/1 → Help) the action is applied and then immediately undone, so the panel "flashes" instead of opening. Because the final visible state depends on event timing/parity, the TUI is effectively unusable on Windows, as reported.

(The original title calls this a missing "debounce", but it is not a timing/bounce problem — it is a press+release double-dispatch problem.)

Background

crossterm reports keyboard events differently per platform:

  • On Unix terminals (without keyboard-enhancement flags, which all-smi does not enable), only KeyEventKind::Press events are delivered. Holding a key produces repeated Press events via the terminal's own auto-repeat.
  • On Windows, the console backend additionally delivers KeyEventKind::Release (and Repeat) events for every keystroke. This is a well-known crossterm/ratatui gotcha.

all-smi's terminal reader (src/view/ui_events.rs) forwards every Event::Key it reads, and both dispatch sites in src/view/ui_loop.rs (around lines 129 and 338) call handle_key_event(...) on the raw event without inspecting key_event.kind. A search confirms KeyEventKind is referenced nowhere in src/.

The Help binding is a pure toggle:

// src/view/event_handler.rs:195
KeyCode::Char('1') | KeyCode::Char('h') => {
    state.show_help = !state.show_help;
    false
}

So a single physical h press on Windows produces: Pressshow_help = true, then Releaseshow_help = false. Net effect: the help page appears and instantly disappears — exactly the "flash" the reporter describes. Holding the key adds Repeat/Release pairs, producing the repeated flashing, and whichever event lands last determines whether it "happens to stop" on the help page. The same double-fire affects every other binding (A alerts, M topology mode, sort keys, T/V tab jumps, navigation, etc.).

crossterm is at version 0.29, which fully supports KeyEventKind.

Proposed Solution

Drop non-Press key events at the single chokepoint where terminal events are read, so both consumers — the live next_event() path and the drain_pending_events() batch path — are protected by one guard.

In src/view/ui_events.rs::terminal_reader_loop, before forwarding, skip key events whose kind is not Press:

use crossterm::event::{Event, KeyEventKind};

Ok(evt) => {
    // Windows delivers Press *and* Release (and Repeat) for every
    // keystroke; Unix delivers only Press. Forward Press-kind keys
    // only so toggle bindings don't fire twice (issue #212).
    if let Event::Key(k) = &evt {
        if k.kind != KeyEventKind::Press {
            continue;
        }
    }
    if tx.blocking_send(evt).is_err() {
        break;
    }
}

This is the minimal, platform-agnostic fix and is a no-op on Unix (where every key event is already Press).

Alternatives / considerations

  • Filtering at the two dispatch sites in src/view/ui_loop.rs instead would also work but duplicates the guard; the reader-loop chokepoint is preferred because it also stops Release events from inflating the drain_pending_events() batch.
  • If held-key navigation (e.g. holding j/k) should keep auto-repeating on Windows, accept Press and Repeat — i.e. reject only Release: if matches!(k.kind, KeyEventKind::Release) { continue; }. Press-only is the safer default and matches typical TUI behavior; revisit if held-key repeat is desired.

Implementation Notes

  • Primary change: src/view/ui_events.rs — add KeyEventKind to the crossterm::event import and the kind != Press guard in terminal_reader_loop (around src/view/ui_events.rs:116-123).
  • No change required in src/view/event_handler.rs; its dispatch logic is correct once Release events are filtered upstream.
  • Testing: add a regression test verifying that an Event::Key with kind: KeyEventKind::Release is filtered out. Note KeyEvent::new(...) defaults kind to Press, so a Release event must be constructed explicitly. Consider extracting the filter predicate into a small testable helper (e.g. fn is_actionable_key(kind: KeyEventKind) -> bool) so it can be unit-tested without a live terminal.
  • Manual verification: on Windows, confirm h/1 opens and holds the Help page, A/M toggle cleanly, and navigation keys move once per press. On macOS/Linux, confirm no behavioral regression.

Acceptance Criteria

  • Key Release (and, by default, Repeat) events are no longer dispatched to handle_key_event on Windows.
  • Pressing h/1 once opens the Help page and it stays open; pressing again closes it.
  • Other toggle/action bindings (A alerts, M topology mode, sort keys, T/V tab jumps) fire exactly once per physical key press on Windows.
  • No behavioral change on macOS/Linux (every key event there is already Press).
  • A regression test asserts that non-Press key events are filtered.
  • cargo fmt --check, cargo clippy, and cargo test pass.

Original Suggestion

Title: [bug] On Windows, the keys are not debounced making them unusable

Please de-bounce the keys.

For example, hitting h for help page, just flashes the page, but not actually showing the help page.
Keeping pressing the h flashes it repeatedly and if you're lucky, it might just stop at the help page.

Same for all other keys.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions