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