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