Skip to main content

mxr_tui/app/
input.rs

1use super::*;
2
3impl App {
4    pub fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> Option<Action> {
5        if self.error_modal.is_some() {
6            return match (key.code, key.modifiers) {
7                (KeyCode::Esc | KeyCode::Enter, _)
8                | (KeyCode::Char('q'), _)
9                | (KeyCode::Char('x'), _) => {
10                    self.error_modal = None;
11                    None
12                }
13                _ => None,
14            };
15        }
16
17        if self.help_modal_open {
18            return match (key.code, key.modifiers) {
19                (KeyCode::Esc | KeyCode::Enter, _)
20                | (KeyCode::Char('?'), _)
21                | (KeyCode::Char('q'), _) => Some(Action::Help),
22                (KeyCode::Char('j') | KeyCode::Down, _) => {
23                    self.help_scroll_offset = self.help_scroll_offset.saturating_add(1);
24                    None
25                }
26                (KeyCode::Char('k') | KeyCode::Up, _) => {
27                    self.help_scroll_offset = self.help_scroll_offset.saturating_sub(1);
28                    None
29                }
30                (KeyCode::Char('d'), KeyModifiers::CONTROL) => {
31                    self.help_scroll_offset = self.help_scroll_offset.saturating_add(8);
32                    None
33                }
34                (KeyCode::Char('u'), KeyModifiers::CONTROL) => {
35                    self.help_scroll_offset = self.help_scroll_offset.saturating_sub(8);
36                    None
37                }
38                _ => None,
39            };
40        }
41
42        if self.command_palette.visible {
43            match (key.code, key.modifiers) {
44                (KeyCode::Enter, _) => return self.command_palette.confirm(),
45                (KeyCode::Esc, _) => return Some(Action::CloseCommandPalette),
46                (KeyCode::Backspace, _) => {
47                    self.command_palette.on_backspace();
48                    return None;
49                }
50                (KeyCode::Down, _) | (KeyCode::Char('n'), KeyModifiers::CONTROL) => {
51                    self.command_palette.select_next();
52                    return None;
53                }
54                (KeyCode::Up, _) | (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
55                    self.command_palette.select_prev();
56                    return None;
57                }
58                (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
59                    self.command_palette.on_char(c);
60                    return None;
61                }
62                _ => return None,
63            }
64        }
65
66        // Route keys to search bar when active
67        if self.search_bar.active {
68            match (key.code, key.modifiers) {
69                (KeyCode::Enter, _) => return Some(Action::SubmitSearch),
70                (KeyCode::Tab, _) => return Some(Action::CycleSearchMode),
71                (KeyCode::Esc, _) => return Some(Action::CloseSearch),
72                (KeyCode::Backspace, _) => {
73                    self.search_bar.on_backspace();
74                    // Live filter as you type
75                    self.trigger_live_search();
76                    return None;
77                }
78                (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
79                    self.search_bar.on_char(c);
80                    // Live filter as you type
81                    self.trigger_live_search();
82                    return None;
83                }
84                _ => return None,
85            }
86        }
87
88        // Route keys to send confirmation prompt
89        if self.pending_send_confirm.is_some() {
90            match (key.code, key.modifiers) {
91                (KeyCode::Char('s'), KeyModifiers::NONE) => {
92                    // Send
93                    if let Some(pending) = self.pending_send_confirm.take() {
94                        if !pending.allow_send {
95                            self.pending_send_confirm = Some(pending);
96                            return None;
97                        }
98                        let parse_addrs = |s: &str| mxr_compose::parse::parse_address_list(s);
99                        let reply_headers = pending.fm.in_reply_to.as_ref().map(|in_reply_to| {
100                            mxr_core::types::ReplyHeaders {
101                                in_reply_to: in_reply_to.clone(),
102                                references: pending.fm.references.clone(),
103                            }
104                        });
105                        let account_id = self
106                            .envelopes
107                            .first()
108                            .or(self.all_envelopes.first())
109                            .map(|e| e.account_id.clone())
110                            .unwrap_or_default();
111                        let now = chrono::Utc::now();
112                        let draft = mxr_core::Draft {
113                            id: mxr_core::id::DraftId::new(),
114                            account_id,
115                            reply_headers,
116                            to: parse_addrs(&pending.fm.to),
117                            cc: parse_addrs(&pending.fm.cc),
118                            bcc: parse_addrs(&pending.fm.bcc),
119                            subject: pending.fm.subject,
120                            body_markdown: pending.body,
121                            attachments: pending
122                                .fm
123                                .attach
124                                .iter()
125                                .map(std::path::PathBuf::from)
126                                .collect(),
127                            created_at: now,
128                            updated_at: now,
129                        };
130                        self.queue_mutation(
131                            Request::SendDraft { draft },
132                            MutationEffect::StatusOnly("Sent!".into()),
133                            "Sending...".into(),
134                        );
135                        let _ = std::fs::remove_file(&pending.draft_path);
136                    }
137                    return None;
138                }
139                (KeyCode::Char('d'), KeyModifiers::NONE) => {
140                    // Save as draft to mail server
141                    if let Some(pending) = self.pending_send_confirm.take() {
142                        if !pending.allow_send {
143                            self.pending_send_confirm = Some(pending);
144                            return None;
145                        }
146                        let parse_addrs = |s: &str| mxr_compose::parse::parse_address_list(s);
147                        let reply_headers = pending.fm.in_reply_to.as_ref().map(|in_reply_to| {
148                            mxr_core::types::ReplyHeaders {
149                                in_reply_to: in_reply_to.clone(),
150                                references: pending.fm.references.clone(),
151                            }
152                        });
153                        let account_id = self
154                            .envelopes
155                            .first()
156                            .or(self.all_envelopes.first())
157                            .map(|e| e.account_id.clone())
158                            .unwrap_or_default();
159                        let now = chrono::Utc::now();
160                        let draft = mxr_core::Draft {
161                            id: mxr_core::id::DraftId::new(),
162                            account_id,
163                            reply_headers,
164                            to: parse_addrs(&pending.fm.to),
165                            cc: parse_addrs(&pending.fm.cc),
166                            bcc: parse_addrs(&pending.fm.bcc),
167                            subject: pending.fm.subject,
168                            body_markdown: pending.body,
169                            attachments: pending
170                                .fm
171                                .attach
172                                .iter()
173                                .map(std::path::PathBuf::from)
174                                .collect(),
175                            created_at: now,
176                            updated_at: now,
177                        };
178                        self.queue_mutation(
179                            Request::SaveDraftToServer { draft },
180                            MutationEffect::StatusOnly("Draft saved to server".into()),
181                            "Saving draft...".into(),
182                        );
183                        let _ = std::fs::remove_file(&pending.draft_path);
184                    }
185                    return None;
186                }
187                (KeyCode::Char('e'), KeyModifiers::NONE) => {
188                    // Edit again — reopen editor
189                    if let Some(pending) = self.pending_send_confirm.take() {
190                        self.pending_compose = Some(ComposeAction::EditDraft(pending.draft_path));
191                    }
192                    return None;
193                }
194                (KeyCode::Esc, _) => {
195                    // Discard
196                    if let Some(pending) = self.pending_send_confirm.take() {
197                        let _ = std::fs::remove_file(&pending.draft_path);
198                        self.status_message = Some("Discarded".into());
199                    }
200                    return None;
201                }
202                _ => return None,
203            }
204        }
205
206        if self.pending_bulk_confirm.is_some() {
207            return match (key.code, key.modifiers) {
208                (KeyCode::Enter, _)
209                | (KeyCode::Char('y'), KeyModifiers::NONE)
210                | (KeyCode::Char('Y'), KeyModifiers::SHIFT) => Some(Action::OpenSelected),
211                (KeyCode::Esc, _) | (KeyCode::Char('n'), KeyModifiers::NONE) => {
212                    self.pending_bulk_confirm = None;
213                    self.status_message = Some("Bulk action cancelled".into());
214                    None
215                }
216                _ => None,
217            };
218        }
219
220        if self.pending_unsubscribe_confirm.is_some() {
221            return match (key.code, key.modifiers) {
222                (KeyCode::Enter, _)
223                | (KeyCode::Char('u'), KeyModifiers::NONE)
224                | (KeyCode::Char('U'), KeyModifiers::SHIFT) => Some(Action::ConfirmUnsubscribeOnly),
225                (KeyCode::Char('a'), KeyModifiers::NONE)
226                | (KeyCode::Char('A'), KeyModifiers::SHIFT) => {
227                    Some(Action::ConfirmUnsubscribeAndArchiveSender)
228                }
229                (KeyCode::Esc, _) => Some(Action::CancelUnsubscribe),
230                _ => None,
231            };
232        }
233
234        if self.snooze_panel.visible {
235            match (key.code, key.modifiers) {
236                (KeyCode::Enter, _) => return Some(Action::Snooze),
237                (KeyCode::Esc, _) => {
238                    self.snooze_panel.visible = false;
239                    return None;
240                }
241                (KeyCode::Char('j') | KeyCode::Down, _) => {
242                    self.snooze_panel.selected_index =
243                        (self.snooze_panel.selected_index + 1) % snooze_presets().len();
244                    return None;
245                }
246                (KeyCode::Char('k') | KeyCode::Up, _) => {
247                    self.snooze_panel.selected_index = self
248                        .snooze_panel
249                        .selected_index
250                        .checked_sub(1)
251                        .unwrap_or(snooze_presets().len() - 1);
252                    return None;
253                }
254                _ => return None,
255            }
256        }
257
258        // Route keys to URL modal when active
259        if let Some(ref mut url_state) = self.url_modal {
260            match (key.code, key.modifiers) {
261                (KeyCode::Enter | KeyCode::Char('o'), _) => {
262                    if let Some(url) = url_state.selected_url().map(|s| s.to_string()) {
263                        ui::url_modal::open_url(&url);
264                        self.status_message = Some(format!("Opening {url}"));
265                    }
266                    self.url_modal = None;
267                    return None;
268                }
269                (KeyCode::Char('y'), _) => {
270                    if let Some(url) = url_state.selected_url().map(|s| s.to_string()) {
271                        // Copy to clipboard via pbcopy (macOS) or xclip (Linux)
272                        #[cfg(target_os = "macos")]
273                        {
274                            use std::io::Write;
275                            if let Ok(mut child) = std::process::Command::new("pbcopy")
276                                .stdin(std::process::Stdio::piped())
277                                .spawn()
278                            {
279                                if let Some(mut stdin) = child.stdin.take() {
280                                    let _ = stdin.write_all(url.as_bytes());
281                                }
282                                let _ = child.wait();
283                            }
284                        }
285                        #[cfg(target_os = "linux")]
286                        {
287                            use std::io::Write;
288                            if let Ok(mut child) = std::process::Command::new("xclip")
289                                .args(["-selection", "clipboard"])
290                                .stdin(std::process::Stdio::piped())
291                                .spawn()
292                            {
293                                if let Some(mut stdin) = child.stdin.take() {
294                                    let _ = stdin.write_all(url.as_bytes());
295                                }
296                                let _ = child.wait();
297                            }
298                        }
299                        self.status_message = Some(format!("Copied: {url}"));
300                    }
301                    self.url_modal = None;
302                    return None;
303                }
304                (KeyCode::Char('j') | KeyCode::Down, _) => {
305                    url_state.next();
306                    return None;
307                }
308                (KeyCode::Char('k') | KeyCode::Up, _) => {
309                    url_state.prev();
310                    return None;
311                }
312                (KeyCode::Esc | KeyCode::Char('q'), _) => {
313                    self.url_modal = None;
314                    return None;
315                }
316                _ => return None,
317            }
318        }
319
320        // Route keys to compose picker when active
321        if self.attachment_panel.visible {
322            match (key.code, key.modifiers) {
323                (KeyCode::Enter | KeyCode::Char('o'), _) => {
324                    self.queue_attachment_action(AttachmentOperation::Open);
325                    return None;
326                }
327                (KeyCode::Char('d'), _) => {
328                    self.queue_attachment_action(AttachmentOperation::Download);
329                    return None;
330                }
331                (KeyCode::Char('j') | KeyCode::Down, _) => {
332                    if self.attachment_panel.selected_index + 1
333                        < self.attachment_panel.attachments.len()
334                    {
335                        self.attachment_panel.selected_index += 1;
336                    }
337                    return None;
338                }
339                (KeyCode::Char('k') | KeyCode::Up, _) => {
340                    self.attachment_panel.selected_index =
341                        self.attachment_panel.selected_index.saturating_sub(1);
342                    return None;
343                }
344                (KeyCode::Esc | KeyCode::Char('A'), _) => {
345                    self.close_attachment_panel();
346                    return None;
347                }
348                _ => return None,
349            }
350        }
351
352        // Route keys to compose picker when active
353        if self.compose_picker.visible {
354            match (key.code, key.modifiers) {
355                (KeyCode::Enter, _) => {
356                    // Confirm all recipients and trigger compose
357                    let to = self.compose_picker.confirm();
358                    if to.is_empty() {
359                        self.pending_compose = Some(ComposeAction::New);
360                    } else {
361                        self.pending_compose = Some(ComposeAction::NewWithTo(to));
362                    }
363                    return None;
364                }
365                (KeyCode::Tab, _) => {
366                    // Tab adds selected contact to recipients
367                    self.compose_picker.add_recipient();
368                    return None;
369                }
370                (KeyCode::Esc, _) => {
371                    self.compose_picker.close();
372                    return None;
373                }
374                (KeyCode::Backspace, _) => {
375                    self.compose_picker.on_backspace();
376                    return None;
377                }
378                (KeyCode::Down, _) | (KeyCode::Char('n'), KeyModifiers::CONTROL) => {
379                    self.compose_picker.select_next();
380                    return None;
381                }
382                (KeyCode::Up, _) | (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
383                    self.compose_picker.select_prev();
384                    return None;
385                }
386                (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
387                    self.compose_picker.on_char(c);
388                    return None;
389                }
390                _ => return None,
391            }
392        }
393
394        // Route keys to label picker when active
395        if self.label_picker.visible {
396            match (key.code, key.modifiers) {
397                (KeyCode::Enter, _) => {
398                    let mode = self.label_picker.mode;
399                    if let Some(label_name) = self.label_picker.confirm() {
400                        self.pending_label_action = Some((mode, label_name));
401                        return match mode {
402                            LabelPickerMode::Apply => Some(Action::ApplyLabel),
403                            LabelPickerMode::Move => Some(Action::MoveToLabel),
404                        };
405                    }
406                    return None;
407                }
408                (KeyCode::Esc, _) => {
409                    self.label_picker.close();
410                    return None;
411                }
412                (KeyCode::Backspace, _) => {
413                    self.label_picker.on_backspace();
414                    return None;
415                }
416                (KeyCode::Down, _) | (KeyCode::Char('n'), KeyModifiers::CONTROL) => {
417                    self.label_picker.select_next();
418                    return None;
419                }
420                (KeyCode::Up, _) | (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
421                    self.label_picker.select_prev();
422                    return None;
423                }
424                (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
425                    self.label_picker.on_char(c);
426                    return None;
427                }
428                _ => return None,
429            }
430        }
431
432        if self.screen != Screen::Mailbox {
433            return self.handle_screen_key(key);
434        }
435
436        // Route keys based on active pane
437        match self.active_pane {
438            ActivePane::MessageView => match (key.code, key.modifiers) {
439                (KeyCode::Char('j') | KeyCode::Down, _) => {
440                    self.move_thread_focus_down();
441                    None
442                }
443                (KeyCode::Char('k') | KeyCode::Up, _) => {
444                    self.move_thread_focus_up();
445                    None
446                }
447                (KeyCode::Char('d'), KeyModifiers::CONTROL) => {
448                    self.message_scroll_offset = self.message_scroll_offset.saturating_add(20);
449                    None
450                }
451                (KeyCode::Char('u'), KeyModifiers::CONTROL) => {
452                    self.message_scroll_offset = self.message_scroll_offset.saturating_sub(20);
453                    None
454                }
455                (KeyCode::Char('G'), KeyModifiers::SHIFT) => {
456                    self.message_scroll_offset = u16::MAX;
457                    None
458                }
459                // h = move left to mail list
460                (KeyCode::Char('h') | KeyCode::Left, KeyModifiers::NONE) => {
461                    self.active_pane = ActivePane::MailList;
462                    None
463                }
464                // o = open in browser (message already open in pane)
465                (KeyCode::Char('o'), KeyModifiers::NONE) => Some(Action::OpenInBrowser),
466                // L = open links picker
467                (KeyCode::Char('L'), KeyModifiers::SHIFT) => Some(Action::OpenLinks),
468                _ => self.input.handle_key(key),
469            },
470            ActivePane::Sidebar => match (key.code, key.modifiers) {
471                (KeyCode::Char('j') | KeyCode::Down, _) => {
472                    self.sidebar_move_down();
473                    None
474                }
475                (KeyCode::Char('k') | KeyCode::Up, _) => {
476                    self.sidebar_move_up();
477                    None
478                }
479                (KeyCode::Char('['), _) => {
480                    self.collapse_current_sidebar_section();
481                    None
482                }
483                (KeyCode::Char(']'), _) => {
484                    self.expand_current_sidebar_section();
485                    None
486                }
487                (KeyCode::Enter | KeyCode::Char('o'), _) => self.sidebar_select(),
488                // l = select label and move to mail list
489                (KeyCode::Char('l') | KeyCode::Right, KeyModifiers::NONE) => self.sidebar_select(),
490                _ => self.input.handle_key(key),
491            },
492            ActivePane::MailList => match (key.code, key.modifiers) {
493                // h = move left to sidebar
494                (KeyCode::Char('h') | KeyCode::Left, KeyModifiers::NONE) => {
495                    self.active_pane = ActivePane::Sidebar;
496                    None
497                }
498                // Right arrow opens selected message
499                (KeyCode::Right, KeyModifiers::NONE) => Some(Action::OpenSelected),
500                _ => self.input.handle_key(key),
501            },
502        }
503    }
504
505    fn handle_screen_key(&mut self, key: crossterm::event::KeyEvent) -> Option<Action> {
506        match self.screen {
507            Screen::Search => self.handle_search_screen_key(key),
508            Screen::Rules => self.handle_rules_screen_key(key),
509            Screen::Diagnostics => self.handle_diagnostics_screen_key(key),
510            Screen::Accounts => self.handle_accounts_screen_key(key),
511            Screen::Mailbox => None,
512        }
513    }
514
515    fn handle_search_screen_key(&mut self, key: crossterm::event::KeyEvent) -> Option<Action> {
516        if self.search_page.editing {
517            return match (key.code, key.modifiers) {
518                (KeyCode::Enter, _) => Some(Action::SubmitSearch),
519                (KeyCode::Esc, _) => {
520                    self.search_page.editing = false;
521                    None
522                }
523                (KeyCode::Backspace, _) => {
524                    self.search_page.query.pop();
525                    self.trigger_live_search();
526                    None
527                }
528                (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
529                    self.search_page.query.push(c);
530                    self.trigger_live_search();
531                    None
532                }
533                _ => None,
534            };
535        }
536
537        match self.search_page.active_pane {
538            SearchPane::Results => match (key.code, key.modifiers) {
539                (KeyCode::Char('/'), _) => {
540                    self.search_page.editing = true;
541                    None
542                }
543                (KeyCode::Char('j') | KeyCode::Down, _) => {
544                    if self.search_page.selected_index + 1 < self.search_row_count() {
545                        self.search_page.selected_index += 1;
546                        self.ensure_search_visible();
547                        self.auto_preview_search();
548                    }
549                    self.maybe_load_more_search_results();
550                    None
551                }
552                (KeyCode::Char('k') | KeyCode::Up, _) => {
553                    if self.search_page.selected_index > 0 {
554                        self.search_page.selected_index -= 1;
555                        self.ensure_search_visible();
556                        self.auto_preview_search();
557                    }
558                    None
559                }
560                (KeyCode::Char('l') | KeyCode::Right, KeyModifiers::NONE)
561                | (KeyCode::Enter | KeyCode::Char('o'), _) => Some(Action::OpenSelected),
562                (KeyCode::Esc, _) => Some(Action::GoToInbox),
563                _ => self.input.handle_key(key),
564            },
565            SearchPane::Preview => match (key.code, key.modifiers) {
566                (KeyCode::Char('/'), _) => {
567                    self.search_page.editing = true;
568                    None
569                }
570                (KeyCode::Char('h') | KeyCode::Left, KeyModifiers::NONE) => {
571                    self.search_page.active_pane = SearchPane::Results;
572                    None
573                }
574                (KeyCode::Char('j') | KeyCode::Down, _) => {
575                    self.move_thread_focus_down();
576                    None
577                }
578                (KeyCode::Char('k') | KeyCode::Up, _) => {
579                    self.move_thread_focus_up();
580                    None
581                }
582                (KeyCode::Char('d'), KeyModifiers::CONTROL) => {
583                    self.message_scroll_offset = self.message_scroll_offset.saturating_add(20);
584                    None
585                }
586                (KeyCode::Char('u'), KeyModifiers::CONTROL) => {
587                    self.message_scroll_offset = self.message_scroll_offset.saturating_sub(20);
588                    None
589                }
590                (KeyCode::Char('G'), KeyModifiers::SHIFT) => {
591                    self.message_scroll_offset = u16::MAX;
592                    None
593                }
594                (KeyCode::Char('o'), KeyModifiers::NONE) => Some(Action::OpenInBrowser),
595                (KeyCode::Char('L'), KeyModifiers::SHIFT) => Some(Action::OpenLinks),
596                (KeyCode::Esc, _) => {
597                    self.search_page.active_pane = SearchPane::Results;
598                    None
599                }
600                _ => self.input.handle_key(key),
601            },
602        }
603    }
604
605    fn handle_rules_screen_key(&mut self, key: crossterm::event::KeyEvent) -> Option<Action> {
606        if self.rules_page.form.visible {
607            return self.handle_rule_form_key(key);
608        }
609
610        match (key.code, key.modifiers) {
611            (KeyCode::Char('j') | KeyCode::Down, _) => {
612                if self.rules_page.selected_index + 1 < self.rules_page.rules.len() {
613                    self.rules_page.selected_index += 1;
614                }
615                None
616            }
617            (KeyCode::Char('k') | KeyCode::Up, _) => {
618                self.rules_page.selected_index = self.rules_page.selected_index.saturating_sub(1);
619                None
620            }
621            (KeyCode::Enter | KeyCode::Char('o'), _) => Some(Action::RefreshRules),
622            (KeyCode::Char('e'), _) => Some(Action::ToggleRuleEnabled),
623            (KeyCode::Char('D'), KeyModifiers::SHIFT) => Some(Action::ShowRuleDryRun),
624            (KeyCode::Char('H'), KeyModifiers::SHIFT) => Some(Action::ShowRuleHistory),
625            (KeyCode::Char('#'), _) => Some(Action::DeleteRule),
626            (KeyCode::Char('n'), _) => Some(Action::OpenRuleFormNew),
627            (KeyCode::Char('E'), KeyModifiers::SHIFT) => Some(Action::OpenRuleFormEdit),
628            (KeyCode::Esc, _) => Some(Action::OpenMailboxScreen),
629            _ => self.input.handle_key(key),
630        }
631    }
632
633    fn handle_rule_form_key(&mut self, key: crossterm::event::KeyEvent) -> Option<Action> {
634        match (key.code, key.modifiers) {
635            (KeyCode::Esc, _) => {
636                self.rules_page.form.visible = false;
637                self.rules_page.panel = RulesPanel::Details;
638                None
639            }
640            (KeyCode::Tab, _) => {
641                self.rules_page.form.active_field = (self.rules_page.form.active_field + 1) % 5;
642                None
643            }
644            (KeyCode::BackTab, _) => {
645                self.rules_page.form.active_field =
646                    self.rules_page.form.active_field.saturating_sub(1);
647                None
648            }
649            (KeyCode::Enter, _) => Some(Action::SaveRuleForm),
650            (KeyCode::Char(' '), _) if self.rules_page.form.active_field == 4 => {
651                self.rules_page.form.enabled = !self.rules_page.form.enabled;
652                None
653            }
654            (KeyCode::Backspace, _) => {
655                match self.rules_page.form.active_field {
656                    0 => {
657                        self.rules_page.form.name.pop();
658                    }
659                    1 => {
660                        self.rules_page.form.condition.pop();
661                    }
662                    2 => {
663                        self.rules_page.form.action.pop();
664                    }
665                    3 => {
666                        self.rules_page.form.priority.pop();
667                    }
668                    _ => {}
669                }
670                None
671            }
672            (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
673                match self.rules_page.form.active_field {
674                    0 => self.rules_page.form.name.push(c),
675                    1 => self.rules_page.form.condition.push(c),
676                    2 => self.rules_page.form.action.push(c),
677                    3 => self.rules_page.form.priority.push(c),
678                    _ => {}
679                }
680                None
681            }
682            _ => None,
683        }
684    }
685
686    fn handle_diagnostics_screen_key(&mut self, key: crossterm::event::KeyEvent) -> Option<Action> {
687        match (key.code, key.modifiers) {
688            (KeyCode::Tab | KeyCode::Right, _) => {
689                self.diagnostics_page.selected_pane = self.diagnostics_page.selected_pane.next();
690                None
691            }
692            (KeyCode::BackTab | KeyCode::Left, _) => {
693                self.diagnostics_page.selected_pane = self.diagnostics_page.selected_pane.prev();
694                None
695            }
696            (KeyCode::Char('j') | KeyCode::Down, _) => {
697                let pane = self.diagnostics_page.active_pane();
698                *self.diagnostics_page.scroll_offset_mut(pane) =
699                    self.diagnostics_page.scroll_offset(pane).saturating_add(1);
700                None
701            }
702            (KeyCode::Char('k') | KeyCode::Up, _) => {
703                let pane = self.diagnostics_page.active_pane();
704                *self.diagnostics_page.scroll_offset_mut(pane) =
705                    self.diagnostics_page.scroll_offset(pane).saturating_sub(1);
706                None
707            }
708            (KeyCode::Char('d'), KeyModifiers::CONTROL) => {
709                let pane = self.diagnostics_page.active_pane();
710                *self.diagnostics_page.scroll_offset_mut(pane) =
711                    self.diagnostics_page.scroll_offset(pane).saturating_add(8);
712                None
713            }
714            (KeyCode::Char('u'), KeyModifiers::CONTROL) => {
715                let pane = self.diagnostics_page.active_pane();
716                *self.diagnostics_page.scroll_offset_mut(pane) =
717                    self.diagnostics_page.scroll_offset(pane).saturating_sub(8);
718                None
719            }
720            (KeyCode::Enter | KeyCode::Char('o'), _) => {
721                self.diagnostics_page.toggle_fullscreen();
722                None
723            }
724            (KeyCode::Char('d'), _) => Some(Action::OpenDiagnosticsPaneDetails),
725            (KeyCode::Char('r'), _) => Some(Action::RefreshDiagnostics),
726            (KeyCode::Char('b'), _) => Some(Action::GenerateBugReport),
727            (KeyCode::Char('L'), KeyModifiers::SHIFT) => Some(Action::OpenLogs),
728            (KeyCode::Esc, _) => Some(Action::OpenMailboxScreen),
729            _ => self.input.handle_key(key),
730        }
731    }
732
733    fn handle_accounts_screen_key(&mut self, key: crossterm::event::KeyEvent) -> Option<Action> {
734        if self.accounts_page.onboarding_modal_open {
735            return match (key.code, key.modifiers) {
736                (KeyCode::Enter | KeyCode::Char(' '), _) => {
737                    self.complete_account_setup_onboarding();
738                    None
739                }
740                (KeyCode::Char('q'), _) => Some(Action::QuitView),
741                (KeyCode::Esc, _) => {
742                    self.accounts_page.onboarding_modal_open = false;
743                    None
744                }
745                _ => None,
746            };
747        }
748
749        if self.accounts_page.form.visible {
750            return self.handle_account_form_key(key);
751        }
752
753        match (key.code, key.modifiers) {
754            (KeyCode::Char('j') | KeyCode::Down, _) => {
755                if self.accounts_page.selected_index + 1 < self.accounts_page.accounts.len() {
756                    self.accounts_page.selected_index += 1;
757                }
758                None
759            }
760            (KeyCode::Char('k') | KeyCode::Up, _) => {
761                self.accounts_page.selected_index =
762                    self.accounts_page.selected_index.saturating_sub(1);
763                None
764            }
765            (KeyCode::Char('n'), _) => Some(Action::OpenAccountFormNew),
766            (KeyCode::Char('r'), _) => Some(Action::RefreshAccounts),
767            (KeyCode::Char('t'), _) => Some(Action::TestAccountForm),
768            (KeyCode::Char('d'), _) => Some(Action::SetDefaultAccount),
769            (KeyCode::Enter | KeyCode::Char('o'), _) => {
770                if let Some(account) = self.selected_account().cloned() {
771                    if let Some(config) = account_summary_to_config(&account) {
772                        self.accounts_page.form = account_form_from_config(config);
773                        self.accounts_page.form.visible = true;
774                    } else {
775                        self.accounts_page.status = Some(
776                            "Runtime-only account is inspectable but not editable here.".into(),
777                        );
778                    }
779                }
780                None
781            }
782            (KeyCode::Esc, _) if self.accounts_page.onboarding_required => None,
783            (KeyCode::Esc, _) => Some(Action::OpenMailboxScreen),
784            _ => self.input.handle_key(key),
785        }
786    }
787
788    fn handle_account_form_key(&mut self, key: crossterm::event::KeyEvent) -> Option<Action> {
789        if self.accounts_page.form.pending_mode_switch.is_some() {
790            return match (key.code, key.modifiers) {
791                (KeyCode::Enter | KeyCode::Char('y'), _) => {
792                    if let Some(mode) = self.accounts_page.form.pending_mode_switch {
793                        self.apply_account_form_mode(mode);
794                    }
795                    None
796                }
797                (KeyCode::Esc | KeyCode::Char('n'), _) => {
798                    self.accounts_page.form.pending_mode_switch = None;
799                    None
800                }
801                _ => None,
802            };
803        }
804
805        if self.accounts_page.form.editing_field {
806            return match (key.code, key.modifiers) {
807                (KeyCode::Esc, _) | (KeyCode::Enter, _) => {
808                    self.accounts_page.form.editing_field = false;
809                    None
810                }
811                (KeyCode::Tab, _) => {
812                    self.accounts_page.form.editing_field = false;
813                    self.accounts_page.form.active_field = (self.accounts_page.form.active_field
814                        + 1)
815                        % self.account_form_field_count();
816                    self.accounts_page.form.field_cursor =
817                        account_form_field_value(&self.accounts_page.form)
818                            .map(|value| value.chars().count())
819                            .unwrap_or(0);
820                    None
821                }
822                (KeyCode::BackTab, _) => {
823                    self.accounts_page.form.editing_field = false;
824                    self.accounts_page.form.active_field =
825                        self.accounts_page.form.active_field.saturating_sub(1);
826                    self.accounts_page.form.field_cursor =
827                        account_form_field_value(&self.accounts_page.form)
828                            .map(|value| value.chars().count())
829                            .unwrap_or(0);
830                    None
831                }
832                (KeyCode::Left, _) => {
833                    self.accounts_page.form.field_cursor =
834                        self.accounts_page.form.field_cursor.saturating_sub(1);
835                    None
836                }
837                (KeyCode::Right, _) => {
838                    if let Some(value) = account_form_field_value(&self.accounts_page.form) {
839                        self.accounts_page.form.field_cursor =
840                            (self.accounts_page.form.field_cursor + 1).min(value.chars().count());
841                    }
842                    None
843                }
844                (KeyCode::Home, _) => {
845                    self.accounts_page.form.field_cursor = 0;
846                    None
847                }
848                (KeyCode::End, _) => {
849                    self.accounts_page.form.field_cursor =
850                        account_form_field_value(&self.accounts_page.form)
851                            .map(|value| value.chars().count())
852                            .unwrap_or(0);
853                    None
854                }
855                (KeyCode::Backspace, _) => {
856                    delete_account_form_char(&mut self.accounts_page.form, true);
857                    self.refresh_account_form_derived_fields();
858                    None
859                }
860                (KeyCode::Delete, _) => {
861                    delete_account_form_char(&mut self.accounts_page.form, false);
862                    self.refresh_account_form_derived_fields();
863                    None
864                }
865                (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
866                    insert_account_form_char(&mut self.accounts_page.form, c);
867                    self.refresh_account_form_derived_fields();
868                    None
869                }
870                _ => None,
871            };
872        }
873
874        match (key.code, key.modifiers) {
875            (KeyCode::Esc, _) => {
876                self.accounts_page.form.visible = false;
877                None
878            }
879            (KeyCode::Left | KeyCode::Char('h'), _) => {
880                self.request_account_form_mode_change(false);
881                None
882            }
883            (KeyCode::Right | KeyCode::Char('l'), _) => {
884                self.request_account_form_mode_change(true);
885                None
886            }
887            (KeyCode::Char('j') | KeyCode::Down, _) => {
888                self.accounts_page.form.active_field =
889                    (self.accounts_page.form.active_field + 1) % self.account_form_field_count();
890                None
891            }
892            (KeyCode::Char('k') | KeyCode::Up, _) => {
893                self.accounts_page.form.active_field = if self.accounts_page.form.active_field == 0
894                {
895                    self.account_form_field_count().saturating_sub(1)
896                } else {
897                    self.accounts_page.form.active_field - 1
898                };
899                None
900            }
901            (KeyCode::Tab, _) => {
902                if self.accounts_page.form.active_field == 0 {
903                    self.request_account_form_mode_change(true);
904                } else {
905                    self.accounts_page.form.active_field = (self.accounts_page.form.active_field
906                        + 1)
907                        % self.account_form_field_count();
908                }
909                None
910            }
911            (KeyCode::BackTab, _) => {
912                if self.accounts_page.form.active_field == 0 {
913                    self.request_account_form_mode_change(false);
914                } else {
915                    self.accounts_page.form.active_field =
916                        self.accounts_page.form.active_field.saturating_sub(1);
917                }
918                None
919            }
920            (KeyCode::Enter | KeyCode::Char('i'), _) => {
921                if account_form_field_is_editable(&self.accounts_page.form) {
922                    self.accounts_page.form.editing_field = true;
923                    self.accounts_page.form.field_cursor =
924                        account_form_field_value(&self.accounts_page.form)
925                            .map(|value| value.chars().count())
926                            .unwrap_or(0);
927                    None
928                } else if self.accounts_page.form.active_field == 0 {
929                    self.request_account_form_mode_change(true);
930                    None
931                } else if matches!(self.accounts_page.form.mode, AccountFormMode::Gmail)
932                    && self.accounts_page.form.active_field == 4
933                {
934                    self.accounts_page.form.gmail_credential_source = next_gmail_credential_source(
935                        self.accounts_page.form.gmail_credential_source.clone(),
936                        true,
937                    );
938                    self.accounts_page.form.active_field = self
939                        .accounts_page
940                        .form
941                        .active_field
942                        .min(self.account_form_field_count().saturating_sub(1));
943                    None
944                } else {
945                    None
946                }
947            }
948            (KeyCode::Char('t'), _) => Some(Action::TestAccountForm),
949            (KeyCode::Char('r'), _)
950                if matches!(self.accounts_page.form.mode, AccountFormMode::Gmail) =>
951            {
952                Some(Action::ReauthorizeAccountForm)
953            }
954            (KeyCode::Char('s'), _) => Some(Action::SaveAccountForm),
955            (KeyCode::Char(' '), _) if self.accounts_page.form.active_field == 0 => {
956                self.request_account_form_mode_change(true);
957                None
958            }
959            (KeyCode::Char(' '), _)
960                if matches!(self.accounts_page.form.mode, AccountFormMode::Gmail)
961                    && self.accounts_page.form.active_field == 4 =>
962            {
963                self.accounts_page.form.gmail_credential_source = next_gmail_credential_source(
964                    self.accounts_page.form.gmail_credential_source.clone(),
965                    true,
966                );
967                self.accounts_page.form.active_field = self
968                    .accounts_page
969                    .form
970                    .active_field
971                    .min(self.account_form_field_count().saturating_sub(1));
972                None
973            }
974            _ => None,
975        }
976    }
977}