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