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