feat: wire which-key popup into TUI with persistent display until resolution
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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Generated
+11
@@ -1660,6 +1660,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tonic-build",
|
"tonic-build",
|
||||||
|
"which-key",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1797,6 +1798,16 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "which-key"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"insta",
|
||||||
|
"proptest",
|
||||||
|
"ratatui",
|
||||||
|
"unicode-width 0.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/evil-keys"]
|
members = ["crates/evil-keys", "crates/which-key"]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ui-agregator"
|
name = "ui-agregator"
|
||||||
@@ -14,6 +14,7 @@ nix = { version = "0.29", features = ["fs"] }
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
evil-keys = { path = "crates/evil-keys" }
|
evil-keys = { path = "crates/evil-keys" }
|
||||||
|
which-key = { path = "crates/which-key" }
|
||||||
|
|
||||||
tonic = "0.12"
|
tonic = "0.12"
|
||||||
prost = "0.13"
|
prost = "0.13"
|
||||||
|
|||||||
+9
-2
@@ -18,6 +18,7 @@ use ui_agregator::app::App;
|
|||||||
use ui_agregator::config::Config;
|
use ui_agregator::config::Config;
|
||||||
use ui_agregator::grpc::{GrpcRequest, spawn_grpc_worker};
|
use ui_agregator::grpc::{GrpcRequest, spawn_grpc_worker};
|
||||||
use ui_agregator::input::{build_insert_keymap, build_normal_keymap};
|
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);
|
const TICK_RATE: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
@@ -78,7 +79,14 @@ async fn run() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
while app.running {
|
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() {
|
if let Ok(response) = grpc_rx.try_recv() {
|
||||||
app.handle_grpc_response(response);
|
app.handle_grpc_response(response);
|
||||||
@@ -124,7 +132,6 @@ async fn run() -> Result<()> {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dispatcher.check_timeout();
|
|
||||||
app.handle_tick();
|
app.handle_tick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ pub mod progress_bar;
|
|||||||
pub mod statusbar;
|
pub mod statusbar;
|
||||||
pub mod topbar;
|
pub mod topbar;
|
||||||
pub mod views;
|
pub mod views;
|
||||||
|
pub mod which_key_popup;
|
||||||
|
|||||||
@@ -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<KeyHint> = 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user