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