Skip to main content

fresh/view/
keybinding_editor.rs

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