kimun_notes/components/
which_key.rs1use ratatui::Frame;
11use ratatui::layout::Rect;
12use ratatui::style::{Modifier, Style};
13use ratatui::text::{Line, Span};
14use ratatui::widgets::Paragraph;
15
16use crate::components::panel::{ModalBg, ModalSpec, modal_chrome};
17use crate::keys::leader::{LeaderEngine, LeaderNode};
18use crate::settings::themes::Theme;
19
20const CELL_WIDTH: u16 = 24;
22
23pub fn desired_height(engine: &LeaderEngine, width: u16) -> u16 {
26 let n = engine.current_node().children().len() as u16;
27 let cols = (width.saturating_sub(2) / CELL_WIDTH).max(1);
28 let grid_rows = n.div_ceil(cols);
29 grid_rows + 3 }
31
32pub fn render(
35 f: &mut Frame,
36 rect: Rect,
37 theme: &Theme,
38 engine: &LeaderEngine,
39 gateway_label: &str,
40) {
41 let inner = modal_chrome(
42 f,
43 rect,
44 theme,
45 ModalSpec {
46 border: Some(Style::default().fg(theme.focus_border.to_ratatui())),
47 bg: ModalBg::Hard,
48 ..Default::default()
49 },
50 );
51 if inner.height == 0 {
52 return;
53 }
54
55 let node = engine.current_node();
56 let keycap = Style::default()
57 .fg(theme.yellow.to_ratatui())
58 .add_modifier(Modifier::BOLD);
59 let muted = Style::default().fg(theme.gray.to_ratatui());
60 let caption_style = Style::default().fg(theme.fg_secondary.to_ratatui());
61
62 let mut pressed = format!(" {gateway_label}");
64 for c in engine.path() {
65 pressed.push(' ');
66 pressed.push(*c);
67 }
68 let controls = "Esc cancel · BkSp up ";
69 let controls_w = controls.len() as u16;
70 let header_cols = ratatui::layout::Layout::default()
71 .direction(ratatui::layout::Direction::Horizontal)
72 .constraints([
73 ratatui::layout::Constraint::Min(0),
74 ratatui::layout::Constraint::Length(controls_w),
75 ])
76 .split(Rect::new(inner.x, inner.y, inner.width, 1));
77 f.render_widget(
78 Paragraph::new(Line::from(vec![
79 Span::styled(pressed, keycap),
80 Span::styled(format!(" {}", node.label()), caption_style),
81 ])),
82 header_cols[0],
83 );
84 f.render_widget(
85 Paragraph::new(Line::from(Span::styled(controls, muted)))
86 .alignment(ratatui::layout::Alignment::Right),
87 header_cols[1],
88 );
89
90 let children = node.children();
92 if children.is_empty() {
93 return;
94 }
95 let cols = (inner.width / CELL_WIDTH).max(1) as usize;
96 let rows = children.len().div_ceil(cols);
97 let arrow = Span::styled(" → ", muted);
98 for (i, (key, child)) in children.iter().enumerate() {
99 let col = i / rows;
102 let row = i % rows;
103 let y = inner.y + 1 + row as u16;
104 if y >= inner.bottom() {
105 continue;
106 }
107 let x = inner.x + (col as u16) * CELL_WIDTH;
108 if x >= inner.right() {
109 continue;
110 }
111 let target_style = match child {
112 LeaderNode::Group { .. } => Style::default().fg(theme.aqua.to_ratatui()),
113 LeaderNode::Leaf { .. } => Style::default().fg(theme.fg.to_ratatui()),
114 };
115 let cell = Rect::new(x, y, CELL_WIDTH.min(inner.right() - x), 1);
116 f.render_widget(
117 Paragraph::new(Line::from(vec![
118 Span::styled(format!(" {key}"), keycap),
119 arrow.clone(),
120 Span::styled(child.label().to_string(), target_style),
121 ])),
122 cell,
123 );
124 }
125}