From edf8d5b160f4dc584def0ab40cd6fbe5d1e00d3e Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 10 May 2026 13:28:27 +0200 Subject: [PATCH] feat: wire which-key popup into TUI with persistent display until resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Popup renders after prefix key (SPC, g) and stays visible indefinitely until sequence completes, Escape cancels, or invalid key clears state. No timeout — matches Doom Emacs behavior where which-key waits for user. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent) Co-authored-by: Sisyphus --- Cargo.lock | 11 ++++++++++ Cargo.toml | 3 ++- src/main.rs | 11 ++++++++-- src/presentation/mod.rs | 1 + src/presentation/which_key_popup.rs | 33 +++++++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 src/presentation/which_key_popup.rs diff --git a/Cargo.lock b/Cargo.lock index c49edac..aa53156 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1660,6 +1660,7 @@ dependencies = [ "tokio", "tonic", "tonic-build", + "which-key", ] [[package]] @@ -1797,6 +1798,16 @@ dependencies = [ "semver", ] +[[package]] +name = "which-key" +version = "0.1.0" +dependencies = [ + "insta", + "proptest", + "ratatui", + "unicode-width 0.2.0", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index ece7aaa..78df9a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/evil-keys"] +members = ["crates/evil-keys", "crates/which-key"] [package] name = "ui-agregator" @@ -14,6 +14,7 @@ nix = { version = "0.29", features = ["fs"] } serde = { version = "1", features = ["derive"] } serde_yaml = "0.9" evil-keys = { path = "crates/evil-keys" } +which-key = { path = "crates/which-key" } tonic = "0.12" prost = "0.13" diff --git a/src/main.rs b/src/main.rs index bf74d9c..4a26497 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ use ui_agregator::app::App; use ui_agregator::config::Config; use ui_agregator::grpc::{GrpcRequest, spawn_grpc_worker}; use ui_agregator::input::{build_insert_keymap, build_normal_keymap}; +use ui_agregator::presentation::which_key_popup::render_which_key_popup; const TICK_RATE: Duration = Duration::from_millis(100); @@ -78,7 +79,14 @@ async fn run() -> Result<()> { } while app.running { - terminal.draw(|frame| app.draw(frame))?; + let which_key_entries = dispatcher.which_key_entries(); + let pending_display = dispatcher.pending_display(); + terminal.draw(|frame| { + app.draw(frame); + if let Some(ref entries) = which_key_entries { + render_which_key_popup(frame, frame.area(), entries, &pending_display); + } + })?; if let Ok(response) = grpc_rx.try_recv() { app.handle_grpc_response(response); @@ -124,7 +132,6 @@ async fn run() -> Result<()> { _ => {} } } else { - dispatcher.check_timeout(); app.handle_tick(); } } diff --git a/src/presentation/mod.rs b/src/presentation/mod.rs index fcc6216..2e985d8 100644 --- a/src/presentation/mod.rs +++ b/src/presentation/mod.rs @@ -7,3 +7,4 @@ pub mod progress_bar; pub mod statusbar; pub mod topbar; pub mod views; +pub mod which_key_popup; diff --git a/src/presentation/which_key_popup.rs b/src/presentation/which_key_popup.rs new file mode 100644 index 0000000..b39836a --- /dev/null +++ b/src/presentation/which_key_popup.rs @@ -0,0 +1,33 @@ +use evil_keys::WhichKeyEntry; +use ratatui::{Frame, layout::Rect, style::{Modifier, Style}}; +use which_key::{KeyHint, Position, WhichKey}; + +use crate::theme; + +pub fn render_which_key_popup( + frame: &mut Frame, + area: Rect, + entries: &[WhichKeyEntry], + pending_display: &str, +) { + let hints: Vec = entries + .iter() + .map(|e| { + let h = KeyHint::new(&e.key, &e.description); + if e.is_group { h.group() } else { h } + }) + .collect(); + + let popup = WhichKey::new(hints) + .title(pending_display) + .position(Position::BottomLeft) + .key_style(Style::default().fg(theme::YELLOW).add_modifier(Modifier::BOLD)) + .separator_style(Style::default().fg(theme::GRAY)) + .desc_style(Style::default().fg(theme::FG2)) + .group_style(Style::default().fg(theme::AQUA)) + .border_style(Style::default().fg(theme::BG3)) + .bg(theme::BG0); + + let popup_rect = popup.layout(area); + frame.render_widget(&popup, popup_rect); +}