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