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 =
263 Some((self.search_page.query.clone(), self.search_bar.mode));
264 } else {
265 let query = self.search_bar.query.clone();
266 self.search_bar.deactivate();
267 if !query.is_empty() {
268 self.pending_search = Some((query, self.search_bar.mode));
269 self.search_active = true;
270 }
271 self.active_pane = ActivePane::MailList;
273 }
274 }
275 Action::CycleSearchMode => {
276 self.search_bar.cycle_mode();
277 if self.search_bar.active {
278 self.trigger_live_search();
279 }
280 }
281 Action::CloseSearch => {
282 self.search_bar.deactivate();
283 self.search_active = false;
284 self.envelopes = self.all_mail_envelopes();
286 self.selected_index = 0;
287 self.scroll_offset = 0;
288 }
289 Action::NextSearchResult => {
290 if self.search_active && self.selected_index + 1 < self.envelopes.len() {
291 self.selected_index += 1;
292 self.ensure_visible();
293 self.auto_preview();
294 }
295 }
296 Action::PrevSearchResult => {
297 if self.search_active && self.selected_index > 0 {
298 self.selected_index -= 1;
299 self.ensure_visible();
300 self.auto_preview();
301 }
302 }
303 Action::GoToInbox => {
305 if let Some(label) = self.labels.iter().find(|l| l.name == "INBOX") {
306 self.apply(Action::SelectLabel(label.id.clone()));
307 } else {
308 self.desired_system_mailbox = Some("INBOX".into());
309 }
310 }
311 Action::GoToStarred => {
312 if let Some(label) = self.labels.iter().find(|l| l.name == "STARRED") {
313 self.apply(Action::SelectLabel(label.id.clone()));
314 } else {
315 self.desired_system_mailbox = Some("STARRED".into());
316 }
317 }
318 Action::GoToSent => {
319 if let Some(label) = self.labels.iter().find(|l| l.name == "SENT") {
320 self.apply(Action::SelectLabel(label.id.clone()));
321 } else {
322 self.desired_system_mailbox = Some("SENT".into());
323 }
324 }
325 Action::GoToDrafts => {
326 if let Some(label) = self.labels.iter().find(|l| l.name == "DRAFT") {
327 self.apply(Action::SelectLabel(label.id.clone()));
328 } else {
329 self.desired_system_mailbox = Some("DRAFT".into());
330 }
331 }
332 Action::GoToAllMail => {
333 self.mailbox_view = MailboxView::Messages;
334 self.apply(Action::ClearFilter);
335 }
336 Action::OpenSubscriptions => {
337 self.mailbox_view = MailboxView::Subscriptions;
338 self.active_label = None;
339 self.pending_active_label = None;
340 self.pending_label_fetch = None;
341 self.desired_system_mailbox = None;
342 self.search_active = false;
343 self.screen = Screen::Mailbox;
344 self.active_pane = ActivePane::MailList;
345 self.selected_index = self
346 .selected_index
347 .min(self.subscriptions_page.entries.len().saturating_sub(1));
348 self.scroll_offset = 0;
349 if self.subscriptions_page.entries.is_empty() {
350 self.pending_subscriptions_refresh = true;
351 }
352 self.auto_preview();
353 }
354 Action::GoToLabel => {
355 self.mailbox_view = MailboxView::Messages;
356 self.apply(Action::ClearFilter);
357 }
358 Action::OpenTab1 => {
359 self.apply(Action::OpenMailboxScreen);
360 }
361 Action::OpenTab2 => {
362 self.apply(Action::OpenSearchScreen);
363 }
364 Action::OpenTab3 => {
365 self.apply(Action::OpenRulesScreen);
366 }
367 Action::OpenTab4 => {
368 self.apply(Action::OpenAccountsScreen);
369 }
370 Action::OpenTab5 => {
371 self.apply(Action::OpenDiagnosticsScreen);
372 }
373 Action::OpenCommandPalette => {
375 self.command_palette.toggle();
376 }
377 Action::CloseCommandPalette => {
378 self.command_palette.visible = false;
379 }
380 Action::SyncNow => {
382 self.pending_mutation_queue.push((
383 Request::SyncNow { account_id: None },
384 MutationEffect::RefreshList,
385 ));
386 self.status_message = Some("Syncing...".into());
387 }
388 Action::OpenMessageView => {
390 if self.mailbox_view == MailboxView::Subscriptions {
391 if let Some(entry) = self.selected_subscription_entry().cloned() {
392 self.open_envelope(entry.envelope);
393 self.layout_mode = LayoutMode::ThreePane;
394 }
395 } else if let Some(row) = self.selected_mail_row() {
396 self.open_envelope(row.representative);
397 self.layout_mode = LayoutMode::ThreePane;
398 }
399 }
400 Action::CloseMessageView => {
401 self.close_attachment_panel();
402 self.layout_mode = LayoutMode::TwoPane;
403 self.active_pane = ActivePane::MailList;
404 self.viewing_envelope = None;
405 self.viewed_thread = None;
406 self.viewed_thread_messages.clear();
407 self.thread_selected_index = 0;
408 self.pending_thread_fetch = None;
409 self.in_flight_thread_fetch = None;
410 self.message_scroll_offset = 0;
411 self.body_view_state = BodyViewState::Empty { preview: None };
412 }
413 Action::ToggleMailListMode => {
414 if self.mailbox_view == MailboxView::Subscriptions {
415 return;
416 }
417 self.mail_list_mode = match self.mail_list_mode {
418 MailListMode::Threads => MailListMode::Messages,
419 MailListMode::Messages => MailListMode::Threads,
420 };
421 self.selected_index = self
422 .selected_index
423 .min(self.mail_row_count().saturating_sub(1));
424 }
425 Action::RefreshRules => {
426 self.rules_page.refresh_pending = true;
427 if let Some(id) = self.selected_rule().and_then(|rule| rule["id"].as_str()) {
428 self.pending_rule_detail = Some(id.to_string());
429 }
430 }
431 Action::ToggleRuleEnabled => {
432 if let Some(rule) = self.selected_rule().cloned() {
433 let mut updated = rule.clone();
434 if let Some(enabled) = updated.get("enabled").and_then(|v| v.as_bool()) {
435 updated["enabled"] = serde_json::Value::Bool(!enabled);
436 self.pending_rule_upsert = Some(updated);
437 self.rules_page.status = Some(if enabled {
438 "Disabling rule...".into()
439 } else {
440 "Enabling rule...".into()
441 });
442 }
443 }
444 }
445 Action::DeleteRule => {
446 if let Some(rule_id) = self
447 .selected_rule()
448 .and_then(|rule| rule["id"].as_str())
449 .map(ToString::to_string)
450 {
451 self.pending_rule_delete = Some(rule_id.clone());
452 self.rules_page.status = Some(format!("Deleting {rule_id}..."));
453 }
454 }
455 Action::ShowRuleHistory => {
456 self.rules_page.panel = RulesPanel::History;
457 self.pending_rule_history = self
458 .selected_rule()
459 .and_then(|rule| rule["id"].as_str())
460 .map(ToString::to_string);
461 }
462 Action::ShowRuleDryRun => {
463 self.rules_page.panel = RulesPanel::DryRun;
464 self.pending_rule_dry_run = self
465 .selected_rule()
466 .and_then(|rule| rule["id"].as_str())
467 .map(ToString::to_string);
468 }
469 Action::OpenRuleFormNew => {
470 self.rules_page.form = RuleFormState {
471 visible: true,
472 enabled: true,
473 priority: "100".to_string(),
474 active_field: 0,
475 ..RuleFormState::default()
476 };
477 self.rules_page.panel = RulesPanel::Form;
478 }
479 Action::OpenRuleFormEdit => {
480 if let Some(rule_id) = self
481 .selected_rule()
482 .and_then(|rule| rule["id"].as_str())
483 .map(ToString::to_string)
484 {
485 self.pending_rule_form_load = Some(rule_id);
486 }
487 }
488 Action::SaveRuleForm => {
489 self.rules_page.status = Some("Saving rule...".into());
490 self.pending_rule_form_save = true;
491 }
492 Action::RefreshDiagnostics => {
493 self.diagnostics_page.refresh_pending = true;
494 }
495 Action::GenerateBugReport => {
496 self.diagnostics_page.status = Some("Generating bug report...".into());
497 self.pending_bug_report = true;
498 }
499 Action::SelectLabel(label_id) => {
500 self.mailbox_view = MailboxView::Messages;
501 self.pending_label_fetch = Some(label_id);
502 self.pending_active_label = self.pending_label_fetch.clone();
503 self.desired_system_mailbox = None;
504 self.active_pane = ActivePane::MailList;
505 self.screen = Screen::Mailbox;
506 }
507 Action::SelectSavedSearch(query, mode) => {
508 self.mailbox_view = MailboxView::Messages;
509 if self.screen == Screen::Search {
510 self.search_page.query = query.clone();
511 self.search_page.editing = false;
512 } else {
513 self.search_active = true;
514 self.active_pane = ActivePane::MailList;
515 }
516 self.search_bar.mode = mode;
517 self.pending_search = Some((query, mode));
518 }
519 Action::ClearFilter => {
520 self.mailbox_view = MailboxView::Messages;
521 self.active_label = None;
522 self.pending_active_label = None;
523 self.desired_system_mailbox = None;
524 self.search_active = false;
525 self.envelopes = self.all_mail_envelopes();
526 self.selected_index = 0;
527 self.scroll_offset = 0;
528 }
529
530 Action::Compose => {
532 let mut seen = std::collections::HashMap::new();
534 for env in &self.all_envelopes {
535 seen.entry(env.from.email.clone()).or_insert_with(|| {
536 crate::ui::compose_picker::Contact {
537 name: env.from.name.clone().unwrap_or_default(),
538 email: env.from.email.clone(),
539 }
540 });
541 }
542 let mut contacts: Vec<_> = seen.into_values().collect();
543 contacts.sort_by(|a, b| a.email.to_lowercase().cmp(&b.email.to_lowercase()));
544 self.compose_picker.open(contacts);
545 }
546 Action::Reply => {
547 if let Some(env) = self.context_envelope() {
548 self.pending_compose = Some(ComposeAction::Reply {
549 message_id: env.id.clone(),
550 });
551 }
552 }
553 Action::ReplyAll => {
554 if let Some(env) = self.context_envelope() {
555 self.pending_compose = Some(ComposeAction::ReplyAll {
556 message_id: env.id.clone(),
557 });
558 }
559 }
560 Action::Forward => {
561 if let Some(env) = self.context_envelope() {
562 self.pending_compose = Some(ComposeAction::Forward {
563 message_id: env.id.clone(),
564 });
565 }
566 }
567 Action::Archive => {
568 let ids = self.mutation_target_ids();
569 if !ids.is_empty() {
570 let effect = remove_from_list_effect(&ids);
571 self.queue_or_confirm_bulk_action(
572 "Archive messages",
573 bulk_message_detail("archive", ids.len()),
574 Request::Mutation(MutationCommand::Archive {
575 message_ids: ids.clone(),
576 }),
577 effect,
578 "Archiving...".into(),
579 ids.len(),
580 );
581 }
582 }
583 Action::Trash => {
584 let ids = self.mutation_target_ids();
585 if !ids.is_empty() {
586 let effect = remove_from_list_effect(&ids);
587 self.queue_or_confirm_bulk_action(
588 "Delete messages",
589 bulk_message_detail("delete", ids.len()),
590 Request::Mutation(MutationCommand::Trash {
591 message_ids: ids.clone(),
592 }),
593 effect,
594 "Trashing...".into(),
595 ids.len(),
596 );
597 }
598 }
599 Action::Spam => {
600 let ids = self.mutation_target_ids();
601 if !ids.is_empty() {
602 let effect = remove_from_list_effect(&ids);
603 self.queue_or_confirm_bulk_action(
604 "Mark as spam",
605 bulk_message_detail("mark as spam", ids.len()),
606 Request::Mutation(MutationCommand::Spam {
607 message_ids: ids.clone(),
608 }),
609 effect,
610 "Marking as spam...".into(),
611 ids.len(),
612 );
613 }
614 }
615 Action::Star => {
616 let ids = self.mutation_target_ids();
617 if !ids.is_empty() {
618 let starred = if ids.len() == 1 {
620 if let Some(env) = self.context_envelope() {
621 !env.flags.contains(MessageFlags::STARRED)
622 } else {
623 true
624 }
625 } else {
626 true
627 };
628 let first = ids[0].clone();
629 let effect = if ids.len() == 1 {
631 if let Some(env) = self.context_envelope() {
632 let mut new_flags = env.flags;
633 if starred {
634 new_flags.insert(MessageFlags::STARRED);
635 } else {
636 new_flags.remove(MessageFlags::STARRED);
637 }
638 MutationEffect::UpdateFlags {
639 message_id: first.clone(),
640 flags: new_flags,
641 }
642 } else {
643 MutationEffect::RefreshList
644 }
645 } else {
646 MutationEffect::RefreshList
647 };
648 let verb = if starred { "star" } else { "unstar" };
649 let status = if starred {
650 "Starring..."
651 } else {
652 "Unstarring..."
653 };
654 self.queue_or_confirm_bulk_action(
655 if starred {
656 "Star messages"
657 } else {
658 "Unstar messages"
659 },
660 bulk_message_detail(verb, ids.len()),
661 Request::Mutation(MutationCommand::Star {
662 message_ids: ids.clone(),
663 starred,
664 }),
665 effect,
666 status.into(),
667 ids.len(),
668 );
669 }
670 }
671 Action::MarkRead => {
672 let ids = self.mutation_target_ids();
673 if !ids.is_empty() {
674 let first = ids[0].clone();
675 let effect = if ids.len() == 1 {
676 if let Some(env) = self.context_envelope() {
677 let mut new_flags = env.flags;
678 new_flags.insert(MessageFlags::READ);
679 MutationEffect::UpdateFlags {
680 message_id: first.clone(),
681 flags: new_flags,
682 }
683 } else {
684 MutationEffect::RefreshList
685 }
686 } else {
687 MutationEffect::RefreshList
688 };
689 self.queue_or_confirm_bulk_action(
690 "Mark messages as read",
691 bulk_message_detail("mark as read", ids.len()),
692 Request::Mutation(MutationCommand::SetRead {
693 message_ids: ids.clone(),
694 read: true,
695 }),
696 effect,
697 "Marking as read...".into(),
698 ids.len(),
699 );
700 }
701 }
702 Action::MarkUnread => {
703 let ids = self.mutation_target_ids();
704 if !ids.is_empty() {
705 let first = ids[0].clone();
706 let effect = if ids.len() == 1 {
707 if let Some(env) = self.context_envelope() {
708 let mut new_flags = env.flags;
709 new_flags.remove(MessageFlags::READ);
710 MutationEffect::UpdateFlags {
711 message_id: first.clone(),
712 flags: new_flags,
713 }
714 } else {
715 MutationEffect::RefreshList
716 }
717 } else {
718 MutationEffect::RefreshList
719 };
720 self.queue_or_confirm_bulk_action(
721 "Mark messages as unread",
722 bulk_message_detail("mark as unread", ids.len()),
723 Request::Mutation(MutationCommand::SetRead {
724 message_ids: ids.clone(),
725 read: false,
726 }),
727 effect,
728 "Marking as unread...".into(),
729 ids.len(),
730 );
731 }
732 }
733 Action::ApplyLabel => {
734 if let Some((_, ref label_name)) = self.pending_label_action.take() {
735 let ids = self.mutation_target_ids();
737 if !ids.is_empty() {
738 self.queue_or_confirm_bulk_action(
739 "Apply label",
740 format!(
741 "You are about to apply '{}' to {} {}.",
742 label_name,
743 ids.len(),
744 pluralize_messages(ids.len())
745 ),
746 Request::Mutation(MutationCommand::ModifyLabels {
747 message_ids: ids.clone(),
748 add: vec![label_name.clone()],
749 remove: vec![],
750 }),
751 MutationEffect::ModifyLabels {
752 message_ids: ids.clone(),
753 add: vec![label_name.clone()],
754 remove: vec![],
755 status: format!("Applied label '{}'", label_name),
756 },
757 format!("Applying label '{}'...", label_name),
758 ids.len(),
759 );
760 }
761 } else {
762 self.label_picker
764 .open(self.labels.clone(), LabelPickerMode::Apply);
765 }
766 }
767 Action::MoveToLabel => {
768 if let Some((_, ref label_name)) = self.pending_label_action.take() {
769 let ids = self.mutation_target_ids();
771 if !ids.is_empty() {
772 self.queue_or_confirm_bulk_action(
773 "Move messages",
774 format!(
775 "You are about to move {} {} to '{}'.",
776 ids.len(),
777 pluralize_messages(ids.len()),
778 label_name
779 ),
780 Request::Mutation(MutationCommand::Move {
781 message_ids: ids.clone(),
782 target_label: label_name.clone(),
783 }),
784 remove_from_list_effect(&ids),
785 format!("Moving to '{}'...", label_name),
786 ids.len(),
787 );
788 }
789 } else {
790 self.label_picker
792 .open(self.labels.clone(), LabelPickerMode::Move);
793 }
794 }
795 Action::Unsubscribe => {
796 if let Some(env) = self.context_envelope() {
797 if matches!(env.unsubscribe, UnsubscribeMethod::None) {
798 self.status_message =
799 Some("No unsubscribe option found for this message".into());
800 } else {
801 let sender_email = env.from.email.clone();
802 let archive_message_ids = self
803 .all_envelopes
804 .iter()
805 .filter(|candidate| {
806 candidate.account_id == env.account_id
807 && candidate.from.email.eq_ignore_ascii_case(&sender_email)
808 })
809 .map(|candidate| candidate.id.clone())
810 .collect();
811 self.pending_unsubscribe_confirm = Some(PendingUnsubscribeConfirm {
812 message_id: env.id.clone(),
813 account_id: env.account_id.clone(),
814 sender_email,
815 method_label: unsubscribe_method_label(&env.unsubscribe).to_string(),
816 archive_message_ids,
817 });
818 }
819 }
820 }
821 Action::ConfirmUnsubscribeOnly => {
822 if let Some(pending) = self.pending_unsubscribe_confirm.take() {
823 self.pending_unsubscribe_action = Some(PendingUnsubscribeAction {
824 message_id: pending.message_id,
825 archive_message_ids: Vec::new(),
826 sender_email: pending.sender_email,
827 });
828 self.status_message = Some("Unsubscribing...".into());
829 }
830 }
831 Action::ConfirmUnsubscribeAndArchiveSender => {
832 if let Some(pending) = self.pending_unsubscribe_confirm.take() {
833 self.pending_unsubscribe_action = Some(PendingUnsubscribeAction {
834 message_id: pending.message_id,
835 archive_message_ids: pending.archive_message_ids,
836 sender_email: pending.sender_email,
837 });
838 self.status_message = Some("Unsubscribing and archiving sender...".into());
839 }
840 }
841 Action::CancelUnsubscribe => {
842 self.pending_unsubscribe_confirm = None;
843 self.status_message = Some("Unsubscribe cancelled".into());
844 }
845 Action::Snooze => {
846 if self.snooze_panel.visible {
847 if let Some(env) = self.context_envelope() {
848 let wake_at = resolve_snooze_preset(
849 snooze_presets()[self.snooze_panel.selected_index],
850 &self.snooze_config,
851 );
852 self.pending_mutation_queue.push((
853 Request::Snooze {
854 message_id: env.id.clone(),
855 wake_at,
856 },
857 MutationEffect::StatusOnly(format!(
858 "Snoozed until {}",
859 wake_at
860 .with_timezone(&chrono::Local)
861 .format("%a %b %e %H:%M")
862 )),
863 ));
864 self.status_message = Some("Snoozing...".into());
865 }
866 self.snooze_panel.visible = false;
867 } else if self.context_envelope().is_some() {
868 self.snooze_panel.visible = true;
869 self.snooze_panel.selected_index = 0;
870 } else {
871 self.status_message = Some("No message selected".into());
872 }
873 }
874 Action::OpenInBrowser => {
875 if let Some(env) = self.context_envelope() {
876 let url = format!(
877 "https://mail.google.com/mail/u/0/#inbox/{}",
878 env.provider_id
879 );
880 #[cfg(target_os = "macos")]
881 let _ = std::process::Command::new("open").arg(&url).spawn();
882 #[cfg(target_os = "linux")]
883 let _ = std::process::Command::new("xdg-open").arg(&url).spawn();
884 self.status_message = Some("Opened in browser".into());
885 }
886 }
887
888 Action::ToggleReaderMode => {
890 if let BodyViewState::Ready { .. } = self.body_view_state {
891 self.reader_mode = !self.reader_mode;
892 if let Some(env) = self.viewing_envelope.clone() {
893 self.body_view_state = self.resolve_body_view_state(&env);
894 }
895 }
896 }
897 Action::ToggleSignature => {
898 self.signature_expanded = !self.signature_expanded;
899 }
900
901 Action::ToggleSelect => {
903 if let Some(env) = self.selected_envelope() {
904 let id = env.id.clone();
905 if self.selected_set.contains(&id) {
906 self.selected_set.remove(&id);
907 } else {
908 self.selected_set.insert(id);
909 }
910 if self.selected_index + 1 < self.mail_row_count() {
912 self.selected_index += 1;
913 self.ensure_visible();
914 self.auto_preview();
915 }
916 let count = self.selected_set.len();
917 self.status_message = Some(format!("{count} selected"));
918 }
919 }
920 Action::VisualLineMode => {
921 if self.visual_mode {
922 self.visual_mode = false;
924 self.visual_anchor = None;
925 self.status_message = Some("Visual mode off".into());
926 } else {
927 self.visual_mode = true;
928 self.visual_anchor = Some(self.selected_index);
929 if let Some(env) = self.selected_envelope() {
931 self.selected_set.insert(env.id.clone());
932 }
933 self.status_message = Some("-- VISUAL LINE --".into());
934 }
935 }
936 Action::PatternSelect(pattern) => {
937 match pattern {
938 PatternKind::All => {
939 self.selected_set = self.envelopes.iter().map(|e| e.id.clone()).collect();
940 }
941 PatternKind::None => {
942 self.selected_set.clear();
943 self.visual_mode = false;
944 self.visual_anchor = None;
945 }
946 PatternKind::Read => {
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::Unread => {
955 self.selected_set = self
956 .envelopes
957 .iter()
958 .filter(|e| !e.flags.contains(MessageFlags::READ))
959 .map(|e| e.id.clone())
960 .collect();
961 }
962 PatternKind::Starred => {
963 self.selected_set = self
964 .envelopes
965 .iter()
966 .filter(|e| e.flags.contains(MessageFlags::STARRED))
967 .map(|e| e.id.clone())
968 .collect();
969 }
970 PatternKind::Thread => {
971 if let Some(env) = self.context_envelope() {
972 let tid = env.thread_id.clone();
973 self.selected_set = self
974 .envelopes
975 .iter()
976 .filter(|e| e.thread_id == tid)
977 .map(|e| e.id.clone())
978 .collect();
979 }
980 }
981 }
982 let count = self.selected_set.len();
983 self.status_message = Some(format!("{count} selected"));
984 }
985
986 Action::AttachmentList => {
988 if self.attachment_panel.visible {
989 self.close_attachment_panel();
990 } else {
991 self.open_attachment_panel();
992 }
993 }
994 Action::OpenLinks => {
995 self.open_url_modal();
996 }
997 Action::ToggleFullscreen => {
998 if self.layout_mode == LayoutMode::FullScreen {
999 self.layout_mode = LayoutMode::ThreePane;
1000 } else if self.viewing_envelope.is_some() {
1001 self.layout_mode = LayoutMode::FullScreen;
1002 }
1003 }
1004 Action::ExportThread => {
1005 if let Some(env) = self.context_envelope() {
1006 self.pending_export_thread = Some(env.thread_id.clone());
1007 self.status_message = Some("Exporting thread...".into());
1008 } else {
1009 self.status_message = Some("No message selected".into());
1010 }
1011 }
1012 Action::Help => {
1013 self.help_modal_open = !self.help_modal_open;
1014 if self.help_modal_open {
1015 self.help_scroll_offset = 0;
1016 }
1017 }
1018 Action::Noop => {}
1019 }
1020 }
1021}