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 (key.code, key.modifiers) {
538            (KeyCode::Char('/'), _) => {
539                self.search_page.editing = true;
540                None
541            }
542            (KeyCode::Char('j') | KeyCode::Down, _) => {
543                if self.search_page.selected_index + 1 < self.search_row_count() {
544                    self.search_page.selected_index += 1;
545                    self.ensure_search_visible();
546                    self.auto_preview_search();
547                }
548                None
549            }
550            (KeyCode::Char('k') | KeyCode::Up, _) => {
551                if self.search_page.selected_index > 0 {
552                    self.search_page.selected_index -= 1;
553                    self.ensure_search_visible();
554                    self.auto_preview_search();
555                }
556                None
557            }
558            (KeyCode::Enter | KeyCode::Char('o'), _) => {
559                if let Some(env) = self.selected_search_envelope().cloned() {
560                    self.open_envelope(env);
561                    self.screen = Screen::Mailbox;
562                    self.layout_mode = LayoutMode::ThreePane;
563                    self.active_pane = ActivePane::MessageView;
564                }
565                None
566            }
567            (KeyCode::Esc, _) => Some(Action::OpenMailboxScreen),
568            _ => self.input.handle_key(key),
569        }
570    }
571
572    fn handle_rules_screen_key(&mut self, key: crossterm::event::KeyEvent) -> Option<Action> {
573        if self.rules_page.form.visible {
574            return self.handle_rule_form_key(key);
575        }
576
577        match (key.code, key.modifiers) {
578            (KeyCode::Char('j') | KeyCode::Down, _) => {
579                if self.rules_page.selected_index + 1 < self.rules_page.rules.len() {
580                    self.rules_page.selected_index += 1;
581                }
582                None
583            }
584            (KeyCode::Char('k') | KeyCode::Up, _) => {
585                self.rules_page.selected_index = self.rules_page.selected_index.saturating_sub(1);
586                None
587            }
588            (KeyCode::Enter | KeyCode::Char('o'), _) => Some(Action::RefreshRules),
589            (KeyCode::Char('e'), _) => Some(Action::ToggleRuleEnabled),
590            (KeyCode::Char('D'), KeyModifiers::SHIFT) => Some(Action::ShowRuleDryRun),
591            (KeyCode::Char('H'), KeyModifiers::SHIFT) => Some(Action::ShowRuleHistory),
592            (KeyCode::Char('#'), _) => Some(Action::DeleteRule),
593            (KeyCode::Char('n'), _) => Some(Action::OpenRuleFormNew),
594            (KeyCode::Char('E'), KeyModifiers::SHIFT) => Some(Action::OpenRuleFormEdit),
595            (KeyCode::Esc, _) => Some(Action::OpenMailboxScreen),
596            _ => self.input.handle_key(key),
597        }
598    }
599
600    fn handle_rule_form_key(&mut self, key: crossterm::event::KeyEvent) -> Option<Action> {
601        match (key.code, key.modifiers) {
602            (KeyCode::Esc, _) => {
603                self.rules_page.form.visible = false;
604                self.rules_page.panel = RulesPanel::Details;
605                None
606            }
607            (KeyCode::Tab, _) => {
608                self.rules_page.form.active_field = (self.rules_page.form.active_field + 1) % 5;
609                None
610            }
611            (KeyCode::BackTab, _) => {
612                self.rules_page.form.active_field =
613                    self.rules_page.form.active_field.saturating_sub(1);
614                None
615            }
616            (KeyCode::Enter, _) => Some(Action::SaveRuleForm),
617            (KeyCode::Char(' '), _) if self.rules_page.form.active_field == 4 => {
618                self.rules_page.form.enabled = !self.rules_page.form.enabled;
619                None
620            }
621            (KeyCode::Backspace, _) => {
622                match self.rules_page.form.active_field {
623                    0 => {
624                        self.rules_page.form.name.pop();
625                    }
626                    1 => {
627                        self.rules_page.form.condition.pop();
628                    }
629                    2 => {
630                        self.rules_page.form.action.pop();
631                    }
632                    3 => {
633                        self.rules_page.form.priority.pop();
634                    }
635                    _ => {}
636                }
637                None
638            }
639            (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
640                match self.rules_page.form.active_field {
641                    0 => self.rules_page.form.name.push(c),
642                    1 => self.rules_page.form.condition.push(c),
643                    2 => self.rules_page.form.action.push(c),
644                    3 => self.rules_page.form.priority.push(c),
645                    _ => {}
646                }
647                None
648            }
649            _ => None,
650        }
651    }
652
653    fn handle_diagnostics_screen_key(&mut self, key: crossterm::event::KeyEvent) -> Option<Action> {
654        match (key.code, key.modifiers) {
655            (KeyCode::Tab | KeyCode::Right, _) => {
656                self.diagnostics_page.selected_pane = self.diagnostics_page.selected_pane.next();
657                None
658            }
659            (KeyCode::BackTab | KeyCode::Left, _) => {
660                self.diagnostics_page.selected_pane = self.diagnostics_page.selected_pane.prev();
661                None
662            }
663            (KeyCode::Char('j') | KeyCode::Down, _) => {
664                let pane = self.diagnostics_page.active_pane();
665                *self.diagnostics_page.scroll_offset_mut(pane) =
666                    self.diagnostics_page.scroll_offset(pane).saturating_add(1);
667                None
668            }
669            (KeyCode::Char('k') | KeyCode::Up, _) => {
670                let pane = self.diagnostics_page.active_pane();
671                *self.diagnostics_page.scroll_offset_mut(pane) =
672                    self.diagnostics_page.scroll_offset(pane).saturating_sub(1);
673                None
674            }
675            (KeyCode::Char('d'), KeyModifiers::CONTROL) => {
676                let pane = self.diagnostics_page.active_pane();
677                *self.diagnostics_page.scroll_offset_mut(pane) =
678                    self.diagnostics_page.scroll_offset(pane).saturating_add(8);
679                None
680            }
681            (KeyCode::Char('u'), KeyModifiers::CONTROL) => {
682                let pane = self.diagnostics_page.active_pane();
683                *self.diagnostics_page.scroll_offset_mut(pane) =
684                    self.diagnostics_page.scroll_offset(pane).saturating_sub(8);
685                None
686            }
687            (KeyCode::Enter | KeyCode::Char('o'), _) => {
688                self.diagnostics_page.toggle_fullscreen();
689                None
690            }
691            (KeyCode::Char('d'), _) => Some(Action::OpenDiagnosticsPaneDetails),
692            (KeyCode::Char('r'), _) => Some(Action::RefreshDiagnostics),
693            (KeyCode::Char('b'), _) => Some(Action::GenerateBugReport),
694            (KeyCode::Char('L'), KeyModifiers::SHIFT) => Some(Action::OpenLogs),
695            (KeyCode::Esc, _) => Some(Action::OpenMailboxScreen),
696            _ => self.input.handle_key(key),
697        }
698    }
699
700    fn handle_accounts_screen_key(&mut self, key: crossterm::event::KeyEvent) -> Option<Action> {
701        if self.accounts_page.onboarding_modal_open {
702            return match (key.code, key.modifiers) {
703                (KeyCode::Enter | KeyCode::Char(' '), _) => {
704                    self.complete_account_setup_onboarding();
705                    None
706                }
707                (KeyCode::Char('q'), _) => Some(Action::QuitView),
708                (KeyCode::Esc, _) => {
709                    self.accounts_page.onboarding_modal_open = false;
710                    None
711                }
712                _ => None,
713            };
714        }
715
716        if self.accounts_page.form.visible {
717            return self.handle_account_form_key(key);
718        }
719
720        match (key.code, key.modifiers) {
721            (KeyCode::Char('j') | KeyCode::Down, _) => {
722                if self.accounts_page.selected_index + 1 < self.accounts_page.accounts.len() {
723                    self.accounts_page.selected_index += 1;
724                }
725                None
726            }
727            (KeyCode::Char('k') | KeyCode::Up, _) => {
728                self.accounts_page.selected_index =
729                    self.accounts_page.selected_index.saturating_sub(1);
730                None
731            }
732            (KeyCode::Char('n'), _) => Some(Action::OpenAccountFormNew),
733            (KeyCode::Char('r'), _) => Some(Action::RefreshAccounts),
734            (KeyCode::Char('t'), _) => Some(Action::TestAccountForm),
735            (KeyCode::Char('d'), _) => Some(Action::SetDefaultAccount),
736            (KeyCode::Enter | KeyCode::Char('o'), _) => {
737                if let Some(account) = self.selected_account().cloned() {
738                    if let Some(config) = account_summary_to_config(&account) {
739                        self.accounts_page.form = account_form_from_config(config);
740                        self.accounts_page.form.visible = true;
741                    } else {
742                        self.accounts_page.status = Some(
743                            "Runtime-only account is inspectable but not editable here.".into(),
744                        );
745                    }
746                }
747                None
748            }
749            (KeyCode::Esc, _) if self.accounts_page.onboarding_required => None,
750            (KeyCode::Esc, _) => Some(Action::OpenMailboxScreen),
751            _ => self.input.handle_key(key),
752        }
753    }
754
755    fn handle_account_form_key(&mut self, key: crossterm::event::KeyEvent) -> Option<Action> {
756        if self.accounts_page.form.pending_mode_switch.is_some() {
757            return match (key.code, key.modifiers) {
758                (KeyCode::Enter | KeyCode::Char('y'), _) => {
759                    if let Some(mode) = self.accounts_page.form.pending_mode_switch {
760                        self.apply_account_form_mode(mode);
761                    }
762                    None
763                }
764                (KeyCode::Esc | KeyCode::Char('n'), _) => {
765                    self.accounts_page.form.pending_mode_switch = None;
766                    None
767                }
768                _ => None,
769            };
770        }
771
772        if self.accounts_page.form.editing_field {
773            return match (key.code, key.modifiers) {
774                (KeyCode::Esc, _) | (KeyCode::Enter, _) => {
775                    self.accounts_page.form.editing_field = false;
776                    None
777                }
778                (KeyCode::Tab, _) => {
779                    self.accounts_page.form.editing_field = false;
780                    self.accounts_page.form.active_field = (self.accounts_page.form.active_field
781                        + 1)
782                        % self.account_form_field_count();
783                    self.accounts_page.form.field_cursor =
784                        account_form_field_value(&self.accounts_page.form)
785                            .map(|value| value.chars().count())
786                            .unwrap_or(0);
787                    None
788                }
789                (KeyCode::BackTab, _) => {
790                    self.accounts_page.form.editing_field = false;
791                    self.accounts_page.form.active_field =
792                        self.accounts_page.form.active_field.saturating_sub(1);
793                    self.accounts_page.form.field_cursor =
794                        account_form_field_value(&self.accounts_page.form)
795                            .map(|value| value.chars().count())
796                            .unwrap_or(0);
797                    None
798                }
799                (KeyCode::Left, _) => {
800                    self.accounts_page.form.field_cursor =
801                        self.accounts_page.form.field_cursor.saturating_sub(1);
802                    None
803                }
804                (KeyCode::Right, _) => {
805                    if let Some(value) = account_form_field_value(&self.accounts_page.form) {
806                        self.accounts_page.form.field_cursor =
807                            (self.accounts_page.form.field_cursor + 1).min(value.chars().count());
808                    }
809                    None
810                }
811                (KeyCode::Home, _) => {
812                    self.accounts_page.form.field_cursor = 0;
813                    None
814                }
815                (KeyCode::End, _) => {
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::Backspace, _) => {
823                    delete_account_form_char(&mut self.accounts_page.form, true);
824                    self.refresh_account_form_derived_fields();
825                    None
826                }
827                (KeyCode::Delete, _) => {
828                    delete_account_form_char(&mut self.accounts_page.form, false);
829                    self.refresh_account_form_derived_fields();
830                    None
831                }
832                (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
833                    insert_account_form_char(&mut self.accounts_page.form, c);
834                    self.refresh_account_form_derived_fields();
835                    None
836                }
837                _ => None,
838            };
839        }
840
841        match (key.code, key.modifiers) {
842            (KeyCode::Esc, _) => {
843                self.accounts_page.form.visible = false;
844                None
845            }
846            (KeyCode::Left | KeyCode::Char('h'), _) => {
847                self.request_account_form_mode_change(false);
848                None
849            }
850            (KeyCode::Right | KeyCode::Char('l'), _) => {
851                self.request_account_form_mode_change(true);
852                None
853            }
854            (KeyCode::Char('j') | KeyCode::Down, _) => {
855                self.accounts_page.form.active_field =
856                    (self.accounts_page.form.active_field + 1) % self.account_form_field_count();
857                None
858            }
859            (KeyCode::Char('k') | KeyCode::Up, _) => {
860                self.accounts_page.form.active_field = if self.accounts_page.form.active_field == 0
861                {
862                    self.account_form_field_count().saturating_sub(1)
863                } else {
864                    self.accounts_page.form.active_field - 1
865                };
866                None
867            }
868            (KeyCode::Tab, _) => {
869                if self.accounts_page.form.active_field == 0 {
870                    self.request_account_form_mode_change(true);
871                } else {
872                    self.accounts_page.form.active_field = (self.accounts_page.form.active_field
873                        + 1)
874                        % self.account_form_field_count();
875                }
876                None
877            }
878            (KeyCode::BackTab, _) => {
879                if self.accounts_page.form.active_field == 0 {
880                    self.request_account_form_mode_change(false);
881                } else {
882                    self.accounts_page.form.active_field =
883                        self.accounts_page.form.active_field.saturating_sub(1);
884                }
885                None
886            }
887            (KeyCode::Enter | KeyCode::Char('i'), _) => {
888                if account_form_field_is_editable(&self.accounts_page.form) {
889                    self.accounts_page.form.editing_field = true;
890                    self.accounts_page.form.field_cursor =
891                        account_form_field_value(&self.accounts_page.form)
892                            .map(|value| value.chars().count())
893                            .unwrap_or(0);
894                    None
895                } else if self.accounts_page.form.active_field == 0 {
896                    self.request_account_form_mode_change(true);
897                    None
898                } else if matches!(self.accounts_page.form.mode, AccountFormMode::Gmail)
899                    && self.accounts_page.form.active_field == 4
900                {
901                    self.accounts_page.form.gmail_credential_source = next_gmail_credential_source(
902                        self.accounts_page.form.gmail_credential_source.clone(),
903                        true,
904                    );
905                    self.accounts_page.form.active_field = self
906                        .accounts_page
907                        .form
908                        .active_field
909                        .min(self.account_form_field_count().saturating_sub(1));
910                    None
911                } else {
912                    None
913                }
914            }
915            (KeyCode::Char('t'), _) => Some(Action::TestAccountForm),
916            (KeyCode::Char('r'), _)
917                if matches!(self.accounts_page.form.mode, AccountFormMode::Gmail) =>
918            {
919                Some(Action::ReauthorizeAccountForm)
920            }
921            (KeyCode::Char('s'), _) => Some(Action::SaveAccountForm),
922            (KeyCode::Char(' '), _) if self.accounts_page.form.active_field == 0 => {
923                self.request_account_form_mode_change(true);
924                None
925            }
926            (KeyCode::Char(' '), _)
927                if matches!(self.accounts_page.form.mode, AccountFormMode::Gmail)
928                    && self.accounts_page.form.active_field == 4 =>
929            {
930                self.accounts_page.form.gmail_credential_source = next_gmail_credential_source(
931                    self.accounts_page.form.gmail_credential_source.clone(),
932                    true,
933                );
934                self.accounts_page.form.active_field = self
935                    .accounts_page
936                    .form
937                    .active_field
938                    .min(self.account_form_field_count().saturating_sub(1));
939                None
940            }
941            _ => None,
942        }
943    }
944}