1use ratatui::{
26 layout::{Alignment, Rect},
27 text::{Line, Span, Text},
28 widgets::{Block, Borders, Clear, Paragraph},
29 Frame,
30};
31
32use crate::Theme;
33
34
35#[derive(Debug, Clone, Copy)]
45pub struct LeaderNode {
46 pub key: char,
47 pub label: &'static str,
48 pub children: &'static [LeaderNode],
49 pub is_divider: bool,
50}
51
52impl LeaderNode {
53 pub const fn new(key: char, label: &'static str, children: &'static [LeaderNode]) -> Self {
54 Self { key, label, children, is_divider: false }
55 }
56
57 pub const fn divider() -> Self {
59 Self { key: '\0', label: "", children: &[], is_divider: true }
60 }
61}
62
63pub const POPUP_DELAY: std::time::Duration = std::time::Duration::from_millis(200);
69
70pub struct LeaderState {
71 pub active: bool,
72 sequence: Vec<char>,
73 pub root: &'static [LeaderNode],
74 last_key_at: Option<std::time::Instant>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum LeaderResult {
83 Matched(Vec<char>),
85 Pending,
87 Cancelled,
89}
90
91impl LeaderState {
92 pub fn new(root: &'static [LeaderNode]) -> Self {
93 Self { active: false, sequence: Vec::new(), root, last_key_at: None }
94 }
95
96 pub fn arm(&mut self) {
98 self.active = true;
99 self.sequence.clear();
100 self.last_key_at = Some(std::time::Instant::now());
101 }
102
103 pub fn cancel(&mut self) {
105 self.active = false;
106 self.sequence.clear();
107 self.last_key_at = None;
108 }
109
110 pub fn push(&mut self, c: char) -> LeaderResult {
112 let still_hiding = self.last_key_at
117 .map(|t| t.elapsed() < POPUP_DELAY)
118 .unwrap_or(false);
119 if still_hiding {
120 self.last_key_at = Some(std::time::Instant::now());
121 }
122 self.sequence.push(c);
123
124 match self.find_node(&self.sequence.clone()) {
125 NodeMatch::Leaf => {
126 let path = self.sequence.clone();
127 self.cancel();
128 LeaderResult::Matched(path)
129 }
130 NodeMatch::Prefix => LeaderResult::Pending,
131 NodeMatch::None => {
132 self.cancel();
133 LeaderResult::Cancelled
134 }
135 }
136 }
137
138 pub fn tick(&mut self) -> bool {
140 false
141 }
142
143 pub fn current_entries(&self) -> &'static [LeaderNode] {
145 let mut level = self.root;
146 for &c in &self.sequence {
147 if let Some(node) = level.iter().filter(|n| !n.is_divider).find(|n| n.key == c) {
148 level = node.children;
149 } else {
150 return &[];
151 }
152 }
153 level
154 }
155
156 fn find_node(&self, path: &[char]) -> NodeMatch {
159 let mut level = self.root;
160 for (i, &c) in path.iter().enumerate() {
161 match level.iter().filter(|n| !n.is_divider).find(|n| n.key == c) {
162 None => return NodeMatch::None,
163 Some(node) => {
164 if i == path.len() - 1 {
165 if node.children.is_empty() {
166 return NodeMatch::Leaf;
167 } else {
168 return NodeMatch::Prefix;
169 }
170 }
171 level = node.children;
172 }
173 }
174 }
175 NodeMatch::Prefix
176 }
177}
178
179enum NodeMatch { Leaf, Prefix, None }
180
181fn entry_line(entry: &LeaderNode, theme: &Theme) -> Line<'static> {
189 let label = entry.label;
190 let key = entry.key;
191 let has_children = !entry.children.is_empty();
192
193 let key_lower = key.to_lowercase().next().unwrap_or(key);
194 let pos = label
195 .char_indices()
196 .find(|(_, c)| c.to_lowercase().next().unwrap_or(*c) == key_lower);
197
198 let mut spans = vec![Span::raw(" ")]; if let Some((byte_idx, _)) = pos {
201 let before = &label[..byte_idx];
203 let char_len = key.len_utf8();
204 let after = &label[byte_idx + char_len..];
205
206 if !before.is_empty() {
207 spans.push(Span::styled(before.to_string(), theme.body));
208 }
209 spans.push(Span::styled(format!("[{}]", key), theme.shortcut_key));
210 if !after.is_empty() {
211 spans.push(Span::styled(after.to_string(), theme.body));
212 }
213 } else {
214 spans.push(Span::styled(format!("[{}]", key), theme.shortcut_key));
216 spans.push(Span::styled(format!(" {}", label), theme.body));
217 }
218
219 if has_children {
220 spans.push(Span::styled(" →".to_string(), theme.hint));
221 }
222
223 Line::from(spans)
224}
225
226fn max_label_width_recursive(nodes: &[LeaderNode]) -> usize {
229 nodes.iter().filter(|n| !n.is_divider).map(|n| {
230 let own = n.label.chars().count() + 3 + if !n.children.is_empty() { 2 } else { 0 };
231 own.max(max_label_width_recursive(n.children))
232 }).max().unwrap_or(0)
233}
234
235pub fn render_leader_bar(f: &mut Frame, state: &LeaderState, area: Rect, theme: &Theme) {
238 if !state.active {
239 return;
240 }
241 if state.last_key_at.map(|t| t.elapsed() < POPUP_DELAY).unwrap_or(false) {
244 return;
245 }
246
247 let entries = state.current_entries();
248 let breadcrumb: String = state.sequence.iter().collect();
249
250 let max_entry_chars = max_label_width_recursive(state.root).max(10) as u16;
253 let bar_width = (max_entry_chars + 4).min(area.width.saturating_sub(4));
254
255 let inner_w = bar_width.saturating_sub(2) as usize;
257
258 let mut padded: Vec<Line> = vec![Line::from("")]; for (i, entry) in entries.iter().enumerate() {
263 if entry.is_divider {
264 if i > 0 {
266 padded.push(Line::from(""));
267 }
268 let rule = "─".repeat(inner_w);
269 padded.push(Line::from(Span::styled(rule, theme.separator)));
270 } else {
272 let prev_is_divider = i > 0 && entries[i - 1].is_divider;
273 if i > 0 {
274 padded.push(Line::from("")); }
276 padded.push(entry_line(entry, theme));
277 let _ = prev_is_divider; }
279 }
280 padded.push(Line::from("")); let bar_height = (padded.len() as u16) + 2; let x = area.x + area.width.saturating_sub(bar_width) / 2;
287 let y = area.y + area.height.saturating_sub(bar_height) / 2;
288 let bar_area = Rect { x, y, width: bar_width, height: bar_height };
289
290 let title = if breadcrumb.is_empty() {
291 " ⌁ ".to_string()
292 } else {
293 format!(" ⌁ {} › ", breadcrumb)
294 };
295
296 f.render_widget(Clear, bar_area);
297 let paragraph = Paragraph::new(Text::from(padded))
298 .block(
299 Block::default()
300 .borders(Borders::ALL)
301 .border_style(theme.border_popup)
302 .title(title)
303 .title_style(theme.section_header)
304 .title_alignment(Alignment::Center),
305 );
306 f.render_widget(paragraph, bar_area);
307}