From dff07caeffc3d26436371d09cc21e5f93e16385f Mon Sep 17 00:00:00 2001 From: TimelordUK Date: Sun, 7 Jun 2026 10:18:56 +0100 Subject: [PATCH] fix(tui): reset chord indicator on timeout + complete yank-mode hint The yank chord (e.g. `yq` = yank query) only resolves if the second key is pressed within the 1s chord window. Two papercuts made this confusing: - The on-screen "Yank mode" hint omitted `v` and `q`, so it wasn't clear which key yanked the query. Now lists y/r=row, c=column, a=all, v=cell, q=query, ESC=cancel. - The chord timeout was only evaluated when the next key arrived, so a dangling chord (`y` with no follow-up) left the hint on screen forever, and a key pressed after the timeout left a stale indicator. Added KeyChordHandler::clear_if_timed_out(), called from the idle loop to cancel a lapsed chord and refresh, and clear the hint on the SingleKey path. The "Yank mode" hint now visibly resets ~1s after `y`. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/enhanced_tui.rs | 15 ++++++++++++++- src/ui/key_handling/chord_handler.rs | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/ui/enhanced_tui.rs b/src/ui/enhanced_tui.rs index afe7408b..25295d71 100644 --- a/src/ui/enhanced_tui.rs +++ b/src/ui/enhanced_tui.rs @@ -1879,7 +1879,10 @@ impl EnhancedTuiApp { Ok(false) } ChordResult::SingleKey(single_key) => { - // Not a chord, process normally + // Not a chord (or a chord that lapsed and was reset on this key). + // Clear any lingering chord hint so the indicator reflects reality. + self.key_sequence_renderer.clear_chord_mode(); + // Process normally self.handle_results_input(single_key) } } @@ -1963,6 +1966,16 @@ impl EnhancedTuiApp { // Ignore other events (mouse, resize, etc.) to reduce CPU } } else { + // No key event. If a pending chord has lapsed, cancel it and refresh + // so the "Yank mode" hint visibly disappears (chord reset indicator). + if self.key_chord_handler.clear_if_timed_out() { + self.key_sequence_renderer.clear_chord_mode(); + self.state_container + .set_status_message("Chord cancelled (timeout)".to_string()); + terminal.draw(|f| self.ui(f))?; + return Ok(false); + } + // No event available, but still redraw if we have pending debounced actions or table needs render if self.search_modes_widget.is_active() || self.table_widget_manager.borrow().needs_render() diff --git a/src/ui/key_handling/chord_handler.rs b/src/ui/key_handling/chord_handler.rs index e253f76f..ca72b707 100644 --- a/src/ui/key_handling/chord_handler.rs +++ b/src/ui/key_handling/chord_handler.rs @@ -263,7 +263,7 @@ impl KeyChordHandler { let description = if self.current_chord.len() == 1 && self.current_chord[0].code == KeyCode::Char('y') { - "Yank mode: y=row, c=column, a=all, ESC=cancel".to_string() + "Yank mode: y/r=row, c=column, a=all, v=cell, q=query, ESC=cancel".to_string() } else { format!("Waiting for: {}", possible.join(", ")) }; @@ -333,6 +333,22 @@ impl KeyChordHandler { self.chord_mode_active } + /// Cancel a pending chord if it has exceeded the timeout. + /// + /// The normal timeout check only runs when the next key arrives, so a chord + /// left dangling (e.g. `y` with no follow-up key) never resets on its own. + /// Calling this from the idle loop lets the UI clear the chord hint once the + /// window lapses. Returns true if a pending chord was cleared. + pub fn clear_if_timed_out(&mut self) -> bool { + if let Some(start) = self.chord_start { + if start.elapsed() > self.chord_timeout { + self.cancel_chord(); + return true; + } + } + false + } + /// Get chord mode description #[must_use] pub fn get_chord_mode_description(&self) -> Option<&str> {