1use super::*;
2
3impl App {
4 pub fn tick(&mut self) {
5 self.input.check_timeout();
6 }
7
8 pub fn apply(&mut self, action: Action) {
9 self.status_message = None;
11
12 match action {
13 Action::OpenMailboxScreen => {
14 if self.accounts_page.onboarding_required {
15 self.screen = Screen::Accounts;
16 self.accounts_page.onboarding_modal_open =
17 self.accounts_page.accounts.is_empty() && !self.accounts_page.form.visible;
18 return;
19 }
20 self.screen = Screen::Mailbox;
21 self.active_pane = if self.layout_mode == LayoutMode::ThreePane {
22 ActivePane::MailList
23 } else {
24 self.active_pane
25 };
26 }
27 Action::OpenSearchScreen => {
28 self.screen = Screen::Search;
29 self.search_page.editing = true;
30 self.search_page.query = self.search_bar.query.clone();
31 self.search_page.results = if self.search_page.query.is_empty() {
32 self.all_envelopes.clone()
33 } else {
34 self.search_page.results.clone()
35 };
36 self.search_page.selected_index = 0;
37 self.search_page.scroll_offset = 0;
38 }
39 Action::OpenRulesScreen => {
40 self.screen = Screen::Rules;
41 self.rules_page.refresh_pending = true;
42 }
43 Action::OpenDiagnosticsScreen => {
44 self.screen = Screen::Diagnostics;
45 self.diagnostics_page.refresh_pending = true;
46 }
47 Action::OpenAccountsScreen => {
48 self.screen = Screen::Accounts;
49 self.accounts_page.refresh_pending = true;
50 }
51 Action::RefreshAccounts => {
52 self.accounts_page.refresh_pending = true;
53 }
54 Action::OpenAccountFormNew => {
55 self.accounts_page.form = AccountFormState::default();
56 self.accounts_page.form.visible = true;
57 self.accounts_page.onboarding_modal_open = false;
58 self.refresh_account_form_derived_fields();
59 self.screen = Screen::Accounts;
60 }
61 Action::SaveAccountForm => {
62 let is_default = self
63 .selected_account()
64 .is_some_and(|account| account.is_default)
65 || self.accounts_page.accounts.is_empty();
66 self.accounts_page.last_result = None;
67 self.accounts_page.form.last_result = None;
68 self.pending_account_save = Some(self.account_form_data(is_default));
69 self.accounts_page.status = Some("Saving account...".into());
70 }
71 Action::TestAccountForm => {
72 let account = if self.accounts_page.form.visible {
73 self.account_form_data(false)
74 } else if let Some(account) = self.selected_account_config() {
75 account
76 } else {
77 self.accounts_page.status = Some("No editable account selected.".into());
78 return;
79 };
80 self.accounts_page.last_result = None;
81 self.accounts_page.form.last_result = None;
82 self.pending_account_test = Some(account);
83 self.accounts_page.status = Some("Testing account...".into());
84 }
85 Action::ReauthorizeAccountForm => {
86 let account = self.account_form_data(false);
87 self.accounts_page.last_result = None;
88 self.accounts_page.form.last_result = None;
89 self.pending_account_authorize = Some((account, true));
90 self.accounts_page.status = Some("Authorizing Gmail account...".into());
91 }
92 Action::SetDefaultAccount => {
93 if let Some(key) = self
94 .selected_account()
95 .and_then(|account| account.key.clone())
96 {
97 self.pending_account_set_default = Some(key);
98 self.accounts_page.status = Some("Setting default account...".into());
99 } else {
100 self.accounts_page.status =
101 Some("Runtime-only account cannot be set default from TUI.".into());
102 }
103 }
104 Action::MoveDown => {
105 if self.screen == Screen::Search {
106 if self.search_page.selected_index + 1 < self.search_row_count() {
107 self.search_page.selected_index += 1;
108 self.ensure_search_visible();
109 self.auto_preview_search();
110 }
111 return;
112 }
113 if self.selected_index + 1 < self.mail_row_count() {
114 self.selected_index += 1;
115 }
116 self.ensure_visible();
117 self.update_visual_selection();
118 self.auto_preview();
119 }
120 Action::MoveUp => {
121 if self.screen == Screen::Search {
122 if self.search_page.selected_index > 0 {
123 self.search_page.selected_index -= 1;
124 self.ensure_search_visible();
125 self.auto_preview_search();
126 }
127 return;
128 }
129 if self.selected_index > 0 {
130 self.selected_index -= 1;
131 }
132 self.ensure_visible();
133 self.update_visual_selection();
134 self.auto_preview();
135 }
136 Action::JumpTop => {
137 self.selected_index = 0;
138 self.scroll_offset = 0;
139 self.auto_preview();
140 }
141 Action::JumpBottom => {
142 if self.mail_row_count() > 0 {
143 self.selected_index = self.mail_row_count() - 1;
144 }
145 self.ensure_visible();
146 self.auto_preview();
147 }
148 Action::PageDown => {
149 let page = self.visible_height.max(1);
150 self.selected_index =
151 (self.selected_index + page).min(self.mail_row_count().saturating_sub(1));
152 self.ensure_visible();
153 self.auto_preview();
154 }
155 Action::PageUp => {
156 let page = self.visible_height.max(1);
157 self.selected_index = self.selected_index.saturating_sub(page);
158 self.ensure_visible();
159 self.auto_preview();
160 }
161 Action::ViewportTop => {
162 self.selected_index = self.scroll_offset;
163 self.auto_preview();
164 }
165 Action::ViewportMiddle => {
166 let visible_height = 20;
167 self.selected_index = (self.scroll_offset + visible_height / 2)
168 .min(self.mail_row_count().saturating_sub(1));
169 self.auto_preview();
170 }
171 Action::ViewportBottom => {
172 let visible_height = 20;
173 self.selected_index = (self.scroll_offset + visible_height)
174 .min(self.mail_row_count().saturating_sub(1));
175 self.auto_preview();
176 }
177 Action::CenterCurrent => {
178 let visible_height = 20;
179 self.scroll_offset = self.selected_index.saturating_sub(visible_height / 2);
180 }
181 Action::SwitchPane => {
182 self.active_pane = match (self.layout_mode, self.active_pane) {
183 (LayoutMode::ThreePane, ActivePane::Sidebar) => ActivePane::MailList,
185 (LayoutMode::ThreePane, ActivePane::MailList) => ActivePane::MessageView,
186 (LayoutMode::ThreePane, ActivePane::MessageView) => ActivePane::Sidebar,
187 (_, ActivePane::Sidebar) => ActivePane::MailList,
189 (_, ActivePane::MailList) => ActivePane::Sidebar,
190 (_, ActivePane::MessageView) => ActivePane::Sidebar,
191 };
192 }
193 Action::OpenSelected => {
194 if let Some(pending) = self.pending_bulk_confirm.take() {
195 self.pending_mutation_queue
196 .push((pending.request, pending.effect));
197 self.status_message = Some(pending.status_message);
198 self.clear_selection();
199 return;
200 }
201 if self.screen == Screen::Search {
202 if let Some(env) = self.selected_search_envelope().cloned() {
203 self.open_envelope(env);
204 self.screen = Screen::Mailbox;
205 self.layout_mode = LayoutMode::ThreePane;
206 self.active_pane = ActivePane::MessageView;
207 }
208 return;
209 }
210 if self.mailbox_view == MailboxView::Subscriptions {
211 if let Some(entry) = self.selected_subscription_entry().cloned() {
212 self.open_envelope(entry.envelope);
213 self.layout_mode = LayoutMode::ThreePane;
214 self.active_pane = ActivePane::MessageView;
215 }
216 return;
217 }
218 if let Some(row) = self.selected_mail_row() {
219 self.open_envelope(row.representative);
220 self.layout_mode = LayoutMode::ThreePane;
221 self.active_pane = ActivePane::MessageView;
222 }
223 }
224 Action::Back => match self.active_pane {
225 _ if self.screen != Screen::Mailbox => {
226 self.screen = Screen::Mailbox;
227 }
228 ActivePane::MessageView => {
229 self.apply(Action::CloseMessageView);
230 }
231 ActivePane::MailList => {
232 if !self.selected_set.is_empty() {
233 self.apply(Action::ClearSelection);
234 } else if self.search_active {
235 self.apply(Action::CloseSearch);
236 } else if self.active_label.is_some() {
237 self.apply(Action::ClearFilter);
238 } else if self.layout_mode == LayoutMode::ThreePane {
239 self.apply(Action::CloseMessageView);
240 }
241 }
242 ActivePane::Sidebar => {}
243 },
244 Action::QuitView => {
245 self.should_quit = true;
246 }
247 Action::ClearSelection => {
248 self.clear_selection();
249 self.status_message = Some("Selection cleared".into());
250 }
251 Action::OpenSearch => {
253 if self.search_active {
254 self.search_bar.activate_existing();
255 } else {
256 self.search_bar.activate();
257 }
258 }
259 Action::SubmitSearch => {
260 if self.screen == Screen::Search {
261 self.search_page.editing = false;
262 self.pending_search = Some(self.search_page.query.clone());
263 } else {
264 let query = self.search_bar.query.clone();
265 self.search_bar.deactivate();
266 if !query.is_empty() {
267 self.pending_search = Some(query);
268 self.search_active = true;
269 }
270 self.active_pane = ActivePane::MailList;
272 }
273 }
274 Action::CloseSearch => {
275 self.search_bar.deactivate();
276 self.search_active = false;
277 self.envelopes = self.all_mail_envelopes();
279 self.selected_index = 0;
280 self.scroll_offset = 0;
281 }
282 Action::NextSearchResult => {
283 if self.search_active && self.selected_index + 1 < self.envelopes.len() {
284 self.selected_index += 1;
285 self.ensure_visible();
286 self.auto_preview();
287 }
288 }
289 Action::PrevSearchResult => {
290 if self.search_active && self.selected_index > 0 {
291 self.selected_index -= 1;
292 self.ensure_visible();
293 self.auto_preview();
294 }
295 }
296 Action::GoToInbox => {
298 if let Some(label) = self.labels.iter().find(|l| l.name == "INBOX") {
299 self.apply(Action::SelectLabel(label.id.clone()));
300 } else {
301 self.desired_system_mailbox = Some("INBOX".into());
302 }
303 }
304 Action::GoToStarred => {
305 if let Some(label) = self.labels.iter().find(|l| l.name == "STARRED") {
306 self.apply(Action::SelectLabel(label.id.clone()));
307 } else {
308 self.desired_system_mailbox = Some("STARRED".into());
309 }
310 }
311 Action::GoToSent => {
312 if let Some(label) = self.labels.iter().find(|l| l.name == "SENT") {
313 self.apply(Action::SelectLabel(label.id.clone()));
314 } else {
315 self.desired_system_mailbox = Some("SENT".into());
316 }
317 }
318 Action::GoToDrafts => {
319 if let Some(label) = self.labels.iter().find(|l| l.name == "DRAFT") {
320 self.apply(Action::SelectLabel(label.id.clone()));
321 } else {
322 self.desired_system_mailbox = Some("DRAFT".into());
323 }
324 }
325 Action::GoToAllMail => {
326 self.mailbox_view = MailboxView::Messages;
327 self.apply(Action::ClearFilter);
328 }
329 Action::OpenSubscriptions => {
330 self.mailbox_view = MailboxView::Subscriptions;
331 self.active_label = None;
332 self.pending_active_label = None;
333 self.pending_label_fetch = None;
334 self.desired_system_mailbox = None;
335 self.search_active = false;
336 self.screen = Screen::Mailbox;
337 self.active_pane = ActivePane::MailList;
338 self.selected_index = self
339 .selected_index
340 .min(self.subscriptions_page.entries.len().saturating_sub(1));
341 self.scroll_offset = 0;
342 if self.subscriptions_page.entries.is_empty() {
343 self.pending_subscriptions_refresh = true;
344 }
345 self.auto_preview();
346 }
347 Action::GoToLabel => {
348 self.mailbox_view = MailboxView::Messages;
349 self.apply(Action::ClearFilter);
350 }
351 Action::OpenTab1 => {
352 self.apply(Action::OpenMailboxScreen);
353 }
354 Action::OpenTab2 => {
355 self.apply(Action::OpenSearchScreen);
356 }
357 Action::OpenTab3 => {
358 self.apply(Action::OpenRulesScreen);
359 }
360 Action::OpenTab4 => {
361 self.apply(Action::OpenAccountsScreen);
362 }
363 Action::OpenTab5 => {
364 self.apply(Action::OpenDiagnosticsScreen);
365 }
366 Action::OpenCommandPalette => {
368 self.command_palette.toggle();
369 }
370 Action::CloseCommandPalette => {
371 self.command_palette.visible = false;
372 }
373 Action::SyncNow => {
375 self.pending_mutation_queue.push((
376 Request::SyncNow { account_id: None },
377 MutationEffect::RefreshList,
378 ));
379 self.status_message = Some("Syncing...".into());
380 }
381 Action::OpenMessageView => {
383 if self.mailbox_view == MailboxView::Subscriptions {
384 if let Some(entry) = self.selected_subscription_entry().cloned() {
385 self.open_envelope(entry.envelope);
386 self.layout_mode = LayoutMode::ThreePane;
387 }
388 } else if let Some(row) = self.selected_mail_row() {
389 self.open_envelope(row.representative);
390 self.layout_mode = LayoutMode::ThreePane;
391 }
392 }
393 Action::CloseMessageView => {
394 self.close_attachment_panel();
395 self.layout_mode = LayoutMode::TwoPane;
396 self.active_pane = ActivePane::MailList;
397 self.viewing_envelope = None;
398 self.viewed_thread = None;
399 self.viewed_thread_messages.clear();
400 self.thread_selected_index = 0;
401 self.pending_thread_fetch = None;
402 self.in_flight_thread_fetch = None;
403 self.message_scroll_offset = 0;
404 self.body_view_state = BodyViewState::Empty { preview: None };
405 }
406 Action::ToggleMailListMode => {
407 if self.mailbox_view == MailboxView::Subscriptions {
408 return;
409 }
410 self.mail_list_mode = match self.mail_list_mode {
411 MailListMode::Threads => MailListMode::Messages,
412 MailListMode::Messages => MailListMode::Threads,
413 };
414 self.selected_index = self
415 .selected_index
416 .min(self.mail_row_count().saturating_sub(1));
417 }
418 Action::RefreshRules => {
419 self.rules_page.refresh_pending = true;
420 if let Some(id) = self.selected_rule().and_then(|rule| rule["id"].as_str()) {
421 self.pending_rule_detail = Some(id.to_string());
422 }
423 }
424 Action::ToggleRuleEnabled => {
425 if let Some(rule) = self.selected_rule().cloned() {
426 let mut updated = rule.clone();
427 if let Some(enabled) = updated.get("enabled").and_then(|v| v.as_bool()) {
428 updated["enabled"] = serde_json::Value::Bool(!enabled);
429 self.pending_rule_upsert = Some(updated);
430 self.rules_page.status = Some(if enabled {
431 "Disabling rule...".into()
432 } else {
433 "Enabling rule...".into()
434 });
435 }
436 }
437 }
438 Action::DeleteRule => {
439 if let Some(rule_id) = self
440 .selected_rule()
441 .and_then(|rule| rule["id"].as_str())
442 .map(ToString::to_string)
443 {
444 self.pending_rule_delete = Some(rule_id.clone());
445 self.rules_page.status = Some(format!("Deleting {rule_id}..."));
446 }
447 }
448 Action::ShowRuleHistory => {
449 self.rules_page.panel = RulesPanel::History;
450 self.pending_rule_history = self
451 .selected_rule()
452 .and_then(|rule| rule["id"].as_str())
453 .map(ToString::to_string);
454 }
455 Action::ShowRuleDryRun => {
456 self.rules_page.panel = RulesPanel::DryRun;
457 self.pending_rule_dry_run = self
458 .selected_rule()
459 .and_then(|rule| rule["id"].as_str())
460 .map(ToString::to_string);
461 }
462 Action::OpenRuleFormNew => {
463 self.rules_page.form = RuleFormState {
464 visible: true,
465 enabled: true,
466 priority: "100".to_string(),
467 active_field: 0,
468 ..RuleFormState::default()
469 };
470 self.rules_page.panel = RulesPanel::Form;
471 }
472 Action::OpenRuleFormEdit => {
473 if let Some(rule_id) = self
474 .selected_rule()
475 .and_then(|rule| rule["id"].as_str())
476 .map(ToString::to_string)
477 {
478 self.pending_rule_form_load = Some(rule_id);
479 }
480 }
481 Action::SaveRuleForm => {
482 self.rules_page.status = Some("Saving rule...".into());
483 self.pending_rule_form_save = true;
484 }
485 Action::RefreshDiagnostics => {
486 self.diagnostics_page.refresh_pending = true;
487 }
488 Action::GenerateBugReport => {
489 self.diagnostics_page.status = Some("Generating bug report...".into());
490 self.pending_bug_report = true;
491 }
492 Action::SelectLabel(label_id) => {
493 self.mailbox_view = MailboxView::Messages;
494 self.pending_label_fetch = Some(label_id);
495 self.pending_active_label = self.pending_label_fetch.clone();
496 self.desired_system_mailbox = None;
497 self.active_pane = ActivePane::MailList;
498 self.screen = Screen::Mailbox;
499 }
500 Action::SelectSavedSearch(query) => {
501 self.mailbox_view = MailboxView::Messages;
502 if self.screen == Screen::Search {
503 self.search_page.query = query.clone();
504 self.search_page.editing = false;
505 } else {
506 self.search_active = true;
507 self.active_pane = ActivePane::MailList;
508 }
509 self.pending_search = Some(query);
510 }
511 Action::ClearFilter => {
512 self.mailbox_view = MailboxView::Messages;
513 self.active_label = None;
514 self.pending_active_label = None;
515 self.desired_system_mailbox = None;
516 self.search_active = false;
517 self.envelopes = self.all_mail_envelopes();
518 self.selected_index = 0;
519 self.scroll_offset = 0;
520 }
521
522 Action::Compose => {
524 let mut seen = std::collections::HashMap::new();
526 for env in &self.all_envelopes {
527 seen.entry(env.from.email.clone()).or_insert_with(|| {
528 crate::ui::compose_picker::Contact {
529 name: env.from.name.clone().unwrap_or_default(),
530 email: env.from.email.clone(),
531 }
532 });
533 }
534 let mut contacts: Vec<_> = seen.into_values().collect();
535 contacts.sort_by(|a, b| a.email.to_lowercase().cmp(&b.email.to_lowercase()));
536 self.compose_picker.open(contacts);
537 }
538 Action::Reply => {
539 if let Some(env) = self.context_envelope() {
540 self.pending_compose = Some(ComposeAction::Reply {
541 message_id: env.id.clone(),
542 });
543 }
544 }
545 Action::ReplyAll => {
546 if let Some(env) = self.context_envelope() {
547 self.pending_compose = Some(ComposeAction::ReplyAll {
548 message_id: env.id.clone(),
549 });
550 }
551 }
552 Action::Forward => {
553 if let Some(env) = self.context_envelope() {
554 self.pending_compose = Some(ComposeAction::Forward {
555 message_id: env.id.clone(),
556 });
557 }
558 }
559 Action::Archive => {
560 let ids = self.mutation_target_ids();
561 if !ids.is_empty() {
562 let effect = remove_from_list_effect(&ids);
563 self.queue_or_confirm_bulk_action(
564 "Archive messages",
565 bulk_message_detail("archive", ids.len()),
566 Request::Mutation(MutationCommand::Archive {
567 message_ids: ids.clone(),
568 }),
569 effect,
570 "Archiving...".into(),
571 ids.len(),
572 );
573 }
574 }
575 Action::Trash => {
576 let ids = self.mutation_target_ids();
577 if !ids.is_empty() {
578 let effect = remove_from_list_effect(&ids);
579 self.queue_or_confirm_bulk_action(
580 "Delete messages",
581 bulk_message_detail("delete", ids.len()),
582 Request::Mutation(MutationCommand::Trash {
583 message_ids: ids.clone(),
584 }),
585 effect,
586 "Trashing...".into(),
587 ids.len(),
588 );
589 }
590 }
591 Action::Spam => {
592 let ids = self.mutation_target_ids();
593 if !ids.is_empty() {
594 let effect = remove_from_list_effect(&ids);
595 self.queue_or_confirm_bulk_action(
596 "Mark as spam",
597 bulk_message_detail("mark as spam", ids.len()),
598 Request::Mutation(MutationCommand::Spam {
599 message_ids: ids.clone(),
600 }),
601 effect,
602 "Marking as spam...".into(),
603 ids.len(),
604 );
605 }
606 }
607 Action::Star => {
608 let ids = self.mutation_target_ids();
609 if !ids.is_empty() {
610 let starred = if ids.len() == 1 {
612 if let Some(env) = self.context_envelope() {
613 !env.flags.contains(MessageFlags::STARRED)
614 } else {
615 true
616 }
617 } else {
618 true
619 };
620 let first = ids[0].clone();
621 let effect = if ids.len() == 1 {
623 if let Some(env) = self.context_envelope() {
624 let mut new_flags = env.flags;
625 if starred {
626 new_flags.insert(MessageFlags::STARRED);
627 } else {
628 new_flags.remove(MessageFlags::STARRED);
629 }
630 MutationEffect::UpdateFlags {
631 message_id: first.clone(),
632 flags: new_flags,
633 }
634 } else {
635 MutationEffect::RefreshList
636 }
637 } else {
638 MutationEffect::RefreshList
639 };
640 let verb = if starred { "star" } else { "unstar" };
641 let status = if starred {
642 "Starring..."
643 } else {
644 "Unstarring..."
645 };
646 self.queue_or_confirm_bulk_action(
647 if starred {
648 "Star messages"
649 } else {
650 "Unstar messages"
651 },
652 bulk_message_detail(verb, ids.len()),
653 Request::Mutation(MutationCommand::Star {
654 message_ids: ids.clone(),
655 starred,
656 }),
657 effect,
658 status.into(),
659 ids.len(),
660 );
661 }
662 }
663 Action::MarkRead => {
664 let ids = self.mutation_target_ids();
665 if !ids.is_empty() {
666 let first = ids[0].clone();
667 let effect = if ids.len() == 1 {
668 if let Some(env) = self.context_envelope() {
669 let mut new_flags = env.flags;
670 new_flags.insert(MessageFlags::READ);
671 MutationEffect::UpdateFlags {
672 message_id: first.clone(),
673 flags: new_flags,
674 }
675 } else {
676 MutationEffect::RefreshList
677 }
678 } else {
679 MutationEffect::RefreshList
680 };
681 self.queue_or_confirm_bulk_action(
682 "Mark messages as read",
683 bulk_message_detail("mark as read", ids.len()),
684 Request::Mutation(MutationCommand::SetRead {
685 message_ids: ids.clone(),
686 read: true,
687 }),
688 effect,
689 "Marking as read...".into(),
690 ids.len(),
691 );
692 }
693 }
694 Action::MarkUnread => {
695 let ids = self.mutation_target_ids();
696 if !ids.is_empty() {
697 let first = ids[0].clone();
698 let effect = if ids.len() == 1 {
699 if let Some(env) = self.context_envelope() {
700 let mut new_flags = env.flags;
701 new_flags.remove(MessageFlags::READ);
702 MutationEffect::UpdateFlags {
703 message_id: first.clone(),
704 flags: new_flags,
705 }
706 } else {
707 MutationEffect::RefreshList
708 }
709 } else {
710 MutationEffect::RefreshList
711 };
712 self.queue_or_confirm_bulk_action(
713 "Mark messages as unread",
714 bulk_message_detail("mark as unread", ids.len()),
715 Request::Mutation(MutationCommand::SetRead {
716 message_ids: ids.clone(),
717 read: false,
718 }),
719 effect,
720 "Marking as unread...".into(),
721 ids.len(),
722 );
723 }
724 }
725 Action::ApplyLabel => {
726 if let Some((_, ref label_name)) = self.pending_label_action.take() {
727 let ids = self.mutation_target_ids();
729 if !ids.is_empty() {
730 self.queue_or_confirm_bulk_action(
731 "Apply label",
732 format!(
733 "You are about to apply '{}' to {} {}.",
734 label_name,
735 ids.len(),
736 pluralize_messages(ids.len())
737 ),
738 Request::Mutation(MutationCommand::ModifyLabels {
739 message_ids: ids.clone(),
740 add: vec![label_name.clone()],
741 remove: vec![],
742 }),
743 MutationEffect::ModifyLabels {
744 message_ids: ids.clone(),
745 add: vec![label_name.clone()],
746 remove: vec![],
747 status: format!("Applied label '{}'", label_name),
748 },
749 format!("Applying label '{}'...", label_name),
750 ids.len(),
751 );
752 }
753 } else {
754 self.label_picker
756 .open(self.labels.clone(), LabelPickerMode::Apply);
757 }
758 }
759 Action::MoveToLabel => {
760 if let Some((_, ref label_name)) = self.pending_label_action.take() {
761 let ids = self.mutation_target_ids();
763 if !ids.is_empty() {
764 self.queue_or_confirm_bulk_action(
765 "Move messages",
766 format!(
767 "You are about to move {} {} to '{}'.",
768 ids.len(),
769 pluralize_messages(ids.len()),
770 label_name
771 ),
772 Request::Mutation(MutationCommand::Move {
773 message_ids: ids.clone(),
774 target_label: label_name.clone(),
775 }),
776 remove_from_list_effect(&ids),
777 format!("Moving to '{}'...", label_name),
778 ids.len(),
779 );
780 }
781 } else {
782 self.label_picker
784 .open(self.labels.clone(), LabelPickerMode::Move);
785 }
786 }
787 Action::Unsubscribe => {
788 if let Some(env) = self.context_envelope() {
789 if matches!(env.unsubscribe, UnsubscribeMethod::None) {
790 self.status_message =
791 Some("No unsubscribe option found for this message".into());
792 } else {
793 let sender_email = env.from.email.clone();
794 let archive_message_ids = self
795 .all_envelopes
796 .iter()
797 .filter(|candidate| {
798 candidate.account_id == env.account_id
799 && candidate.from.email.eq_ignore_ascii_case(&sender_email)
800 })
801 .map(|candidate| candidate.id.clone())
802 .collect();
803 self.pending_unsubscribe_confirm = Some(PendingUnsubscribeConfirm {
804 message_id: env.id.clone(),
805 account_id: env.account_id.clone(),
806 sender_email,
807 method_label: unsubscribe_method_label(&env.unsubscribe).to_string(),
808 archive_message_ids,
809 });
810 }
811 }
812 }
813 Action::ConfirmUnsubscribeOnly => {
814 if let Some(pending) = self.pending_unsubscribe_confirm.take() {
815 self.pending_unsubscribe_action = Some(PendingUnsubscribeAction {
816 message_id: pending.message_id,
817 archive_message_ids: Vec::new(),
818 sender_email: pending.sender_email,
819 });
820 self.status_message = Some("Unsubscribing...".into());
821 }
822 }
823 Action::ConfirmUnsubscribeAndArchiveSender => {
824 if let Some(pending) = self.pending_unsubscribe_confirm.take() {
825 self.pending_unsubscribe_action = Some(PendingUnsubscribeAction {
826 message_id: pending.message_id,
827 archive_message_ids: pending.archive_message_ids,
828 sender_email: pending.sender_email,
829 });
830 self.status_message = Some("Unsubscribing and archiving sender...".into());
831 }
832 }
833 Action::CancelUnsubscribe => {
834 self.pending_unsubscribe_confirm = None;
835 self.status_message = Some("Unsubscribe cancelled".into());
836 }
837 Action::Snooze => {
838 if self.snooze_panel.visible {
839 if let Some(env) = self.context_envelope() {
840 let wake_at = resolve_snooze_preset(
841 snooze_presets()[self.snooze_panel.selected_index],
842 &self.snooze_config,
843 );
844 self.pending_mutation_queue.push((
845 Request::Snooze {
846 message_id: env.id.clone(),
847 wake_at,
848 },
849 MutationEffect::StatusOnly(format!(
850 "Snoozed until {}",
851 wake_at
852 .with_timezone(&chrono::Local)
853 .format("%a %b %e %H:%M")
854 )),
855 ));
856 self.status_message = Some("Snoozing...".into());
857 }
858 self.snooze_panel.visible = false;
859 } else if self.context_envelope().is_some() {
860 self.snooze_panel.visible = true;
861 self.snooze_panel.selected_index = 0;
862 } else {
863 self.status_message = Some("No message selected".into());
864 }
865 }
866 Action::OpenInBrowser => {
867 if let Some(env) = self.context_envelope() {
868 let url = format!(
869 "https://mail.google.com/mail/u/0/#inbox/{}",
870 env.provider_id
871 );
872 #[cfg(target_os = "macos")]
873 let _ = std::process::Command::new("open").arg(&url).spawn();
874 #[cfg(target_os = "linux")]
875 let _ = std::process::Command::new("xdg-open").arg(&url).spawn();
876 self.status_message = Some("Opened in browser".into());
877 }
878 }
879
880 Action::ToggleReaderMode => {
882 if let BodyViewState::Ready { .. } = self.body_view_state {
883 self.reader_mode = !self.reader_mode;
884 if let Some(env) = self.viewing_envelope.clone() {
885 self.body_view_state = self.resolve_body_view_state(&env);
886 }
887 }
888 }
889 Action::ToggleSignature => {
890 self.signature_expanded = !self.signature_expanded;
891 }
892
893 Action::ToggleSelect => {
895 if let Some(env) = self.selected_envelope() {
896 let id = env.id.clone();
897 if self.selected_set.contains(&id) {
898 self.selected_set.remove(&id);
899 } else {
900 self.selected_set.insert(id);
901 }
902 if self.selected_index + 1 < self.mail_row_count() {
904 self.selected_index += 1;
905 self.ensure_visible();
906 self.auto_preview();
907 }
908 let count = self.selected_set.len();
909 self.status_message = Some(format!("{count} selected"));
910 }
911 }
912 Action::VisualLineMode => {
913 if self.visual_mode {
914 self.visual_mode = false;
916 self.visual_anchor = None;
917 self.status_message = Some("Visual mode off".into());
918 } else {
919 self.visual_mode = true;
920 self.visual_anchor = Some(self.selected_index);
921 if let Some(env) = self.selected_envelope() {
923 self.selected_set.insert(env.id.clone());
924 }
925 self.status_message = Some("-- VISUAL LINE --".into());
926 }
927 }
928 Action::PatternSelect(pattern) => {
929 match pattern {
930 PatternKind::All => {
931 self.selected_set = self.envelopes.iter().map(|e| e.id.clone()).collect();
932 }
933 PatternKind::None => {
934 self.selected_set.clear();
935 self.visual_mode = false;
936 self.visual_anchor = None;
937 }
938 PatternKind::Read => {
939 self.selected_set = self
940 .envelopes
941 .iter()
942 .filter(|e| e.flags.contains(MessageFlags::READ))
943 .map(|e| e.id.clone())
944 .collect();
945 }
946 PatternKind::Unread => {
947 self.selected_set = self
948 .envelopes
949 .iter()
950 .filter(|e| !e.flags.contains(MessageFlags::READ))
951 .map(|e| e.id.clone())
952 .collect();
953 }
954 PatternKind::Starred => {
955 self.selected_set = self
956 .envelopes
957 .iter()
958 .filter(|e| e.flags.contains(MessageFlags::STARRED))
959 .map(|e| e.id.clone())
960 .collect();
961 }
962 PatternKind::Thread => {
963 if let Some(env) = self.context_envelope() {
964 let tid = env.thread_id.clone();
965 self.selected_set = self
966 .envelopes
967 .iter()
968 .filter(|e| e.thread_id == tid)
969 .map(|e| e.id.clone())
970 .collect();
971 }
972 }
973 }
974 let count = self.selected_set.len();
975 self.status_message = Some(format!("{count} selected"));
976 }
977
978 Action::AttachmentList => {
980 if self.attachment_panel.visible {
981 self.close_attachment_panel();
982 } else {
983 self.open_attachment_panel();
984 }
985 }
986 Action::OpenLinks => {
987 self.open_url_modal();
988 }
989 Action::ToggleFullscreen => {
990 if self.layout_mode == LayoutMode::FullScreen {
991 self.layout_mode = LayoutMode::ThreePane;
992 } else if self.viewing_envelope.is_some() {
993 self.layout_mode = LayoutMode::FullScreen;
994 }
995 }
996 Action::ExportThread => {
997 if let Some(env) = self.context_envelope() {
998 self.pending_export_thread = Some(env.thread_id.clone());
999 self.status_message = Some("Exporting thread...".into());
1000 } else {
1001 self.status_message = Some("No message selected".into());
1002 }
1003 }
1004 Action::Help => {
1005 self.help_modal_open = !self.help_modal_open;
1006 if self.help_modal_open {
1007 self.help_scroll_offset = 0;
1008 }
1009 }
1010 Action::Noop => {}
1011 }
1012 }
1013}