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