1use crate::app::keybinding_editor::{
6 BindingSource, ContextFilter, DeleteResult, DisplayRow, EditMode, KeybindingEditor, SearchMode,
7 SourceFilter,
8};
9use crate::input::keybindings::{format_keybinding, KeybindingResolver};
10use crate::view::dimming::apply_dimming;
11use crate::view::theme::Theme;
12use crate::view::ui::scrollbar::{render_scrollbar, ScrollbarColors};
13use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
14use ratatui::{
15 layout::{Constraint, Layout, Rect},
16 style::{Modifier, Style},
17 text::{Line, Span},
18 widgets::{Block, Borders, Clear, Paragraph},
19 Frame,
20};
21use rust_i18n::t;
22
23fn keybinding_modal_area(area: Rect) -> Rect {
33 let modal_width = (area.width as f32 * 0.90).min(120.0) as u16;
35 let modal_height = (area.height as f32 * 0.90) as u16;
36 let modal_width = modal_width.max(60).min(area.width.saturating_sub(2));
37 let modal_height = modal_height.max(20).min(area.height.saturating_sub(2));
38
39 let x = area.x + (area.width.saturating_sub(modal_width)) / 2;
40 let y = area.y + (area.height.saturating_sub(modal_height)) / 2;
41
42 Rect {
43 x,
44 y,
45 width: modal_width,
46 height: modal_height,
47 }
48}
49
50pub fn render_keybinding_editor(
52 frame: &mut Frame,
53 area: Rect,
54 editor: &mut KeybindingEditor,
55 theme: &Theme,
56) {
57 let modal_area = keybinding_modal_area(area);
58
59 frame.render_widget(Clear, modal_area);
61
62 let title = format!(
64 " {} \u{2500} [{}] ",
65 t!("keybinding_editor.title"),
66 editor.active_keymap
67 );
68 let block = Block::default()
69 .title(title)
70 .borders(Borders::ALL)
71 .border_style(Style::default().fg(theme.popup_border_fg))
72 .style(Style::default().bg(theme.popup_bg).fg(theme.popup_text_fg));
73
74 let inner = block.inner(modal_area);
75 frame.render_widget(block, modal_area);
76
77 let chunks = Layout::vertical([
79 Constraint::Length(3), Constraint::Min(5), Constraint::Length(1), ])
83 .split(inner);
84
85 editor.layout.modal_area = modal_area;
87 editor.layout.table_area = chunks[1];
88 editor.layout.table_first_row_y = chunks[1].y + 2; editor.layout.search_bar = Some(Rect {
90 x: inner.x,
91 y: inner.y + 1, width: inner.width,
93 height: 1,
94 });
95 editor.layout.dialog_buttons = None;
97 editor.layout.dialog_key_field = None;
98 editor.layout.dialog_action_field = None;
99 editor.layout.dialog_context_field = None;
100 editor.layout.confirm_buttons = None;
101 editor.layout.table_scrollbar = None;
102
103 render_header(frame, chunks[0], editor, theme);
104 render_table(frame, chunks[1], editor, theme);
105 render_footer(frame, chunks[2], editor, theme);
106
107 if editor.showing_help {
109 render_help_overlay(frame, inner, theme);
110 }
111
112 if let Some(dialog) = editor.edit_dialog.take() {
114 apply_dimming(frame, modal_area);
115 render_edit_dialog(frame, inner, &dialog, editor, theme);
116 editor.edit_dialog = Some(dialog);
117 }
118
119 if editor.showing_confirm_dialog {
120 apply_dimming(frame, modal_area);
121 render_confirm_dialog(frame, inner, editor, theme);
122 }
123}
124
125fn render_header(frame: &mut Frame, area: Rect, editor: &KeybindingEditor, theme: &Theme) {
127 let chunks = Layout::vertical([
128 Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), ])
132 .split(area);
133
134 let mut path_spans = vec![
136 Span::styled(
137 format!(" {} ", t!("keybinding_editor.label_config")),
138 Style::default().fg(theme.popup_text_fg),
139 ),
140 Span::styled(
141 &editor.config_file_path,
142 Style::default().fg(theme.diagnostic_info_fg),
143 ),
144 ];
145 if !editor.keymap_names.is_empty() {
146 path_spans.push(Span::styled(
147 format!(" {} ", t!("keybinding_editor.label_maps")),
148 Style::default().fg(theme.popup_text_fg),
149 ));
150 path_spans.push(Span::styled(
151 editor.keymap_names.join(", "),
152 Style::default().fg(theme.popup_text_fg),
153 ));
154 }
155 frame.render_widget(Paragraph::new(Line::from(path_spans)), chunks[0]);
156
157 if editor.search_active {
159 let search_spans = match editor.search_mode {
160 SearchMode::Text => {
161 let mut spans = vec![
162 Span::styled(
163 format!(" {} ", t!("keybinding_editor.label_search")),
164 Style::default()
165 .fg(theme.help_key_fg)
166 .add_modifier(Modifier::BOLD),
167 ),
168 Span::styled(
169 &editor.search_query,
170 Style::default().fg(theme.popup_text_fg),
171 ),
172 ];
173 if editor.search_focused {
174 spans.push(Span::styled("_", Style::default().fg(theme.cursor)));
175 spans.push(Span::styled(
176 format!(" {}", t!("keybinding_editor.search_text_hint")),
177 Style::default().fg(theme.popup_text_fg),
178 ));
179 }
180 spans
181 }
182 SearchMode::RecordKey => {
183 let key_text = if editor.search_key_display.is_empty() {
184 t!("keybinding_editor.press_a_key").to_string()
185 } else {
186 editor.search_key_display.clone()
187 };
188 vec![
189 Span::styled(
190 format!(" {} ", t!("keybinding_editor.label_record_key")),
191 Style::default()
192 .fg(theme.diagnostic_warning_fg)
193 .add_modifier(Modifier::BOLD),
194 ),
195 Span::styled(key_text, Style::default().fg(theme.popup_text_fg)),
196 Span::styled(
197 format!(" {}", t!("keybinding_editor.search_record_hint")),
198 Style::default().fg(theme.popup_text_fg),
199 ),
200 ]
201 }
202 };
203 frame.render_widget(Paragraph::new(Line::from(search_spans)), chunks[1]);
204 } else {
205 let hint = Line::from(vec![
206 Span::styled(" ", Style::default()),
207 Span::styled(
208 t!("keybinding_editor.search_hint").to_string(),
209 Style::default().fg(theme.popup_text_fg),
210 ),
211 ]);
212 frame.render_widget(Paragraph::new(hint), chunks[1]);
213 }
214
215 let total = editor.bindings.len();
217 let filtered = editor.filtered_indices.len();
218 let count_str = if filtered == total {
219 t!("keybinding_editor.bindings_count", count = total).to_string()
220 } else {
221 t!(
222 "keybinding_editor.bindings_filtered",
223 filtered = filtered,
224 total = total
225 )
226 .to_string()
227 };
228
229 let filter_spans = vec![
230 Span::styled(
231 format!(" {} ", t!("keybinding_editor.label_context")),
232 Style::default().fg(theme.popup_text_fg),
233 ),
234 Span::styled(
235 format!("[{}]", editor.context_filter_display()),
236 Style::default().fg(if editor.context_filter == ContextFilter::All {
237 theme.popup_text_fg
238 } else {
239 theme.diagnostic_info_fg
240 }),
241 ),
242 Span::styled(
243 format!(" {} ", t!("keybinding_editor.label_source")),
244 Style::default().fg(theme.popup_text_fg),
245 ),
246 Span::styled(
247 format!("[{}]", editor.source_filter_display()),
248 Style::default().fg(if editor.source_filter == SourceFilter::All {
249 theme.popup_text_fg
250 } else {
251 theme.diagnostic_info_fg
252 }),
253 ),
254 Span::styled(
255 format!(" {}", count_str),
256 Style::default().fg(theme.popup_text_fg),
257 ),
258 Span::styled(
259 if editor.has_changes {
260 format!(" {}", t!("keybinding_editor.modified"))
261 } else {
262 String::new()
263 },
264 Style::default().fg(theme.diagnostic_warning_fg),
265 ),
266 ];
267 frame.render_widget(Paragraph::new(Line::from(filter_spans)), chunks[2]);
268}
269
270fn render_table(frame: &mut Frame, area: Rect, editor: &mut KeybindingEditor, theme: &Theme) {
272 if area.height < 2 {
273 return;
274 }
275
276 let inner_width = area.width.saturating_sub(2); let key_col_width = (inner_width as f32 * 0.16).min(20.0) as u16;
280 let action_name_col_width = (inner_width as f32 * 0.22).min(28.0) as u16;
281 let context_col_width = (inner_width as f32 * 0.18).clamp(14.0, 30.0) as u16;
282 let source_col_width = 8u16;
283 let fixed_cols =
284 key_col_width + action_name_col_width + context_col_width + source_col_width + 5; let description_col_width = inner_width.saturating_sub(fixed_cols);
286
287 let header = Line::from(vec![
289 Span::styled(" ", Style::default()),
290 Span::styled(
291 pad_right(&t!("keybinding_editor.header_key"), key_col_width as usize),
292 Style::default()
293 .fg(theme.help_key_fg)
294 .add_modifier(Modifier::BOLD),
295 ),
296 Span::styled(" ", Style::default()),
297 Span::styled(
298 pad_right(
299 &t!("keybinding_editor.header_action"),
300 action_name_col_width as usize,
301 ),
302 Style::default()
303 .fg(theme.help_key_fg)
304 .add_modifier(Modifier::BOLD),
305 ),
306 Span::styled(" ", Style::default()),
307 Span::styled(
308 pad_right(
309 &t!("keybinding_editor.header_description"),
310 description_col_width as usize,
311 ),
312 Style::default()
313 .fg(theme.help_key_fg)
314 .add_modifier(Modifier::BOLD),
315 ),
316 Span::styled(" ", Style::default()),
317 Span::styled(
318 pad_right(
319 &t!("keybinding_editor.header_context"),
320 context_col_width as usize,
321 ),
322 Style::default()
323 .fg(theme.help_key_fg)
324 .add_modifier(Modifier::BOLD),
325 ),
326 Span::styled(" ", Style::default()),
327 Span::styled(
328 pad_right(
329 &t!("keybinding_editor.header_source"),
330 source_col_width as usize,
331 ),
332 Style::default()
333 .fg(theme.help_key_fg)
334 .add_modifier(Modifier::BOLD),
335 ),
336 ]);
337 frame.render_widget(Paragraph::new(header), Rect { height: 1, ..area });
338
339 if area.height > 1 {
341 let sep = "\u{2500}".repeat(inner_width as usize);
342 frame.render_widget(
343 Paragraph::new(Line::from(Span::styled(
344 format!(" {}", sep),
345 Style::default().fg(theme.popup_text_fg),
346 ))),
347 Rect {
348 y: area.y + 1,
349 height: 1,
350 ..area
351 },
352 );
353 }
354
355 let table_area = Rect {
357 y: area.y + 2,
358 height: area.height.saturating_sub(2),
359 ..area
360 };
361
362 editor.scroll.set_viewport(table_area.height);
364 editor
365 .scroll
366 .set_content_height(editor.display_rows.len() as u16);
367
368 let visible_rows = table_area.height as usize;
369 let scroll_offset = editor.scroll.offset as usize;
370
371 for (display_idx, display_row) in editor
372 .display_rows
373 .iter()
374 .skip(scroll_offset)
375 .take(visible_rows)
376 .enumerate()
377 {
378 let row_y = table_area.y + display_idx as u16;
379 if row_y >= table_area.y + table_area.height {
380 break;
381 }
382
383 let is_selected = scroll_offset + display_idx == editor.selected;
384 let row_area = Rect {
385 y: row_y,
386 height: 1,
387 ..table_area
388 };
389
390 match display_row {
391 DisplayRow::SectionHeader {
392 plugin_name,
393 collapsed,
394 binding_count,
395 } => {
396 let (row_bg, row_fg) = if is_selected {
397 (theme.popup_selection_bg, theme.popup_text_fg)
398 } else {
399 (theme.popup_bg, theme.help_key_fg)
400 };
401
402 let chevron = if *collapsed { "\u{25b6}" } else { "\u{25bc}" };
403 let label = match plugin_name {
404 Some(name) => name.as_str(),
405 None => "Builtin",
406 };
407
408 let header_text = format!("{} {} ({})", chevron, label, binding_count);
409 let header_style = Style::default()
410 .fg(row_fg)
411 .bg(row_bg)
412 .add_modifier(Modifier::BOLD);
413
414 let indicator = if is_selected { ">" } else { " " };
415 let row = Line::from(vec![
416 Span::styled(indicator, Style::default().fg(theme.help_key_fg).bg(row_bg)),
417 Span::styled(header_text, header_style),
418 ]);
419
420 frame.render_widget(
421 Paragraph::new("").style(Style::default().bg(row_bg)),
422 row_area,
423 );
424 frame.render_widget(Paragraph::new(row), row_area);
425 }
426 DisplayRow::Binding(binding_idx) => {
427 let binding = &editor.bindings[*binding_idx];
428
429 let (row_bg, row_fg) = if is_selected {
430 (theme.popup_selection_bg, theme.popup_text_fg)
431 } else {
432 (theme.popup_bg, theme.popup_text_fg)
433 };
434
435 let key_style = Style::default()
436 .fg(if is_selected {
437 theme.popup_text_fg
438 } else {
439 theme.help_key_fg
440 })
441 .bg(row_bg);
442 let action_name_style = Style::default()
443 .fg(if is_selected {
444 theme.popup_text_fg
445 } else {
446 theme.diagnostic_info_fg
447 })
448 .bg(row_bg);
449 let action_style = Style::default().fg(row_fg).bg(row_bg);
450 let context_style = Style::default()
451 .fg(if is_selected {
452 row_fg
453 } else {
454 theme.popup_text_fg
455 })
456 .bg(row_bg);
457 let source_style = Style::default()
458 .fg(
459 if binding.source == BindingSource::Custom
460 || binding.source == BindingSource::Plugin
461 {
462 if is_selected {
463 theme.popup_text_fg
464 } else {
465 theme.diagnostic_info_fg
466 }
467 } else {
468 context_style.fg.unwrap_or(theme.popup_text_fg)
469 },
470 )
471 .bg(row_bg);
472
473 let indicator = if is_selected { ">" } else { " " };
474
475 let row = Line::from(vec![
476 Span::styled(indicator, Style::default().fg(theme.help_key_fg).bg(row_bg)),
477 Span::styled(
478 pad_right(&binding.key_display, key_col_width as usize),
479 key_style,
480 ),
481 Span::styled(" ", action_name_style),
482 Span::styled(
483 pad_right(&binding.action, action_name_col_width as usize),
484 action_name_style,
485 ),
486 Span::styled(" ", action_style),
487 Span::styled(
488 pad_right(&binding.action_display, description_col_width as usize),
489 action_style,
490 ),
491 Span::styled(" ", context_style),
492 Span::styled(
493 pad_right(&binding.context, context_col_width as usize),
494 context_style,
495 ),
496 Span::styled(" ", source_style),
497 Span::styled(
498 pad_right(
499 &match binding.source {
500 BindingSource::Custom => {
501 t!("keybinding_editor.source_custom").to_string()
502 }
503 BindingSource::Keymap => {
504 t!("keybinding_editor.source_keymap").to_string()
505 }
506 BindingSource::Plugin => {
507 t!("keybinding_editor.source_plugin", default = "Plugin")
508 .to_string()
509 }
510 BindingSource::Unbound => String::new(),
511 },
512 source_col_width as usize,
513 ),
514 source_style,
515 ),
516 ]);
517
518 frame.render_widget(
519 Paragraph::new("").style(Style::default().bg(row_bg)),
520 row_area,
521 );
522 frame.render_widget(Paragraph::new(row), row_area);
523 }
524 }
525 }
526
527 if editor.scroll.needs_scrollbar() {
529 let sb_area = Rect::new(
530 table_area.x + table_area.width.saturating_sub(1),
531 table_area.y,
532 1,
533 table_area.height,
534 );
535 let sb_state = editor.scroll.to_scrollbar_state();
536 let sb_colors = ScrollbarColors::from_theme(theme);
537 render_scrollbar(frame, sb_area, &sb_state, &sb_colors);
538 editor.layout.table_scrollbar = Some(sb_area);
539 }
540}
541
542fn render_footer(frame: &mut Frame, area: Rect, editor: &KeybindingEditor, theme: &Theme) {
544 let hints = if editor.search_active && editor.search_focused {
545 vec![
546 Span::styled(" Esc", Style::default().fg(theme.help_key_fg)),
547 Span::styled(
548 format!(":{} ", t!("keybinding_editor.footer_cancel")),
549 Style::default().fg(theme.popup_text_fg),
550 ),
551 Span::styled("Tab", Style::default().fg(theme.help_key_fg)),
552 Span::styled(
553 format!(":{} ", t!("keybinding_editor.footer_toggle_mode")),
554 Style::default().fg(theme.popup_text_fg),
555 ),
556 Span::styled("Enter", Style::default().fg(theme.help_key_fg)),
557 Span::styled(
558 format!(":{}", t!("keybinding_editor.footer_confirm")),
559 Style::default().fg(theme.popup_text_fg),
560 ),
561 ]
562 } else {
563 vec![
564 Span::styled(" Enter", Style::default().fg(theme.help_key_fg)),
565 Span::styled(
566 format!(":{} ", t!("keybinding_editor.footer_edit")),
567 Style::default().fg(theme.popup_text_fg),
568 ),
569 Span::styled("a", Style::default().fg(theme.help_key_fg)),
570 Span::styled(
571 format!(":{} ", t!("keybinding_editor.footer_add")),
572 Style::default().fg(theme.popup_text_fg),
573 ),
574 Span::styled("d", Style::default().fg(theme.help_key_fg)),
575 Span::styled(
576 format!(":{} ", t!("keybinding_editor.footer_delete")),
577 Style::default().fg(theme.popup_text_fg),
578 ),
579 Span::styled("/", Style::default().fg(theme.help_key_fg)),
580 Span::styled(
581 format!(":{} ", t!("keybinding_editor.footer_search")),
582 Style::default().fg(theme.popup_text_fg),
583 ),
584 Span::styled("r", Style::default().fg(theme.help_key_fg)),
585 Span::styled(
586 format!(":{} ", t!("keybinding_editor.footer_record_key")),
587 Style::default().fg(theme.popup_text_fg),
588 ),
589 Span::styled("c", Style::default().fg(theme.help_key_fg)),
590 Span::styled(
591 format!(":{} ", t!("keybinding_editor.footer_context")),
592 Style::default().fg(theme.popup_text_fg),
593 ),
594 Span::styled("s", Style::default().fg(theme.help_key_fg)),
595 Span::styled(
596 format!(":{} ", t!("keybinding_editor.footer_source")),
597 Style::default().fg(theme.popup_text_fg),
598 ),
599 Span::styled("?", Style::default().fg(theme.help_key_fg)),
600 Span::styled(
601 format!(":{} ", t!("keybinding_editor.footer_help")),
602 Style::default().fg(theme.popup_text_fg),
603 ),
604 Span::styled("Ctrl+S", Style::default().fg(theme.help_key_fg)),
605 Span::styled(
606 format!(":{} ", t!("keybinding_editor.footer_save")),
607 Style::default().fg(theme.popup_text_fg),
608 ),
609 Span::styled("Esc", Style::default().fg(theme.help_key_fg)),
610 Span::styled(
611 format!(":{}", t!("keybinding_editor.footer_close")),
612 Style::default().fg(theme.popup_text_fg),
613 ),
614 ]
615 };
616
617 frame.render_widget(Paragraph::new(Line::from(hints)), area);
618}
619
620fn render_help_overlay(frame: &mut Frame, area: Rect, theme: &Theme) {
622 let width = 52u16.min(area.width.saturating_sub(4));
623 let height = 22u16.min(area.height.saturating_sub(4));
624 let x = area.x + (area.width.saturating_sub(width)) / 2;
625 let y = area.y + (area.height.saturating_sub(height)) / 2;
626
627 let dialog_area = Rect {
628 x,
629 y,
630 width,
631 height,
632 };
633 frame.render_widget(Clear, dialog_area);
634
635 let block = Block::default()
636 .title(format!(" {} ", t!("keybinding_editor.help_title")))
637 .borders(Borders::ALL)
638 .border_style(Style::default().fg(theme.popup_border_fg))
639 .style(Style::default().bg(theme.popup_bg).fg(theme.popup_text_fg));
640 let inner = block.inner(dialog_area);
641 frame.render_widget(block, dialog_area);
642
643 let h_nav = t!("keybinding_editor.help_navigation").to_string();
644 let h_move = t!("keybinding_editor.help_move_up_down").to_string();
645 let h_page = t!("keybinding_editor.help_page_up_down").to_string();
646 let h_first = t!("keybinding_editor.help_first_last").to_string();
647 let h_search = t!("keybinding_editor.help_search").to_string();
648 let h_by_name = t!("keybinding_editor.help_search_by_name").to_string();
649 let h_by_key = t!("keybinding_editor.help_search_by_key").to_string();
650 let h_toggle = t!("keybinding_editor.help_toggle_search").to_string();
651 let h_cancel = t!("keybinding_editor.help_cancel_search").to_string();
652 let h_editing = t!("keybinding_editor.help_editing").to_string();
653 let h_edit = t!("keybinding_editor.help_edit_binding").to_string();
654 let h_add = t!("keybinding_editor.help_add_binding").to_string();
655 let h_del = t!("keybinding_editor.help_delete_binding").to_string();
656 let h_filters = t!("keybinding_editor.help_filters").to_string();
657 let h_ctx = t!("keybinding_editor.help_cycle_context").to_string();
658 let h_src = t!("keybinding_editor.help_cycle_source").to_string();
659 let h_save = t!("keybinding_editor.help_save_changes").to_string();
660 let h_close = t!("keybinding_editor.help_close_help").to_string();
661
662 let help_lines = vec![
663 help_line(&h_nav, "", theme, true),
664 help_line(" \u{2191} / \u{2193}", &h_move, theme, false),
665 help_line(" PgUp / PgDn", &h_page, theme, false),
666 help_line(" Home / End", &h_first, theme, false),
667 help_line("", "", theme, false),
668 help_line(&h_search, "", theme, true),
669 help_line(" /", &h_by_name, theme, false),
670 help_line(" r", &h_by_key, theme, false),
671 help_line(" Tab", &h_toggle, theme, false),
672 help_line(" Esc", &h_cancel, theme, false),
673 help_line("", "", theme, false),
674 help_line(&h_editing, "", theme, true),
675 help_line(" Enter", &h_edit, theme, false),
676 help_line(" a", &h_add, theme, false),
677 help_line(" d / Delete", &h_del, theme, false),
678 help_line("", "", theme, false),
679 help_line(&h_filters, "", theme, true),
680 help_line(" c", &h_ctx, theme, false),
681 help_line(" s", &h_src, theme, false),
682 help_line("", "", theme, false),
683 help_line(" Ctrl+S", &h_save, theme, false),
684 help_line(" Esc / ?", &h_close, theme, false),
685 ];
686
687 let para = Paragraph::new(help_lines);
688 frame.render_widget(para, inner);
689}
690
691fn help_line<'a>(key: &'a str, desc: &'a str, theme: &Theme, is_header: bool) -> Line<'a> {
692 if is_header {
693 Line::from(vec![Span::styled(
694 key,
695 Style::default()
696 .fg(theme.popup_text_fg)
697 .add_modifier(Modifier::BOLD),
698 )])
699 } else {
700 Line::from(vec![
701 Span::styled(
702 format!("{:16}", key),
703 Style::default()
704 .fg(theme.help_key_fg)
705 .add_modifier(Modifier::BOLD),
706 ),
707 Span::styled(desc, Style::default().fg(theme.popup_text_fg)),
708 ])
709 }
710}
711
712const MAX_AUTOCOMPLETE_VISIBLE: usize = 8;
714
715fn render_edit_dialog(
717 frame: &mut Frame,
718 area: Rect,
719 dialog: &crate::app::keybinding_editor::EditBindingState,
720 editor: &mut KeybindingEditor,
721 theme: &Theme,
722) {
723 let width = 56u16.min(area.width.saturating_sub(4));
724 let height = 18u16.min(area.height.saturating_sub(4));
725 let x = area.x + (area.width.saturating_sub(width)) / 2;
726 let y = area.y + (area.height.saturating_sub(height)) / 2;
727
728 let dialog_area = Rect {
729 x,
730 y,
731 width,
732 height,
733 };
734 frame.render_widget(Clear, dialog_area);
735
736 let title = if dialog.editing_index.is_some() {
737 format!(" {} ", t!("keybinding_editor.dialog_edit_title"))
738 } else {
739 format!(" {} ", t!("keybinding_editor.dialog_add_title"))
740 };
741
742 let block = Block::default()
743 .title(title)
744 .borders(Borders::ALL)
745 .border_style(Style::default().fg(theme.popup_border_fg))
746 .style(Style::default().bg(theme.popup_bg).fg(theme.popup_text_fg));
747 let inner = block.inner(dialog_area);
748 frame.render_widget(block, dialog_area);
749
750 let chunks = Layout::vertical([
751 Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Min(3), Constraint::Length(1), ])
761 .split(inner);
762
763 let instr = if dialog.capturing_special && dialog.focus_area == 0 {
765 t!("keybinding_editor.instr_capturing_special").to_string()
766 } else {
767 match dialog.mode {
768 EditMode::RecordingKey => t!("keybinding_editor.instr_recording_key").to_string(),
769 EditMode::EditingAction => t!("keybinding_editor.instr_editing_action").to_string(),
770 EditMode::EditingContext => t!("keybinding_editor.instr_editing_context").to_string(),
771 }
772 };
773 frame.render_widget(
774 Paragraph::new(Line::from(Span::styled(
775 format!(" {}", instr),
776 Style::default().fg(theme.popup_text_fg),
777 ))),
778 chunks[0],
779 );
780
781 let key_focused = dialog.focus_area == 0;
783 let key_none_text;
784 let key_recording_text;
785 let key_text = if dialog.key_display.is_empty() {
786 if dialog.mode == EditMode::RecordingKey {
787 key_recording_text = t!("keybinding_editor.key_recording").to_string();
788 &key_recording_text
789 } else {
790 key_none_text = t!("keybinding_editor.key_none").to_string();
791 &key_none_text
792 }
793 } else {
794 &dialog.key_display
795 };
796 let field_bg = if key_focused {
797 theme.popup_selection_bg
798 } else {
799 theme.popup_bg
800 };
801 let key_label_style = if key_focused {
802 Style::default()
803 .fg(theme.help_key_fg)
804 .bg(field_bg)
805 .add_modifier(Modifier::BOLD)
806 } else {
807 Style::default().fg(theme.popup_text_fg).bg(field_bg)
808 };
809 let key_value_style = Style::default().fg(theme.popup_text_fg).bg(field_bg);
810 if key_focused {
811 frame.render_widget(
812 Paragraph::new("").style(Style::default().bg(field_bg)),
813 chunks[2],
814 );
815 }
816 let mut key_spans = vec![
817 Span::styled(
818 format!(" {:9}", t!("keybinding_editor.label_key")),
819 key_label_style,
820 ),
821 Span::styled(key_text, key_value_style),
822 ];
823 if key_focused && dialog.capturing_special {
824 key_spans.push(Span::styled(
825 format!(" {}", t!("keybinding_editor.capture_any_key_hint")),
826 Style::default()
827 .fg(theme.diagnostic_warning_fg)
828 .bg(field_bg)
829 .add_modifier(Modifier::BOLD),
830 ));
831 } else if key_focused && !dialog.capturing_special {
832 key_spans.push(Span::styled(
833 format!(" {}", t!("keybinding_editor.capture_special_hint")),
834 Style::default().fg(theme.popup_text_fg).bg(field_bg),
835 ));
836 }
837 frame.render_widget(Paragraph::new(Line::from(key_spans)), chunks[2]);
838
839 let action_focused = dialog.focus_area == 1;
841 let field_bg = if action_focused {
842 theme.popup_selection_bg
843 } else {
844 theme.popup_bg
845 };
846 let action_label_style = if action_focused {
847 Style::default()
848 .fg(theme.help_key_fg)
849 .bg(field_bg)
850 .add_modifier(Modifier::BOLD)
851 } else {
852 Style::default().fg(theme.popup_text_fg).bg(field_bg)
853 };
854 let has_error = dialog.action_error.is_some();
855 let action_value_style = if has_error {
856 Style::default().fg(theme.diagnostic_error_fg).bg(field_bg)
857 } else {
858 Style::default().fg(theme.popup_text_fg).bg(field_bg)
859 };
860 let action_placeholder;
861 let action_display = if dialog.action_text.is_empty() && dialog.mode != EditMode::EditingAction
862 {
863 action_placeholder = t!("keybinding_editor.action_placeholder").to_string();
864 &action_placeholder
865 } else {
866 &dialog.action_text
867 };
868 if action_focused {
869 frame.render_widget(
870 Paragraph::new("").style(Style::default().bg(field_bg)),
871 chunks[3],
872 );
873 }
874 let mut action_spans = vec![
875 Span::styled(
876 format!(" {:9}", t!("keybinding_editor.label_action")),
877 action_label_style,
878 ),
879 Span::styled(action_display, action_value_style),
880 ];
881 if action_focused && dialog.mode == EditMode::EditingAction {
882 action_spans.push(Span::styled(
883 "_",
884 Style::default().fg(theme.cursor).bg(field_bg),
885 ));
886 }
887 frame.render_widget(Paragraph::new(Line::from(action_spans)), chunks[3]);
888
889 if !dialog.action_text.is_empty() {
891 let description = KeybindingResolver::format_action_from_str(&dialog.action_text);
892 if description.to_lowercase() != dialog.action_text.replace('_', " ").to_lowercase() {
894 frame.render_widget(
895 Paragraph::new(Line::from(vec![
896 Span::styled(" ", Style::default().fg(theme.popup_text_fg)),
897 Span::styled(
898 format!("\u{2192} {}", description),
899 Style::default()
900 .fg(theme.popup_text_fg)
901 .add_modifier(Modifier::ITALIC),
902 ),
903 ])),
904 chunks[4],
905 );
906 }
907 }
908
909 let ctx_focused = dialog.focus_area == 2;
911 let field_bg = if ctx_focused {
912 theme.popup_selection_bg
913 } else {
914 theme.popup_bg
915 };
916 let ctx_label_style = if ctx_focused {
917 Style::default()
918 .fg(theme.help_key_fg)
919 .bg(field_bg)
920 .add_modifier(Modifier::BOLD)
921 } else {
922 Style::default().fg(theme.popup_text_fg).bg(field_bg)
923 };
924 if ctx_focused {
925 frame.render_widget(
926 Paragraph::new("").style(Style::default().bg(field_bg)),
927 chunks[5],
928 );
929 }
930 frame.render_widget(
931 Paragraph::new(Line::from(vec![
932 Span::styled(
933 format!(" {:9}", t!("keybinding_editor.label_context")),
934 ctx_label_style,
935 ),
936 Span::styled(
937 format!("[{}]", dialog.context),
938 Style::default().fg(theme.popup_text_fg).bg(field_bg),
939 ),
940 if ctx_focused {
941 Span::styled(
942 format!(" {}", t!("keybinding_editor.context_change_hint")),
943 Style::default().fg(theme.popup_text_fg).bg(field_bg),
944 )
945 } else {
946 Span::raw("")
947 },
948 ])),
949 chunks[5],
950 );
951
952 let mut info_lines: Vec<Line> = Vec::new();
954 if let Some(ref err) = dialog.action_error {
955 info_lines.push(Line::from(Span::styled(
956 format!(" \u{2717} {}", err),
957 Style::default()
958 .fg(theme.diagnostic_error_fg)
959 .add_modifier(Modifier::BOLD),
960 )));
961 }
962 if !dialog.conflicts.is_empty() {
963 info_lines.push(Line::from(Span::styled(
964 format!(" {}", t!("keybinding_editor.conflicts_label")),
965 Style::default()
966 .fg(theme.diagnostic_warning_fg)
967 .add_modifier(Modifier::BOLD),
968 )));
969 for conflict in &dialog.conflicts {
970 info_lines.push(Line::from(Span::styled(
971 format!(" {}", conflict),
972 Style::default().fg(theme.diagnostic_warning_fg),
973 )));
974 }
975 }
976 if !info_lines.is_empty() {
977 frame.render_widget(Paragraph::new(info_lines), chunks[7]);
978 }
979
980 let btn_focused = dialog.focus_area == 3;
982 let save_style = if btn_focused && dialog.selected_button == 0 {
983 Style::default()
984 .fg(theme.popup_bg)
985 .bg(theme.help_key_fg)
986 .add_modifier(Modifier::BOLD)
987 } else {
988 Style::default().fg(theme.popup_text_fg)
989 };
990 let cancel_style = if btn_focused && dialog.selected_button == 1 {
991 Style::default()
992 .fg(theme.popup_bg)
993 .bg(theme.help_key_fg)
994 .add_modifier(Modifier::BOLD)
995 } else {
996 Style::default().fg(theme.popup_text_fg)
997 };
998 editor.layout.dialog_key_field = Some(chunks[2]);
1000 editor.layout.dialog_action_field = Some(chunks[3]);
1001 editor.layout.dialog_context_field = Some(chunks[5]);
1002
1003 let save_text = format!(" {} ", t!("keybinding_editor.btn_save"));
1004 let cancel_text = format!(" {} ", t!("keybinding_editor.btn_cancel"));
1005 let save_x = chunks[8].x + 3;
1006 let cancel_x = save_x + save_text.len() as u16 + 2;
1007 editor.layout.dialog_buttons = Some((
1008 Rect {
1009 x: save_x,
1010 y: chunks[8].y,
1011 width: save_text.len() as u16,
1012 height: 1,
1013 },
1014 Rect {
1015 x: cancel_x,
1016 y: chunks[8].y,
1017 width: cancel_text.len() as u16,
1018 height: 1,
1019 },
1020 ));
1021
1022 frame.render_widget(
1023 Paragraph::new(Line::from(vec![
1024 Span::raw(" "),
1025 Span::styled(save_text, save_style),
1026 Span::raw(" "),
1027 Span::styled(cancel_text, cancel_style),
1028 ])),
1029 chunks[8],
1030 );
1031
1032 if dialog.autocomplete_visible && !dialog.autocomplete_suggestions.is_empty() {
1034 render_autocomplete_popup(frame, chunks[3], dialog, theme);
1035 }
1036}
1037
1038fn render_autocomplete_popup(
1040 frame: &mut Frame,
1041 action_field_area: Rect,
1042 dialog: &crate::app::keybinding_editor::EditBindingState,
1043 theme: &Theme,
1044) {
1045 let suggestion_count = dialog
1046 .autocomplete_suggestions
1047 .len()
1048 .min(MAX_AUTOCOMPLETE_VISIBLE);
1049 if suggestion_count == 0 {
1050 return;
1051 }
1052
1053 let popup_x = action_field_area.x + 12; let popup_y = action_field_area.y + 1;
1056 let popup_width = 36u16.min(action_field_area.width.saturating_sub(12));
1057 let popup_height = (suggestion_count as u16) + 2; let popup_area = Rect {
1060 x: popup_x,
1061 y: popup_y,
1062 width: popup_width,
1063 height: popup_height,
1064 };
1065
1066 frame.render_widget(Clear, popup_area);
1067
1068 let block = Block::default()
1069 .borders(Borders::ALL)
1070 .border_style(Style::default().fg(theme.popup_border_fg))
1071 .style(Style::default().bg(theme.popup_bg).fg(theme.popup_text_fg));
1072 let inner = block.inner(popup_area);
1073 frame.render_widget(block, popup_area);
1074
1075 let selected = dialog.autocomplete_selected.unwrap_or(0);
1077 let scroll_offset = if selected >= MAX_AUTOCOMPLETE_VISIBLE {
1078 selected - MAX_AUTOCOMPLETE_VISIBLE + 1
1079 } else {
1080 0
1081 };
1082
1083 let mut lines: Vec<Line> = Vec::new();
1084 for (i, suggestion) in dialog
1085 .autocomplete_suggestions
1086 .iter()
1087 .skip(scroll_offset)
1088 .take(MAX_AUTOCOMPLETE_VISIBLE)
1089 .enumerate()
1090 {
1091 let actual_idx = i + scroll_offset;
1092 let is_selected = Some(actual_idx) == dialog.autocomplete_selected;
1093
1094 let style = if is_selected {
1095 Style::default()
1096 .fg(theme.popup_bg)
1097 .bg(theme.help_key_fg)
1098 .add_modifier(Modifier::BOLD)
1099 } else {
1100 Style::default().fg(theme.popup_text_fg).bg(theme.popup_bg)
1101 };
1102
1103 let display = pad_right(suggestion, inner.width as usize);
1105 lines.push(Line::from(Span::styled(display, style)));
1106 }
1107
1108 frame.render_widget(Paragraph::new(lines), inner);
1109}
1110
1111fn render_confirm_dialog(
1113 frame: &mut Frame,
1114 area: Rect,
1115 editor: &mut KeybindingEditor,
1116 theme: &Theme,
1117) {
1118 let width = 44u16.min(area.width.saturating_sub(4));
1119 let height = 7u16.min(area.height.saturating_sub(4));
1120 let x = area.x + (area.width.saturating_sub(width)) / 2;
1121 let y = area.y + (area.height.saturating_sub(height)) / 2;
1122
1123 let dialog_area = Rect {
1124 x,
1125 y,
1126 width,
1127 height,
1128 };
1129 frame.render_widget(Clear, dialog_area);
1130
1131 let block = Block::default()
1132 .title(format!(" {} ", t!("keybinding_editor.confirm_title")))
1133 .borders(Borders::ALL)
1134 .border_style(Style::default().fg(theme.diagnostic_warning_fg))
1135 .style(Style::default().bg(theme.popup_bg).fg(theme.popup_text_fg));
1136 let inner = block.inner(dialog_area);
1137 frame.render_widget(block, dialog_area);
1138
1139 let chunks = Layout::vertical([
1140 Constraint::Length(2), Constraint::Length(1), Constraint::Length(1), ])
1144 .split(inner);
1145
1146 frame.render_widget(
1147 Paragraph::new(Line::from(Span::styled(
1148 format!(" {}", t!("keybinding_editor.confirm_message")),
1149 Style::default().fg(theme.popup_text_fg),
1150 ))),
1151 chunks[0],
1152 );
1153
1154 let options = [
1155 t!("keybinding_editor.btn_save").to_string(),
1156 t!("keybinding_editor.btn_discard").to_string(),
1157 t!("keybinding_editor.btn_cancel").to_string(),
1158 ];
1159 let mut x_offset = chunks[2].x + 1;
1161 let mut btn_rects = Vec::new();
1162 let mut spans = vec![Span::raw(" ")];
1163 for (i, opt) in options.iter().enumerate() {
1164 let style = if i == editor.confirm_selection {
1165 Style::default()
1166 .fg(theme.popup_bg)
1167 .bg(theme.help_key_fg)
1168 .add_modifier(Modifier::BOLD)
1169 } else {
1170 Style::default().fg(theme.popup_text_fg)
1171 };
1172 let text = format!(" {} ", opt);
1173 let text_len = text.len() as u16;
1174 btn_rects.push(Rect {
1175 x: x_offset,
1176 y: chunks[2].y,
1177 width: text_len,
1178 height: 1,
1179 });
1180 x_offset += text_len + 2; spans.push(Span::styled(text, style));
1182 spans.push(Span::raw(" "));
1183 }
1184 if btn_rects.len() == 3 {
1185 editor.layout.confirm_buttons = Some((btn_rects[0], btn_rects[1], btn_rects[2]));
1186 }
1187
1188 frame.render_widget(Paragraph::new(Line::from(spans)), chunks[2]);
1189}
1190
1191fn pad_right(s: &str, width: usize) -> String {
1193 let char_count = s.chars().count();
1194 if char_count >= width {
1195 s.chars().take(width).collect()
1196 } else {
1197 let padding = width - char_count;
1198 format!("{}{}", s, " ".repeat(padding))
1199 }
1200}
1201
1202pub fn handle_keybinding_editor_input(
1206 editor: &mut KeybindingEditor,
1207 event: &KeyEvent,
1208) -> KeybindingEditorAction {
1209 if editor.showing_help {
1211 match event.code {
1212 KeyCode::Esc | KeyCode::Char('?') | KeyCode::Enter => {
1213 editor.showing_help = false;
1214 }
1215 _ => {}
1216 }
1217 return KeybindingEditorAction::Consumed;
1218 }
1219
1220 if editor.showing_confirm_dialog {
1222 return handle_confirm_input(editor, event);
1223 }
1224
1225 if editor.edit_dialog.is_some() {
1227 return handle_edit_dialog_input(editor, event);
1228 }
1229
1230 if editor.search_active && editor.search_focused {
1232 return handle_search_input(editor, event);
1233 }
1234
1235 handle_main_input(editor, event)
1237}
1238
1239pub enum KeybindingEditorAction {
1241 Consumed,
1243 Close,
1245 SaveAndClose,
1247 StatusMessage(String),
1249}
1250
1251fn handle_main_input(editor: &mut KeybindingEditor, event: &KeyEvent) -> KeybindingEditorAction {
1252 match (event.code, event.modifiers) {
1253 (KeyCode::Esc, KeyModifiers::NONE) => {
1255 if editor.search_active {
1256 editor.cancel_search();
1258 KeybindingEditorAction::Consumed
1259 } else if editor.has_changes {
1260 editor.showing_confirm_dialog = true;
1261 editor.confirm_selection = 0;
1262 KeybindingEditorAction::Consumed
1263 } else {
1264 KeybindingEditorAction::Close
1265 }
1266 }
1267
1268 (KeyCode::Char('s'), m) if m.contains(KeyModifiers::CONTROL) => {
1270 KeybindingEditorAction::SaveAndClose
1271 }
1272
1273 (KeyCode::Up, KeyModifiers::NONE) | (KeyCode::Char('k'), KeyModifiers::NONE) => {
1275 editor.select_prev();
1276 KeybindingEditorAction::Consumed
1277 }
1278 (KeyCode::Down, KeyModifiers::NONE) | (KeyCode::Char('j'), KeyModifiers::NONE) => {
1279 editor.select_next();
1280 KeybindingEditorAction::Consumed
1281 }
1282 (KeyCode::PageUp, _) => {
1283 editor.page_up();
1284 KeybindingEditorAction::Consumed
1285 }
1286 (KeyCode::PageDown, _) => {
1287 editor.page_down();
1288 KeybindingEditorAction::Consumed
1289 }
1290 (KeyCode::Home, _) => {
1291 editor.selected = 0;
1292 editor.scroll.offset = 0;
1293 KeybindingEditorAction::Consumed
1294 }
1295 (KeyCode::End, _) => {
1296 editor.selected = editor.display_rows.len().saturating_sub(1);
1297 editor.ensure_visible_public();
1298 KeybindingEditorAction::Consumed
1299 }
1300
1301 (KeyCode::Char('/'), KeyModifiers::NONE) => {
1303 editor.start_search();
1304 KeybindingEditorAction::Consumed
1305 }
1306
1307 (KeyCode::Char('r'), KeyModifiers::NONE) => {
1309 editor.start_record_key_search();
1310 KeybindingEditorAction::Consumed
1311 }
1312
1313 (KeyCode::Char('?'), _) => {
1315 editor.showing_help = true;
1316 KeybindingEditorAction::Consumed
1317 }
1318
1319 (KeyCode::Char('a'), KeyModifiers::NONE) => {
1321 editor.open_add_dialog();
1322 KeybindingEditorAction::Consumed
1323 }
1324
1325 (KeyCode::Enter, KeyModifiers::NONE) => {
1327 if editor.selected_is_section_header() {
1328 editor.toggle_section_at_selected();
1329 } else {
1330 editor.open_edit_dialog();
1331 }
1332 KeybindingEditorAction::Consumed
1333 }
1334
1335 (KeyCode::Char('d'), KeyModifiers::NONE) | (KeyCode::Delete, _) => {
1337 match editor.delete_selected() {
1338 DeleteResult::CustomRemoved => KeybindingEditorAction::StatusMessage(
1339 t!("keybinding_editor.status_binding_removed").to_string(),
1340 ),
1341 DeleteResult::KeymapOverridden => KeybindingEditorAction::StatusMessage(
1342 t!("keybinding_editor.status_keymap_overridden").to_string(),
1343 ),
1344 DeleteResult::CannotDelete | DeleteResult::NothingSelected => {
1345 KeybindingEditorAction::StatusMessage(
1346 t!("keybinding_editor.status_cannot_delete").to_string(),
1347 )
1348 }
1349 }
1350 }
1351
1352 (KeyCode::Char('c'), KeyModifiers::NONE) => {
1354 editor.cycle_context_filter();
1355 KeybindingEditorAction::Consumed
1356 }
1357
1358 (KeyCode::Char('s'), KeyModifiers::NONE) => {
1360 editor.cycle_source_filter();
1361 KeybindingEditorAction::Consumed
1362 }
1363
1364 _ => KeybindingEditorAction::Consumed,
1365 }
1366}
1367
1368fn handle_search_input(editor: &mut KeybindingEditor, event: &KeyEvent) -> KeybindingEditorAction {
1369 match editor.search_mode {
1370 SearchMode::Text => match (event.code, event.modifiers) {
1371 (KeyCode::Esc, _) => {
1372 editor.cancel_search();
1373 KeybindingEditorAction::Consumed
1374 }
1375 (KeyCode::Enter, _) | (KeyCode::Down, _) => {
1376 editor.search_focused = false;
1378 KeybindingEditorAction::Consumed
1379 }
1380 (KeyCode::Up, _) => {
1381 editor.search_focused = false;
1383 editor.selected = editor.filtered_indices.len().saturating_sub(1);
1384 editor.ensure_visible_public();
1385 KeybindingEditorAction::Consumed
1386 }
1387 (KeyCode::Tab, _) => {
1388 editor.search_mode = SearchMode::RecordKey;
1390 editor.search_key_display.clear();
1391 editor.search_key_code = None;
1392 KeybindingEditorAction::Consumed
1393 }
1394 (KeyCode::Backspace, _) => {
1395 editor.search_query.pop();
1396 editor.apply_filters();
1397 KeybindingEditorAction::Consumed
1398 }
1399 (KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => {
1400 editor.search_query.push(c);
1401 editor.apply_filters();
1402 KeybindingEditorAction::Consumed
1403 }
1404 _ => KeybindingEditorAction::Consumed,
1405 },
1406 SearchMode::RecordKey => match (event.code, event.modifiers) {
1407 (KeyCode::Esc, KeyModifiers::NONE) => {
1408 editor.cancel_search();
1409 KeybindingEditorAction::Consumed
1410 }
1411 (KeyCode::Tab, KeyModifiers::NONE) => {
1412 editor.search_mode = SearchMode::Text;
1414 editor.apply_filters();
1415 KeybindingEditorAction::Consumed
1416 }
1417 (KeyCode::Enter, KeyModifiers::NONE) => {
1418 editor.search_focused = false;
1420 KeybindingEditorAction::Consumed
1421 }
1422 _ => {
1423 editor.record_search_key(event);
1425 KeybindingEditorAction::Consumed
1426 }
1427 },
1428 }
1429}
1430
1431fn handle_edit_dialog_input(
1432 editor: &mut KeybindingEditor,
1433 event: &KeyEvent,
1434) -> KeybindingEditorAction {
1435 let mut dialog = match editor.edit_dialog.take() {
1437 Some(d) => d,
1438 None => return KeybindingEditorAction::Consumed,
1439 };
1440
1441 if dialog.capturing_special && dialog.focus_area == 0 {
1444 match event.code {
1445 KeyCode::Modifier(_) => {} _ => {
1447 dialog.key_code = Some(event.code);
1448 dialog.modifiers = event.modifiers;
1449 dialog.key_display = format_keybinding(&event.code, &event.modifiers);
1450 dialog.conflicts =
1451 editor.find_conflicts(event.code, event.modifiers, &dialog.context);
1452 dialog.capturing_special = false;
1453 }
1454 }
1455 editor.edit_dialog = Some(dialog);
1456 return KeybindingEditorAction::Consumed;
1457 }
1458
1459 if event.code == KeyCode::Esc && event.modifiers == KeyModifiers::NONE {
1461 return KeybindingEditorAction::Consumed;
1463 }
1464
1465 match dialog.focus_area {
1466 0 => {
1467 match (event.code, event.modifiers) {
1469 (KeyCode::Enter, KeyModifiers::NONE) => {
1471 dialog.capturing_special = true;
1472 }
1473 (KeyCode::Tab | KeyCode::Down, KeyModifiers::NONE) => {
1474 dialog.focus_area = 1;
1475 dialog.mode = EditMode::EditingAction;
1476 }
1477 _ => {
1478 }
1481 }
1482 }
1483 1 => {
1484 match (event.code, event.modifiers) {
1486 (KeyCode::Tab, KeyModifiers::NONE) => {
1487 if dialog.autocomplete_visible {
1489 if let Some(sel) = dialog.autocomplete_selected {
1490 if sel < dialog.autocomplete_suggestions.len() {
1491 let suggestion = dialog.autocomplete_suggestions[sel].clone();
1492 dialog.action_text = suggestion;
1493 dialog.action_cursor = dialog.action_text.len();
1494 dialog.autocomplete_visible = false;
1495 dialog.autocomplete_selected = None;
1496 dialog.action_error = None;
1497 }
1498 }
1499 } else {
1500 dialog.focus_area = 2;
1501 dialog.mode = EditMode::EditingContext;
1502 }
1503 }
1504 (KeyCode::BackTab, _) => {
1505 dialog.autocomplete_visible = false;
1506 dialog.focus_area = 0;
1507 dialog.mode = EditMode::RecordingKey;
1508 }
1509 (KeyCode::Enter, KeyModifiers::NONE) => {
1510 if dialog.autocomplete_visible {
1512 if let Some(sel) = dialog.autocomplete_selected {
1513 if sel < dialog.autocomplete_suggestions.len() {
1514 let suggestion = dialog.autocomplete_suggestions[sel].clone();
1515 dialog.action_text = suggestion;
1516 dialog.action_cursor = dialog.action_text.len();
1517 dialog.autocomplete_visible = false;
1518 dialog.autocomplete_selected = None;
1519 dialog.action_error = None;
1520 }
1521 }
1522 } else {
1523 dialog.focus_area = 3;
1524 dialog.selected_button = 0;
1525 dialog.mode = EditMode::EditingContext;
1526 }
1527 }
1528 (KeyCode::Up, _) if dialog.autocomplete_visible => {
1529 if let Some(sel) = dialog.autocomplete_selected {
1531 if sel > 0 {
1532 dialog.autocomplete_selected = Some(sel - 1);
1533 }
1534 }
1535 }
1536 (KeyCode::Down, _) if dialog.autocomplete_visible => {
1537 if let Some(sel) = dialog.autocomplete_selected {
1539 let max = dialog.autocomplete_suggestions.len().saturating_sub(1);
1540 if sel < max {
1541 dialog.autocomplete_selected = Some(sel + 1);
1542 }
1543 }
1544 }
1545 (KeyCode::Up, KeyModifiers::NONE) => {
1546 dialog.autocomplete_visible = false;
1548 dialog.focus_area = 0;
1549 dialog.mode = EditMode::RecordingKey;
1550 }
1551 (KeyCode::Down, KeyModifiers::NONE) => {
1552 dialog.focus_area = 2;
1554 dialog.mode = EditMode::EditingContext;
1555 }
1556 (KeyCode::Esc, _) if dialog.autocomplete_visible => {
1557 dialog.autocomplete_visible = false;
1559 dialog.autocomplete_selected = None;
1560 editor.edit_dialog = Some(dialog);
1562 return KeybindingEditorAction::Consumed;
1563 }
1564 (KeyCode::Backspace, _) => {
1565 if dialog.action_cursor > 0 {
1566 dialog.action_cursor -= 1;
1567 dialog.action_text.remove(dialog.action_cursor);
1568 dialog.action_error = None;
1569 }
1570 editor.edit_dialog = Some(dialog);
1572 editor.update_autocomplete();
1573 return KeybindingEditorAction::Consumed;
1574 }
1575 (KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => {
1576 dialog.action_text.insert(dialog.action_cursor, c);
1577 dialog.action_cursor += 1;
1578 dialog.action_error = None;
1579 editor.edit_dialog = Some(dialog);
1581 editor.update_autocomplete();
1582 return KeybindingEditorAction::Consumed;
1583 }
1584 _ => {}
1585 }
1586 }
1587 2 => {
1588 match (event.code, event.modifiers) {
1590 (KeyCode::Tab | KeyCode::Down, KeyModifiers::NONE) => {
1591 dialog.focus_area = 3;
1592 dialog.selected_button = 0;
1593 }
1594 (KeyCode::BackTab, _) | (KeyCode::Up, KeyModifiers::NONE) => {
1595 dialog.focus_area = 1;
1596 dialog.mode = EditMode::EditingAction;
1597 }
1598 (KeyCode::Left, _) => {
1599 if dialog.context_option_index > 0 {
1600 dialog.context_option_index -= 1;
1601 dialog.context =
1602 dialog.context_options[dialog.context_option_index].clone();
1603 if let Some(key_code) = dialog.key_code {
1605 dialog.conflicts =
1606 editor.find_conflicts(key_code, dialog.modifiers, &dialog.context);
1607 }
1608 }
1609 }
1610 (KeyCode::Right, _) => {
1611 if dialog.context_option_index + 1 < dialog.context_options.len() {
1612 dialog.context_option_index += 1;
1613 dialog.context =
1614 dialog.context_options[dialog.context_option_index].clone();
1615 if let Some(key_code) = dialog.key_code {
1616 dialog.conflicts =
1617 editor.find_conflicts(key_code, dialog.modifiers, &dialog.context);
1618 }
1619 }
1620 }
1621 (KeyCode::Enter, _) => {
1622 dialog.focus_area = 3;
1623 dialog.selected_button = 0;
1624 }
1625 _ => {}
1626 }
1627 }
1628 3 => {
1629 match (event.code, event.modifiers) {
1631 (KeyCode::Tab, KeyModifiers::NONE) => {
1632 if dialog.selected_button < 1 {
1633 dialog.selected_button = 1;
1635 } else {
1636 dialog.focus_area = 0;
1638 dialog.mode = EditMode::RecordingKey;
1639 }
1640 }
1641 (KeyCode::BackTab, _) => {
1642 if dialog.selected_button > 0 {
1643 dialog.selected_button = 0;
1645 } else {
1646 dialog.focus_area = 2;
1648 dialog.mode = EditMode::EditingContext;
1649 }
1650 }
1651 (KeyCode::Up, KeyModifiers::NONE) => {
1652 dialog.focus_area = 2;
1653 dialog.mode = EditMode::EditingContext;
1654 }
1655 (KeyCode::Left, _) => {
1656 if dialog.selected_button > 0 {
1657 dialog.selected_button -= 1;
1658 }
1659 }
1660 (KeyCode::Right, _) => {
1661 if dialog.selected_button < 1 {
1662 dialog.selected_button += 1;
1663 }
1664 }
1665 (KeyCode::Enter, _) => {
1666 if dialog.selected_button == 0 {
1667 editor.edit_dialog = Some(dialog);
1669 if let Some(err) = editor.apply_edit_dialog() {
1670 return KeybindingEditorAction::StatusMessage(err);
1672 }
1673 return KeybindingEditorAction::Consumed;
1674 } else {
1675 return KeybindingEditorAction::Consumed;
1677 }
1678 }
1679 _ => {}
1680 }
1681 }
1682 _ => {}
1683 }
1684
1685 editor.edit_dialog = Some(dialog);
1687 KeybindingEditorAction::Consumed
1688}
1689
1690fn handle_confirm_input(editor: &mut KeybindingEditor, event: &KeyEvent) -> KeybindingEditorAction {
1691 match (event.code, event.modifiers) {
1692 (KeyCode::Left, _) => {
1693 if editor.confirm_selection > 0 {
1694 editor.confirm_selection -= 1;
1695 }
1696 KeybindingEditorAction::Consumed
1697 }
1698 (KeyCode::Right, _) => {
1699 if editor.confirm_selection < 2 {
1700 editor.confirm_selection += 1;
1701 }
1702 KeybindingEditorAction::Consumed
1703 }
1704 (KeyCode::Enter, _) => match editor.confirm_selection {
1705 0 => KeybindingEditorAction::SaveAndClose,
1706 1 => KeybindingEditorAction::Close, _ => {
1708 editor.showing_confirm_dialog = false;
1709 KeybindingEditorAction::Consumed
1710 }
1711 },
1712 (KeyCode::Esc, _) => {
1713 editor.showing_confirm_dialog = false;
1714 KeybindingEditorAction::Consumed
1715 }
1716 _ => KeybindingEditorAction::Consumed,
1717 }
1718}
1719
1720#[cfg(test)]
1721mod tests {
1722 use super::*;
1723
1724 #[test]
1731 fn modal_centres_within_offset_area_left_of_dock() {
1732 let chrome = Rect::new(34, 0, 120 - 34, 40);
1735 let modal = keybinding_modal_area(chrome);
1736
1737 assert!(modal.x >= chrome.x, "modal bleeds left under the dock");
1740 assert!(
1741 modal.x + modal.width <= chrome.x + chrome.width,
1742 "modal overflows the right edge"
1743 );
1744 assert!(
1745 modal.y + modal.height <= chrome.y + chrome.height,
1746 "modal overflows the bottom edge"
1747 );
1748
1749 let left_margin = modal.x - chrome.x;
1752 let right_margin = (chrome.x + chrome.width) - (modal.x + modal.width);
1753 assert!(
1754 left_margin.abs_diff(right_margin) <= 1,
1755 "modal not centred: left={left_margin} right={right_margin}"
1756 );
1757 }
1758
1759 #[test]
1762 fn modal_centres_within_full_screen() {
1763 let area = Rect::new(0, 0, 120, 40);
1764 let modal = keybinding_modal_area(area);
1765
1766 let left_margin = modal.x;
1767 let right_margin = area.width - (modal.x + modal.width);
1768 assert!(
1769 left_margin.abs_diff(right_margin) <= 1,
1770 "modal not centred on full screen: left={left_margin} right={right_margin}"
1771 );
1772 assert!(modal.x + modal.width <= area.width);
1773 assert!(modal.y + modal.height <= area.height);
1774 }
1775}