diff --git a/docs/meli-themes.5 b/docs/meli-themes.5 index c6bfdcac..83cd8adc 100644 --- a/docs/meli-themes.5 +++ b/docs/meli-themes.5 @@ -173,8 +173,6 @@ theme_default .It error_message .It -email_header -.It highlight .It status.bar diff --git a/docs/meli.conf.5 b/docs/meli.conf.5 index 8d4bbbff..fdb940e4 100644 --- a/docs/meli.conf.5 +++ b/docs/meli.conf.5 @@ -1120,7 +1120,7 @@ Play sound file in notifications if possible. .Sh PAGER Default values are shown in parentheses. .Bl -tag -width 36n -.It Ic headers_sticky Ar boolean +.It Ic sticky_headers Ar boolean .Pq Em optional Always show headers when scrolling. .\" default value diff --git a/docs/samples/sample-config.toml b/docs/samples/sample-config.toml index 5498128d..2fce2961 100644 --- a/docs/samples/sample-config.toml +++ b/docs/samples/sample-config.toml @@ -91,7 +91,7 @@ #[pager] #filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email" #pager_context = 0 # default, optional -#headers_sticky = true # default, optional +#sticky_headers = true # default, optional # #[notifications] #script = "notify-send" diff --git a/src/components.rs b/src/components.rs index 0ea6d0d8..d38a94e2 100644 --- a/src/components.rs +++ b/src/components.rs @@ -32,6 +32,7 @@ use crate::{ melib::text_processing::{TextProcessing, Truncate}, terminal::boundaries::*, }; +use smallvec::SmallVec; pub mod mail; pub use crate::mail::*; @@ -163,6 +164,31 @@ pub trait Component: Display + Debug + Send + Sync { fn status(&self, _context: &Context) -> String { String::new() } + + fn attributes(&self) -> &'static ComponentAttr { + &ComponentAttr::DEFAULT + } + + fn children(&self) -> IndexMap { + IndexMap::default() + } + + fn children_mut(&mut self) -> IndexMap { + IndexMap::default() + } + + fn realize(&self, parent: Option, context: &mut Context) { + log::debug!("Realizing id {} w/ parent {:?}", self.id(), &parent); + context.realized.insert(self.id(), parent); + } + + fn unrealize(&self, context: &mut Context) { + log::debug!("Unrealizing id {}", self.id()); + context.unrealized.insert(self.id()); + context + .replies + .push_back(UIEvent::ComponentUnrealize(self.id())); + } } impl Component for Box { @@ -205,4 +231,89 @@ impl Component for Box { fn status(&self, context: &Context) -> String { (**self).status(context) } + + fn attributes(&self) -> &'static ComponentAttr { + (**self).attributes() + } + + fn children(&self) -> IndexMap { + (**self).children() + } + + fn children_mut(&mut self) -> IndexMap { + (**self).children_mut() + } + + fn realize(&self, parent: Option, context: &mut Context) { + (**self).realize(parent, context) + } + + fn unrealize(&self, context: &mut Context) { + (**self).unrealize(context) + } +} + +bitflags::bitflags! { + /// Attributes of a [`Component`] widget. + /// + /// `ComponentAttr::DEFAULT` represents no attribute. + pub struct ComponentAttr: u8 { + /// Nothing special going on. + const DEFAULT = 0; + const HAS_ANIMATIONS = 1; + const CONTAINER = 1 << 1; + } +} + +impl Default for ComponentAttr { + fn default() -> Self { + Self::DEFAULT + } +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct ComponentPath { + id: ComponentId, + tail: SmallVec<[ComponentId; 8]>, +} + +impl ComponentPath { + pub fn new(id: ComponentId) -> Self { + Self { + id, + tail: SmallVec::default(), + } + } + + pub fn push_front(&mut self, id: ComponentId) { + self.tail.insert(0, self.id); + self.id = id; + } + + pub fn push_back(&mut self, id: ComponentId) { + self.tail.push(id); + } + + pub fn resolve<'c>(&self, root: &'c dyn Component) -> Option<&'c dyn Component> { + let mut cursor = root; + for id in self.tail.iter().rev().chain(std::iter::once(&self.id)) { + log::trace!("resolve cursor = {} next id is {}", cursor.id(), &id); + if *id == cursor.id() { + log::trace!("continue;"); + continue; + } + cursor = cursor.children().remove(id)?; + } + Some(cursor) + } + + #[inline] + pub fn parent(&self) -> Option<&ComponentId> { + self.tail.first() + } + + #[inline] + pub fn root(&self) -> Option<&ComponentId> { + self.tail.last() + } } diff --git a/src/components/contacts.rs b/src/components/contacts.rs index f8705f3e..e992d72a 100644 --- a/src/components/contacts.rs +++ b/src/components/contacts.rs @@ -221,10 +221,10 @@ impl Component for ContactManager { context.replies.push_back(UIEvent::StatusEvent( StatusEvent::DisplayMessage("Saved.".into()), )); - context.replies.push_back(UIEvent::ComponentKill(self.id)); + self.unrealize(context); } Some(false) => { - context.replies.push_back(UIEvent::ComponentKill(self.id)); + self.unrealize(context); } } self.set_dirty(true); @@ -237,7 +237,7 @@ impl Component for ContactManager { ViewMode::ReadOnly => { if let &mut UIEvent::Input(Key::Esc) = event { if self.can_quit_cleanly(context) { - context.replies.push_back(UIEvent::ComponentKill(self.id)); + self.unrealize(context); } return true; } diff --git a/src/components/contacts/contact_list.rs b/src/components/contacts/contact_list.rs index 76390637..40be08f9 100644 --- a/src/components/contacts/contact_list.rs +++ b/src/components/contacts/contact_list.rs @@ -899,7 +899,9 @@ impl Component for ContactList { } } else { match event { - UIEvent::ComponentKill(ref kill_id) if self.mode == ViewMode::View(*kill_id) => { + UIEvent::ComponentUnrealize(ref kill_id) + if self.mode == ViewMode::View(*kill_id) => + { self.mode = ViewMode::List; self.view.take(); self.set_dirty(true); diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index f8a290a4..87620806 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -1158,7 +1158,7 @@ impl Component for Composer { Flag::SEEN, ) { Ok(job) => { - let handle = context.job_executor.spawn_blocking(job); + let handle = context.main_loop_handler.job_executor.spawn_blocking(job); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::NewJob( @@ -1208,26 +1208,29 @@ impl Component for Composer { self.set_dirty(true); return true; } - (ViewMode::Send(ref dialog), UIEvent::ComponentKill(ref id)) if *id == dialog.id() => { + (ViewMode::Send(ref dialog), UIEvent::ComponentUnrealize(ref id)) + if *id == dialog.id() => + { self.mode = ViewMode::Edit; self.set_dirty(true); } - (ViewMode::SelectRecipients(ref dialog), UIEvent::ComponentKill(ref id)) + (ViewMode::SelectRecipients(ref dialog), UIEvent::ComponentUnrealize(ref id)) if *id == dialog.id() => { self.mode = ViewMode::Edit; self.set_dirty(true); } - (ViewMode::Discard(_, ref dialog), UIEvent::ComponentKill(ref id)) + (ViewMode::Discard(_, ref dialog), UIEvent::ComponentUnrealize(ref id)) if *id == dialog.id() => { self.mode = ViewMode::Edit; self.set_dirty(true); } #[cfg(feature = "gpgme")] - (ViewMode::SelectEncryptKey(_, ref mut selector), UIEvent::ComponentKill(ref id)) - if *id == selector.id() => - { + ( + ViewMode::SelectEncryptKey(_, ref mut selector), + UIEvent::ComponentUnrealize(ref id), + ) if *id == selector.id() => { self.mode = ViewMode::Edit; self.set_dirty(true); return true; @@ -2315,7 +2318,7 @@ pub fn send_draft_async( ) -> Result> + Send>>> { let store_sent_mail = *account_settings!(context[account_hash].composing.store_sent_mail); let format_flowed = *account_settings!(context[account_hash].composing.format_flowed); - let event_sender = context.sender.clone(); + let event_sender = context.main_loop_handler.sender.clone(); #[cfg(feature = "gpgme")] #[allow(clippy::type_complexity)] let mut filters_stack: Vec< diff --git a/src/components/mail/compose/gpg.rs b/src/components/mail/compose/gpg.rs index 21be6b41..cd0e9894 100644 --- a/src/components/mail/compose/gpg.rs +++ b/src/components/mail/compose/gpg.rs @@ -68,7 +68,10 @@ impl KeySelection { ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?; } let job = ctx.keylist(secret, Some(pattern.clone()))?; - let handle = context.job_executor.spawn_specialized(job); + let handle = context + .main_loop_handler + .job_executor + .spawn_specialized(job); let mut progress_spinner = ProgressSpinner::new(8, context); progress_spinner.start(); Ok(KeySelection::LoadingKeys { diff --git a/src/components/mail/listing.rs b/src/components/mail/listing.rs index 0cebe7db..97885218 100644 --- a/src/components/mail/listing.rs +++ b/src/components/mail/listing.rs @@ -55,6 +55,7 @@ pub const DEFAULT_SNOOZED_FLAG: &str = "💤"; pub struct RowsState { pub selection: HashMap, pub row_updates: SmallVec<[EnvelopeHash; 8]>, + /// FIXME: env vec should have at least one element guaranteed pub thread_to_env: HashMap>, pub env_to_thread: HashMap, pub thread_order: HashMap, @@ -412,6 +413,20 @@ struct AccountMenuEntry { } pub trait MailListingTrait: ListingTrait { + fn as_component(&self) -> &dyn Component + where + Self: Sized, + { + self + } + + fn as_component_mut(&mut self) -> &mut dyn Component + where + Self: Sized, + { + self + } + fn perform_action( &mut self, context: &mut Context, @@ -450,7 +465,10 @@ pub trait MailListingTrait: ListingTrait { )); } Ok(fut) => { - let handle = account.job_executor.spawn_specialized(fut); + let handle = account + .main_loop_handler + .job_executor + .spawn_specialized(fut); account .insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle }); } @@ -469,7 +487,10 @@ pub trait MailListingTrait: ListingTrait { )); } Ok(fut) => { - let handle = account.job_executor.spawn_specialized(fut); + let handle = account + .main_loop_handler + .job_executor + .spawn_specialized(fut); account .insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle }); } @@ -488,7 +509,10 @@ pub trait MailListingTrait: ListingTrait { )); } Ok(fut) => { - let handle = account.job_executor.spawn_specialized(fut); + let handle = account + .main_loop_handler + .job_executor + .spawn_specialized(fut); account .insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle }); } @@ -507,7 +531,10 @@ pub trait MailListingTrait: ListingTrait { )); } Ok(fut) => { - let handle = account.job_executor.spawn_specialized(fut); + let handle = account + .main_loop_handler + .job_executor + .spawn_specialized(fut); account .insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle }); } @@ -526,7 +553,10 @@ pub trait MailListingTrait: ListingTrait { )); } Ok(fut) => { - let handle = account.job_executor.spawn_specialized(fut); + let handle = account + .main_loop_handler + .job_executor + .spawn_specialized(fut); account.insert_job( handle.job_id, JobRequest::DeleteMessages { env_hashes, handle }, @@ -551,7 +581,10 @@ pub trait MailListingTrait: ListingTrait { )); } Ok(fut) => { - let handle = account.job_executor.spawn_specialized(fut); + let handle = account + .main_loop_handler + .job_executor + .spawn_specialized(fut); account.insert_job( handle.job_id, JobRequest::Generic { @@ -588,7 +621,10 @@ pub trait MailListingTrait: ListingTrait { )); } Ok(fut) => { - let handle = account.job_executor.spawn_specialized(fut); + let handle = account + .main_loop_handler + .job_executor + .spawn_specialized(fut); account.insert_job( handle.job_id, JobRequest::Generic { @@ -667,7 +703,7 @@ pub trait MailListingTrait: ListingTrait { let _ = sender.send(r); Ok(()) }); - let handle = account.job_executor.spawn_blocking(fut); + let handle = account.main_loop_handler.job_executor.spawn_blocking(fut); let path = path.to_path_buf(); account.insert_job( handle.job_id, @@ -743,12 +779,27 @@ pub trait ListingTrait: Component { ) { } fn unfocused(&self) -> bool; + fn view_area(&self) -> Option; fn set_modifier_active(&mut self, _new_val: bool); fn set_modifier_command(&mut self, _new_val: Option); fn modifier_command(&self) -> Option; fn set_movement(&mut self, mvm: PageMovement); fn focus(&self) -> Focus; fn set_focus(&mut self, new_value: Focus, context: &mut Context); + + fn kick_parent(&self, parent: ComponentId, msg: ListingMessage, context: &mut Context) { + log::trace!( + "kick_parent self is {} parent is {} msg is {:?}", + self.id(), + parent, + &msg + ); + context.replies.push_back(UIEvent::IntraComm { + from: self.id(), + to: parent, + content: Box::new(msg), + }); + } } #[derive(Debug)] @@ -788,32 +839,13 @@ impl core::ops::DerefMut for ListingComponent { } impl ListingComponent { - fn set_style(&mut self, new_style: IndexStyle) { - match new_style { - IndexStyle::Plain => { - if let Plain(_) = self { - return; - } - *self = Plain(PlainListing::new(self.coordinates())); - } - IndexStyle::Threaded => { - if let Threaded(_) = self { - return; - } - *self = Threaded(ThreadListing::new(self.coordinates())); - } - IndexStyle::Compact => { - if let Compact(_) = self { - return; - } - *self = Compact(CompactListing::new(self.coordinates())); - } - IndexStyle::Conversations => { - if let Conversations(_) = self { - return; - } - *self = Conversations(ConversationsListing::new(self.coordinates())); - } + fn id(&self) -> ComponentId { + match self { + Compact(l) => l.as_component().id(), + Plain(l) => l.as_component().id(), + Threaded(l) => l.as_component().id(), + Conversations(l) => l.as_component().id(), + Offline(l) => l.as_component().id(), } } } @@ -862,6 +894,7 @@ pub struct Listing { prev_ratio: usize, menu_width: WidgetWidth, focus: ListingFocus, + view: Box, } impl fmt::Display for Listing { @@ -930,14 +963,20 @@ impl Component for Listing { if context.is_online(account_hash).is_err() && !matches!(self.component, ListingComponent::Offline(_)) { + self.component.unrealize(context); self.component = Offline(OfflineListing::new((account_hash, MailboxHash::default()))); + self.component.realize(self.id().into(), context); } if let Some(s) = self.status.as_mut() { s.draw(grid, area, context); } else { self.component.draw(grid, area, context); + if self.component.unfocused() { + self.view + .draw(grid, self.component.view_area().unwrap_or(area), context); + } } } else if right_component_width == 0 { self.draw_menu(grid, area, context); @@ -950,14 +989,20 @@ impl Component for Listing { if context.is_online(account_hash).is_err() && !matches!(self.component, ListingComponent::Offline(_)) { + self.component.unrealize(context); self.component = Offline(OfflineListing::new((account_hash, MailboxHash::default()))); + self.component.realize(self.id().into(), context); } if let Some(s) = self.status.as_mut() { s.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context); } else { - self.component - .draw(grid, (set_x(upper_left, mid + 1), bottom_right), context); + let area = (set_x(upper_left, mid + 1), bottom_right); + self.component.draw(grid, area, context); + if self.component.unfocused() { + self.view + .draw(grid, self.component.view_area().unwrap_or(area), context); + } } } self.dirty = false; @@ -1132,9 +1177,73 @@ impl Component for Listing { } return true; } + UIEvent::IntraComm { + from, + to, + ref content, + } if (*from, *to) == (self.component.id(), self.id()) => { + match content.downcast_ref::().map(|msg| *msg) { + None => {} + Some(ListingMessage::FocusUpdate { new_value }) => { + self.view.process_event( + &mut UIEvent::VisibilityChange(!matches!(new_value, Focus::None)), + context, + ); + if matches!(new_value, Focus::Entry) { + // Need to clear gap between sidebar and listing component, if any. + self.dirty = true; + } + } + Some(ListingMessage::UpdateView) => { + log::trace!("UpdateView"); + } + Some(ListingMessage::OpenEntryUnderCursor { + env_hash, + thread_hash, + show_thread, + }) => { + let (a, m) = self.component.coordinates(); + self.view.unrealize(context); + self.view = Box::new(ThreadView::new( + (a, m, env_hash), + thread_hash, + Some(env_hash), + if show_thread { + None + } else { + Some(ThreadViewFocus::MailView) + }, + context, + )); + } + } + } + #[cfg(feature = "debug-tracing")] + UIEvent::IntraComm { + from, + to, + ref content, + } => { + if *from == self.component.id() || *to == self.id() { + log::debug!( + "BUG intracomm event: {:?} downcast content {:?}", + event, + content.downcast_ref::().map(|msg| *msg) + ); + log::debug!( + "BUG component is {} and self id is {}", + self.component.id(), + self.id() + ); + } + } _ => {} } + if self.component.unfocused() && self.view.process_event(event, context) { + return true; + } + if self.focus == ListingFocus::Mailbox && self.status.is_some() { if let Some(s) = self.status.as_mut() { if s.process_event(event, context) { @@ -1142,11 +1251,12 @@ impl Component for Listing { } } } - if self.focus == ListingFocus::Mailbox - && self.status.is_none() - && self.component.process_event(event, context) - { - return true; + if self.focus == ListingFocus::Mailbox && self.status.is_none() { + if self.component.unfocused() && self.view.process_event(event, context) { + return true; + } else if self.component.process_event(event, context) { + return true; + } } let shortcuts = self.shortcuts(context); @@ -1336,19 +1446,19 @@ impl Component for Listing { match event { UIEvent::Action(ref action) => match action { Action::Listing(ListingAction::SetPlain) => { - self.component.set_style(IndexStyle::Plain); + self.set_style(IndexStyle::Plain, context); return true; } Action::Listing(ListingAction::SetThreaded) => { - self.component.set_style(IndexStyle::Threaded); + self.set_style(IndexStyle::Threaded, context); return true; } Action::Listing(ListingAction::SetCompact) => { - self.component.set_style(IndexStyle::Compact); + self.set_style(IndexStyle::Compact, context); return true; } Action::Listing(ListingAction::SetConversations) => { - self.component.set_style(IndexStyle::Conversations); + self.set_style(IndexStyle::Conversations, context); return true; } Action::Listing(ListingAction::Import(file_path, mailbox_path)) => { @@ -1952,6 +2062,11 @@ impl Component for Listing { .as_ref() .map(Component::is_dirty) .unwrap_or_else(|| self.component.is_dirty()) + || if self.component.unfocused() { + self.view.is_dirty() + } else { + self.component.is_dirty() + } } fn set_dirty(&mut self, value: bool) { @@ -1960,6 +2075,9 @@ impl Component for Listing { s.set_dirty(value); } else { self.component.set_dirty(value); + if self.component.unfocused() { + self.view.set_dirty(value); + } } } @@ -1972,6 +2090,9 @@ impl Component for Listing { let mut config_map = context.settings.shortcuts.listing.key_values(); if self.focus != ListingFocus::Menu { config_map.remove("open_mailbox"); + if self.component.unfocused() { + map.extend(self.view.shortcuts(context).into_iter()); + } } map.insert(Shortcuts::LISTING, config_map); @@ -1979,7 +2100,7 @@ impl Component for Listing { } fn id(&self) -> ComponentId { - self.component.id() + self.id } fn status(&self, context: &Context) -> String { @@ -2022,6 +2143,38 @@ impl Component for Listing { MailboxStatus::Failed(_) | MailboxStatus::None => account[&mailbox_hash].status(), } } + + fn children(&self) -> IndexMap { + let mut ret = IndexMap::default(); + ret.insert( + self.component.id(), + match &self.component { + Compact(l) => l.as_component(), + Plain(l) => l.as_component(), + Threaded(l) => l.as_component(), + Conversations(l) => l.as_component(), + Offline(l) => l.as_component(), + }, + ); + + ret + } + + fn children_mut(&mut self) -> IndexMap { + let mut ret = IndexMap::default(); + ret.insert( + self.component.id(), + match &mut self.component { + Compact(l) => l.as_component_mut(), + Plain(l) => l.as_component_mut(), + Threaded(l) => l.as_component_mut(), + Conversations(l) => l.as_component_mut(), + Offline(l) => l.as_component_mut(), + }, + ); + + ret + } } impl Listing { @@ -2059,18 +2212,23 @@ impl Listing { first_account_hash, MailboxHash::default(), ))), + view: Box::new(ThreadView::default()), accounts: account_entries, status: None, dirty: true, cursor_pos: (0, MenuEntryCursor::Mailbox(0)), menu_cursor_pos: (0, MenuEntryCursor::Mailbox(0)), menu_content: CellBuffer::new_with_context(0, 0, None, context), - menu_scrollbar_show_timer: context.job_executor.clone().create_timer( + menu_scrollbar_show_timer: context.main_loop_handler.job_executor.clone().create_timer( std::time::Duration::from_secs(0), std::time::Duration::from_millis(1200), ), show_menu_scrollbar: ShowMenuScrollbar::Never, - startup_checks_rate: RateLimit::new(2, 1000, context.job_executor.clone()), + startup_checks_rate: RateLimit::new( + 2, + 1000, + context.main_loop_handler.job_executor.clone(), + ), theme_default: conf::value(context, "theme_default"), id: ComponentId::default(), sidebar_divider: *account_settings!( @@ -2084,6 +2242,7 @@ impl Listing { focus: ListingFocus::Mailbox, cmd_buf: String::with_capacity(4), }; + ret.component.realize(ret.id().into(), context); ret.change_account(context); ret } @@ -2580,10 +2739,12 @@ impl Listing { let index_style = mailbox_settings!(context[account_hash][mailbox_hash].listing.index_style); - self.component.set_style(*index_style); + self.set_style(*index_style, context); } else if !matches!(self.component, ListingComponent::Offline(_)) { + self.component.unrealize(context); self.component = Offline(OfflineListing::new((account_hash, MailboxHash::default()))); + self.component.realize(self.id().into(), context); } self.status = None; context @@ -2622,4 +2783,64 @@ impl Listing { fn is_menu_visible(&self) -> bool { !matches!(self.component.focus(), Focus::EntryFullscreen) && self.menu_visibility } + + fn set_style(&mut self, new_style: IndexStyle, context: &mut Context) { + let old = match new_style { + IndexStyle::Plain => { + if matches!(self.component, Plain(_)) { + return; + } + let coordinates = self.component.coordinates(); + std::mem::replace( + &mut self.component, + Plain(PlainListing::new(self.id, coordinates)), + ) + } + IndexStyle::Threaded => { + if matches!(self.component, Threaded(_)) { + return; + } + let coordinates = self.component.coordinates(); + std::mem::replace( + &mut self.component, + Threaded(ThreadListing::new(self.id, coordinates, context)), + ) + } + IndexStyle::Compact => { + if matches!(self.component, Compact(_)) { + return; + } + let coordinates = self.component.coordinates(); + std::mem::replace( + &mut self.component, + Compact(CompactListing::new(self.id, coordinates)), + ) + } + IndexStyle::Conversations => { + if matches!(self.component, Conversations(_)) { + return; + } + let coordinates = self.component.coordinates(); + std::mem::replace( + &mut self.component, + Conversations(ConversationsListing::new(self.id, coordinates)), + ) + } + }; + old.unrealize(context); + self.component.realize(self.id.into(), context); + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ListingMessage { + FocusUpdate { + new_value: Focus, + }, + OpenEntryUnderCursor { + env_hash: EnvelopeHash, + thread_hash: ThreadHash, + show_thread: bool, + }, + UpdateView, } diff --git a/src/components/mail/listing/compact.rs b/src/components/mail/listing/compact.rs index 728b37a7..973385e7 100644 --- a/src/components/mail/listing/compact.rs +++ b/src/components/mail/listing/compact.rs @@ -185,12 +185,13 @@ pub struct CompactListing { force_draw: bool, /// If `self.view` exists or not. focus: Focus, - view: Box, color_cache: ColorCache, movement: Option, modifier_active: bool, modifier_command: Option, + view_area: Option, + parent: ComponentId, id: ComponentId, } @@ -287,6 +288,7 @@ impl MailListingTrait for CompactListing { self.sort, &context.accounts[&self.cursor_pos.0].collection.envelopes, ); + drop(threads); self.redraw_threads_list( context, @@ -294,10 +296,22 @@ impl MailListingTrait for CompactListing { ); if !force && old_cursor_pos == self.new_cursor_pos { - self.view.update(context); + self.kick_parent(self.parent, ListingMessage::UpdateView, context); } else if self.unfocused() { - if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) { - self.view = Box::new(ThreadView::new(self.new_cursor_pos, thread, None, context)); + if let Some((thread_hash, env_hash)) = self + .get_thread_under_cursor(self.cursor_pos.2) + .and_then(|thread| self.rows.thread_to_env.get(&thread).map(|e| (thread, e[0]))) + { + self.kick_parent( + self.parent, + ListingMessage::OpenEntryUnderCursor { + thread_hash, + env_hash, + show_thread: true, + }, + context, + ); + self.set_focus(Focus::Entry, context); } } } @@ -564,7 +578,6 @@ impl ListingTrait for CompactListing { fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) { self.new_cursor_pos = (coordinates.0, coordinates.1, 0); self.focus = Focus::None; - self.view = Box::::default(); self.filtered_selection.clear(); self.filtered_order.clear(); self.filter_term.clear(); @@ -812,6 +825,10 @@ impl ListingTrait for CompactListing { ); } + fn view_area(&self) -> Option { + self.view_area + } + fn unfocused(&self) -> bool { !matches!(self.focus, Focus::None) } @@ -836,8 +853,6 @@ impl ListingTrait for CompactListing { fn set_focus(&mut self, new_value: Focus, context: &mut Context) { match new_value { Focus::None => { - self.view - .process_event(&mut UIEvent::VisibilityChange(false), context); self.dirty = true; /* If self.rows.row_updates is not empty and we exit a thread, the row_update * events will be performed but the list will not be drawn. @@ -848,13 +863,17 @@ impl ListingTrait for CompactListing { Focus::Entry => { self.force_draw = true; self.dirty = true; - self.view.set_dirty(true); } Focus::EntryFullscreen => { - self.view.set_dirty(true); + self.dirty = true; } } self.focus = new_value; + self.kick_parent( + self.parent, + ListingMessage::FocusUpdate { new_value }, + context, + ); } fn focus(&self) -> Focus { @@ -870,7 +889,7 @@ impl fmt::Display for CompactListing { impl CompactListing { pub const DESCRIPTION: &'static str = "compact listing"; - pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box { + pub fn new(parent: ComponentId, coordinates: (AccountHash, MailboxHash)) -> Box { Box::new(CompactListing { cursor_pos: (coordinates.0, MailboxHash::default(), 0), new_cursor_pos: (coordinates.0, coordinates.1, 0), @@ -889,11 +908,12 @@ impl CompactListing { rows: RowsState::default(), dirty: true, force_draw: true, - view: Box::::default(), color_cache: ColorCache::default(), movement: None, modifier_active: false, modifier_command: None, + view_area: None, + parent, id: ComponentId::default(), }) } @@ -1440,12 +1460,13 @@ impl CompactListing { impl Component for CompactListing { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if !self.is_dirty() { + if matches!(self.focus, Focus::EntryFullscreen) { + self.view_area = area.into(); return; } - if matches!(self.focus, Focus::EntryFullscreen) { - return self.view.draw(grid, area, context); + if !self.is_dirty() { + return; } if !self.unfocused() { @@ -1681,7 +1702,7 @@ impl Component for CompactListing { return; } - self.view.draw(grid, area, context); + self.view_area = area.into(); } self.dirty = false; } @@ -1711,10 +1732,6 @@ impl Component for CompactListing { _ => {} } - if self.unfocused() && self.view.process_event(event, context) { - return true; - } - if self.length > 0 { match *event { UIEvent::Input(ref k) @@ -1722,9 +1739,21 @@ impl Component for CompactListing { && (shortcut!(k == shortcuts[Shortcuts::LISTING]["open_entry"]) || shortcut!(k == shortcuts[Shortcuts::LISTING]["focus_right"])) => { - if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) { - self.view = - Box::new(ThreadView::new(self.cursor_pos, thread, None, context)); + if let Some((thread_hash, env_hash)) = self + .get_thread_under_cursor(self.cursor_pos.2) + .and_then(|thread| { + self.rows.thread_to_env.get(&thread).map(|e| (thread, e[0])) + }) + { + self.kick_parent( + self.parent, + ListingMessage::OpenEntryUnderCursor { + thread_hash, + env_hash, + show_thread: true, + }, + context, + ); self.set_focus(Focus::Entry, context); } return true; @@ -1837,7 +1866,7 @@ impl Component for CompactListing { self.refresh_mailbox(context, false); self.set_dirty(true); } - UIEvent::EnvelopeRename(ref old_hash, ref new_hash) => { + UIEvent::EnvelopeRename(_, ref new_hash) => { let account = &context.accounts[&self.cursor_pos.0]; let threads = account.collection.get_threads(self.cursor_pos.1); if !account.collection.contains_key(new_hash) { @@ -1855,13 +1884,8 @@ impl Component for CompactListing { } self.set_dirty(true); - - if self.unfocused() { - self.view - .process_event(&mut UIEvent::EnvelopeRename(*old_hash, *new_hash), context); - } } - UIEvent::EnvelopeRemove(ref _env_hash, ref thread_hash) => { + UIEvent::EnvelopeRemove(_, ref thread_hash) => { if self.rows.thread_order.contains_key(thread_hash) { self.refresh_mailbox(context, false); self.set_dirty(true); @@ -1885,11 +1909,6 @@ impl Component for CompactListing { } self.set_dirty(true); - - if self.unfocused() { - self.view - .process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context); - } } UIEvent::ChangeMode(UIMode::Normal) => { self.set_dirty(true); @@ -1926,6 +1945,7 @@ impl Component for CompactListing { ) { Ok(job) => { let handle = context.accounts[&self.cursor_pos.0] + .main_loop_handler .job_executor .spawn_specialized(job); self.search_job = Some((filter_term.to_string(), handle)); @@ -1948,6 +1968,7 @@ impl Component for CompactListing { ) { Ok(job) => { let mut handle = context.accounts[&self.cursor_pos.0] + .main_loop_handler .job_executor .spawn_specialized(job); if let Ok(Some(search_result)) = try_recv_timeout!(&mut handle.chan) { @@ -2011,24 +2032,17 @@ impl Component for CompactListing { fn is_dirty(&self) -> bool { match self.focus { Focus::None => self.dirty, - Focus::Entry => self.dirty || self.view.is_dirty(), - Focus::EntryFullscreen => self.view.is_dirty(), + Focus::Entry => self.dirty, + Focus::EntryFullscreen => false, } } fn set_dirty(&mut self, value: bool) { self.dirty = value; - if self.unfocused() { - self.view.set_dirty(value); - } } fn shortcuts(&self, context: &Context) -> ShortcutMaps { - let mut map = if self.unfocused() { - self.view.shortcuts(context) - } else { - ShortcutMaps::default() - }; + let mut map = ShortcutMaps::default(); map.insert( Shortcuts::LISTING, diff --git a/src/components/mail/listing/conversations.rs b/src/components/mail/listing/conversations.rs index a5125d9c..6a13eeef 100644 --- a/src/components/mail/listing/conversations.rs +++ b/src/components/mail/listing/conversations.rs @@ -113,14 +113,15 @@ pub struct ConversationsListing { /// If we must redraw on next redraw event dirty: bool, force_draw: bool, - /// If `self.view` exists or not. + /// If `self.view` is visible or not. focus: Focus, - view: ThreadView, color_cache: ColorCache, movement: Option, modifier_active: bool, modifier_command: Option, + view_area: Option, + parent: ComponentId, id: ComponentId, } @@ -204,6 +205,7 @@ impl MailListingTrait for ConversationsListing { self.sort, &context.accounts[&self.cursor_pos.0].collection.envelopes, ); + drop(threads); self.redraw_threads_list( context, @@ -212,10 +214,22 @@ impl MailListingTrait for ConversationsListing { if !force && old_cursor_pos == self.new_cursor_pos && old_mailbox_hash == self.cursor_pos.1 { - self.view.update(context); + self.kick_parent(self.parent, ListingMessage::UpdateView, context); } else if self.unfocused() { - if let Some(thread_group) = self.get_thread_under_cursor(self.cursor_pos.2) { - self.view = ThreadView::new(self.new_cursor_pos, thread_group, None, context); + if let Some((thread_hash, env_hash)) = self + .get_thread_under_cursor(self.cursor_pos.2) + .and_then(|thread| self.rows.thread_to_env.get(&thread).map(|e| (thread, e[0]))) + { + self.kick_parent( + self.parent, + ListingMessage::OpenEntryUnderCursor { + thread_hash, + env_hash, + show_thread: true, + }, + context, + ); + self.set_focus(Focus::Entry, context); } } } @@ -377,7 +391,6 @@ impl ListingTrait for ConversationsListing { fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) { self.new_cursor_pos = (coordinates.0, coordinates.1, 0); self.focus = Focus::None; - self.view = ThreadView::default(); self.filtered_selection.clear(); self.filtered_order.clear(); self.filter_term.clear(); @@ -556,6 +569,10 @@ impl ListingTrait for ConversationsListing { ); } + fn view_area(&self) -> Option { + self.view_area + } + fn unfocused(&self) -> bool { !matches!(self.focus, Focus::None) } @@ -580,8 +597,6 @@ impl ListingTrait for ConversationsListing { fn set_focus(&mut self, new_value: Focus, context: &mut Context) { match new_value { Focus::None => { - self.view - .process_event(&mut UIEvent::VisibilityChange(false), context); self.dirty = true; /* If self.rows.row_updates is not empty and we exit a thread, the row_update * events will be performed but the list will not be drawn. @@ -592,13 +607,15 @@ impl ListingTrait for ConversationsListing { Focus::Entry => { self.force_draw = true; self.dirty = true; - self.view.set_dirty(true); - } - Focus::EntryFullscreen => { - self.view.set_dirty(true); } + Focus::EntryFullscreen => {} } self.focus = new_value; + self.kick_parent( + self.parent, + ListingMessage::FocusUpdate { new_value }, + context, + ); } fn focus(&self) -> Focus { @@ -615,7 +632,7 @@ impl fmt::Display for ConversationsListing { impl ConversationsListing { //const PADDING_CHAR: char = ' '; //░'; - pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box { + pub fn new(parent: ComponentId, coordinates: (AccountHash, MailboxHash)) -> Box { Box::new(Self { cursor_pos: (coordinates.0, MailboxHash::default(), 0), new_cursor_pos: (coordinates.0, coordinates.1, 0), @@ -631,11 +648,12 @@ impl ConversationsListing { dirty: true, force_draw: true, focus: Focus::None, - view: ThreadView::default(), color_cache: ColorCache::default(), movement: None, modifier_active: false, modifier_command: None, + view_area: None, + parent, id: ComponentId::default(), }) } @@ -969,12 +987,13 @@ impl ConversationsListing { impl Component for ConversationsListing { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if !self.is_dirty() { + if matches!(self.focus, Focus::EntryFullscreen) { + self.view_area = area.into(); return; } - if matches!(self.focus, Focus::EntryFullscreen) { - return self.view.draw(grid, area, context); + if !self.is_dirty() { + return; } let (upper_left, bottom_right) = area; @@ -1228,7 +1247,7 @@ impl Component for ConversationsListing { ); clear_area(grid, gap_area, self.color_cache.theme_default); context.dirty_areas.push_back(gap_area); - self.view.draw(grid, entry_area, context); + self.view_area = entry_area.into(); } self.dirty = false; } @@ -1258,10 +1277,6 @@ impl Component for ConversationsListing { _ => {} } - if self.unfocused() && self.view.process_event(event, context) { - return true; - } - if self.length > 0 { match *event { UIEvent::Input(ref k) @@ -1269,8 +1284,21 @@ impl Component for ConversationsListing { && (shortcut!(k == shortcuts[Shortcuts::LISTING]["open_entry"]) || shortcut!(k == shortcuts[Shortcuts::LISTING]["focus_right"])) => { - if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) { - self.view = ThreadView::new(self.cursor_pos, thread, None, context); + if let Some((thread_hash, env_hash)) = self + .get_thread_under_cursor(self.cursor_pos.2) + .and_then(|thread| { + self.rows.thread_to_env.get(&thread).map(|e| (thread, e[0])) + }) + { + self.kick_parent( + self.parent, + ListingMessage::OpenEntryUnderCursor { + thread_hash, + env_hash, + show_thread: true, + }, + context, + ); self.set_focus(Focus::Entry, context); } return true; @@ -1336,13 +1364,6 @@ impl Component for ConversationsListing { } self.set_dirty(true); - - if self.unfocused() { - self.view.process_event( - &mut UIEvent::EnvelopeRename(*old_hash, *new_hash), - context, - ); - } } UIEvent::EnvelopeRemove(ref _env_hash, ref thread_hash) => { if self.rows.thread_order.contains_key(thread_hash) { @@ -1368,11 +1389,6 @@ impl Component for ConversationsListing { } self.set_dirty(true); - - if self.unfocused() { - self.view - .process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context); - } } UIEvent::Action(ref action) => match action { Action::SubSort(field, order) if !self.unfocused() => { @@ -1464,6 +1480,7 @@ impl Component for ConversationsListing { ) { Ok(job) => { let handle = context.accounts[&self.cursor_pos.0] + .main_loop_handler .job_executor .spawn_specialized(job); self.search_job = Some((filter_term.to_string(), handle)); @@ -1533,24 +1550,17 @@ impl Component for ConversationsListing { fn is_dirty(&self) -> bool { match self.focus { Focus::None => self.dirty, - Focus::Entry => self.dirty || self.view.is_dirty(), - Focus::EntryFullscreen => self.view.is_dirty(), + Focus::Entry => self.dirty, + Focus::EntryFullscreen => false, } } fn set_dirty(&mut self, value: bool) { - if self.unfocused() { - self.view.set_dirty(value); - } self.dirty = value; } fn shortcuts(&self, context: &Context) -> ShortcutMaps { - let mut map = if self.unfocused() { - self.view.shortcuts(context) - } else { - ShortcutMaps::default() - }; + let mut map = ShortcutMaps::default(); map.insert( Shortcuts::LISTING, diff --git a/src/components/mail/listing/offline.rs b/src/components/mail/listing/offline.rs index c6392993..6bfe9ec1 100644 --- a/src/components/mail/listing/offline.rs +++ b/src/components/mail/listing/offline.rs @@ -82,6 +82,10 @@ impl ListingTrait for OfflineListing { fn draw_list(&mut self, _: &mut CellBuffer, _: Area, _: &mut Context) {} + fn view_area(&self) -> Option { + None + } + fn unfocused(&self) -> bool { false } diff --git a/src/components/mail/listing/plain.rs b/src/components/mail/listing/plain.rs index c4dc1128..5165ce27 100644 --- a/src/components/mail/listing/plain.rs +++ b/src/components/mail/listing/plain.rs @@ -141,13 +141,14 @@ pub struct PlainListing { /// If we must redraw on next redraw event dirty: bool, force_draw: bool, - /// If `self.view` exists or not. + /// If view is visible or not. focus: Focus, - view: MailView, color_cache: ColorCache, movement: Option, modifier_active: bool, modifier_command: Option, + view_area: Option, + parent: ComponentId, id: ComponentId, } @@ -261,11 +262,21 @@ impl MailListingTrait for PlainListing { drop(env_lck); if let Some(env_hash) = self.get_env_under_cursor(self.cursor_pos.2) { - let temp = (self.new_cursor_pos.0, self.new_cursor_pos.1, env_hash); if !force && old_cursor_pos == self.new_cursor_pos { - self.view.update(temp, context); + self.kick_parent(self.parent, ListingMessage::UpdateView, context); } else if self.unfocused() { - self.view = MailView::new(temp, None, None, context); + let thread_hash = self.rows.env_to_thread[&env_hash]; + self.force_draw = true; + self.dirty = true; + self.kick_parent( + self.parent, + ListingMessage::OpenEntryUnderCursor { + thread_hash, + env_hash, + show_thread: false, + }, + context, + ); } } } @@ -304,7 +315,6 @@ impl ListingTrait for PlainListing { fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) { self.new_cursor_pos = (coordinates.0, coordinates.1, 0); self.focus = Focus::None; - self.view = MailView::default(); self.filtered_selection.clear(); self.filtered_order.clear(); self.filter_term.clear(); @@ -544,6 +554,10 @@ impl ListingTrait for PlainListing { ); } + fn view_area(&self) -> Option { + self.view_area + } + fn unfocused(&self) -> bool { !matches!(self.focus, Focus::None) } @@ -568,8 +582,7 @@ impl ListingTrait for PlainListing { fn set_focus(&mut self, new_value: Focus, context: &mut Context) { match new_value { Focus::None => { - self.view - .process_event(&mut UIEvent::VisibilityChange(false), context); + //self.view .process_event(&mut UIEvent::VisibilityChange(false), context); self.dirty = true; /* If self.rows.row_updates is not empty and we exit a thread, the row_update * events will be performed but the list will not be drawn. @@ -578,20 +591,33 @@ impl ListingTrait for PlainListing { self.force_draw = true; } Focus::Entry => { - if let Some(env_hash) = self.get_env_under_cursor(self.cursor_pos.2) { - let temp = (self.cursor_pos.0, self.cursor_pos.1, env_hash); - self.view = MailView::new(temp, None, None, context); + if let Some((thread_hash, env_hash)) = self + .get_env_under_cursor(self.cursor_pos.2) + .map(|env_hash| (self.rows.env_to_thread[&env_hash], env_hash)) + { self.force_draw = true; self.dirty = true; - self.view.set_dirty(true); + self.kick_parent( + self.parent, + ListingMessage::OpenEntryUnderCursor { + thread_hash, + env_hash, + show_thread: false, + }, + context, + ); } } Focus::EntryFullscreen => { self.dirty = true; - self.view.set_dirty(true); } } self.focus = new_value; + self.kick_parent( + self.parent, + ListingMessage::FocusUpdate { new_value }, + context, + ); } fn focus(&self) -> Focus { @@ -606,7 +632,7 @@ impl fmt::Display for PlainListing { } impl PlainListing { - pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box { + pub fn new(parent: ComponentId, coordinates: (AccountHash, MailboxHash)) -> Box { Box::new(PlainListing { cursor_pos: (AccountHash::default(), MailboxHash::default(), 0), new_cursor_pos: (coordinates.0, coordinates.1, 0), @@ -623,11 +649,12 @@ impl PlainListing { dirty: true, force_draw: true, focus: Focus::None, - view: MailView::default(), color_cache: ColorCache::default(), movement: None, modifier_active: false, modifier_command: None, + view_area: None, + parent, id: ComponentId::default(), }) } @@ -1097,12 +1124,13 @@ impl PlainListing { impl Component for PlainListing { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if !self.is_dirty() { + if matches!(self.focus, Focus::EntryFullscreen) { + self.view_area = area.into(); return; } - if matches!(self.focus, Focus::EntryFullscreen) { - return self.view.draw(grid, area, context); + if !self.is_dirty() { + return; } if matches!(self.focus, Focus::None) { @@ -1340,7 +1368,7 @@ impl Component for PlainListing { return; } - self.view.draw(grid, area, context); + self.view_area = area.into(); } self.dirty = false; } @@ -1370,10 +1398,6 @@ impl Component for PlainListing { _ => {} } - if self.unfocused() && self.view.process_event(event, context) { - return true; - } - if self.length > 0 { match *event { UIEvent::Input(ref k) @@ -1481,11 +1505,6 @@ impl Component for PlainListing { } self.set_dirty(true); - - if self.unfocused() { - self.view - .process_event(&mut UIEvent::EnvelopeRename(*old_hash, *new_hash), context); - } } UIEvent::EnvelopeUpdate(ref env_hash) => { let account = &context.accounts[&self.cursor_pos.0]; @@ -1500,11 +1519,6 @@ impl Component for PlainListing { self.rows.row_updates.push(*env_hash); self.set_dirty(true); - - if self.unfocused() { - self.view - .process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context); - } } UIEvent::ChangeMode(UIMode::Normal) => { self.set_dirty(true); @@ -1539,6 +1553,7 @@ impl Component for PlainListing { ) { Ok(job) => { let handle = context.accounts[&self.cursor_pos.0] + .main_loop_handler .job_executor .spawn_specialized(job); self.search_job = Some((filter_term.to_string(), handle)); @@ -1583,23 +1598,16 @@ impl Component for PlainListing { fn is_dirty(&self) -> bool { match self.focus { Focus::None => self.dirty, - Focus::Entry | Focus::EntryFullscreen => self.view.is_dirty(), + Focus::Entry | Focus::EntryFullscreen => false, } } fn set_dirty(&mut self, value: bool) { self.dirty = value; - if self.unfocused() { - self.view.set_dirty(value); - } } fn shortcuts(&self, context: &Context) -> ShortcutMaps { - let mut map = if self.unfocused() { - self.view.shortcuts(context) - } else { - ShortcutMaps::default() - }; + let mut map = ShortcutMaps::default(); map.insert( Shortcuts::LISTING, diff --git a/src/components/mail/listing/thread.rs b/src/components/mail/listing/thread.rs index 883b8852..b8e40307 100644 --- a/src/components/mail/listing/thread.rs +++ b/src/components/mail/listing/thread.rs @@ -102,8 +102,8 @@ macro_rules! row_attr { }}; } -/// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the -/// `Envelope` content in a `MailView`. +/// A list of all mail ([`Envelope`](melib::Envelope)s) in a `Mailbox`. On `\n` it opens the +/// [`Envelope`](melib::Envelope) content in a [`MailView`]. #[derive(Debug)] pub struct ThreadListing { /// (x, y, z): x is accounts, y is mailboxes, z is index inside a mailbox. @@ -126,13 +126,14 @@ pub struct ThreadListing { /// If we must redraw on next redraw event dirty: bool, force_draw: bool, - /// If `self.view` is focused or not. + /// If `self.view` is visible or not. focus: Focus, - initialised: bool, - view: Option>, + initialized: bool, modifier_active: bool, modifier_command: Option, movement: Option, + view_area: Option, + parent: ComponentId, id: ComponentId, } @@ -171,6 +172,7 @@ impl MailListingTrait for ThreadListing { /// mailbox the user has chosen. fn refresh_mailbox(&mut self, context: &mut Context, _force: bool) { self.set_dirty(true); + self.initialized = true; if !(self.cursor_pos.0 == self.new_cursor_pos.0 && self.cursor_pos.1 == self.new_cursor_pos.1) { @@ -425,13 +427,14 @@ impl ListingTrait for ThreadListing { fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) { self.new_cursor_pos = (coordinates.0, coordinates.1, 0); self.focus = Focus::None; - self.view = None; self.rows.clear(); - self.initialised = false; + self.initialized = false; } fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if self.cursor_pos.1 != self.new_cursor_pos.1 || self.cursor_pos.0 != self.new_cursor_pos.0 + if !self.initialized + || self.cursor_pos.1 != self.new_cursor_pos.1 + || self.cursor_pos.0 != self.new_cursor_pos.0 { self.refresh_mailbox(context, false); } @@ -608,6 +611,10 @@ impl ListingTrait for ThreadListing { let _account = &context.accounts[&self.cursor_pos.0]; } + fn view_area(&self) -> Option { + self.view_area + } + fn unfocused(&self) -> bool { !matches!(self.focus, Focus::None) } @@ -632,7 +639,6 @@ impl ListingTrait for ThreadListing { fn set_focus(&mut self, new_value: Focus, context: &mut Context) { match new_value { Focus::None => { - self.view = None; self.dirty = true; /* If self.rows.row_updates is not empty and we exit a thread, the row_update * events will be performed but the list will not be drawn. @@ -641,29 +647,34 @@ impl ListingTrait for ThreadListing { self.force_draw = true; } Focus::Entry => { - if let Some(env_hash) = self.get_env_under_cursor(self.cursor_pos.2) { + if let Some((thread_hash, env_hash)) = self + .get_env_under_cursor(self.cursor_pos.2) + .map(|env_hash| (self.rows.env_to_thread[&env_hash], env_hash)) + { self.force_draw = true; self.dirty = true; - let coordinates = (self.cursor_pos.0, self.cursor_pos.1, env_hash); - - if let Some(ref mut v) = self.view { - v.update(coordinates, context); - } else { - self.view = Some(Box::new(MailView::new(coordinates, None, None, context))); - } - if let Some(ref mut s) = self.view { - s.set_dirty(true); - } + self.kick_parent( + self.parent, + ListingMessage::OpenEntryUnderCursor { + thread_hash, + env_hash, + show_thread: false, + }, + context, + ); } } Focus::EntryFullscreen => { - if let Some(ref mut s) = self.view { - s.set_dirty(true); - } + self.dirty = true; } } self.focus = new_value; + self.kick_parent( + self.parent, + ListingMessage::FocusUpdate { new_value }, + context, + ); } fn focus(&self) -> Focus { @@ -678,26 +689,31 @@ impl fmt::Display for ThreadListing { } impl ThreadListing { - pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box { + pub fn new( + parent: ComponentId, + coordinates: (AccountHash, MailboxHash), + context: &mut Context, + ) -> Box { Box::new(ThreadListing { cursor_pos: (coordinates.0, MailboxHash::default(), 0), new_cursor_pos: (coordinates.0, coordinates.1, 0), length: 0, sort: (Default::default(), Default::default()), subsort: (Default::default(), Default::default()), - color_cache: ColorCache::default(), + color_cache: ColorCache::new(context, IndexStyle::Threaded), data_columns: DataColumns::default(), rows: RowsState::default(), + search_job: None, dirty: true, force_draw: true, focus: Focus::None, - view: None, - initialised: false, + initialized: false, movement: None, modifier_active: false, modifier_command: None, + view_area: None, + parent, id: ComponentId::default(), - search_job: None, }) } @@ -1026,6 +1042,11 @@ impl ThreadListing { impl Component for ThreadListing { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + if matches!(self.focus, Focus::EntryFullscreen) { + self.view_area = area.into(); + return; + } + let (upper_left, bottom_right) = area; let rows = get_y(bottom_right) - get_y(upper_left) + 1; @@ -1210,12 +1231,6 @@ impl Component for ThreadListing { return; } - if matches!(self.focus, Focus::EntryFullscreen) { - if let Some(v) = self.view.as_mut() { - return v.draw(grid, area, context); - } - } - if !self.unfocused() { self.dirty = false; /* Draw the entire list */ @@ -1290,27 +1305,7 @@ impl Component for ThreadListing { .push_back((set_y(upper_left, mid), set_y(bottom_right, mid))); } - if !self.dirty { - if let Some(v) = self.view.as_mut() { - v.draw(grid, (set_y(upper_left, mid + 1), bottom_right), context); - } - return; - } - - if let Some(env_hash) = self.get_env_under_cursor(self.cursor_pos.2) { - let coordinates = (self.cursor_pos.0, self.cursor_pos.1, env_hash); - - if let Some(ref mut v) = self.view { - v.update(coordinates, context); - } else { - self.view = Some(Box::new(MailView::new(coordinates, None, None, context))); - } - } - - if let Some(v) = self.view.as_mut() { - v.draw(grid, (set_y(upper_left, mid + 1), bottom_right), context); - } - + self.view_area = (set_y(upper_left, mid + 1), bottom_right).into(); self.dirty = false; } } @@ -1340,12 +1335,6 @@ impl Component for ThreadListing { _ => {} } - if let Some(ref mut v) = self.view { - if !matches!(self.focus, Focus::None) && v.process_event(event, context) { - return true; - } - } - match *event { UIEvent::ConfigReload { old_settings: _ } => { self.color_cache = ColorCache::new(context, IndexStyle::Threaded); @@ -1411,15 +1400,6 @@ impl Component for ThreadListing { } self.set_dirty(true); - - if self.unfocused() { - if let Some(v) = self.view.as_mut() { - v.process_event( - &mut UIEvent::EnvelopeRename(*old_hash, *new_hash), - context, - ); - } - } } UIEvent::EnvelopeRemove(ref env_hash, _) => { if self.rows.contains_env(*env_hash) { @@ -1437,12 +1417,6 @@ impl Component for ThreadListing { } self.set_dirty(true); - - if self.unfocused() { - if let Some(v) = self.view.as_mut() { - v.process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context); - } - } } UIEvent::ChangeMode(UIMode::Normal) => { self.set_dirty(true); @@ -1500,6 +1474,7 @@ impl Component for ThreadListing { ) { Ok(job) => { let handle = context.accounts[&self.cursor_pos.0] + .main_loop_handler .job_executor .spawn_specialized(job); self.search_job = Some((filter_term.to_string(), handle)); @@ -1547,27 +1522,17 @@ impl Component for ThreadListing { fn is_dirty(&self) -> bool { match self.focus { Focus::None => self.dirty, - Focus::Entry => self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false), - Focus::EntryFullscreen => self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false), + Focus::Entry => self.dirty, + Focus::EntryFullscreen => false, } } fn set_dirty(&mut self, value: bool) { - if let Some(p) = self.view.as_mut() { - p.set_dirty(value); - }; self.dirty = value; } fn shortcuts(&self, context: &Context) -> ShortcutMaps { - let mut map = if self.unfocused() { - self.view - .as_ref() - .map(|p| p.shortcuts(context)) - .unwrap_or_default() - } else { - ShortcutMaps::default() - }; + let mut map = ShortcutMaps::default(); map.insert( Shortcuts::LISTING, diff --git a/src/components/mail/view.rs b/src/components/mail/view.rs index 3acf98f5..42f72bb8 100644 --- a/src/components/mail/view.rs +++ b/src/components/mail/view.rs @@ -24,7 +24,6 @@ use std::{ convert::TryFrom, fmt::Write as _, io::Write, - os::unix::fs::PermissionsExt, process::{Command, Stdio}, }; @@ -35,287 +34,42 @@ use melib::{ use smallvec::SmallVec; use super::*; -use crate::{ - conf::accounts::JobRequest, - jobs::{JobId, JoinHandle}, -}; +use crate::{conf::accounts::JobRequest, jobs::JobId}; + +mod utils; +pub use utils::*; mod html; -pub use self::html::*; +pub use html::*; mod thread; -pub use self::thread::*; - -mod envelope; -use linkify::LinkFinder; -use melib::xdg_utils::query_default_app; - -pub use self::envelope::*; - -#[derive(Debug, Default)] -enum ForceCharset { - #[default] - None, - Dialog(Box>>), - Forced(Charset), -} +pub use thread::*; +mod types; +pub use types::*; +mod state; +use state::*; -impl Into> for &ForceCharset { - fn into(self) -> Option { - match self { - ForceCharset::Forced(val) => Some(*val), - ForceCharset::None | ForceCharset::Dialog(_) => None, - } - } -} - -#[derive(PartialEq, Eq, Copy, Clone, Debug)] -enum Source { - Decoded, - Raw, -} - -#[derive(PartialEq, Debug, Default)] -enum ViewMode { - #[default] - Normal, - Url, - Attachment(usize), - Source(Source), - //Ansi(RawBuffer), - Subview, - ContactSelector(Box>), -} - -impl ViewMode { - /* - fn is_ansi(&self) -> bool { - match self { - ViewMode::Ansi(_) => true, - _ => false, - } - } - */ - - fn is_attachment(&self) -> bool { - matches!(self, ViewMode::Attachment(_)) - } - - fn is_contact_selector(&self) -> bool { - matches!(self, ViewMode::ContactSelector(_)) - } -} - -#[derive(Debug)] -pub enum AttachmentDisplay { - Alternative { - inner: Box, - shown_display: usize, - display: Vec, - }, - InlineText { - inner: Box, - comment: Option, - text: String, - }, - InlineOther { - inner: Box, - }, - Attachment { - inner: Box, - }, - SignedPending { - inner: Box, - display: Vec, - handle: JoinHandle>, - job_id: JobId, - }, - SignedFailed { - inner: Box, - display: Vec, - error: Error, - }, - SignedUnverified { - inner: Box, - display: Vec, - }, - SignedVerified { - inner: Box, - display: Vec, - description: String, - }, - EncryptedPending { - inner: Box, - handle: JoinHandle)>>, - }, - EncryptedFailed { - inner: Box, - error: Error, - }, - EncryptedSuccess { - inner: Box, - plaintext: Box, - plaintext_display: Vec, - description: String, - }, -} +pub mod envelope; +pub use envelope::EnvelopeView; /// Contains an Envelope view, with sticky headers, a pager for the body, and /// subviews for more menus #[derive(Debug, Default)] pub struct MailView { - coordinates: (AccountHash, MailboxHash, EnvelopeHash), - pager: Pager, - subview: Option>, + coordinates: Option<(AccountHash, MailboxHash, EnvelopeHash)>, dirty: bool, - initialised: bool, - mode: ViewMode, - expand_headers: bool, - attachment_tree: String, - attachment_paths: Vec>, - headers_no: usize, - headers_cursor: usize, - force_draw_headers: bool, + contact_selector: Option>>, theme_default: ThemeAttribute, active_jobs: HashSet, state: MailViewState, - force_charset: ForceCharset, - - cmd_buf: String, id: ComponentId, } -#[derive(Debug, Copy, Clone)] -pub enum PendingReplyAction { - Reply, - ReplyToAuthor, - ReplyToAll, - ForwardAttachment, - ForwardInline, -} - -#[derive(Debug)] -enum MailViewState { - Init { - pending_action: Option, - }, - LoadingBody { - handle: JoinHandle>>, - pending_action: Option, - }, - Error { - err: Error, - }, - Loaded { - bytes: Vec, - env: Box, - body: Box, - display: Vec, - body_text: String, - links: Vec, - }, -} - -impl MailViewState { - fn load_bytes(self_: &mut MailView, bytes: Vec, context: &mut Context) { - let account = &mut context.accounts[&self_.coordinates.0]; - if account - .collection - .get_env(self_.coordinates.2) - .other_headers() - .is_empty() - { - let _ = account - .collection - .get_env_mut(self_.coordinates.2) - .populate_headers(&bytes); - } - let env = Box::new(account.collection.get_env(self_.coordinates.2).clone()); - let body = Box::new(AttachmentBuilder::new(&bytes).build()); - let display = MailView::attachment_to( - &body, - context, - self_.coordinates, - &mut self_.active_jobs, - (&self_.force_charset).into(), - ); - let (paths, attachment_tree_s) = self_.attachment_displays_to_tree(&display); - self_.attachment_tree = attachment_tree_s; - self_.attachment_paths = paths; - let body_text = self_.attachment_displays_to_text(&display, context, true); - self_.state = MailViewState::Loaded { - display, - env, - body, - bytes, - body_text, - links: vec![], - }; - } - - fn redecode(self_: &mut MailView, context: &mut Context) { - let (new_display, new_body_text) = - if let MailViewState::Loaded { ref body, .. } = self_.state { - let new_display = MailView::attachment_to( - body, - context, - self_.coordinates, - &mut self_.active_jobs, - (&self_.force_charset).into(), - ); - let (paths, attachment_tree_s) = self_.attachment_displays_to_tree(&new_display); - self_.attachment_tree = attachment_tree_s; - self_.attachment_paths = paths; - let body_text = self_.attachment_displays_to_text(&new_display, context, true); - (new_display, body_text) - } else { - return; - }; - - if let MailViewState::Loaded { - ref mut display, - ref mut body_text, - .. - } = self_.state - { - *display = new_display; - *body_text = new_body_text; - } - } -} - -#[derive(Copy, Clone, Debug)] -enum LinkKind { - Url, - Email, -} - -#[derive(Debug, Copy, Clone)] -struct Link { - start: usize, - end: usize, - kind: LinkKind, -} - -impl Default for MailViewState { - fn default() -> Self { - MailViewState::Init { - pending_action: None, - } - } -} - impl Clone for MailView { fn clone(&self) -> Self { MailView { - subview: None, - cmd_buf: String::with_capacity(4), - pager: self.pager.clone(), - mode: ViewMode::Normal, - attachment_tree: self.attachment_tree.clone(), - attachment_paths: self.attachment_paths.clone(), + contact_selector: None, state: MailViewState::default(), active_jobs: self.active_jobs.clone(), - force_charset: ForceCharset::None, ..*self } } @@ -329,31 +83,16 @@ impl fmt::Display for MailView { impl MailView { pub fn new( - coordinates: (AccountHash, MailboxHash, EnvelopeHash), - pager: Option, - subview: Option>, + coordinates: Option<(AccountHash, MailboxHash, EnvelopeHash)>, context: &mut Context, ) -> Self { let mut ret = MailView { coordinates, - pager: pager.unwrap_or_default(), - subview, dirty: true, - initialised: false, - mode: ViewMode::Normal, - expand_headers: false, - attachment_tree: String::new(), - attachment_paths: vec![], - - headers_no: 5, - headers_cursor: 0, - force_draw_headers: false, - + contact_selector: None, theme_default: crate::conf::value(context, "mail.view.body"), active_jobs: Default::default(), state: MailViewState::default(), - force_charset: ForceCharset::None, - cmd_buf: String::with_capacity(4), id: ComponentId::default(), }; @@ -362,18 +101,22 @@ impl MailView { } fn init_futures(&mut self, context: &mut Context) { - debug!("init_futures"); + log::trace!("MailView::init_futures"); self.theme_default = crate::conf::value(context, "mail.view.body"); let mut pending_action = None; - let account = &mut context.accounts[&self.coordinates.0]; - if debug!(account.contains_key(self.coordinates.2)) { + let Some(coordinates) = self.coordinates else { return; }; + let account = &mut context.accounts[&coordinates.0]; + if account.contains_key(coordinates.2) { { match account - .operation(self.coordinates.2) + .operation(coordinates.2) .and_then(|mut op| op.as_bytes()) { Ok(fut) => { - let mut handle = account.job_executor.spawn_specialized(fut); + let mut handle = account + .main_loop_handler + .job_executor + .spawn_specialized(fut); let job_id = handle.job_id; pending_action = if let MailViewState::Init { ref mut pending_action, @@ -410,20 +153,23 @@ impl MailView { } } } - let account = &mut context.accounts[&self.coordinates.0]; - if !account.collection.get_env(self.coordinates.2).is_seen() { + let account = &mut context.accounts[&coordinates.0]; + if !account.collection.get_env(coordinates.2).is_seen() { let job = account.backend.write().unwrap().set_flags( - self.coordinates.2.into(), - self.coordinates.1, + coordinates.2.into(), + coordinates.1, smallvec::smallvec![(Ok(Flag::SEEN), true)], ); match job { Ok(fut) => { - let handle = account.job_executor.spawn_specialized(fut); + let handle = account + .main_loop_handler + .job_executor + .spawn_specialized(fut); account.insert_job( handle.job_id, JobRequest::SetFlags { - env_hashes: self.coordinates.2.into(), + env_hashes: coordinates.2.into(), handle, }, ); @@ -445,6 +191,7 @@ impl MailView { } fn perform_action(&mut self, action: PendingReplyAction, context: &mut Context) { + let Some(coordinates) = self.coordinates else { return; }; let (bytes, reply_body, env) = match self.state { MailViewState::Init { ref mut pending_action, @@ -454,19 +201,17 @@ impl MailView { ref mut pending_action, .. } => { - if pending_action.is_none() { - *pending_action = Some(action); - } + *pending_action = Some(action); return; } MailViewState::Loaded { ref bytes, - ref display, ref env, + ref env_view, .. } => ( bytes, - self.attachment_displays_to_text(display, context, false), + env_view.attachment_displays_to_text(&env_view.display, false), env, ), MailViewState::Error { .. } => { @@ -474,554 +219,26 @@ impl MailView { } }; let composer = match action { - PendingReplyAction::Reply => Box::new(Composer::reply_to_select( - self.coordinates, - reply_body, - context, - )), - PendingReplyAction::ReplyToAuthor => Box::new(Composer::reply_to_author( - self.coordinates, - reply_body, - context, - )), - PendingReplyAction::ReplyToAll => Box::new(Composer::reply_to_all( - self.coordinates, - reply_body, - context, - )), - PendingReplyAction::ForwardAttachment => Box::new(Composer::forward( - self.coordinates, - bytes, - env, - true, - context, - )), - PendingReplyAction::ForwardInline => Box::new(Composer::forward( - self.coordinates, - bytes, - env, - false, - context, - )), - }; - - context - .replies - .push_back(UIEvent::Action(Tab(New(Some(composer))))); - } - - fn attachment_displays_to_text( - &self, - displays: &[AttachmentDisplay], - context: &mut Context, - show_comments: bool, - ) -> String { - let mut acc = String::new(); - for d in displays { - use AttachmentDisplay::*; - match d { - Alternative { - inner: _, - shown_display, - display, - } => { - acc.push_str(&self.attachment_displays_to_text( - &display[*shown_display..(*shown_display + 1)], - context, - show_comments, - )); - } - InlineText { - inner: _, - text, - comment: Some(comment), - } if show_comments => { - acc.push_str(comment); - if !acc.ends_with("\n\n") { - acc.push_str("\n\n"); - } - acc.push_str(text); - } - InlineText { - inner: _, - text, - comment: _, - } => acc.push_str(text), - InlineOther { inner } => { - if !acc.ends_with("\n\n") { - acc.push_str("\n\n"); - } - acc.push_str(&inner.to_string()); - if !acc.ends_with("\n\n") { - acc.push_str("\n\n"); - } - } - Attachment { inner: _ } => {} - SignedPending { - inner: _, - display, - handle: _, - job_id: _, - } => { - if show_comments { - acc.push_str("Waiting for signature verification.\n\n"); - } - acc.push_str(&self.attachment_displays_to_text( - display, - context, - show_comments, - )); - } - SignedUnverified { inner: _, display } => { - if show_comments { - acc.push_str("Unverified signature.\n\n"); - } - acc.push_str(&self.attachment_displays_to_text(display, context, show_comments)) - } - SignedFailed { - inner: _, - display, - error, - } => { - if show_comments { - let _ = writeln!(acc, "Failed to verify signature: {}.\n", error); - } - acc.push_str(&self.attachment_displays_to_text( - display, - context, - show_comments, - )); - } - SignedVerified { - inner: _, - display, - description, - } => { - if show_comments { - if description.is_empty() { - acc.push_str("Verified signature.\n\n"); - } else { - acc.push_str(description); - acc.push_str("\n\n"); - } - } - acc.push_str(&self.attachment_displays_to_text( - display, - context, - show_comments, - )); - } - EncryptedPending { .. } => acc.push_str("Waiting for decryption result."), - EncryptedFailed { inner: _, error } => { - let _ = write!(acc, "Decryption failed: {}.", &error); - } - EncryptedSuccess { - inner: _, - plaintext: _, - plaintext_display, - description, - } => { - if show_comments { - if description.is_empty() { - acc.push_str("Succesfully decrypted.\n\n"); - } else { - acc.push_str(description); - acc.push_str("\n\n"); - } - } - acc.push_str(&self.attachment_displays_to_text( - plaintext_display, - context, - show_comments, - )); - } + PendingReplyAction::Reply => { + Box::new(Composer::reply_to_select(coordinates, reply_body, context)) } - } - acc - } - - fn attachment_displays_to_tree( - &self, - displays: &[AttachmentDisplay], - ) -> (Vec>, String) { - let mut acc = String::new(); - let mut branches = SmallVec::new(); - let mut paths = Vec::with_capacity(displays.len()); - let mut cur_path = vec![]; - let mut idx = 0; - - fn append_entry( - (idx, (depth, att_display)): (&mut usize, (usize, &AttachmentDisplay)), - branches: &mut SmallVec<[bool; 8]>, - paths: &mut Vec>, - cur_path: &mut Vec, - has_sibling: bool, - s: &mut String, - ) { - use AttachmentDisplay::*; - let mut default_alternative: Option = None; - let (att, sub_att_display_vec) = match att_display { - Alternative { - inner, - shown_display, - display, - } => { - default_alternative = Some(*shown_display); - (inner, display.as_slice()) - } - InlineText { - inner, - text: _, - comment: _, - } - | InlineOther { inner } - | Attachment { inner } - | EncryptedPending { inner, handle: _ } - | EncryptedFailed { inner, error: _ } => (inner, &[][..]), - SignedPending { - inner, - display, - handle: _, - job_id: _, - } - | SignedUnverified { inner, display } - | SignedFailed { - inner, - display, - error: _, - } - | SignedVerified { - inner, - display, - description: _, - } - | EncryptedSuccess { - inner: _, - plaintext: inner, - plaintext_display: display, - description: _, - } => (inner, display.as_slice()), - }; - s.extend(format!("\n[{}]", idx).chars()); - for &b in branches.iter() { - if b { - s.push('|'); - } else { - s.push(' '); - } - s.push(' '); + PendingReplyAction::ReplyToAuthor => { + Box::new(Composer::reply_to_author(coordinates, reply_body, context)) } - if depth > 0 { - if has_sibling { - s.push('|'); - } else { - s.push(' '); - } - s.push_str("\\_ "); - } else { - s.push(' '); - s.push(' '); + PendingReplyAction::ReplyToAll => { + Box::new(Composer::reply_to_all(coordinates, reply_body, context)) } - - s.push_str(&att.to_string()); - paths.push(cur_path.clone()); - if matches!(att.content_type, ContentType::Multipart { .. }) { - let mut iter = (0..sub_att_display_vec.len()).peekable(); - if has_sibling { - branches.push(true); - } else { - branches.push(false); - } - while let Some(i) = iter.next() { - *idx += 1; - cur_path.push(i); - append_entry( - (idx, (depth + 1, &sub_att_display_vec[i])), - branches, - paths, - cur_path, - iter.peek().is_some(), - s, - ); - if Some(i) == default_alternative { - s.push_str(" (displayed by default)"); - } - cur_path.pop(); - } - branches.pop(); + PendingReplyAction::ForwardAttachment => { + Box::new(Composer::forward(coordinates, bytes, env, true, context)) } - } - - for (i, d) in displays.iter().enumerate() { - cur_path.push(i); - append_entry( - (&mut idx, (0, d)), - &mut branches, - &mut paths, - &mut cur_path, - i + 1 < displays.len(), - &mut acc, - ); - cur_path.pop(); - idx += 1; - } - (paths, acc) - } - - fn attachment_to( - body: &Attachment, - context: &mut Context, - coordinates: (AccountHash, MailboxHash, EnvelopeHash), - active_jobs: &mut HashSet, - force_charset: Option, - ) -> Vec { - let mut ret = vec![]; - fn rec( - a: &Attachment, - context: &mut Context, - coordinates: (AccountHash, MailboxHash, EnvelopeHash), - acc: &mut Vec, - active_jobs: &mut HashSet, - force_charset: Option, - ) { - if a.content_disposition.kind.is_attachment() || a.content_type == "message/rfc822" { - acc.push(AttachmentDisplay::Attachment { - inner: Box::new(a.clone()), - }); - } else if a.content_type().is_text_html() { - let bytes = a.decode(force_charset.into()); - let filter_invocation = - mailbox_settings!(context[coordinates.0][&coordinates.1].pager.html_filter) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or("w3m -I utf-8 -T text/html"); - let command_obj = Command::new("sh") - .args(["-c", filter_invocation]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .and_then(|mut cmd| { - cmd.stdin.as_mut().unwrap().write_all(&bytes)?; - Ok(String::from_utf8_lossy(&cmd.wait_with_output()?.stdout).to_string()) - }); - match command_obj { - Err(err) => { - context.replies.push_back(UIEvent::Notification( - Some(format!( - "Failed to start html filter process: {}", - filter_invocation, - )), - err.to_string(), - Some(NotificationType::Error(melib::ErrorKind::External)), - )); - let comment = Some(format!( - "Failed to start html filter process: `{}`. Press `v` to open in web \ - browser. \n\n", - filter_invocation - )); - let text = String::from_utf8_lossy(&bytes).to_string(); - acc.push(AttachmentDisplay::InlineText { - inner: Box::new(a.clone()), - comment, - text, - }); - } - Ok(text) => { - let comment = Some(format!( - "Text piped through `{}`. Press `v` to open in web browser. \n\n", - filter_invocation - )); - acc.push(AttachmentDisplay::InlineText { - inner: Box::new(a.clone()), - comment, - text, - }); - } - } - } else if a.is_text() { - let bytes = a.decode(force_charset.into()); - acc.push(AttachmentDisplay::InlineText { - inner: Box::new(a.clone()), - comment: None, - text: String::from_utf8_lossy(&bytes).to_string(), - }); - } else if let ContentType::Multipart { - ref kind, - ref parts, - .. - } = a.content_type - { - match kind { - MultipartType::Alternative => { - if parts.is_empty() { - return; - } - let mut display = vec![]; - let mut chosen_attachment_idx = 0; - if let Some(text_attachment_pos) = - parts.iter().position(|a| a.content_type == "text/plain") - { - let bytes = &parts[text_attachment_pos].decode(force_charset.into()); - if bytes.trim().is_empty() - && mailbox_settings!( - context[coordinates.0][&coordinates.1] - .pager - .auto_choose_multipart_alternative - ) - .is_true() - { - if let Some(text_attachment_pos) = - parts.iter().position(|a| a.content_type == "text/html") - { - /* Select html alternative since text/plain is empty */ - chosen_attachment_idx = text_attachment_pos; - } - } else { - /* Select text/plain alternative */ - chosen_attachment_idx = text_attachment_pos; - } - } - for a in parts { - rec( - a, - context, - coordinates, - &mut display, - active_jobs, - force_charset, - ); - } - acc.push(AttachmentDisplay::Alternative { - inner: Box::new(a.clone()), - shown_display: chosen_attachment_idx, - display, - }); - } - MultipartType::Signed => { - #[cfg(not(feature = "gpgme"))] - { - acc.push(AttachmentDisplay::SignedUnverified { - inner: Box::new(a.clone()), - display: { - let mut v = vec![]; - rec( - &parts[0], - context, - coordinates, - &mut v, - active_jobs, - force_charset, - ); - v - }, - }); - } - #[cfg(feature = "gpgme")] - { - if *mailbox_settings!( - context[coordinates.0][&coordinates.1] - .pgp - .auto_verify_signatures - ) { - let verify_fut = crate::components::mail::pgp::verify(a.clone()); - let handle = context.job_executor.spawn_specialized(verify_fut); - active_jobs.insert(handle.job_id); - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::NewJob(handle.job_id), - )); - acc.push(AttachmentDisplay::SignedPending { - inner: Box::new(a.clone()), - job_id: handle.job_id, - display: { - let mut v = vec![]; - rec( - &parts[0], - context, - coordinates, - &mut v, - active_jobs, - force_charset, - ); - v - }, - handle, - }); - } else { - acc.push(AttachmentDisplay::SignedUnverified { - inner: Box::new(a.clone()), - display: { - let mut v = vec![]; - rec( - &parts[0], - context, - coordinates, - &mut v, - active_jobs, - force_charset, - ); - v - }, - }); - } - } - } - MultipartType::Encrypted => { - for a in parts { - if a.content_type == "application/octet-stream" { - #[cfg(not(feature = "gpgme"))] - { - acc.push(AttachmentDisplay::EncryptedFailed { - inner: Box::new(a.clone()), - error: Error::new( - "Cannot decrypt: meli must be compiled with libgpgme \ - support.", - ), - }); - } - #[cfg(feature = "gpgme")] - { - if *mailbox_settings!( - context[coordinates.0][&coordinates.1].pgp.auto_decrypt - ) { - let decrypt_fut = - crate::components::mail::pgp::decrypt(a.raw().to_vec()); - let handle = - context.job_executor.spawn_specialized(decrypt_fut); - active_jobs.insert(handle.job_id); - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::NewJob(handle.job_id), - )); - acc.push(AttachmentDisplay::EncryptedPending { - inner: Box::new(a.clone()), - handle, - }); - } else { - acc.push(AttachmentDisplay::EncryptedFailed { - inner: Box::new(a.clone()), - error: Error::new("Undecrypted."), - }); - } - } - } - } - } - _ => { - for a in parts { - rec(a, context, coordinates, acc, active_jobs, force_charset); - } - } - } + PendingReplyAction::ForwardInline => { + Box::new(Composer::forward(coordinates, bytes, env, false, context)) } - } - rec( - body, - context, - coordinates, - &mut ret, - active_jobs, - force_charset, - ); - ret + }; + + context + .replies + .push_back(UIEvent::Action(Tab(New(Some(composer))))); } pub fn update( @@ -1029,116 +246,17 @@ impl MailView { new_coordinates: (AccountHash, MailboxHash, EnvelopeHash), context: &mut Context, ) { - if self.coordinates != new_coordinates { - self.coordinates = new_coordinates; - self.mode = ViewMode::Normal; - self.initialised = false; + if self.coordinates != Some(new_coordinates) { + self.coordinates = Some(new_coordinates); self.init_futures(context); self.set_dirty(true); } } - fn open_attachment( - &'_ self, - lidx: usize, - context: &mut Context, - ) -> Option<&'_ melib::Attachment> { - let display = if let MailViewState::Loaded { ref display, .. } = self.state { - display - } else { - return None; - }; - if let Some(path) = self.attachment_paths.get(lidx).and_then(|path| { - if !path.is_empty() { - Some(path) - } else { - None - } - }) { - let first = path[0]; - use AttachmentDisplay::*; - let root_attachment = match &display[first] { - Alternative { - inner, - shown_display: _, - display: _, - } - | InlineText { - inner, - text: _, - comment: _, - } - | InlineOther { inner } - | Attachment { inner } - | SignedPending { - inner, - display: _, - handle: _, - job_id: _, - } - | SignedFailed { - inner, - display: _, - error: _, - } - | SignedVerified { - inner, - display: _, - description: _, - } - | SignedUnverified { inner, display: _ } - | EncryptedPending { inner, handle: _ } - | EncryptedFailed { inner, error: _ } - | EncryptedSuccess { - inner: _, - plaintext: inner, - plaintext_display: _, - description: _, - } => inner, - }; - fn find_attachment<'a>( - a: &'a melib::Attachment, - path: &[usize], - ) -> Option<&'a melib::Attachment> { - if path.is_empty() { - return Some(a); - } - if let ContentType::Multipart { ref parts, .. } = a.content_type { - let first = path[0]; - if first < parts.len() { - return find_attachment(&parts[first], &path[1..]); - } - } - None - } - - let ret = find_attachment(root_attachment, &path[1..]); - if lidx == 0 { - return ret.and_then(|a| { - if a.content_disposition.kind.is_attachment() - || a.content_type == "message/rfc822" - { - Some(a) - } else { - None - } - }); - } else { - return ret; - } - } - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!( - "Attachment `{}` not found.", - lidx - )))); - None - } - fn start_contact_selector(&mut self, context: &mut Context) { - let account = &context.accounts[&self.coordinates.0]; - if !account.contains_key(self.coordinates.2) { + let Some(coordinates) = self.coordinates else { return; }; + let account = &context.accounts[&coordinates.0]; + if !account.contains_key(coordinates.2) { context .replies .push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage( @@ -1146,7 +264,7 @@ impl MailView { ))); return; } - let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2); + let envelope: EnvelopeRef = account.collection.get_env(coordinates.2); let mut entries = Vec::new(); for addr in envelope.from().iter().chain(envelope.to().iter()) { @@ -1158,7 +276,7 @@ impl MailView { entries.push((new_card, format!("{}", addr))); } drop(envelope); - self.mode = ViewMode::ContactSelector(Box::new(Selector::new( + self.contact_selector = Some(Box::new(Selector::new( "select contacts to add", entries, false, @@ -1168,587 +286,47 @@ impl MailView { context, ))); self.dirty = true; - self.initialised = false; } } impl Component for MailView { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if !self.is_dirty() && !self.force_draw_headers { + if !self.is_dirty() { return; } - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - - let y: usize = { - let account = &context.accounts[&self.coordinates.0]; - if !account.contains_key(self.coordinates.2) { - /* The envelope has been renamed or removed, so wait for the appropriate - * event to arrive */ - return; - } - let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2); - - let headers = crate::conf::value(context, "mail.view.headers"); - let headers_names = crate::conf::value(context, "mail.view.headers_names"); - let headers_area = crate::conf::value(context, "mail.view.headers_area"); - - if let ViewMode::Source(_) = self.mode { - clear_area(grid, area, self.theme_default); - context.dirty_areas.push_back(area); - get_y(upper_left) - } else { - let height_p = self.pager.size().1; - - let height = height!(area) - self.headers_no - 1; - - self.headers_no = 0; - let mut skip_header_ctr = self.headers_cursor; - let sticky = *mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .headers_sticky - ) || height_p < height; - let (_, mut y) = upper_left; - macro_rules! print_header { - ($(($header:path, $string:expr)),*$(,)?) => { - $({ - if sticky || skip_header_ctr == 0 { - if y <= get_y(bottom_right) { - let (_x, _y) = write_string_to_grid( - &format!("{}:", $header), - grid, - headers_names.fg, - headers_names.bg, - headers_names.attrs, - (set_y(upper_left, y), bottom_right), - Some(get_x(upper_left)), - ); - if let Some(cell) = grid.get_mut(_x, _y) { - cell.set_ch(' ') - .set_fg(headers_area.fg) - .set_bg(headers_area.bg) - .set_attrs(headers_area.attrs); - } - - let (_x, _y) = write_string_to_grid( - &$string, - grid, - headers.fg, - headers.bg, - headers.attrs, - ((_x + 1, _y), bottom_right), - Some(get_x(upper_left)), - ); - clear_area( - grid, - ( - (std::cmp::min(_x, get_x(bottom_right)), _y), - (get_x(bottom_right), _y), - ), - headers_area, - ); - y = _y + 1; - } - } else { - skip_header_ctr -= 1; - } - self.headers_no += 1; - })+ - }; - } - let find_offset = |s: &str| -> (bool, (i64, i64)) { - let mut diff = (true, (0, 0)); - if let Some(pos) = s.as_bytes().iter().position(|b| *b == b'+' || *b == b'-') { - let offset = &s[pos..]; - diff.0 = offset.starts_with('+'); - if let (Ok(hr_offset), Ok(min_offset)) = - (offset[1..3].parse::(), offset[3..5].parse::()) - { - diff.1 .0 = hr_offset; - diff.1 .1 = min_offset; - } - } - diff - }; - let orig_date = envelope.date_as_str(); - let date_str: std::borrow::Cow = if mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .show_date_in_my_timezone - ) - .is_true() - { - let local_date = datetime::timestamp_to_string( - envelope.timestamp, - Some(datetime::formats::RFC822_DATE), - false, - ); - let orig_offset = find_offset(orig_date); - let local_offset = find_offset(&local_date); - if orig_offset == local_offset { - orig_date.into() - } else { - format!( - "{} [actual timezone: {}{:02}{:02}]", - local_date, - if orig_offset.0 { '+' } else { '-' }, - orig_offset.1 .0, - orig_offset.1 .1 - ) - .into() - } - } else { - orig_date.into() - }; - print_header!( - (HeaderName::DATE, date_str), - (HeaderName::FROM, envelope.field_from_to_string()), - (HeaderName::TO, envelope.field_to_to_string()), - ); - if envelope.other_headers().contains_key(HeaderName::CC) - && !envelope.other_headers()[HeaderName::CC].is_empty() - { - print_header!((HeaderName::CC, envelope.field_cc_to_string())); - } - print_header!( - (HeaderName::SUBJECT, envelope.subject()), - ( - HeaderName::MESSAGE_ID, - format!("<{}>", envelope.message_id_raw()) - ) - ); - if self.expand_headers { - if let Some(val) = envelope.in_reply_to_display() { - print_header!( - (HeaderName::IN_REPLY_TO, val), - ( - HeaderName::REFERENCES, - envelope - .references() - .iter() - .map(std::string::ToString::to_string) - .collect::>() - .join(", ") - ) - ); - } - } - for hdr in mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .show_extra_headers - ) { - if let Some((val, hdr)) = HeaderName::try_from(hdr) - .ok() - .and_then(|hdr| Some((envelope.other_headers().get(&hdr)?, hdr))) - { - print_header!((hdr, val)); - } - } - if let Some(list_management::ListActions { - ref id, - ref archive, - ref post, - ref unsubscribe, - }) = list_management::ListActions::detect(&envelope) - { - let mut x = get_x(upper_left); - if let Some(id) = id { - if sticky || skip_header_ctr == 0 { - clear_area( - grid, - (set_y(upper_left, y), set_y(bottom_right, y)), - headers_area, - ); - let (_x, _) = write_string_to_grid( - "List-ID: ", - grid, - headers_names.fg, - headers_names.bg, - headers_names.attrs, - (set_y(upper_left, y), bottom_right), - None, - ); - let (_x, _y) = write_string_to_grid( - id, - grid, - headers.fg, - headers.bg, - headers.attrs, - ((_x, y), bottom_right), - None, - ); - x = _x; - if _y != y { - x = get_x(upper_left); - } - y = _y; - } - self.headers_no += 1; - } - if sticky || skip_header_ctr == 0 { - if archive.is_some() || post.is_some() || unsubscribe.is_some() { - let (_x, _y) = write_string_to_grid( - " Available actions: [ ", - grid, - headers_names.fg, - headers_names.bg, - headers_names.attrs, - ((x, y), bottom_right), - Some(get_x(upper_left)), - ); - x = _x; - y = _y; - } - if archive.is_some() { - let (_x, _y) = write_string_to_grid( - "list-archive, ", - grid, - headers.fg, - headers.bg, - headers.attrs, - ((x, y), bottom_right), - Some(get_x(upper_left)), - ); - x = _x; - y = _y; - } - if post.is_some() { - let (_x, _y) = write_string_to_grid( - "list-post, ", - grid, - headers.fg, - headers.bg, - headers.attrs, - ((x, y), bottom_right), - Some(get_x(upper_left)), - ); - x = _x; - y = _y; - } - if unsubscribe.is_some() { - let (_x, _y) = write_string_to_grid( - "list-unsubscribe, ", - grid, - headers.fg, - headers.bg, - headers.attrs, - ((x, y), bottom_right), - Some(get_x(upper_left)), - ); - x = _x; - y = _y; - } - if archive.is_some() || post.is_some() || unsubscribe.is_some() { - if x >= 2 { - grid[(x - 2, y)].set_ch(' '); - } - if x > 0 { - grid[(x - 1, y)] - .set_ch(']') - .set_fg(headers_names.fg) - .set_bg(headers_names.bg) - .set_attrs(headers_names.attrs); - } - } - for x in x..=get_x(bottom_right) { - grid[(x, y)] - .set_ch(' ') - .set_fg(headers_area.fg) - .set_bg(headers_area.bg); - } - y += 1; - } - } - - self.force_draw_headers = false; - clear_area( - grid, - (set_y(upper_left, y), set_y(bottom_right, y)), - headers_area, - ); - context - .dirty_areas - .push_back((upper_left, set_y(bottom_right, y + 3))); - if !*mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .headers_sticky - ) { - let height_p = self.pager.size().1; - - let height = height!(area).saturating_sub(y).saturating_sub(1); - if self.pager.cursor_pos() >= self.headers_no { - get_y(upper_left) - } else if (height_p > height && self.headers_cursor < self.headers_no + 1) - || self.headers_cursor == 0 - || height_p < height - { - y + 1 - } else { - get_y(upper_left) - } - } else { - y + 1 - } - } - }; - - if !self.initialised { - let (body, body_text, bytes, links) = if let MailViewState::Loaded { - ref body, - ref body_text, - ref bytes, - ref mut links, - .. - } = self.state - { - (body, body_text, bytes, links) - } else if let MailViewState::Error { ref err } = self.state { - clear_area( - grid, - (set_y(upper_left, y), bottom_right), - self.theme_default, - ); - context - .dirty_areas - .push_back((set_y(upper_left, y), bottom_right)); - context.replies.push_back(UIEvent::Notification( - Some("Failed to open e-mail".to_string()), - err.to_string(), - Some(NotificationType::Error(err.kind)), - )); - log::error!("Failed to open envelope: {err}"); - self.init_futures(context); - return; - } else { - clear_area( - grid, - (set_y(upper_left, y), bottom_right), - self.theme_default, - ); - context - .dirty_areas - .push_back((set_y(upper_left, y), bottom_right)); - return; - }; - self.initialised = true; - match self.mode { - ViewMode::Attachment(aidx) => { - let mut text = "Viewing attachment. Press `r` to return \n".to_string(); - if let Some(attachment) = self.open_attachment(aidx, context) { - if attachment.is_html() { - let mut subview = Box::new(HtmlView::new(attachment, context)); - subview.set_coordinates(Some(self.coordinates)); - self.subview = Some(subview); - self.mode = ViewMode::Subview; - } else { - text.push_str(&attachment.text()); - let colors = crate::conf::value(context, "mail.view.body"); - self.pager = - Pager::from_string(text, Some(context), Some(0), None, colors); - if let Some(ref filter) = mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .filter - ) { - self.pager.filter(filter); - } - self.subview = None; - } - } else { - text.push_str("Internal error. MailView::open_attachment failed."); - let colors = crate::conf::value(context, "mail.view.body"); - self.pager = Pager::from_string(text, Some(context), Some(0), None, colors); - if let Some(ref filter) = mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .filter - ) { - self.pager.filter(filter); - } - self.subview = None; - } - } - ViewMode::Normal if body.is_html() => { - let mut subview = Box::new(HtmlView::new(body, context)); - subview.set_coordinates(Some(self.coordinates)); - self.subview = Some(subview); - self.mode = ViewMode::Subview; - } - ViewMode::Normal - if mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .auto_choose_multipart_alternative - ) - .is_true() - && match body.content_type { - ContentType::Multipart { - kind: MultipartType::Alternative, - ref parts, - .. - } => parts.iter().all(|p| { - p.is_html() || (p.is_text() && p.body().trim().is_empty()) - }), - _ => false, - } => - { - let mut subview = Box::new(HtmlView::new( - body.content_type - .parts() - .unwrap() - .iter() - .find(|a| a.is_html()) - .unwrap_or(body), - context, - )); - subview.set_coordinates(Some(self.coordinates)); - self.subview = Some(subview); - self.mode = ViewMode::Subview; - self.initialised = false; - } - ViewMode::Subview | ViewMode::ContactSelector(_) => {} - ViewMode::Source(source) => { - let text = { - if source == Source::Raw { - String::from_utf8_lossy(bytes).into_owned() - } else { - /* Decode each header value */ - let mut ret = melib::email::parser::headers::headers(bytes) - .map(|(_, v)| v) - .map_err(|err| err.into()) - .and_then(|headers| { - Ok(headers - .into_iter() - .map(|(h, v)| { - melib::email::parser::encodings::phrase(v, true) - .map(|(_, v)| { - let mut h = h.to_vec(); - h.push(b':'); - h.push(b' '); - h.extend(v.into_iter()); - h - }) - .map_err(|err| err.into()) - }) - .collect::>>>()? - .join(&b"\n"[..])) - }) - .map(|v| String::from_utf8_lossy(&v).into_owned()) - .unwrap_or_else(|err: Error| err.to_string()); - if !ret.ends_with("\n\n") { - ret.push_str("\n\n"); - } - ret.push_str(body_text); - if !ret.ends_with("\n\n") { - ret.push_str("\n\n"); - } - ret.push_str(&self.attachment_tree); - ret - } - }; - let colors = crate::conf::value(context, "mail.view.body"); - self.pager = Pager::from_string(text, Some(context), None, None, colors); - if let Some(ref filter) = mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .filter - ) { - self.pager.filter(filter); - } - } - ViewMode::Url => { - let mut text = body_text.clone(); - if links.is_empty() { - let finder = LinkFinder::new(); - *links = finder - .links(&text) - .filter_map(|l| { - if *l.kind() == linkify::LinkKind::Url { - Some(Link { - start: l.start(), - end: l.end(), - kind: LinkKind::Url, - }) - } else if *l.kind() == linkify::LinkKind::Email { - Some(Link { - start: l.start(), - end: l.end(), - kind: LinkKind::Email, - }) - } else { - None - } - }) - .collect::>(); - } - for (lidx, l) in links.iter().enumerate().rev() { - text.insert_str(l.start, &format!("[{}]", lidx)); - } - if !text.ends_with("\n\n") { - text.push_str("\n\n"); - } - text.push_str(&self.attachment_tree); - - let cursor_pos = self.pager.cursor_pos(); - let colors = crate::conf::value(context, "mail.view.body"); - self.pager = - Pager::from_string(text, Some(context), Some(cursor_pos), None, colors); - if let Some(ref filter) = mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .filter - ) { - self.pager.filter(filter); - } - self.subview = None; - } - _ => { - let mut text = body_text.clone(); - if !text.ends_with("\n\n") { - text.push_str("\n\n"); - } - text.push_str(&self.attachment_tree); - let cursor_pos = if self.mode.is_attachment() { - 0 - } else { - self.pager.cursor_pos() - }; - let colors = crate::conf::value(context, "mail.view.body"); - self.pager = - Pager::from_string(text, Some(context), Some(cursor_pos), None, colors); - if let Some(ref filter) = mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .filter - ) { - self.pager.filter(filter); - } - self.subview = None; - } - }; - } - match self.mode { - ViewMode::Subview if self.subview.is_some() => { - if let Some(s) = self.subview.as_mut() { - if !s.is_dirty() { - s.set_dirty(true); - } - s.draw(grid, (set_y(upper_left, y), bottom_right), context); - } - } - _ => { - self.pager - .draw(grid, (set_y(upper_left, y), bottom_right), context); + let Some(coordinates) = self.coordinates else { return; }; + + { + let account = &context.accounts[&coordinates.0]; + if !account.contains_key(coordinates.2) { + /* The envelope has been renamed or removed, so wait for the appropriate + * event to arrive */ + return; } } - if let ViewMode::ContactSelector(ref mut s) = self.mode { - s.draw(grid, area, context); - } - if let ForceCharset::Dialog(ref mut s) = self.force_charset { + if let MailViewState::Loaded { + ref mut env_view, .. + } = self.state + { + env_view.draw(grid, area, context); + } else if let MailViewState::Error { ref err } = self.state { + clear_area(grid, area, self.theme_default); + context.dirty_areas.push_back(area); + context.replies.push_back(UIEvent::Notification( + Some("Failed to open e-mail".to_string()), + err.to_string(), + Some(NotificationType::Error(err.kind)), + )); + log::error!("Failed to open envelope: {err}"); + self.init_futures(context); + return; + } else { + clear_area(grid, area, self.theme_default); + context.dirty_areas.push_back(area); + return; + }; + if let Some(ref mut s) = self.contact_selector.as_mut() { s.draw(grid, area, context); } @@ -1756,110 +334,35 @@ impl Component for MailView { } fn process_event(&mut self, mut event: &mut UIEvent, context: &mut Context) -> bool { - if self.coordinates.0.is_null() || self.coordinates.1.is_null() { + let Some(coordinates) = self.coordinates else { return false; }; + if coordinates.0.is_null() || coordinates.1.is_null() { return false; } - match (&mut self.force_charset, &event) { - (ForceCharset::Dialog(selector), UIEvent::FinishedUIDialog(id, results)) - if *id == selector.id() => - { - self.force_charset = - if let Some(results) = results.downcast_ref::>>() { - if results.len() != 1 { - ForceCharset::None - } else if let Some(charset) = results[0] { - ForceCharset::Forced(charset) - } else { - ForceCharset::None - } - } else { - ForceCharset::None - }; - MailViewState::redecode(self, context); - self.initialised = false; - self.set_dirty(true); - return true; - } - (ForceCharset::Dialog(selector), _) => { - if selector.process_event(event, context) { - return true; - } - } - _ => {} + /* If envelope data is loaded, pass it to envelope views */ + if self.state.process_event(event, context) { + return true; } - let shortcuts = self.shortcuts(context); - match (&mut self.mode, &mut event) { - /*(ViewMode::Ansi(ref mut buf), _) => { - if buf.process_event(event, context) { - return true; - } - }*/ - (ViewMode::Subview, _) => { - if let Some(s) = self.subview.as_mut() { - if s.process_event(event, context) { - return true; - } - } - } - (ViewMode::ContactSelector(ref s), UIEvent::FinishedUIDialog(id, results)) - if *id == s.id() => - { + match (&mut self.contact_selector, &mut event) { + (Some(ref s), UIEvent::FinishedUIDialog(id, results)) if *id == s.id() => { if let Some(results) = results.downcast_ref::>() { - let account = &mut context.accounts[&self.coordinates.0]; + let account = &mut context.accounts[&coordinates.0]; { for card in results.iter() { account.address_book.add_card(card.clone()); } } } - self.mode = ViewMode::Normal; - self.initialised = false; self.set_dirty(true); return true; } - (ViewMode::ContactSelector(ref mut s), _) => { + (Some(ref mut s), _) => { if s.process_event(event, context) { return true; } - if self.pager.process_event(event, context) { - return true; - } } _ => match event { - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_up"]) - && !*mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .headers_sticky - ) - && self.headers_cursor <= self.headers_no => - { - self.force_draw_headers = true; - if self.pager.cursor_pos() == 0 { - self.headers_cursor = self.headers_cursor.saturating_sub(1); - } else if self.pager.process_event(event, context) { - return true; - } - self.pager.set_dirty(true); - return true; - } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_down"]) - && !*mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .headers_sticky - ) - && self.headers_cursor < self.headers_no => - { - self.force_draw_headers = true; - self.headers_cursor += 1; - self.pager.set_dirty(true); - return true; - } UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)) if self.active_jobs.contains(job_id) => { @@ -1884,134 +387,15 @@ impl Component for MailView { MailViewState::Init { .. } => { self.init_futures(context); } - MailViewState::Loaded { - ref mut display, .. - } => { - let mut caught = false; - for d in display.iter_mut() { - match d { - AttachmentDisplay::SignedPending { - inner, - handle, - display, - job_id: our_job_id, - } if *our_job_id == *job_id => { - caught = true; - self.initialised = false; - match handle.chan.try_recv() { - Err(_) => { /* Job was canceled */ } - Ok(None) => { /* something happened, - * perhaps a worker thread - * panicked */ - } - Ok(Some(Ok(()))) => { - *d = AttachmentDisplay::SignedVerified { - inner: std::mem::replace( - inner, - Box::new( - AttachmentBuilder::new(&[]).build(), - ), - ), - display: std::mem::take(display), - description: String::new(), - }; - } - Ok(Some(Err(error))) => { - *d = AttachmentDisplay::SignedFailed { - inner: std::mem::replace( - inner, - Box::new( - AttachmentBuilder::new(&[]).build(), - ), - ), - display: std::mem::take(display), - error, - }; - } - } - } - AttachmentDisplay::EncryptedPending { inner, handle } - if handle.job_id == *job_id => - { - caught = true; - self.initialised = false; - match handle.chan.try_recv() { - Err(_) => { /* Job was canceled */ } - Ok(None) => { /* something happened, - * perhaps a worker thread - * panicked */ - } - Ok(Some(Ok((metadata, decrypted_bytes)))) => { - let plaintext = Box::new( - AttachmentBuilder::new(&decrypted_bytes) - .build(), - ); - let plaintext_display = Self::attachment_to( - &plaintext, - context, - self.coordinates, - &mut self.active_jobs, - (&self.force_charset).into(), - ); - *d = AttachmentDisplay::EncryptedSuccess { - inner: std::mem::replace( - inner, - Box::new( - AttachmentBuilder::new(&[]).build(), - ), - ), - plaintext, - plaintext_display, - description: format!("{:?}", metadata), - }; - } - Ok(Some(Err(error))) => { - *d = AttachmentDisplay::EncryptedFailed { - inner: std::mem::replace( - inner, - Box::new( - AttachmentBuilder::new(&[]).build(), - ), - ), - error, - }; - } - } - } - _ => {} - } - } - if caught { - let mut new_body_text = String::new(); - if let MailViewState::Loaded { ref display, .. } = self.state { - new_body_text = - self.attachment_displays_to_text(display, context, true); - let (paths, attachment_tree_s) = - self.attachment_displays_to_tree(display); - self.attachment_tree = attachment_tree_s; - self.attachment_paths = paths; - } - if let MailViewState::Loaded { - ref mut body_text, - ref mut links, - .. - } = self.state - { - links.clear(); - *body_text = new_body_text; - } - } + MailViewState::Loaded { .. } => { + log::debug!("MailView.active_jobs contains job id {:?} but MailViewState is already loaded; what job was this and why was it in active_jobs?", job_id); } _ => {} } self.active_jobs.remove(job_id); self.set_dirty(true); } - _ => { - if self.pager.process_event(event, context) { - return true; - } - } + _ => {} }, } @@ -2043,7 +427,7 @@ impl Component for MailView { if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["forward"]) => { match mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] + context[coordinates.0][&coordinates.1] .composing .forward_as_attachment ) { @@ -2089,8 +473,8 @@ impl Component for MailView { UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["edit"]) => { - let account_hash = self.coordinates.0; - let env_hash = self.coordinates.2; + let account_hash = coordinates.0; + let env_hash = coordinates.2; let (sender, mut receiver) = crate::jobs::oneshot::channel(); let operation = context.accounts[&account_hash].operation(env_hash); let bytes_job = async move { @@ -2101,11 +485,13 @@ impl Component for MailView { .backend_capabilities .is_async { - context.accounts[&account_hash] + context + .main_loop_handler .job_executor .spawn_specialized(bytes_job) } else { - context.accounts[&account_hash] + context + .main_loop_handler .job_executor .spawn_blocking(bytes_job) }; @@ -2163,7 +549,7 @@ impl Component for MailView { return true; } UIEvent::Input(ref key) - if !self.mode.is_contact_selector() + if self.contact_selector.is_none() && shortcut!( key == shortcuts[Shortcuts::ENVELOPE_VIEW]["add_addresses_to_contacts"] ) => @@ -2172,473 +558,23 @@ impl Component for MailView { return true; } UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) - if self.mode.is_contact_selector() => - { - self.mode = ViewMode::Normal; - self.set_dirty(true); - self.initialised = false; - return true; - } - UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) if !self.cmd_buf.is_empty() => { - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - return true; - } - UIEvent::Input(Key::Char(c)) if c.is_ascii_digit() => { - self.cmd_buf.push(c); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufSet( - self.cmd_buf.clone(), - ))); - return true; - } - UIEvent::Input(ref key) - if (self.mode == ViewMode::Normal - || self.mode == ViewMode::Subview - || self.mode == ViewMode::Source(Source::Decoded) - || self.mode == ViewMode::Source(Source::Raw)) - && shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["view_raw_source"]) => - { - self.mode = match self.mode { - ViewMode::Source(Source::Decoded) => ViewMode::Source(Source::Raw), - _ => ViewMode::Source(Source::Decoded), - }; - self.set_dirty(true); - self.initialised = false; - return true; - } - UIEvent::Input(ref key) - if (self.mode.is_attachment() - /*|| self.mode.is_ansi()*/ - || self.mode == ViewMode::Subview - || self.mode == ViewMode::Url - || self.mode == ViewMode::Source(Source::Decoded) - || self.mode == ViewMode::Source(Source::Raw)) - && shortcut!( - key == shortcuts[Shortcuts::ENVELOPE_VIEW]["return_to_normal_view"] - ) => - { - self.mode = ViewMode::Normal; - self.set_dirty(true); - self.initialised = false; - return true; - } - UIEvent::Input(ref key) - if (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) - && !self.cmd_buf.is_empty() - && shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["open_mailcap"]) => - { - let lidx = self.cmd_buf.parse::().unwrap(); - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - match self.state { - MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {} - MailViewState::Loaded { .. } => { - if let Some(attachment) = self.open_attachment(lidx, context) { - if let Ok(()) = - crate::mailcap::MailcapEntry::execute(attachment, context) - { - self.set_dirty(true); - } else { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(format!( - "no mailcap entry found for {}", - attachment.content_type() - )), - )); - } - } - } - MailViewState::Init { .. } => { - self.init_futures(context); - } - } - return true; - } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["open_attachment"]) - && !self.cmd_buf.is_empty() - && (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) => - { - let lidx = self.cmd_buf.parse::().unwrap(); - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - match self.state { - MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {} - MailViewState::Loaded { .. } => { - if let Some(attachment) = self.open_attachment(lidx, context) { - match attachment.content_type() { - ContentType::MessageRfc822 => { - match Mail::new(attachment.body().to_vec(), Some(Flag::SEEN)) { - Ok(wrapper) => { - context.replies.push_back(UIEvent::Action(Tab(New( - Some(Box::new(EnvelopeView::new( - wrapper, - None, - None, - self.coordinates.0, - ))), - )))); - } - Err(e) => { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(format!("{}", e)), - )); - } - } - } - - ContentType::Text { .. } - | ContentType::PGPSignature - | ContentType::CMSSignature => { - self.mode = ViewMode::Attachment(lidx); - self.initialised = false; - self.dirty = true; - } - ContentType::Multipart { .. } => { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage( - "Multipart attachments are not supported yet." - .to_string(), - ), - )); - } - ContentType::Other { .. } => { - let attachment_type = attachment.mime_type(); - let filename = attachment.filename(); - if let Ok(command) = query_default_app(&attachment_type) { - let p = create_temp_file( - &attachment.decode(Default::default()), - filename.as_deref(), - None, - None, - true, - ); - let exec_cmd = desktop_exec_to_command( - &command, - p.path.display().to_string(), - false, - ); - match Command::new("sh") - .args(["-c", &exec_cmd]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - { - Ok(child) => { - context.temp_files.push(p); - context.children.push(child); - } - Err(err) => { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(format!( - "Failed to start `{}`: {}", - &exec_cmd, err - )), - )); - } - } - } else { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage( - if let Some(filename) = filename.as_ref() { - format!( - "Couldn't find a default application for \ - file {} (type {})", - filename, attachment_type - ) - } else { - format!( - "Couldn't find a default application for \ - type {}", - attachment_type - ) - }, - ), - )); - } - } - ContentType::OctetStream { - ref name, - parameters: _, - } => { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(format!( - "Failed to open {}. application/octet-stream isn't \ - supported yet", - name.as_ref().map(|n| n.as_str()).unwrap_or("file") - )), - )); - } - } - } - } - MailViewState::Init { .. } => { - self.init_futures(context); - } - } - return true; - } - UIEvent::Input(ref key) - if (self.mode == ViewMode::Normal || self.mode == ViewMode::Url) - && shortcut!( - key == shortcuts[Shortcuts::ENVELOPE_VIEW]["toggle_expand_headers"] - ) => + if self.contact_selector.is_some() => { - self.expand_headers = !self.expand_headers; + self.contact_selector = None; self.set_dirty(true); return true; } - UIEvent::Input(ref key) - if !self.cmd_buf.is_empty() - && self.mode == ViewMode::Url - && shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["go_to_url"]) => - { - let lidx = self.cmd_buf.parse::().unwrap(); - self.cmd_buf.clear(); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - match self.state { - MailViewState::Init { .. } => { - self.init_futures(context); - } - MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {} - MailViewState::Loaded { - body: _, - bytes: _, - display: _, - env: _, - ref body_text, - ref links, - } => { - let (_kind, url) = { - if let Some(l) = links - .get(lidx) - .and_then(|l| Some((l.kind, body_text.get(l.start..l.end)?))) - { - l - } else { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(format!( - "Link `{}` not found.", - lidx - )), - )); - return true; - } - }; - - let url_launcher = mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .url_launcher - ) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or( - #[cfg(target_os = "macos")] - { - "open" - }, - #[cfg(not(target_os = "macos"))] - { - "xdg-open" - }, - ); - match Command::new(url_launcher) - .arg(url) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - { - Ok(child) => { - context.children.push(child); - } - Err(err) => { - context.replies.push_back(UIEvent::Notification( - Some(format!("Failed to launch {:?}", url_launcher)), - err.to_string(), - Some(NotificationType::Error(melib::ErrorKind::External)), - )); - } - } - } - } - return true; - } - UIEvent::Input(ref key) - if (self.mode == ViewMode::Normal || self.mode == ViewMode::Url) - && shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["toggle_url_mode"]) => - { - match self.mode { - ViewMode::Normal => self.mode = ViewMode::Url, - ViewMode::Url => self.mode = ViewMode::Normal, - _ => {} - } - self.initialised = false; - self.dirty = true; - return true; - } - UIEvent::EnvelopeRename(old_hash, new_hash) if self.coordinates.2 == old_hash => { - self.coordinates.2 = new_hash; - } - UIEvent::Action(View(ViewAction::ExportMail(ref path))) => { - // Save entire message as eml - let account = &context.accounts[&self.coordinates.0]; - if !account.contains_key(self.coordinates.2) { - /* The envelope has been renamed or removed, so wait for the appropriate - * event to arrive */ - return true; - } - let bytes = if let MailViewState::Loaded { ref bytes, .. } = self.state { - bytes - } else if let MailViewState::Error { ref err } = self.state { - context.replies.push_back(UIEvent::Notification( - Some("Failed to open e-mail".to_string()), - err.to_string(), - Some(NotificationType::Error(err.kind)), - )); - log::error!("Failed to open envelope: {err}"); - self.init_futures(context); - return true; - } else { - return true; - }; - - let mut path = std::path::Path::new(path).to_path_buf(); - - if path.is_dir() { - let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2); - path.push(format!("{}.eml", envelope.message_id_raw())); - } - match save_attachment(&path, bytes) { - Err(err) => { - context.replies.push_back(UIEvent::Notification( - Some(format!("Failed to create file at {}", path.display())), - err.to_string(), - Some(NotificationType::Error(melib::ErrorKind::External)), - )); - log::error!("Failed to create file at {}: {err}", path.display()); - return true; - } - Ok(()) => { - context.replies.push_back(UIEvent::Notification( - None, - format!("Saved at {}", &path.display()), - Some(NotificationType::Info), - )); - } - } - - return true; - } - UIEvent::Action(View(ViewAction::SaveAttachment(a_i, ref path))) => { - { - let account = &context.accounts[&self.coordinates.0]; - if !account.contains_key(self.coordinates.2) { - /* The envelope has been renamed or removed, so wait for the appropriate - * event to arrive */ - return true; - } - } - let bytes = if let MailViewState::Loaded { ref bytes, .. } = self.state { - bytes - } else if let MailViewState::Error { ref err } = self.state { - context.replies.push_back(UIEvent::Notification( - Some("Failed to open e-mail".to_string()), - err.to_string(), - Some(NotificationType::Error(err.kind)), - )); - log::error!("Failed to open envelope: {err}"); - self.init_futures(context); - return true; - } else { - return true; - }; - - let mut path = std::path::Path::new(path).to_path_buf(); - - if let Some(u) = self.open_attachment(a_i, context) { - if path.is_dir() { - if let Some(filename) = u.filename() { - path.push(filename); - } else { - path.push(format!( - "meli_attachment_{a_i}_{}", - Uuid::new_v4().as_simple() - )); - } - } - match save_attachment(&path, &u.decode(Default::default())) { - Err(err) => { - context.replies.push_back(UIEvent::Notification( - Some(format!("Failed to create file at {}", path.display())), - err.to_string(), - Some(NotificationType::Error(melib::ErrorKind::External)), - )); - log::error!("Failed to create file at {}: {err}", path.display()); - } - Ok(()) => { - context.replies.push_back(UIEvent::Notification( - None, - format!("Saved at {}", path.display()), - Some(NotificationType::Info), - )); - } - } - } else if a_i == 0 { - let account = &context.accounts[&self.coordinates.0]; - // Save entire message as eml - if path.is_dir() { - let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2); - path.push(format!("{}.eml", envelope.message_id_raw())); - } - match save_attachment(&path, bytes) { - Err(err) => { - context.replies.push_back(UIEvent::Notification( - Some(format!("Failed to create file at {}", path.display())), - err.to_string(), - Some(NotificationType::Error(melib::ErrorKind::External)), - )); - log::error!("Failed to create file at {}: {err}", path.display()); - return true; - } - Ok(()) => { - context.replies.push_back(UIEvent::Notification( - None, - format!("Saved at {}", &path.display()), - Some(NotificationType::Info), - )); - } - } - - return true; - } else { - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!( - "Attachment `{}` not found.", - a_i - )))); - } - return true; + UIEvent::EnvelopeRename(old_hash, new_hash) if coordinates.2 == old_hash => { + self.coordinates.as_mut().unwrap().2 = new_hash; } UIEvent::Action(MailingListAction(ref e)) => { - let account = &context.accounts[&self.coordinates.0]; - if !account.contains_key(self.coordinates.2) { + let account = &context.accounts[&coordinates.0]; + if !account.contains_key(coordinates.2) { /* The envelope has been renamed or removed, so wait for the appropriate * event to arrive */ return true; } - let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2); + let envelope: EnvelopeRef = account.collection.get_env(coordinates.2); let detect = list_management::ListActions::detect(&envelope); if let Some(ref actions) = detect { match e { @@ -2651,7 +587,7 @@ impl Component for MailView { if let Ok(mailto) = Mailto::try_from(list_post_addr) { let draft: Draft = mailto.into(); let mut composer = - Composer::with_account(self.coordinates.0, context); + Composer::with_account(coordinates.0, context); composer.set_draft(draft); context.replies.push_back(UIEvent::Action(Tab(New(Some( Box::new(composer), @@ -2679,7 +615,7 @@ impl Component for MailView { let mut draft: Draft = mailto.into(); draft.set_header( HeaderName::FROM, - context.accounts[&self.coordinates.0] + context.accounts[&coordinates.0] .settings .account() .make_display_name(), @@ -2691,7 +627,7 @@ impl Component for MailView { if let Err(err) = super::compose::send_draft( ToggleFlag::False, context, - self.coordinates.0, + coordinates.0, draft, SpecialUsageMailbox::Sent, Flag::SEEN, @@ -2709,7 +645,7 @@ impl Component for MailView { } list_management::ListAction::Url(url) => { let url_launcher = mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] + context[coordinates.0][&coordinates.1] .pager .url_launcher ) @@ -2752,9 +688,7 @@ impl Component for MailView { MailingListAction::ListArchive if actions.archive.is_some() => { /* open archive url with url_launcher */ let url_launcher = mailbox_settings!( - context[self.coordinates.0][&self.coordinates.1] - .pager - .url_launcher + context[coordinates.0][&coordinates.1].pager.url_launcher ) .as_ref() .map(|s| s.as_str()) @@ -2796,55 +730,6 @@ impl Component for MailView { .push_back(UIEvent::Action(Tab(New(Some(Box::new(self.clone())))))); return true; } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["change_charset"]) => - { - let entries = vec![ - (None, "default".to_string()), - (Some(Charset::Ascii), Charset::Ascii.to_string()), - (Some(Charset::UTF8), Charset::UTF8.to_string()), - (Some(Charset::UTF16), Charset::UTF16.to_string()), - (Some(Charset::ISO8859_1), Charset::ISO8859_1.to_string()), - (Some(Charset::ISO8859_2), Charset::ISO8859_2.to_string()), - (Some(Charset::ISO8859_3), Charset::ISO8859_3.to_string()), - (Some(Charset::ISO8859_4), Charset::ISO8859_4.to_string()), - (Some(Charset::ISO8859_5), Charset::ISO8859_5.to_string()), - (Some(Charset::ISO8859_6), Charset::ISO8859_6.to_string()), - (Some(Charset::ISO8859_7), Charset::ISO8859_7.to_string()), - (Some(Charset::ISO8859_8), Charset::ISO8859_8.to_string()), - (Some(Charset::ISO8859_10), Charset::ISO8859_10.to_string()), - (Some(Charset::ISO8859_13), Charset::ISO8859_13.to_string()), - (Some(Charset::ISO8859_14), Charset::ISO8859_14.to_string()), - (Some(Charset::ISO8859_15), Charset::ISO8859_15.to_string()), - (Some(Charset::ISO8859_16), Charset::ISO8859_16.to_string()), - (Some(Charset::Windows1250), Charset::Windows1250.to_string()), - (Some(Charset::Windows1251), Charset::Windows1251.to_string()), - (Some(Charset::Windows1252), Charset::Windows1252.to_string()), - (Some(Charset::Windows1253), Charset::Windows1253.to_string()), - (Some(Charset::GBK), Charset::GBK.to_string()), - (Some(Charset::GB2312), Charset::GB2312.to_string()), - (Some(Charset::GB18030), Charset::GB18030.to_string()), - (Some(Charset::BIG5), Charset::BIG5.to_string()), - (Some(Charset::ISO2022JP), Charset::ISO2022JP.to_string()), - (Some(Charset::EUCJP), Charset::EUCJP.to_string()), - (Some(Charset::KOI8R), Charset::KOI8R.to_string()), - (Some(Charset::KOI8U), Charset::KOI8U.to_string()), - ]; - self.force_charset = ForceCharset::Dialog(Box::new(Selector::new( - "select charset to force", - entries, - true, - Some(Box::new( - move |id: ComponentId, results: &[Option]| { - Some(UIEvent::FinishedUIDialog(id, Box::new(results.to_vec()))) - }, - )), - context, - ))); - self.initialised = false; - self.dirty = true; - return true; - } _ => {} } false @@ -2852,57 +737,24 @@ impl Component for MailView { fn is_dirty(&self) -> bool { self.dirty - || self.pager.is_dirty() - || self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false) - || matches!(self.force_charset, ForceCharset::Dialog(ref s) if s.is_dirty()) - || matches!(self.mode, ViewMode::ContactSelector(ref s) if s.is_dirty()) + || self.state.is_dirty() + || self + .contact_selector + .as_ref() + .map(|s| s.is_dirty()) + .unwrap_or(false) } fn set_dirty(&mut self, value: bool) { self.dirty = value; - match self.mode { - ViewMode::Normal | ViewMode::Url | ViewMode::Source(_) | ViewMode::Attachment(_) => { - self.pager.set_dirty(value); - } - ViewMode::ContactSelector(ref mut s) => { - self.pager.set_dirty(value); - s.set_dirty(value); - } - ViewMode::Subview => { - if let Some(s) = self.subview.as_mut() { - s.set_dirty(value); - } - } + if let Some(ref mut s) = self.contact_selector { + s.set_dirty(value); } + self.state.set_dirty(value); } fn shortcuts(&self, context: &Context) -> ShortcutMaps { - let mut map = if let Some(ref sbv) = self.subview { - sbv.shortcuts(context) - } else { - self.pager.shortcuts(context) - }; - - let mut our_map = context.settings.shortcuts.envelope_view.key_values(); - - if !(self.mode.is_attachment() - /*|| self.mode.is_ansi()*/ - || self.mode == ViewMode::Subview - || self.mode == ViewMode::Source(Source::Decoded) - || self.mode == ViewMode::Source(Source::Raw) - || self.mode == ViewMode::Url) - { - our_map.remove("return_to_normal_view"); - } - if self.mode != ViewMode::Url { - our_map.remove("go_to_url"); - } - if !(self.mode == ViewMode::Normal || self.mode == ViewMode::Url) { - our_map.remove("toggle_url_mode"); - } - map.insert(Shortcuts::ENVELOPE_VIEW, our_map); - - map + self.state.shortcuts(context) } fn id(&self) -> ComponentId { @@ -2917,77 +769,3 @@ impl Component for MailView { } } } - -fn save_attachment(path: &std::path::Path, bytes: &[u8]) -> Result<()> { - let mut f = std::fs::File::create(path)?; - let mut permissions = f.metadata()?.permissions(); - permissions.set_mode(0o600); // Read/write for owner only. - f.set_permissions(permissions)?; - f.write_all(bytes)?; - f.flush()?; - Ok(()) -} - -fn desktop_exec_to_command(command: &str, path: String, is_url: bool) -> String { - /* Purge unused field codes */ - let command = command - .replace("%i", "") - .replace("%c", "") - .replace("%k", ""); - if command.contains("%f") { - command.replacen("%f", &path.replace(' ', "\\ "), 1) - } else if command.contains("%F") { - command.replacen("%F", &path.replace(' ', "\\ "), 1) - } else if command.contains("%u") || command.contains("%U") { - let from_pattern = if command.contains("%u") { "%u" } else { "%U" }; - if is_url { - command.replacen(from_pattern, &path, 1) - } else { - command.replacen( - from_pattern, - &format!("file://{}", path).replace(' ', "\\ "), - 1, - ) - } - } else if is_url { - format!("{} {}", command, path) - } else { - format!("{} {}", command, path.replace(' ', "\\ ")) - } -} - -#[test] -fn test_desktop_exec() { - assert_eq!( - "ristretto /tmp/file".to_string(), - desktop_exec_to_command("ristretto %F", "/tmp/file".to_string(), false) - ); - assert_eq!( - "/usr/lib/firefox-esr/firefox-esr file:///tmp/file".to_string(), - desktop_exec_to_command( - "/usr/lib/firefox-esr/firefox-esr %u", - "/tmp/file".to_string(), - false - ) - ); - assert_eq!( - "/usr/lib/firefox-esr/firefox-esr www.example.com".to_string(), - desktop_exec_to_command( - "/usr/lib/firefox-esr/firefox-esr %u", - "www.example.com".to_string(), - true - ) - ); - assert_eq!( - "/usr/bin/vlc --started-from-file www.example.com".to_string(), - desktop_exec_to_command( - "/usr/bin/vlc --started-from-file %U", - "www.example.com".to_string(), - true - ) - ); - assert_eq!( - "zathura --fork file:///tmp/file".to_string(), - desktop_exec_to_command("zathura --fork %U", "file:///tmp/file".to_string(), true) - ); -} diff --git a/src/components/mail/view/envelope.rs b/src/components/mail/view/envelope.rs index bb51dd4f..561d1c0c 100644 --- a/src/components/mail/view/envelope.rs +++ b/src/components/mail/view/envelope.rs @@ -21,42 +21,55 @@ use std::process::{Command, Stdio}; -use linkify::{Link, LinkFinder}; +use linkify::LinkFinder; use melib::xdg_utils::query_default_app; use super::*; +use crate::ThreadEvent; -#[derive(PartialEq, Eq, Debug)] -enum ViewMode { - Normal, - Url, - Attachment(usize), - Raw, - Subview, +/// Envelope view, with sticky headers, a pager for the body, and +/// subviews for more menus. +/// +/// Doesn't have a concept of accounts, mailboxes or mail backends. +/// Therefore all settings it needs need to be provided through the `view_settings` field of type +/// [`ViewSettings`]. +#[derive(Debug)] +pub struct EnvelopeView { + pub pager: Pager, + pub subview: Option>, + pub dirty: bool, + pub initialised: bool, + pub force_draw_headers: bool, + pub mode: ViewMode, + pub mail: Mail, + pub body: Box, + pub display: Vec, + pub body_text: String, + pub links: Vec, + pub attachment_tree: String, + pub attachment_paths: Vec>, + pub headers_no: usize, + pub headers_cursor: usize, + pub force_charset: ForceCharset, + pub view_settings: ViewSettings, + pub cmd_buf: String, + pub active_jobs: HashSet, + pub main_loop_handler: MainLoopHandler, + pub id: ComponentId, } -impl ViewMode { - fn is_attachment(&self) -> bool { - matches!(self, ViewMode::Attachment(_)) +impl Clone for EnvelopeView { + fn clone(&self) -> Self { + Self::new( + self.mail.clone(), + Some(self.pager.clone()), + None, + Some(self.view_settings.clone()), + self.main_loop_handler.clone(), + ) } } -/// Contains an Envelope view, with sticky headers, a pager for the body, and -/// subviews for more menus -#[derive(Debug)] -pub struct EnvelopeView { - pager: Option, - subview: Option>, - dirty: bool, - mode: ViewMode, - mail: Mail, - - _account_hash: AccountHash, - force_charset: ForceCharset, - cmd_buf: String, - id: ComponentId, -} - impl fmt::Display for EnvelopeView { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "view mail") @@ -68,23 +81,537 @@ impl EnvelopeView { mail: Mail, pager: Option, subview: Option>, - _account_hash: AccountHash, + view_settings: Option, + main_loop_handler: MainLoopHandler, ) -> Self { - EnvelopeView { - pager, + let view_settings = view_settings.unwrap_or_default(); + let body = Box::new(AttachmentBuilder::new(&mail.bytes).build()); + let mut ret = EnvelopeView { + pager: pager.unwrap_or_default(), subview, dirty: true, + initialised: false, + force_draw_headers: false, mode: ViewMode::Normal, force_charset: ForceCharset::None, + attachment_tree: String::new(), + attachment_paths: vec![], + body, + display: vec![], + links: vec![], + body_text: String::new(), + view_settings, + headers_no: 5, + headers_cursor: 0, mail, - _account_hash, + main_loop_handler, + active_jobs: HashSet::default(), cmd_buf: String::with_capacity(4), id: ComponentId::default(), + }; + + ret.parse_attachments(); + + ret + } + + fn attachment_to_display_helper( + a: &Attachment, + main_loop_handler: &MainLoopHandler, + active_jobs: &mut HashSet, + acc: &mut Vec, + view_settings: &ViewSettings, + force_charset: Option, + ) { + if a.content_disposition.kind.is_attachment() || a.content_type == "message/rfc822" { + acc.push(AttachmentDisplay::Attachment { + inner: Box::new(a.clone()), + }); + } else if a.content_type().is_text_html() { + let bytes = a.decode(force_charset.into()); + let filter_invocation = view_settings + .html_filter + .as_deref() + .unwrap_or("w3m -I utf-8 -T text/html"); + let command_obj = Command::new("sh") + .args(["-c", filter_invocation]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .and_then(|mut cmd| { + cmd.stdin.as_mut().unwrap().write_all(&bytes)?; + Ok(String::from_utf8_lossy(&cmd.wait_with_output()?.stdout).to_string()) + }); + match command_obj { + Err(err) => { + main_loop_handler.send(ThreadEvent::UIEvent(UIEvent::Notification( + Some(format!( + "Failed to start html filter process: {}", + filter_invocation, + )), + err.to_string(), + Some(NotificationType::Error(melib::ErrorKind::External)), + ))); + let comment = Some(format!( + "Failed to start html filter process: `{}`. Press `v` to open in web \ + browser. \n\n", + filter_invocation + )); + let text = String::from_utf8_lossy(&bytes).to_string(); + acc.push(AttachmentDisplay::InlineText { + inner: Box::new(a.clone()), + comment, + text, + }); + } + Ok(text) => { + let comment = Some(format!( + "Text piped through `{}`. Press `v` to open in web browser. \n\n", + filter_invocation + )); + acc.push(AttachmentDisplay::InlineText { + inner: Box::new(a.clone()), + comment, + text, + }); + } + } + } else if a.is_text() { + let bytes = a.decode(force_charset.into()); + acc.push(AttachmentDisplay::InlineText { + inner: Box::new(a.clone()), + comment: None, + text: String::from_utf8_lossy(&bytes).to_string(), + }); + } else if let ContentType::Multipart { + ref kind, + ref parts, + .. + } = a.content_type + { + match kind { + MultipartType::Alternative => { + if parts.is_empty() { + return; + } + let mut display = vec![]; + let mut chosen_attachment_idx = 0; + if let Some(text_attachment_pos) = + parts.iter().position(|a| a.content_type == "text/plain") + { + let bytes = &parts[text_attachment_pos].decode(force_charset.into()); + if bytes.trim().is_empty() + && view_settings.auto_choose_multipart_alternative + { + if let Some(text_attachment_pos) = + parts.iter().position(|a| a.content_type == "text/html") + { + /* Select html alternative since text/plain is empty */ + chosen_attachment_idx = text_attachment_pos; + } + } else { + /* Select text/plain alternative */ + chosen_attachment_idx = text_attachment_pos; + } + } + for a in parts { + EnvelopeView::attachment_to_display_helper( + a, + main_loop_handler, + active_jobs, + &mut display, + view_settings, + force_charset, + ); + } + acc.push(AttachmentDisplay::Alternative { + inner: Box::new(a.clone()), + shown_display: chosen_attachment_idx, + display, + }); + } + MultipartType::Signed => { + #[cfg(not(feature = "gpgme"))] + { + acc.push(AttachmentDisplay::SignedUnverified { + inner: Box::new(a.clone()), + display: { + let mut v = vec![]; + EnvelopeView::attachment_to_display_helper( + &parts[0], + main_loop_handler, + active_jobs, + &mut v, + view_settings, + force_charset, + ); + v + }, + }); + } + #[cfg(feature = "gpgme")] + { + if view_settings.auto_verify_signatures { + let verify_fut = crate::components::mail::pgp::verify(a.clone()); + let handle = + main_loop_handler.job_executor.spawn_specialized(verify_fut); + active_jobs.insert(handle.job_id); + main_loop_handler.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( + StatusEvent::NewJob(handle.job_id), + ))); + acc.push(AttachmentDisplay::SignedPending { + inner: Box::new(a.clone()), + job_id: handle.job_id, + display: { + let mut v = vec![]; + EnvelopeView::attachment_to_display_helper( + &parts[0], + main_loop_handler, + active_jobs, + &mut v, + view_settings, + force_charset, + ); + v + }, + handle, + }); + } else { + acc.push(AttachmentDisplay::SignedUnverified { + inner: Box::new(a.clone()), + display: { + let mut v = vec![]; + EnvelopeView::attachment_to_display_helper( + &parts[0], + main_loop_handler, + active_jobs, + &mut v, + view_settings, + force_charset, + ); + v + }, + }); + } + } + } + MultipartType::Encrypted => { + for a in parts { + if a.content_type == "application/octet-stream" { + #[cfg(not(feature = "gpgme"))] + { + acc.push(AttachmentDisplay::EncryptedFailed { + inner: Box::new(a.clone()), + error: Error::new( + "Cannot decrypt: meli must be compiled with libgpgme \ + support.", + ), + }); + } + #[cfg(feature = "gpgme")] + { + if view_settings.auto_decrypt { + let decrypt_fut = + crate::components::mail::pgp::decrypt(a.raw().to_vec()); + let handle = main_loop_handler + .job_executor + .spawn_specialized(decrypt_fut); + active_jobs.insert(handle.job_id); + main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::StatusEvent(StatusEvent::NewJob(handle.job_id)), + )); + acc.push(AttachmentDisplay::EncryptedPending { + inner: Box::new(a.clone()), + handle, + }); + } else { + acc.push(AttachmentDisplay::EncryptedFailed { + inner: Box::new(a.clone()), + error: Error::new("Undecrypted."), + }); + } + } + } + } + } + _ => { + for a in parts { + EnvelopeView::attachment_to_display_helper( + a, + main_loop_handler, + active_jobs, + acc, + view_settings, + force_charset, + ); + } + } + } + } + } + + pub fn parse_attachments(&mut self) { + let mut display = vec![]; + Self::attachment_to_display_helper( + &self.body, + &self.main_loop_handler, + &mut self.active_jobs, + &mut display, + &self.view_settings, + (&self.force_charset).into(), + ); + let (attachment_paths, attachment_tree) = self.attachment_displays_to_tree(&display); + let body_text = self.attachment_displays_to_text(&display, true); + self.display = display; + self.body_text = body_text; + self.attachment_tree = attachment_tree; + self.attachment_paths = attachment_paths; + } + + pub fn attachment_displays_to_text( + &self, + displays: &[AttachmentDisplay], + show_comments: bool, + ) -> String { + let mut acc = String::new(); + for d in displays { + use AttachmentDisplay::*; + match d { + Alternative { + inner: _, + shown_display, + display, + } => { + acc.push_str(&self.attachment_displays_to_text( + &display[*shown_display..(*shown_display + 1)], + show_comments, + )); + } + InlineText { + inner: _, + text, + comment: Some(comment), + } if show_comments => { + acc.push_str(comment); + if !acc.ends_with("\n\n") { + acc.push_str("\n\n"); + } + acc.push_str(text); + } + InlineText { + inner: _, + text, + comment: _, + } => acc.push_str(text), + InlineOther { inner } => { + if !acc.ends_with("\n\n") { + acc.push_str("\n\n"); + } + acc.push_str(&inner.to_string()); + if !acc.ends_with("\n\n") { + acc.push_str("\n\n"); + } + } + Attachment { inner: _ } => {} + SignedPending { + inner: _, + display, + handle: _, + job_id: _, + } => { + if show_comments { + acc.push_str("Waiting for signature verification.\n\n"); + } + acc.push_str(&self.attachment_displays_to_text(display, show_comments)); + } + SignedUnverified { inner: _, display } => { + if show_comments { + acc.push_str("Unverified signature.\n\n"); + } + acc.push_str(&self.attachment_displays_to_text(display, show_comments)) + } + SignedFailed { + inner: _, + display, + error, + } => { + if show_comments { + let _ = writeln!(acc, "Failed to verify signature: {}.\n", error); + } + acc.push_str(&self.attachment_displays_to_text(display, show_comments)); + } + SignedVerified { + inner: _, + display, + description, + } => { + if show_comments { + if description.is_empty() { + acc.push_str("Verified signature.\n\n"); + } else { + acc.push_str(description); + acc.push_str("\n\n"); + } + } + acc.push_str(&self.attachment_displays_to_text(display, show_comments)); + } + EncryptedPending { .. } => acc.push_str("Waiting for decryption result."), + EncryptedFailed { inner: _, error } => { + let _ = write!(acc, "Decryption failed: {}.", &error); + } + EncryptedSuccess { + inner: _, + plaintext: _, + plaintext_display, + description, + } => { + if show_comments { + if description.is_empty() { + acc.push_str("Succesfully decrypted.\n\n"); + } else { + acc.push_str(description); + acc.push_str("\n\n"); + } + } + acc.push_str( + &self.attachment_displays_to_text(plaintext_display, show_comments), + ); + } + } + } + acc + } + + fn attachment_displays_to_tree( + &self, + displays: &[AttachmentDisplay], + ) -> (Vec>, String) { + let mut acc = String::new(); + let mut branches = SmallVec::new(); + let mut paths = Vec::with_capacity(displays.len()); + let mut cur_path = vec![]; + let mut idx = 0; + + fn append_entry( + (idx, (depth, att_display)): (&mut usize, (usize, &AttachmentDisplay)), + branches: &mut SmallVec<[bool; 8]>, + paths: &mut Vec>, + cur_path: &mut Vec, + has_sibling: bool, + s: &mut String, + ) { + use AttachmentDisplay::*; + let mut default_alternative: Option = None; + let (att, sub_att_display_vec) = match att_display { + Alternative { + inner, + shown_display, + display, + } => { + default_alternative = Some(*shown_display); + (inner, display.as_slice()) + } + InlineText { + inner, + text: _, + comment: _, + } + | InlineOther { inner } + | Attachment { inner } + | EncryptedPending { inner, handle: _ } + | EncryptedFailed { inner, error: _ } => (inner, &[][..]), + SignedPending { + inner, + display, + handle: _, + job_id: _, + } + | SignedUnverified { inner, display } + | SignedFailed { + inner, + display, + error: _, + } + | SignedVerified { + inner, + display, + description: _, + } + | EncryptedSuccess { + inner: _, + plaintext: inner, + plaintext_display: display, + description: _, + } => (inner, display.as_slice()), + }; + s.extend(format!("\n[{}]", idx).chars()); + for &b in branches.iter() { + if b { + s.push('|'); + } else { + s.push(' '); + } + s.push(' '); + } + if depth > 0 { + if has_sibling { + s.push('|'); + } else { + s.push(' '); + } + s.push_str("\\_ "); + } else { + s.push(' '); + s.push(' '); + } + + s.push_str(&att.to_string()); + paths.push(cur_path.clone()); + if matches!(att.content_type, ContentType::Multipart { .. }) { + let mut iter = (0..sub_att_display_vec.len()).peekable(); + if has_sibling { + branches.push(true); + } else { + branches.push(false); + } + while let Some(i) = iter.next() { + *idx += 1; + cur_path.push(i); + append_entry( + (idx, (depth + 1, &sub_att_display_vec[i])), + branches, + paths, + cur_path, + iter.peek().is_some(), + s, + ); + if Some(i) == default_alternative { + s.push_str(" (displayed by default)"); + } + cur_path.pop(); + } + branches.pop(); + } } + + for (i, d) in displays.iter().enumerate() { + cur_path.push(i); + append_entry( + (&mut idx, (0, d)), + &mut branches, + &mut paths, + &mut cur_path, + i + 1 < displays.len(), + &mut acc, + ); + cur_path.pop(); + idx += 1; + } + (paths, acc) } /// Returns the string to be displayed in the Viewer - fn attachment_to_text(&self, body: &Attachment, context: &mut Context) -> String { + pub fn attachment_to_text(&mut self, body: &Attachment, context: &mut Context) -> String { let finder = LinkFinder::new(); let body_text = String::from_utf8_lossy(&body.decode_rec(DecodeOptions { filter: Some(Box::new(|a: &Attachment, v: &mut Vec| { @@ -148,7 +675,48 @@ impl EnvelopeView { } t } - ViewMode::Raw => String::from_utf8_lossy(body.body()).into_owned(), + ViewMode::Source(Source::Raw) => { + let text = { String::from_utf8_lossy(body.body()).into_owned() }; + self.view_settings.body_theme = crate::conf::value(context, "mail.view.body"); + text + } + ViewMode::Source(Source::Decoded) => { + let text = { + /* Decode each header value */ + let mut ret = melib::email::parser::headers::headers(body.body()) + .map(|(_, v)| v) + .map_err(|err| err.into()) + .and_then(|headers| { + Ok(headers + .into_iter() + .map(|(h, v)| { + melib::email::parser::encodings::phrase(v, true) + .map(|(_, v)| { + let mut h = h.to_vec(); + h.push(b':'); + h.push(b' '); + h.extend(v.into_iter()); + h + }) + .map_err(|err| err.into()) + }) + .collect::>>>()? + .join(&b"\n"[..])) + }) + .map(|v| String::from_utf8_lossy(&v).into_owned()) + .unwrap_or_else(|err: Error| err.to_string()); + if !ret.ends_with("\n\n") { + ret.push_str("\n\n"); + } + ret.push_str(&body_text); + if !ret.ends_with("\n\n") { + ret.push_str("\n\n"); + } + // ret.push_str(&self.attachment_tree); + ret + }; + text + } ViewMode::Url => { let mut t = body_text; for (lidx, l) in finder.links(&body.text()).enumerate() { @@ -183,147 +751,608 @@ impl EnvelopeView { } } } + + fn open_attachment( + &'_ self, + lidx: usize, + context: &mut Context, + ) -> Option<&'_ melib::Attachment> { + if let Some(path) = self.attachment_paths.get(lidx).and_then(|path| { + if !path.is_empty() { + Some(path) + } else { + None + } + }) { + let first = path[0]; + use AttachmentDisplay::*; + let root_attachment = match &self.display[first] { + Alternative { + inner, + shown_display: _, + display: _, + } + | InlineText { + inner, + text: _, + comment: _, + } + | InlineOther { inner } + | Attachment { inner } + | SignedPending { + inner, + display: _, + handle: _, + job_id: _, + } + | SignedFailed { + inner, + display: _, + error: _, + } + | SignedVerified { + inner, + display: _, + description: _, + } + | SignedUnverified { inner, display: _ } + | EncryptedPending { inner, handle: _ } + | EncryptedFailed { inner, error: _ } + | EncryptedSuccess { + inner: _, + plaintext: inner, + plaintext_display: _, + description: _, + } => inner, + }; + fn find_attachment<'a>( + a: &'a melib::Attachment, + path: &[usize], + ) -> Option<&'a melib::Attachment> { + if path.is_empty() { + return Some(a); + } + if let ContentType::Multipart { ref parts, .. } = a.content_type { + let first = path[0]; + if first < parts.len() { + return find_attachment(&parts[first], &path[1..]); + } + } + None + } + + let ret = find_attachment(root_attachment, &path[1..]); + if lidx == 0 { + return ret.and_then(|a| { + if a.content_disposition.kind.is_attachment() + || a.content_type == "message/rfc822" + { + Some(a) + } else { + None + } + }); + } else { + return ret; + } + } + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!( + "Attachment `{}` not found.", + lidx + )))); + None + } } impl Component for EnvelopeView { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); - let theme_default = crate::conf::value(context, "theme_default"); - let email_header_theme = crate::conf::value(context, "email_header"); + + self.view_settings.theme_default = crate::conf::value(context, "theme_default"); + + let headers = crate::conf::value(context, "mail.view.headers"); + let headers_names = crate::conf::value(context, "mail.view.headers_names"); + let headers_area = crate::conf::value(context, "mail.view.headers_area"); let y: usize = { - if self.mode == ViewMode::Raw { - clear_area(grid, area, crate::conf::value(context, "theme_default")); + if self.mode.is_source() { + clear_area(grid, area, self.view_settings.theme_default); context.dirty_areas.push_back(area); - get_y(upper_left).saturating_sub(1) + get_y(upper_left) } else { - let (x, y) = write_string_to_grid( - &format!("Date: {}", self.mail.date_as_str()), - grid, - email_header_theme.fg, - email_header_theme.bg, - email_header_theme.attrs, - area, - Some(get_x(upper_left)), - ); - for x in x..=get_x(bottom_right) { - grid[(x, y)] - .set_ch(' ') - .set_fg(theme_default.fg) - .set_bg(theme_default.bg); - } - let (x, y) = write_string_to_grid( - &format!("From: {}", self.mail.field_from_to_string()), - grid, - email_header_theme.fg, - email_header_theme.bg, - email_header_theme.attrs, - (set_y(upper_left, y + 1), bottom_right), - Some(get_x(upper_left)), - ); - for x in x..=get_x(bottom_right) { - grid[(x, y)] - .set_ch(' ') - .set_fg(theme_default.fg) - .set_bg(theme_default.bg); - } - let (x, y) = write_string_to_grid( - &format!("To: {}", self.mail.field_to_to_string()), - grid, - email_header_theme.fg, - email_header_theme.bg, - email_header_theme.attrs, - (set_y(upper_left, y + 1), bottom_right), - Some(get_x(upper_left)), - ); - for x in x..=get_x(bottom_right) { - grid[(x, y)] - .set_ch(' ') - .set_fg(theme_default.fg) - .set_bg(theme_default.bg); - } - let (x, y) = write_string_to_grid( - &format!("Subject: {}", self.mail.subject()), - grid, - email_header_theme.fg, - email_header_theme.bg, - email_header_theme.attrs, - (set_y(upper_left, y + 1), bottom_right), - Some(get_x(upper_left)), + let envelope = &self.mail; + let height_p = self.pager.size().1; + + let height = height!(area) - self.headers_no - 1; + + self.headers_no = 0; + let mut skip_header_ctr = self.headers_cursor; + let sticky = self.view_settings.sticky_headers || height_p < height; + let (_, mut y) = upper_left; + macro_rules! print_header { + ($(($header:path, $string:expr)),*$(,)?) => { + $({ + if sticky || skip_header_ctr == 0 { + if y <= get_y(bottom_right) { + let (_x, _y) = write_string_to_grid( + &format!("{}:", $header), + grid, + headers_names.fg, + headers_names.bg, + headers_names.attrs, + (set_y(upper_left, y), bottom_right), + Some(get_x(upper_left)), + ); + if let Some(cell) = grid.get_mut(_x, _y) { + cell.set_ch(' ') + .set_fg(headers_area.fg) + .set_bg(headers_area.bg) + .set_attrs(headers_area.attrs); + } + + let (_x, _y) = write_string_to_grid( + &$string, + grid, + headers.fg, + headers.bg, + headers.attrs, + ((_x + 1, _y), bottom_right), + Some(get_x(upper_left)), + ); + clear_area( + grid, + ( + (std::cmp::min(_x, get_x(bottom_right)), _y), + (get_x(bottom_right), _y), + ), + headers_area, + ); + y = _y + 1; + } + } else { + skip_header_ctr -= 1; + } + self.headers_no += 1; + })+ + }; + } + let find_offset = |s: &str| -> (bool, (i64, i64)) { + let mut diff = (true, (0, 0)); + if let Some(pos) = s.as_bytes().iter().position(|b| *b == b'+' || *b == b'-') { + let offset = &s[pos..]; + diff.0 = offset.starts_with('+'); + if let (Ok(hr_offset), Ok(min_offset)) = + (offset[1..3].parse::(), offset[3..5].parse::()) + { + diff.1 .0 = hr_offset; + diff.1 .1 = min_offset; + } + } + diff + }; + let orig_date = envelope.date_as_str(); + let date_str: std::borrow::Cow = if self.view_settings.show_date_in_my_timezone + { + let local_date = datetime::timestamp_to_string( + envelope.timestamp, + Some(datetime::formats::RFC822_DATE), + false, + ); + let orig_offset = find_offset(orig_date); + let local_offset = find_offset(&local_date); + if orig_offset == local_offset { + orig_date.into() + } else { + format!( + "{} [actual timezone: {}{:02}{:02}]", + local_date, + if orig_offset.0 { '+' } else { '-' }, + orig_offset.1 .0, + orig_offset.1 .1 + ) + .into() + } + } else { + orig_date.into() + }; + print_header!( + (HeaderName::DATE, date_str), + (HeaderName::FROM, envelope.field_from_to_string()), + (HeaderName::TO, envelope.field_to_to_string()), ); - for x in x..=get_x(bottom_right) { - grid[(x, y)] - .set_ch(' ') - .set_fg(theme_default.fg) - .set_bg(theme_default.bg); - } - let (x, y) = write_string_to_grid( - &format!("Message-ID: <{}>", self.mail.message_id_raw()), - grid, - email_header_theme.fg, - email_header_theme.bg, - email_header_theme.attrs, - (set_y(upper_left, y + 1), bottom_right), - Some(get_x(upper_left)), + if envelope.other_headers().contains_key(HeaderName::CC) + && !envelope.other_headers()[HeaderName::CC].is_empty() + { + print_header!((HeaderName::CC, envelope.field_cc_to_string())); + } + print_header!( + (HeaderName::SUBJECT, envelope.subject()), + ( + HeaderName::MESSAGE_ID, + format!("<{}>", envelope.message_id_raw()) + ) ); - for x in x..=get_x(bottom_right) { - grid[(x, y)] - .set_ch(' ') - .set_fg(theme_default.fg) - .set_bg(theme_default.bg); + if self.view_settings.expand_headers { + if let Some(val) = envelope.in_reply_to_display() { + print_header!( + (HeaderName::IN_REPLY_TO, val), + ( + HeaderName::REFERENCES, + envelope + .references() + .iter() + .map(std::string::ToString::to_string) + .collect::>() + .join(", ") + ) + ); + } } + for hdr in &self.view_settings.show_extra_headers { + if let Some((val, hdr)) = HeaderName::try_from(hdr) + .ok() + .and_then(|hdr| Some((envelope.other_headers().get(&hdr)?, hdr))) + { + print_header!((hdr, val)); + } + } + if let Some(list_management::ListActions { + ref id, + ref archive, + ref post, + ref unsubscribe, + }) = list_management::ListActions::detect(&envelope) + { + let mut x = get_x(upper_left); + if let Some(id) = id { + if sticky || skip_header_ctr == 0 { + clear_area( + grid, + (set_y(upper_left, y), set_y(bottom_right, y)), + headers_area, + ); + let (_x, _) = write_string_to_grid( + "List-ID: ", + grid, + headers_names.fg, + headers_names.bg, + headers_names.attrs, + (set_y(upper_left, y), bottom_right), + None, + ); + let (_x, _y) = write_string_to_grid( + id, + grid, + headers.fg, + headers.bg, + headers.attrs, + ((_x, y), bottom_right), + None, + ); + x = _x; + if _y != y { + x = get_x(upper_left); + } + y = _y; + } + self.headers_no += 1; + } + if sticky || skip_header_ctr == 0 { + if archive.is_some() || post.is_some() || unsubscribe.is_some() { + let (_x, _y) = write_string_to_grid( + " Available actions: [ ", + grid, + headers_names.fg, + headers_names.bg, + headers_names.attrs, + ((x, y), bottom_right), + Some(get_x(upper_left)), + ); + x = _x; + y = _y; + } + if archive.is_some() { + let (_x, _y) = write_string_to_grid( + "list-archive, ", + grid, + headers.fg, + headers.bg, + headers.attrs, + ((x, y), bottom_right), + Some(get_x(upper_left)), + ); + x = _x; + y = _y; + } + if post.is_some() { + let (_x, _y) = write_string_to_grid( + "list-post, ", + grid, + headers.fg, + headers.bg, + headers.attrs, + ((x, y), bottom_right), + Some(get_x(upper_left)), + ); + x = _x; + y = _y; + } + if unsubscribe.is_some() { + let (_x, _y) = write_string_to_grid( + "list-unsubscribe, ", + grid, + headers.fg, + headers.bg, + headers.attrs, + ((x, y), bottom_right), + Some(get_x(upper_left)), + ); + x = _x; + y = _y; + } + if archive.is_some() || post.is_some() || unsubscribe.is_some() { + if x >= 2 { + grid[(x - 2, y)].set_ch(' '); + } + if x > 0 { + grid[(x - 1, y)] + .set_ch(']') + .set_fg(headers_names.fg) + .set_bg(headers_names.bg) + .set_attrs(headers_names.attrs); + } + } + for x in x..=get_x(bottom_right) { + grid[(x, y)] + .set_ch(' ') + .set_fg(headers_area.fg) + .set_bg(headers_area.bg); + } + y += 1; + } + } + + self.force_draw_headers = false; clear_area( grid, - (set_y(upper_left, y + 1), set_y(bottom_right, y + 2)), - crate::conf::value(context, "theme_default"), + (set_y(upper_left, y), set_y(bottom_right, y)), + headers_area, ); context .dirty_areas - .push_back((upper_left, set_y(bottom_right, y + 1))); - y + 1 + .push_back((upper_left, set_y(bottom_right, y + 3))); + if !self.view_settings.sticky_headers { + let height_p = self.pager.size().1; + + let height = height!(area).saturating_sub(y).saturating_sub(1); + if self.pager.cursor_pos() >= self.headers_no { + get_y(upper_left) + } else if (height_p > height && self.headers_cursor < self.headers_no + 1) + || self.headers_cursor == 0 + || height_p < height + { + y + 1 + } else { + get_y(upper_left) + } + } else { + y + 1 + } } }; - if self.dirty { + if !self.initialised { + self.initialised = true; let body = self.mail.body(); match self.mode { ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => { let attachment = &body.attachments()[aidx]; self.subview = Some(Box::new(HtmlView::new(attachment, context))); } + ViewMode::Attachment(aidx) => { + let mut text = "Viewing attachment. Press `r` to return \n".to_string(); + let attachment = &body.attachments()[aidx]; + text.push_str(&attachment.text()); + self.pager = Pager::from_string( + text, + Some(context), + Some(0), + None, + self.view_settings.theme_default, + ); + if let Some(ref filter) = self.view_settings.pager_filter { + self.pager.filter(filter); + } + self.subview = None; + } ViewMode::Normal if body.is_html() => { self.subview = Some(Box::new(HtmlView::new(&body, context))); self.mode = ViewMode::Subview; } + ViewMode::Normal + if self.view_settings.auto_choose_multipart_alternative + && match body.content_type { + ContentType::Multipart { + kind: MultipartType::Alternative, + ref parts, + .. + } => parts.iter().all(|p| { + p.is_html() || (p.is_text() && p.body().trim().is_empty()) + }), + _ => false, + } => + { + let subview = Box::new(HtmlView::new( + body.content_type + .parts() + .unwrap() + .iter() + .find(|a| a.is_html()) + .unwrap_or(&body), + context, + )); + self.subview = Some(subview); + self.mode = ViewMode::Subview; + } + ViewMode::Subview => {} + ViewMode::Source(Source::Raw) => { + let text = { String::from_utf8_lossy(body.body()).into_owned() }; + self.view_settings.body_theme = crate::conf::value(context, "mail.view.body"); + self.pager = Pager::from_string( + text, + Some(context), + None, + None, + self.view_settings.body_theme, + ); + if let Some(ref filter) = self.view_settings.pager_filter { + self.pager.filter(filter); + } + } + ViewMode::Source(Source::Decoded) => { + let text = { + /* Decode each header value */ + let mut ret = melib::email::parser::headers::headers(body.body()) + .map(|(_, v)| v) + .map_err(|err| err.into()) + .and_then(|headers| { + Ok(headers + .into_iter() + .map(|(h, v)| { + melib::email::parser::encodings::phrase(v, true) + .map(|(_, v)| { + let mut h = h.to_vec(); + h.push(b':'); + h.push(b' '); + h.extend(v.into_iter()); + h + }) + .map_err(|err| err.into()) + }) + .collect::>>>()? + .join(&b"\n"[..])) + }) + .map(|v| String::from_utf8_lossy(&v).into_owned()) + .unwrap_or_else(|err: Error| err.to_string()); + if !ret.ends_with("\n\n") { + ret.push_str("\n\n"); + } + ret.push_str(&self.body_text); + if !ret.ends_with("\n\n") { + ret.push_str("\n\n"); + } + // ret.push_str(&self.attachment_tree); + ret + }; + self.view_settings.body_theme = crate::conf::value(context, "mail.view.body"); + self.pager = Pager::from_string( + text, + Some(context), + None, + None, + self.view_settings.body_theme, + ); + if let Some(ref filter) = self.view_settings.pager_filter { + self.pager.filter(filter); + } + } + ViewMode::Url => { + let mut text = self.body_text.clone(); + if self.links.is_empty() { + let finder = LinkFinder::new(); + self.links = finder + .links(&text) + .filter_map(|l| { + if *l.kind() == linkify::LinkKind::Url { + Some(Link { + start: l.start(), + end: l.end(), + kind: LinkKind::Url, + }) + } else if *l.kind() == linkify::LinkKind::Email { + Some(Link { + start: l.start(), + end: l.end(), + kind: LinkKind::Email, + }) + } else { + None + } + }) + .collect::>(); + } + for (lidx, l) in self.links.iter().enumerate().rev() { + text.insert_str(l.start, &format!("[{}]", lidx)); + } + if !text.ends_with("\n\n") { + text.push_str("\n\n"); + } + text.push_str(&self.attachment_tree); + + let cursor_pos = self.pager.cursor_pos(); + self.view_settings.body_theme = crate::conf::value(context, "mail.view.body"); + self.pager = Pager::from_string( + text, + Some(context), + Some(cursor_pos), + None, + self.view_settings.body_theme, + ); + if let Some(ref filter) = self.view_settings.pager_filter { + self.pager.filter(filter); + } + self.subview = None; + } _ => { - let text = { self.attachment_to_text(&body, context) }; + let mut text = self.body_text.clone(); + if !text.ends_with("\n\n") { + text.push_str("\n\n"); + } + text.push_str(&self.attachment_tree); let cursor_pos = if self.mode.is_attachment() { - Some(0) + 0 } else { - self.pager.as_ref().map(Pager::cursor_pos) + self.pager.cursor_pos() }; - let colors = crate::conf::value(context, "mail.view.body"); - self.pager = Some(Pager::from_string( + self.view_settings.body_theme = crate::conf::value(context, "mail.view.body"); + self.pager = Pager::from_string( text, Some(context), - cursor_pos, + Some(cursor_pos), None, - colors, - )); + self.view_settings.body_theme, + ); + if let Some(ref filter) = self.view_settings.pager_filter { + self.pager.filter(filter); + } + self.subview = None; } }; self.dirty = false; } - if let Some(s) = self.subview.as_mut() { - s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); - } else if let Some(p) = self.pager.as_mut() { - p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); - } + match self.mode { + ViewMode::Subview if self.subview.is_some() => { + if let Some(s) = self.subview.as_mut() { + if !s.is_dirty() { + s.set_dirty(true); + } + s.draw(grid, (set_y(upper_left, y), bottom_right), context); + } + } + _ => { + self.pager + .draw(grid, (set_y(upper_left, y), bottom_right), context); + } + } if let ForceCharset::Dialog(ref mut s) = self.force_charset { s.draw(grid, area, context); } + + self.dirty = false; } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { @@ -360,13 +1389,16 @@ impl Component for EnvelopeView { if sub.process_event(event, context) { return true; } - } else if let Some(ref mut p) = self.pager { - if p.process_event(event, context) { - return true; - } + } else if self.pager.process_event(event, context) { + return true; } + let shortcuts = &self.shortcuts(context); + match *event { + UIEvent::Resize | UIEvent::VisibilityChange(true) => { + self.set_dirty(true); + } UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) if !self.cmd_buf.is_empty() => { self.cmd_buf.clear(); context @@ -378,53 +1410,197 @@ impl Component for EnvelopeView { self.cmd_buf.push(c); return true; } - UIEvent::Input(Key::Char('r')) - if self.mode == ViewMode::Normal || self.mode == ViewMode::Raw => - { - self.mode = if self.mode == ViewMode::Raw { + UIEvent::Input(ref key) + if matches!( + self.mode, ViewMode::Normal - } else { - ViewMode::Raw + | ViewMode::Subview + | ViewMode::Source(Source::Decoded) + | ViewMode::Source(Source::Raw) + ) && shortcut!( + key == shortcuts[Shortcuts::ENVELOPE_VIEW]["view_raw_source"] + ) => + { + self.mode = match self.mode { + ViewMode::Source(Source::Decoded) => ViewMode::Source(Source::Raw), + _ => ViewMode::Source(Source::Decoded), }; - self.dirty = true; + self.set_dirty(true); + self.initialised = false; return true; } - UIEvent::Input(Key::Char('r')) - if self.mode.is_attachment() || self.mode == ViewMode::Subview => + UIEvent::Input(ref key) + if matches!( + self.mode, + ViewMode::Attachment(_) + | ViewMode::Subview + | ViewMode::Url + | ViewMode::Source(Source::Decoded) + | ViewMode::Source(Source::Raw) + ) && shortcut!( + key == shortcuts[Shortcuts::ENVELOPE_VIEW]["return_to_normal_view"] + ) => { self.mode = ViewMode::Normal; - self.subview.take(); - self.dirty = true; + self.set_dirty(true); + self.initialised = false; return true; } - UIEvent::Input(Key::Char('a')) - if !self.cmd_buf.is_empty() && self.mode == ViewMode::Normal => + UIEvent::Input(ref key) + if (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) + && !self.cmd_buf.is_empty() + && shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["open_mailcap"]) => { let lidx = self.cmd_buf.parse::().unwrap(); self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + if let Some(attachment) = self.open_attachment(lidx, context) { + if let Ok(()) = crate::mailcap::MailcapEntry::execute(attachment, context) { + self.set_dirty(true); + } else { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(format!( + "no mailcap entry found for {}", + attachment.content_type() + )), + )); + } + } + return true; + } + UIEvent::Action(View(ViewAction::ExportMail(ref path))) => { + // Save entire message as eml + let mut path = std::path::Path::new(path).to_path_buf(); - if let Some(u) = self.mail.body().attachments().get(lidx) { - match u.content_type() { - ContentType::MessageRfc822 => { - self.mode = ViewMode::Subview; - let colors = crate::conf::value(context, "mail.view.body"); - self.subview = Some(Box::new(Pager::from_string( - String::from_utf8_lossy(&u.decode_rec(Default::default())) - .to_string(), - Some(context), + if path.is_dir() { + path.push(format!("{}.eml", self.mail.message_id_raw())); + } + match save_attachment(&path, &self.mail.bytes) { + Err(err) => { + context.replies.push_back(UIEvent::Notification( + Some(format!("Failed to create file at {}", path.display())), + err.to_string(), + Some(NotificationType::Error(melib::ErrorKind::External)), + )); + log::error!("Failed to create file at {}: {err}", path.display()); + return true; + } + Ok(()) => { + context.replies.push_back(UIEvent::Notification( + None, + format!("Saved at {}", &path.display()), + Some(NotificationType::Info), + )); + } + } + + return true; + } + UIEvent::Action(View(ViewAction::SaveAttachment(a_i, ref path))) => { + let mut path = std::path::Path::new(path).to_path_buf(); + + if let Some(u) = self.open_attachment(a_i, context) { + if path.is_dir() { + if let Some(filename) = u.filename() { + path.push(filename); + } else { + path.push(format!( + "meli_attachment_{a_i}_{}", + Uuid::new_v4().as_simple() + )); + } + } + match save_attachment(&path, &u.decode(Default::default())) { + Err(err) => { + context.replies.push_back(UIEvent::Notification( + Some(format!("Failed to create file at {}", path.display())), + err.to_string(), + Some(NotificationType::Error(melib::ErrorKind::External)), + )); + log::error!("Failed to create file at {}: {err}", path.display()); + } + Ok(()) => { + context.replies.push_back(UIEvent::Notification( None, + format!("Saved at {}", path.display()), + Some(NotificationType::Info), + )); + } + } + } else if a_i == 0 { + // Save entire message as eml + if path.is_dir() { + path.push(format!("{}.eml", self.mail.message_id_raw())); + } + match save_attachment(&path, &self.mail.bytes) { + Err(err) => { + context.replies.push_back(UIEvent::Notification( + Some(format!("Failed to create file at {}", path.display())), + err.to_string(), + Some(NotificationType::Error(melib::ErrorKind::External)), + )); + log::error!("Failed to create file at {}: {err}", path.display()); + return true; + } + Ok(()) => { + context.replies.push_back(UIEvent::Notification( None, - colors, - ))); + format!("Saved at {}", &path.display()), + Some(NotificationType::Info), + )); } + } + return true; + } else { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!( + "Attachment `{}` not found.", + a_i + )))); + } + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["open_attachment"]) + && !self.cmd_buf.is_empty() + && (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) => + { + let lidx = self.cmd_buf.parse::().unwrap(); + self.cmd_buf.clear(); + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + if let Some(attachment) = self.open_attachment(lidx, context) { + match attachment.content_type() { + ContentType::MessageRfc822 => { + match Mail::new(attachment.body().to_vec(), Some(Flag::SEEN)) { + Ok(wrapper) => { + context.replies.push_back(UIEvent::Action(Tab(New(Some( + Box::new(EnvelopeView::new( + wrapper, + None, + None, + Some(self.view_settings.clone()), + context.main_loop_handler.clone(), + )), + ))))); + } + Err(e) => { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(format!("{}", e)), + )); + } + } + } ContentType::Text { .. } | ContentType::PGPSignature | ContentType::CMSSignature => { self.mode = ViewMode::Attachment(lidx); + self.initialised = false; self.dirty = true; } ContentType::Multipart { .. } => { @@ -433,20 +1609,19 @@ impl Component for EnvelopeView { "Multipart attachments are not supported yet.".to_string(), ), )); - return true; } ContentType::Other { .. } => { - let attachment_type = u.mime_type(); - let filename = u.filename(); + let attachment_type = attachment.mime_type(); + let filename = attachment.filename(); if let Ok(command) = query_default_app(&attachment_type) { let p = create_temp_file( - &u.decode(Default::default()), + &attachment.decode(Default::default()), filename.as_deref(), None, None, true, ); - let exec_cmd = super::desktop_exec_to_command( + let exec_cmd = desktop_exec_to_command( &command, p.path.display().to_string(), false, @@ -487,43 +1662,52 @@ impl Component for EnvelopeView { }, ), )); - return true; } } - ContentType::OctetStream { .. } => { + ContentType::OctetStream { + ref name, + parameters: _, + } => { context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage( - "application/octet-stream isn't supported yet".to_string(), - ), + StatusEvent::DisplayMessage(format!( + "Failed to open {}. application/octet-stream isn't supported \ + yet", + name.as_ref().map(|n| n.as_str()).unwrap_or("file") + )), )); - return true; } } - } else { - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(format!( - "Attachment `{}` not found.", - lidx - )))); - return true; } return true; } - UIEvent::Input(Key::Char('g')) - if !self.cmd_buf.is_empty() && self.mode == ViewMode::Url => + UIEvent::Input(ref key) + if (self.mode == ViewMode::Normal || self.mode == ViewMode::Url) + && shortcut!( + key == shortcuts[Shortcuts::ENVELOPE_VIEW]["toggle_expand_headers"] + ) => + { + self.view_settings.expand_headers = !self.view_settings.expand_headers; + self.set_dirty(true); + return true; + } + UIEvent::Input(ref key) + if !self.cmd_buf.is_empty() + && self.mode == ViewMode::Url + && shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["go_to_url"]) => { let lidx = self.cmd_buf.parse::().unwrap(); self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - let url = { - let finder = LinkFinder::new(); - let t = self.mail.body().text(); - let links: Vec = finder.links(&t).collect(); - if let Some(u) = links.get(lidx) { - u.as_str().to_string() + let body_text = &self.body_text; + let links = &self.links; + let (_kind, url) = { + if let Some(l) = links + .get(lidx) + .and_then(|l| Some((l.kind, body_text.get(l.start..l.end)?))) + { + l } else { context.replies.push_back(UIEvent::StatusEvent( StatusEvent::DisplayMessage(format!("Link `{}` not found.", lidx)), @@ -532,7 +1716,7 @@ impl Component for EnvelopeView { } }; - let url_launcher = context.settings.pager.url_launcher.as_deref().unwrap_or( + let url_launcher = self.view_settings.url_launcher.as_deref().unwrap_or( #[cfg(target_os = "macos")] { "open" @@ -548,25 +1732,35 @@ impl Component for EnvelopeView { .stdout(Stdio::piped()) .spawn() { - Ok(child) => context.children.push(child), - Err(err) => context.replies.push_back(UIEvent::Notification( - Some(format!("Failed to launch {:?}", url_launcher)), - err.to_string(), - Some(NotificationType::Error(melib::ErrorKind::External)), - )), + Ok(child) => { + context.children.push(child); + } + Err(err) => { + context.replies.push_back(UIEvent::Notification( + Some(format!("Failed to launch {:?}", url_launcher)), + err.to_string(), + Some(NotificationType::Error(melib::ErrorKind::External)), + )); + } } return true; } - UIEvent::Input(Key::Char('u')) => { + UIEvent::Input(ref key) + if (self.mode == ViewMode::Normal || self.mode == ViewMode::Url) + && shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["toggle_url_mode"]) => + { match self.mode { ViewMode::Normal => self.mode = ViewMode::Url, ViewMode::Url => self.mode = ViewMode::Normal, _ => {} } + self.initialised = false; self.dirty = true; return true; } - UIEvent::Input(Key::Char('d')) => { + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["change_charset"]) => + { let entries = vec![ (None, "default".to_string()), (Some(Charset::Ascii), Charset::Ascii.to_string()), @@ -612,20 +1806,151 @@ impl Component for EnvelopeView { self.dirty = true; return true; } + UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)) + if self.active_jobs.contains(job_id) => + { + let mut caught = false; + for d in self.display.iter_mut() { + match d { + AttachmentDisplay::SignedPending { + ref mut inner, + handle, + display, + job_id: our_job_id, + } if *our_job_id == *job_id => { + caught = true; + match handle.chan.try_recv() { + Err(_) => { /* Job was canceled */ } + Ok(None) => { /* something happened, + * perhaps a worker thread + * panicked */ + } + Ok(Some(Ok(()))) => { + *d = AttachmentDisplay::SignedVerified { + inner: std::mem::replace( + inner, + Box::new(AttachmentBuilder::new(&[]).build()), + ), + display: std::mem::take(display), + description: String::new(), + }; + } + Ok(Some(Err(error))) => { + *d = AttachmentDisplay::SignedFailed { + inner: std::mem::replace( + inner, + Box::new(AttachmentBuilder::new(&[]).build()), + ), + display: std::mem::take(display), + error, + }; + } + } + } + AttachmentDisplay::EncryptedPending { + ref mut inner, + handle, + } if handle.job_id == *job_id => { + caught = true; + match handle.chan.try_recv() { + Err(_) => { /* Job was canceled */ } + Ok(None) => { /* something happened, + * perhaps a worker thread + * panicked */ + } + Ok(Some(Ok((metadata, decrypted_bytes)))) => { + let plaintext = + Box::new(AttachmentBuilder::new(&decrypted_bytes).build()); + let mut plaintext_display = vec![]; + Self::attachment_to_display_helper( + &plaintext, + &self.main_loop_handler, + &mut self.active_jobs, + &mut plaintext_display, + &self.view_settings, + (&self.force_charset).into(), + ); + *d = AttachmentDisplay::EncryptedSuccess { + inner: std::mem::replace( + inner, + Box::new(AttachmentBuilder::new(&[]).build()), + ), + plaintext, + plaintext_display, + description: format!("{:?}", metadata), + }; + } + Ok(Some(Err(error))) => { + *d = AttachmentDisplay::EncryptedFailed { + inner: std::mem::replace( + inner, + Box::new(AttachmentBuilder::new(&[]).build()), + ), + error, + }; + } + } + } + _ => {} + } + } + if caught { + self.links.clear(); + self.parse_attachments(); + } + + self.active_jobs.remove(job_id); + self.set_dirty(true); + } _ => {} } false } + fn shortcuts(&self, context: &Context) -> ShortcutMaps { + let mut map = if let Some(ref sbv) = self.subview { + sbv.shortcuts(context) + } else { + self.pager.shortcuts(context) + }; + + let mut our_map = self.view_settings.env_view_shortcuts.clone(); + + if !(self.mode.is_attachment() + || self.mode == ViewMode::Subview + || self.mode == ViewMode::Source(Source::Decoded) + || self.mode == ViewMode::Source(Source::Raw) + || self.mode == ViewMode::Url) + { + our_map.remove("return_to_normal_view"); + } + if self.mode != ViewMode::Url { + our_map.remove("go_to_url"); + } + if !(self.mode == ViewMode::Normal || self.mode == ViewMode::Url) { + our_map.remove("toggle_url_mode"); + } + map.insert(Shortcuts::ENVELOPE_VIEW, our_map); + + map + } + fn is_dirty(&self) -> bool { self.dirty - || self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false) + || self.pager.is_dirty() || self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false) || matches!(self.force_charset, ForceCharset::Dialog(ref s) if s.is_dirty()) } fn set_dirty(&mut self, value: bool) { self.dirty = value; + self.pager.set_dirty(value); + if let Some(ref mut s) = self.subview { + s.set_dirty(value); + } + if let ForceCharset::Dialog(ref mut s) = self.force_charset { + s.set_dirty(value); + } } fn id(&self) -> ComponentId { diff --git a/src/components/mail/view/html.rs b/src/components/mail/view/html.rs index 15fd3750..6b2e9487 100644 --- a/src/components/mail/view/html.rs +++ b/src/components/mail/view/html.rs @@ -24,6 +24,8 @@ use std::{ process::{Command, Stdio}, }; +use melib::xdg_utils::query_default_app; + use super::*; #[derive(Debug)] diff --git a/src/components/mail/view/state.rs b/src/components/mail/view/state.rs new file mode 100644 index 00000000..dd01e978 --- /dev/null +++ b/src/components/mail/view/state.rs @@ -0,0 +1,180 @@ +/* + * meli + * + * Copyright 2017 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use melib::{Envelope, Error, Mail, Result}; + +use super::{EnvelopeView, MailView, ViewSettings}; +use crate::{jobs::JoinHandle, mailbox_settings, Component, Context, ShortcutMaps, UIEvent}; + +#[derive(Debug, Copy, Clone)] +pub enum PendingReplyAction { + Reply, + ReplyToAuthor, + ReplyToAll, + ForwardAttachment, + ForwardInline, +} + +#[derive(Debug)] +pub enum MailViewState { + Init { + pending_action: Option, + }, + LoadingBody { + handle: JoinHandle>>, + pending_action: Option, + }, + Error { + err: Error, + }, + Loaded { + bytes: Vec, + env: Box, + env_view: Box, + stack: Vec>, + }, +} + +impl MailViewState { + pub fn load_bytes(self_: &mut MailView, bytes: Vec, context: &mut Context) { + let Some(coordinates) = self_.coordinates else { return; }; + let account = &mut context.accounts[&coordinates.0]; + if account + .collection + .get_env(coordinates.2) + .other_headers() + .is_empty() + { + let _ = account + .collection + .get_env_mut(coordinates.2) + .populate_headers(&bytes); + } + let env = Box::new(account.collection.get_env(coordinates.2).clone()); + let env_view = Box::new(EnvelopeView::new( + Mail { + envelope: *env.clone(), + bytes: bytes.clone(), + }, + None, + None, + Some(ViewSettings { + theme_default: crate::conf::value(context, "theme_default"), + body_theme: crate::conf::value(context, "mail.view.body"), + env_view_shortcuts: mailbox_settings!( + context[coordinates.0][&coordinates.1] + .shortcuts + .envelope_view + ) + .key_values(), + pager_filter: mailbox_settings!( + context[coordinates.0][&coordinates.1].pager.filter + ) + .clone(), + html_filter: mailbox_settings!( + context[coordinates.0][&coordinates.1].pager.html_filter + ) + .clone(), + url_launcher: mailbox_settings!( + context[coordinates.0][&coordinates.1].pager.url_launcher + ) + .clone(), + auto_choose_multipart_alternative: mailbox_settings!( + context[coordinates.0][&coordinates.1] + .pager + .auto_choose_multipart_alternative + ) + .is_true(), + expand_headers: false, + sticky_headers: *mailbox_settings!( + context[coordinates.0][&coordinates.1].pager.sticky_headers + ), + show_date_in_my_timezone: mailbox_settings!( + context[coordinates.0][&coordinates.1] + .pager + .show_date_in_my_timezone + ) + .is_true(), + show_extra_headers: mailbox_settings!( + context[coordinates.0][&coordinates.1] + .pager + .show_extra_headers + ) + .clone(), + auto_verify_signatures: *mailbox_settings!( + context[coordinates.0][&coordinates.1] + .pgp + .auto_verify_signatures + ), + auto_decrypt: *mailbox_settings!( + context[coordinates.0][&coordinates.1].pgp.auto_decrypt + ), + }), + context.main_loop_handler.clone(), + )); + self_.state = MailViewState::Loaded { + env, + bytes, + env_view, + stack: vec![], + }; + } + + pub fn is_dirty(&self) -> bool { + matches!(self, Self::Loaded { ref env_view, .. } if env_view.is_dirty()) + } + + pub fn set_dirty(&mut self, dirty: bool) { + if let Self::Loaded { + ref mut env_view, .. + } = self + { + env_view.set_dirty(dirty); + } + } + + pub fn shortcuts(&self, context: &Context) -> ShortcutMaps { + if let Self::Loaded { ref env_view, .. } = self { + env_view.shortcuts(context) + } else { + ShortcutMaps::default() + } + } + + pub fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { + if let Self::Loaded { + ref mut env_view, .. + } = self + { + env_view.process_event(event, context) + } else { + false + } + } +} + +impl Default for MailViewState { + fn default() -> Self { + MailViewState::Init { + pending_action: None, + } + } +} diff --git a/src/components/mail/view/thread.rs b/src/components/mail/view/thread.rs index 2d51b6f0..ef1d4e55 100644 --- a/src/components/mail/view/thread.rs +++ b/src/components/mail/view/thread.rs @@ -37,6 +37,15 @@ struct ThreadEntry { hidden: bool, heading: String, timestamp: UnixTimestamp, + mailview: Box, +} + +#[derive(Debug, Default, Copy, Clone)] +pub enum ThreadViewFocus { + #[default] + None, + Thread, + MailView, } #[derive(Debug, Default, Clone)] @@ -46,11 +55,9 @@ pub struct ThreadView { expanded_pos: usize, new_expanded_pos: usize, reversed: bool, - coordinates: (AccountHash, MailboxHash, usize), + coordinates: (AccountHash, MailboxHash, EnvelopeHash), thread_group: ThreadHash, - mailview: MailView, - show_mailview: bool, - show_thread: bool, + focus: ThreadViewFocus, entries: Vec, visible_entries: Vec>, indentation_colors: [ThemeAttribute; 6], @@ -64,24 +71,24 @@ pub struct ThreadView { impl ThreadView { /* - * coordinates: (account index, mailbox_hash, root set thread_node index) - * expanded_hash: optional position of expanded entry when we render the - * threadview. Default expanded message is the last one. - * context: current context + * @coordinates: (account index, mailbox_hash, root set thread_node index) + * @expanded_hash: optional position of expanded entry when we render the + * ThreadView. + * default: expanded message is the last one. + * @context: current context */ pub fn new( - coordinates: (AccountHash, MailboxHash, usize), + coordinates: (AccountHash, MailboxHash, EnvelopeHash), thread_group: ThreadHash, - expanded_hash: Option, - context: &Context, + expanded_hash: Option, + focus: Option, + context: &mut Context, ) -> Self { let mut view = ThreadView { reversed: false, coordinates, thread_group, - mailview: MailView::default(), - show_mailview: true, - show_thread: true, + focus: focus.unwrap_or_default(), entries: Vec::new(), cursor_pos: 1, new_cursor_pos: 0, @@ -103,7 +110,7 @@ impl ThreadView { view } - pub fn update(&mut self, context: &Context) { + pub fn update(&mut self, context: &mut Context) { if self.entries.is_empty() { return; } @@ -122,7 +129,7 @@ impl ThreadView { None }; - let expanded_hash = old_expanded_entry.as_ref().map(|e| e.index.1); + let expanded_hash = old_expanded_entry.as_ref().map(|e| e.msg_hash); self.initiate(expanded_hash, context); let mut old_cursor = 0; @@ -165,18 +172,25 @@ impl ThreadView { self.set_dirty(true); } - fn initiate(&mut self, expanded_hash: Option, context: &Context) { + fn initiate(&mut self, expanded_hash: Option, context: &mut Context) { #[inline(always)] fn make_entry( i: (usize, ThreadNodeHash, usize), + account_hash: AccountHash, + mailbox_hash: MailboxHash, msg_hash: EnvelopeHash, seen: bool, timestamp: UnixTimestamp, + context: &mut Context, ) -> ThreadEntry { let (ind, _, _) = i; ThreadEntry { index: i, indentation: ind, + mailview: Box::new(MailView::new( + Some((account_hash, mailbox_hash, msg_hash)), + context, + )), msg_hash, seen, dirty: true, @@ -186,36 +200,43 @@ impl ThreadView { } } - let account = &context.accounts[&self.coordinates.0]; - let threads = account.collection.get_threads(self.coordinates.1); + let collection = context.accounts[&self.coordinates.0].collection.clone(); + let threads = collection.get_threads(self.coordinates.1); if !threads.groups.contains_key(&self.thread_group) { return; } + let (account_hash, mailbox_hash, _) = self.coordinates; let thread_iter = threads.thread_group_iter(self.thread_group); self.entries.clear(); for (line, (ind, thread_node_hash)) in thread_iter.enumerate() { let entry = if let Some(msg_hash) = threads.thread_nodes()[&thread_node_hash].message() { - let env_ref = account.collection.get_env(msg_hash); + let (is_seen, timestamp) = { + let env_ref = collection.get_env(msg_hash); + (env_ref.is_seen(), env_ref.timestamp) + }; make_entry( (ind, thread_node_hash, line), + account_hash, + mailbox_hash, msg_hash, - env_ref.is_seen(), - env_ref.timestamp, + is_seen, + timestamp, + context, ) } else { continue; }; - self.entries.push(entry); match expanded_hash { - Some(expanded_hash) if expanded_hash == thread_node_hash => { + Some(expanded_hash) if expanded_hash == entry.msg_hash => { self.new_expanded_pos = self.entries.len().saturating_sub(1); self.expanded_pos = self.new_expanded_pos + 1; } _ => {} } + self.entries.push(entry); } if expanded_hash.is_none() { self.new_expanded_pos = self @@ -712,18 +733,21 @@ impl ThreadView { .set_bg(theme_default.bg); } - match (self.show_mailview, self.show_thread) { - (true, true) => { + match self.focus { + ThreadViewFocus::None => { self.draw_list( grid, (set_y(upper_left, y), set_x(bottom_right, mid - 1)), context, ); let upper_left = (mid + 1, get_y(upper_left) + y - 1); - self.mailview - .draw(grid, (upper_left, bottom_right), context); + self.entries[self.new_expanded_pos].mailview.draw( + grid, + (upper_left, bottom_right), + context, + ); } - (false, true) => { + ThreadViewFocus::Thread => { clear_area( grid, ((mid + 1, get_y(upper_left) + y - 1), bottom_right), @@ -731,8 +755,10 @@ impl ThreadView { ); self.draw_list(grid, (set_y(upper_left, y), bottom_right), context); } - (_, false) => { - self.mailview.draw(grid, area, context); + ThreadViewFocus::MailView => { + self.entries[self.new_expanded_pos] + .mailview + .draw(grid, area, context); } } } @@ -820,8 +846,8 @@ impl ThreadView { ); let (width, height) = self.content.size(); - match (self.show_mailview, self.show_thread) { - (true, true) => { + match self.focus { + ThreadViewFocus::None => { let area = (set_y(upper_left, y), set_y(bottom_right, mid)); let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); @@ -841,7 +867,7 @@ impl ThreadView { ); context.dirty_areas.push_back(area); } - (false, true) => { + ThreadViewFocus::Thread => { let area = (set_y(upper_left, y), bottom_right); let upper_left = upper_left!(area); @@ -859,11 +885,11 @@ impl ThreadView { ); context.dirty_areas.push_back(area); } - (_, false) => { /* show only envelope */ } + ThreadViewFocus::MailView => { /* show only envelope */ } } - match (self.show_mailview, self.show_thread) { - (true, true) => { + match self.focus { + ThreadViewFocus::None => { let area = (set_y(upper_left, mid), set_y(bottom_right, mid)); context.dirty_areas.push_back(area); for x in get_x(upper_left)..=get_x(bottom_right) { @@ -874,15 +900,20 @@ impl ThreadView { } let area = (set_y(upper_left, y), set_y(bottom_right, mid - 1)); self.draw_list(grid, area, context); - self.mailview - .draw(grid, (set_y(upper_left, mid + 1), bottom_right), context); + self.entries[self.new_expanded_pos].mailview.draw( + grid, + (set_y(upper_left, mid + 1), bottom_right), + context, + ); } - (false, true) => { + ThreadViewFocus::Thread => { self.dirty = true; self.draw_list(grid, (set_y(upper_left, y), bottom_right), context); } - (_, false) => { - self.mailview.draw(grid, area, context); + ThreadViewFocus::MailView => { + self.entries[self.new_expanded_pos] + .mailview + .draw(grid, area, context); } } } @@ -971,16 +1002,12 @@ impl Component for ThreadView { /* If user has selected another mail to view, change to it */ if self.new_expanded_pos != self.expanded_pos { self.expanded_pos = self.new_expanded_pos; - let coordinates = ( - self.coordinates.0, - self.coordinates.1, - self.entries[self.current_pos()].msg_hash, - ); - self.mailview.update(coordinates, context); } if self.entries.len() == 1 { - self.mailview.draw(grid, area, context); + self.entries[self.new_expanded_pos] + .mailview + .draw(grid, area, context); } else if total_cols >= self.content.size().0 + 74 { self.draw_vert(grid, area, context); } else { @@ -998,7 +1025,13 @@ impl Component for ThreadView { return true; } - if self.show_mailview && self.mailview.process_event(event, context) { + if matches!( + self.focus, + ThreadViewFocus::None | ThreadViewFocus::MailView + ) && self.entries[self.new_expanded_pos] + .mailview + .process_event(event, context) + { return true; } @@ -1035,34 +1068,45 @@ impl Component for ThreadView { self.movement = Some(PageMovement::PageDown(1)); self.dirty = true; } - UIEvent::Input(ref key) if *key == Key::Home => { + UIEvent::Input(ref k) if shortcut!(k == shortcuts[Shortcuts::GENERAL]["home_page"]) => { self.movement = Some(PageMovement::Home); self.dirty = true; } - UIEvent::Input(ref key) if *key == Key::End => { + UIEvent::Input(ref k) if shortcut!(k == shortcuts[Shortcuts::GENERAL]["end_page"]) => { self.movement = Some(PageMovement::End); self.dirty = true; } - UIEvent::Input(Key::Char('\n')) => { + UIEvent::Input(ref k) + if shortcut!(k == shortcuts[Shortcuts::GENERAL]["open_entry"]) => + { if self.entries.len() < 2 { return true; } self.new_expanded_pos = self.current_pos(); - self.show_mailview = true; + self.expanded_pos = self.current_pos(); + if matches!(self.focus, ThreadViewFocus::Thread) { + self.focus = ThreadViewFocus::None; + } self.set_dirty(true); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["toggle_mailview"]) => { - self.show_mailview = !self.show_mailview; + self.focus = match self.focus { + ThreadViewFocus::None | ThreadViewFocus::MailView => ThreadViewFocus::Thread, + ThreadViewFocus::Thread => ThreadViewFocus::None, + }; self.set_dirty(true); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["toggle_threadview"]) => { - self.show_thread = !self.show_thread; + self.focus = match self.focus { + ThreadViewFocus::None | ThreadViewFocus::Thread => ThreadViewFocus::MailView, + ThreadViewFocus::MailView => ThreadViewFocus::None, + }; self.set_dirty(true); return true; } @@ -1070,7 +1114,7 @@ impl Component for ThreadView { if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["reverse_thread_order"]) => { self.reversed = !self.reversed; - let expanded_hash = self.entries[self.expanded_pos].index.1; + let expanded_hash = self.entries[self.expanded_pos].msg_hash; self.initiate(Some(expanded_hash), context); self.dirty = true; return true; @@ -1107,7 +1151,7 @@ impl Component for ThreadView { self.dirty = true; return true; } - UIEvent::Resize => { + UIEvent::Resize | UIEvent::VisibilityChange(true) => { self.set_dirty(true); } UIEvent::EnvelopeRename(ref old_hash, ref new_hash) => { @@ -1116,31 +1160,35 @@ impl Component for ThreadView { if e.msg_hash == *old_hash { e.msg_hash = *new_hash; let seen: bool = account.collection.get_env(*new_hash).is_seen(); - if seen != e.seen { - self.dirty = true; - } e.seen = seen; + e.mailview.process_event( + &mut UIEvent::EnvelopeRename(*old_hash, *new_hash), + context, + ); + self.set_dirty(true); + break; } } - self.mailview - .process_event(&mut UIEvent::EnvelopeRename(*old_hash, *new_hash), context); } UIEvent::EnvelopeUpdate(ref env_hash) => { let account = &context.accounts[&self.coordinates.0]; for e in self.entries.iter_mut() { if e.msg_hash == *env_hash { let seen: bool = account.collection.get_env(*env_hash).is_seen(); - if seen != e.seen { - self.dirty = true; - } e.seen = seen; + e.mailview + .process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context); + self.set_dirty(true); + break; } } - self.mailview - .process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context); } _ => { - if self.mailview.process_event(event, context) { + if self + .entries + .iter_mut() + .any(|entry| entry.mailview.process_event(event, context)) + { return true; } } @@ -1149,20 +1197,41 @@ impl Component for ThreadView { } fn is_dirty(&self) -> bool { - self.dirty || (self.show_mailview && self.mailview.is_dirty()) + self.dirty + || (!matches!(self.focus, ThreadViewFocus::Thread) + && !self.entries.is_empty() + && self.entries[self.new_expanded_pos].mailview.is_dirty()) } fn set_dirty(&mut self, value: bool) { self.dirty = value; - self.mailview.set_dirty(value); + self.entries[self.new_expanded_pos] + .mailview + .set_dirty(value); } fn shortcuts(&self, context: &Context) -> ShortcutMaps { - let mut map = self.mailview.shortcuts(context); + let mut map = self.entries[self.new_expanded_pos] + .mailview + .shortcuts(context); + map.insert( + Shortcuts::GENERAL, + mailbox_settings!( + context[self.coordinates.0][&self.coordinates.1] + .shortcuts + .general + ) + .key_values(), + ); map.insert( Shortcuts::THREAD_VIEW, - context.settings.shortcuts.thread_view.key_values(), + mailbox_settings!( + context[self.coordinates.0][&self.coordinates.1] + .shortcuts + .thread_view + ) + .key_values(), ); map diff --git a/src/components/mail/view/types.rs b/src/components/mail/view/types.rs new file mode 100644 index 00000000..190bc92b --- /dev/null +++ b/src/components/mail/view/types.rs @@ -0,0 +1,183 @@ +/* + * meli + * + * Copyright 2017 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use melib::{attachment_types::Charset, pgp::DecryptionMetadata, Attachment, Error, Result}; + +use crate::{ + conf::shortcuts::EnvelopeViewShortcuts, + jobs::{JobId, JoinHandle}, + ShortcutMap, ThemeAttribute, UIDialog, +}; + +#[derive(Debug, Clone)] +pub struct ViewSettings { + pub pager_filter: Option, + pub html_filter: Option, + pub url_launcher: Option, + pub expand_headers: bool, + pub theme_default: ThemeAttribute, + pub env_view_shortcuts: ShortcutMap, + /// `"mail.view.body"` + pub body_theme: ThemeAttribute, + pub auto_choose_multipart_alternative: bool, + pub sticky_headers: bool, + pub show_date_in_my_timezone: bool, + pub show_extra_headers: Vec, + pub auto_verify_signatures: bool, + pub auto_decrypt: bool, +} + +impl Default for ViewSettings { + fn default() -> Self { + Self { + theme_default: Default::default(), + body_theme: Default::default(), + pager_filter: None, + html_filter: None, + url_launcher: None, + env_view_shortcuts: EnvelopeViewShortcuts::default().key_values(), + auto_choose_multipart_alternative: true, + expand_headers: false, + sticky_headers: false, + show_date_in_my_timezone: false, + show_extra_headers: vec![], + auto_verify_signatures: true, + auto_decrypt: true, + } + } +} + +#[derive(Eq, PartialEq, Copy, Clone, Debug)] +pub enum LinkKind { + Url, + Email, +} + +#[derive(Eq, PartialEq, Copy, Clone, Debug)] +pub struct Link { + pub start: usize, + pub end: usize, + pub kind: self::LinkKind, +} + +#[derive(Debug, Default)] +pub enum ForceCharset { + #[default] + None, + Dialog(Box>>), + Forced(Charset), +} + +impl Into> for &ForceCharset { + fn into(self) -> Option { + match self { + ForceCharset::Forced(val) => Some(*val), + ForceCharset::None | ForceCharset::Dialog(_) => None, + } + } +} + +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub enum Source { + Decoded, + Raw, +} + +#[derive(PartialEq, Debug, Default)] +pub enum ViewMode { + #[default] + Normal, + Url, + Attachment(usize), + Source(Source), + Subview, +} + +macro_rules! is_variant { + ($n:ident, $($var:tt)+) => { + #[inline] + pub fn $n(&self) -> bool { + matches!(self, Self::$($var)*) + } + }; +} + +impl ViewMode { + is_variant! { is_normal, Normal } + is_variant! { is_url, Url } + is_variant! { is_attachment, Attachment(_) } + is_variant! { is_source, Source(_) } + is_variant! { is_subview, Subview } +} + +#[derive(Debug)] +pub enum AttachmentDisplay { + Alternative { + inner: Box, + shown_display: usize, + display: Vec, + }, + InlineText { + inner: Box, + comment: Option, + text: String, + }, + InlineOther { + inner: Box, + }, + Attachment { + inner: Box, + }, + SignedPending { + inner: Box, + display: Vec, + handle: JoinHandle>, + job_id: JobId, + }, + SignedFailed { + inner: Box, + display: Vec, + error: Error, + }, + SignedUnverified { + inner: Box, + display: Vec, + }, + SignedVerified { + inner: Box, + display: Vec, + description: String, + }, + EncryptedPending { + inner: Box, + handle: JoinHandle)>>, + }, + EncryptedFailed { + inner: Box, + error: Error, + }, + EncryptedSuccess { + inner: Box, + plaintext: Box, + plaintext_display: Vec, + description: String, + }, +} diff --git a/src/components/mail/view/utils.rs b/src/components/mail/view/utils.rs new file mode 100644 index 00000000..d00ddce2 --- /dev/null +++ b/src/components/mail/view/utils.rs @@ -0,0 +1,103 @@ +/* + * meli + * + * Copyright 2017-2018 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use std::{fs::File, io::Write, os::unix::fs::PermissionsExt, path::Path}; + +use melib::Result; + +pub fn save_attachment(path: &Path, bytes: &[u8]) -> Result<()> { + let mut f = File::create(path)?; + let mut permissions = f.metadata()?.permissions(); + permissions.set_mode(0o600); // Read/write for owner only. + f.set_permissions(permissions)?; + f.write_all(bytes)?; + f.flush()?; + Ok(()) +} + +pub fn desktop_exec_to_command(command: &str, path: String, is_url: bool) -> String { + /* Purge unused field codes */ + let command = command + .replace("%i", "") + .replace("%c", "") + .replace("%k", ""); + if command.contains("%f") { + command.replacen("%f", &path.replace(' ', "\\ "), 1) + } else if command.contains("%F") { + command.replacen("%F", &path.replace(' ', "\\ "), 1) + } else if command.contains("%u") || command.contains("%U") { + let from_pattern = if command.contains("%u") { "%u" } else { "%U" }; + if is_url { + command.replacen(from_pattern, &path, 1) + } else { + command.replacen( + from_pattern, + &format!("file://{}", path).replace(' ', "\\ "), + 1, + ) + } + } else if is_url { + format!("{} {}", command, path) + } else { + format!("{} {}", command, path.replace(' ', "\\ ")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_desktop_exec() { + assert_eq!( + "ristretto /tmp/file".to_string(), + desktop_exec_to_command("ristretto %F", "/tmp/file".to_string(), false) + ); + assert_eq!( + "/usr/lib/firefox-esr/firefox-esr file:///tmp/file".to_string(), + desktop_exec_to_command( + "/usr/lib/firefox-esr/firefox-esr %u", + "/tmp/file".to_string(), + false + ) + ); + assert_eq!( + "/usr/lib/firefox-esr/firefox-esr www.example.com".to_string(), + desktop_exec_to_command( + "/usr/lib/firefox-esr/firefox-esr %u", + "www.example.com".to_string(), + true + ) + ); + assert_eq!( + "/usr/bin/vlc --started-from-file www.example.com".to_string(), + desktop_exec_to_command( + "/usr/bin/vlc --started-from-file %U", + "www.example.com".to_string(), + true + ) + ); + assert_eq!( + "zathura --fork file:///tmp/file".to_string(), + desktop_exec_to_command("zathura --fork %U", "file:///tmp/file".to_string(), true) + ); + } +} diff --git a/src/components/notifications.rs b/src/components/notifications.rs index 49198d49..688f0049 100644 --- a/src/components/notifications.rs +++ b/src/components/notifications.rs @@ -50,7 +50,11 @@ mod dbus { impl DbusNotifications { pub fn new(context: &Context) -> Self { DbusNotifications { - rate_limit: RateLimit::new(1000, 1000, context.job_executor.clone()), + rate_limit: RateLimit::new( + 1000, + 1000, + context.main_loop_handler.job_executor.clone(), + ), id: ComponentId::default(), } } diff --git a/src/components/utilities.rs b/src/components/utilities.rs index 650c2a06..1b84f20b 100644 --- a/src/components/utilities.rs +++ b/src/components/utilities.rs @@ -795,6 +795,57 @@ impl Component for StatusBar { fn can_quit_cleanly(&mut self, context: &Context) -> bool { self.container.can_quit_cleanly(context) } + + fn attributes(&self) -> &'static ComponentAttr { + &ComponentAttr::CONTAINER + } + + fn children(&self) -> IndexMap { + let mut ret = IndexMap::default(); + ret.insert(self.container.id(), &self.container as _); + ret.insert(self.ex_buffer.id(), &self.ex_buffer as _); + ret.insert(self.progress_spinner.id(), &self.progress_spinner as _); + ret + } + + fn children_mut(&mut self) -> IndexMap { + IndexMap::default() + } + + fn realize(&self, parent: Option, context: &mut Context) { + context.realized.insert(self.id(), parent); + log::debug!( + "Realizing statusbar id {} w/ parent {:?}", + self.id(), + &parent + ); + log::debug!( + "Realizing statusbar container id {} w/ parent {:?}", + self.container.id(), + &self.id + ); + self.container.realize(self.id().into(), context); + log::debug!( + "Realizing progress_spinner container id {} w/ parent {:?}", + self.progress_spinner.id(), + &self.id + ); + log::debug!( + "Realizing ex_buffer container id {} w/ parent {:?}", + self.ex_buffer.id(), + &self.id + ); + self.progress_spinner.realize(self.id().into(), context); + self.ex_buffer.realize(self.id().into(), context); + } + + fn unrealize(&self, context: &mut Context) { + log::debug!("Unrealizing id {}", self.id()); + context.unrealized.insert(self.id()); + self.container.unrealize(context); + self.progress_spinner.unrealize(context); + self.ex_buffer.unrealize(context); + } } #[derive(Debug)] @@ -837,6 +888,7 @@ impl Tabbed { .extend(ret.shortcuts(context).into_iter()); ret } + fn draw_tabs(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); @@ -911,7 +963,8 @@ impl Tabbed { context.dirty_areas.push_back(area); } - pub fn add_component(&mut self, new: Box) { + pub fn add_component(&mut self, new: Box, context: &mut Context) { + new.realize(self.id().into(), context); self.children.push(new); } } @@ -1386,7 +1439,7 @@ impl Component for Tabbed { return true; } UIEvent::Action(Tab(New(ref mut e))) if e.is_some() => { - self.add_component(e.take().unwrap()); + self.add_component(e.take().unwrap(), context); self.children[self.cursor_pos] .process_event(&mut UIEvent::VisibilityChange(false), context); self.cursor_pos = self.children.len() - 1; @@ -1415,6 +1468,7 @@ impl Component for Tabbed { if let Some(c_idx) = self.children.iter().position(|x| x.id() == *id) { self.children[c_idx] .process_event(&mut UIEvent::VisibilityChange(false), context); + self.children[c_idx].unrealize(context); self.children.remove(c_idx); self.cursor_pos = 0; self.set_dirty(true); @@ -1555,6 +1609,41 @@ impl Component for Tabbed { } true } + + fn attributes(&self) -> &'static ComponentAttr { + &ComponentAttr::CONTAINER + } + + fn children(&self) -> IndexMap { + let mut ret = IndexMap::default(); + for c in &self.children { + ret.insert(c.id(), c as _); + } + ret + } + + fn children_mut(&mut self) -> IndexMap { + IndexMap::default() + } + + fn realize(&self, parent: Option, context: &mut Context) { + log::debug!("Realizing Tabbed id {} w/ parent {:?}", self.id(), &parent); + context.realized.insert(self.id(), parent); + for c in &self.children { + log::debug!("Realizing child id {} w/ parent {:?}", c.id(), &self.id); + c.realize(self.id().into(), context); + } + } + + fn unrealize(&self, context: &mut Context) { + log::debug!("Unrealizing id {}", self.id()); + context + .replies + .push_back(UIEvent::ComponentUnrealize(self.id())); + for c in &self.children { + c.unrealize(context); + } + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/src/components/utilities/dialogs.rs b/src/components/utilities/dialogs.rs index f3368e12..04e9bb79 100644 --- a/src/components/utilities/dialogs.rs +++ b/src/components/utilities/dialogs.rs @@ -124,7 +124,7 @@ impl Component for UIDialo self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); - context.replies.push_back(UIEvent::ComponentKill(self.id)); + self.unrealize(context); } return true; } @@ -160,7 +160,7 @@ impl Component for UIDialo self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); - context.replies.push_back(UIEvent::ComponentKill(self.id)); + self.unrealize(context); } return true; } @@ -171,7 +171,7 @@ impl Component for UIDialo self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); - context.replies.push_back(UIEvent::ComponentKill(self.id)); + self.unrealize(context); } return true; } @@ -182,7 +182,7 @@ impl Component for UIDialo self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); - context.replies.push_back(UIEvent::ComponentKill(self.id)); + self.unrealize(context); } return true; } @@ -450,7 +450,7 @@ impl Component for UIConfirmationDialog { self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); - context.replies.push_back(UIEvent::ComponentKill(self.id)); + self.unrealize(context); } return true; } @@ -486,7 +486,7 @@ impl Component for UIConfirmationDialog { self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); - context.replies.push_back(UIEvent::ComponentKill(self.id)); + self.unrealize(context); } return true; } @@ -497,7 +497,7 @@ impl Component for UIConfirmationDialog { self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); - context.replies.push_back(UIEvent::ComponentKill(self.id)); + self.unrealize(context); } return true; } @@ -508,7 +508,7 @@ impl Component for UIConfirmationDialog { self.done = true; if let Some(event) = self.done() { context.replies.push_back(event); - context.replies.push_back(UIEvent::ComponentKill(self.id)); + self.unrealize(context); } return true; } diff --git a/src/components/utilities/pager.rs b/src/components/utilities/pager.rs index c3720735..a86c29f0 100644 --- a/src/components/utilities/pager.rs +++ b/src/components/utilities/pager.rs @@ -41,6 +41,12 @@ pub struct Pager { colors: ThemeAttribute, initialised: bool, show_scrollbar: bool, + /// At the last draw, were the visible columns plus horizontal cursor less than total width? + /// Used to decide whether to accept `scroll_right` key events. + cols_lt_width: bool, + /// At the last draw, were the visible rows plus vertical cursor less than total height? + /// Used to decide whether to accept `scroll_down` key events. + rows_lt_height: bool, filtered_content: Option<(String, Result)>, text_lines: Vec, line_breaker: LineBreakText, @@ -525,9 +531,6 @@ impl Component for Pager { clear_area(grid, area, crate::conf::value(context, "theme_default")); let (mut cols, mut rows) = (width!(area), height!(area)); - if cols < 2 || rows < 2 { - return; - } let (has_more_lines, (width, height)) = if self.filtered_content.is_some() { (false, (self.width, self.height)) } else { @@ -536,6 +539,14 @@ impl Component for Pager { (self.line_breaker.width().unwrap_or(cols), self.height), ) }; + + self.cols_lt_width = cols + self.cursor.0 < width; + self.rows_lt_height = rows + self.cursor.1 < height; + + if cols < 2 || rows < 2 { + return; + } + if self.show_scrollbar && rows < height { cols -= 1; rows -= 1; @@ -655,56 +666,60 @@ impl Component for Pager { self.set_dirty(true); } UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_up"]) => + if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_up"]) + && self.cursor.1 > 0 => { self.movement = Some(PageMovement::Up(1)); self.dirty = true; return true; } UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_down"]) => + if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_down"]) + && self.rows_lt_height => { self.movement = Some(PageMovement::Down(1)); self.dirty = true; return true; } UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) => + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) + && self.cursor.0 > 0 => { - self.movement = Some(PageMovement::Home); + self.movement = Some(PageMovement::Left(1)); self.dirty = true; return true; } UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) => + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) + && dbg!(self.cols_lt_width) => { - self.movement = Some(PageMovement::End); + self.movement = Some(PageMovement::Right(1)); self.dirty = true; return true; } - UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) => - { - self.movement = Some(PageMovement::Left(1)); + UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::PAGER]["page_up"]) => { + self.movement = Some(PageMovement::PageUp(1)); self.dirty = true; return true; } UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) => + if shortcut!(key == shortcuts[Shortcuts::PAGER]["page_down"]) => { - self.movement = Some(PageMovement::Right(1)); + self.movement = Some(PageMovement::PageDown(1)); self.dirty = true; return true; } - UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::PAGER]["page_up"]) => { - self.movement = Some(PageMovement::PageUp(1)); + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) => + { + self.movement = Some(PageMovement::Home); self.dirty = true; return true; } UIEvent::Input(ref key) - if shortcut!(key == shortcuts[Shortcuts::PAGER]["page_down"]) => + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) => { - self.movement = Some(PageMovement::PageDown(1)); + self.movement = Some(PageMovement::End); self.dirty = true; return true; } @@ -802,7 +817,10 @@ impl Component for Pager { } UIEvent::Resize => { self.initialised = false; - self.dirty = true; + self.set_dirty(true); + } + UIEvent::VisibilityChange(true) => { + self.set_dirty(true); } UIEvent::VisibilityChange(false) => { context diff --git a/src/components/utilities/widgets.rs b/src/components/utilities/widgets.rs index f32a6aec..0fc27752 100644 --- a/src/components/utilities/widgets.rs +++ b/src/components/utilities/widgets.rs @@ -1122,6 +1122,7 @@ impl ProgressSpinner { .unwrap_or(0); let interval = Self::KINDS[kind].0; let timer = context + .main_loop_handler .job_executor .clone() .create_timer(interval, interval); diff --git a/src/conf/accounts.rs b/src/conf/accounts.rs index 5f6433f8..741c8bb8 100644 --- a/src/conf/accounts.rs +++ b/src/conf/accounts.rs @@ -37,7 +37,6 @@ use std::{ sync::{Arc, RwLock}, }; -use crossbeam::channel::Sender; use futures::{ future::FutureExt, stream::{Stream, StreamExt}, @@ -57,9 +56,9 @@ use smallvec::SmallVec; use super::{AccountConf, FileMailboxConf}; use crate::{ command::actions::AccountAction, - jobs::{JobExecutor, JobId, JoinHandle}, + jobs::{JobId, JoinHandle}, types::UIEvent::{self, EnvelopeRemove, EnvelopeRename, EnvelopeUpdate, Notification}, - StatusEvent, ThreadEvent, + MainLoopHandler, StatusEvent, ThreadEvent, }; #[macro_export] @@ -177,10 +176,9 @@ pub struct Account { pub settings: AccountConf, pub backend: Arc>>, - pub job_executor: Arc, + pub main_loop_handler: MainLoopHandler, pub active_jobs: HashMap, pub active_job_instants: BTreeMap, - pub sender: Sender, pub event_queue: VecDeque<(MailboxHash, RefreshEvent)>, pub backend_capabilities: MailBackendCapabilities, } @@ -434,8 +432,7 @@ impl Account { name: String, mut settings: AccountConf, map: &Backends, - job_executor: Arc, - sender: Sender, + main_loop_handler: MainLoopHandler, event_consumer: BackendEventConsumer, ) -> Result { let s = settings.clone(); @@ -490,18 +487,20 @@ impl Account { if let Ok(mailboxes_job) = backend.mailboxes() { if let Ok(online_job) = backend.is_online() { let handle = if backend.capabilities().is_async { - job_executor.spawn_specialized(online_job.then(|_| mailboxes_job)) + main_loop_handler + .job_executor + .spawn_specialized(online_job.then(|_| mailboxes_job)) } else { - job_executor.spawn_blocking(online_job.then(|_| mailboxes_job)) + main_loop_handler + .job_executor + .spawn_blocking(online_job.then(|_| mailboxes_job)) }; let job_id = handle.job_id; active_jobs.insert(job_id, JobRequest::Mailboxes { handle }); active_job_instants.insert(std::time::Instant::now(), job_id); - sender - .send(ThreadEvent::UIEvent(UIEvent::StatusEvent( - StatusEvent::NewJob(job_id), - ))) - .unwrap(); + main_loop_handler.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( + StatusEvent::NewJob(job_id), + ))); } } @@ -509,15 +508,13 @@ impl Account { if settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 { let db_path = match crate::sqlite3::db_path() { Err(err) => { - sender - .send(ThreadEvent::UIEvent(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(format!( - "Error with setting up an sqlite3 search database for account \ + main_loop_handler.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(format!( + "Error with setting up an sqlite3 search database for account \ `{}`: {}", - name, err - )), - ))) - .unwrap(); + name, err + )), + ))); None } Ok(path) => Some(path), @@ -529,11 +526,9 @@ impl Account { one will be created.", name ); - sender - .send(ThreadEvent::UIEvent(UIEvent::Action( - (name.clone(), AccountAction::ReIndex).into(), - ))) - .unwrap(); + main_loop_handler.send(ThreadEvent::UIEvent(UIEvent::Action( + (name.clone(), AccountAction::ReIndex).into(), + ))); } } } @@ -553,8 +548,7 @@ impl Account { sent_mailbox: Default::default(), collection: backend.collection(), settings, - sender, - job_executor, + main_loop_handler, active_jobs, active_job_instants, event_queue: VecDeque::with_capacity(8), @@ -643,15 +637,14 @@ impl Account { &self.name, missing_mailbox, ); - self.sender + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::StatusEvent( StatusEvent::DisplayMessage(format!( "Account `{}` mailbox `{}` configured but not present in account's \ mailboxes. Is it misspelled?", &self.name, missing_mailbox, )), - ))) - .unwrap(); + ))); } if !mailbox_conf_hash_set.is_empty() { let mut mailbox_comma_sep_list_string = mailbox_entries @@ -671,14 +664,13 @@ impl Account { &self.name, mailbox_comma_sep_list_string, ); - self.sender + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::StatusEvent( StatusEvent::DisplayMessage(format!( "Account `{}` has the following mailboxes: [{}]", &self.name, mailbox_comma_sep_list_string, )), - ))) - .unwrap(); + ))); } let mut tree: Vec = Vec::new(); @@ -697,16 +689,19 @@ impl Account { if let Ok(mailbox_job) = self.backend.write().unwrap().fetch(*h) { let mailbox_job = mailbox_job.into_future(); let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(mailbox_job) + self.main_loop_handler + .job_executor + .spawn_specialized(mailbox_job) } else { - self.job_executor.spawn_blocking(mailbox_job) + self.main_loop_handler + .job_executor + .spawn_blocking(mailbox_job) }; let job_id = handle.job_id; - self.sender + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::StatusEvent( StatusEvent::NewJob(job_id), - ))) - .unwrap(); + ))); self.active_jobs.insert( job_id, JobRequest::Fetch { @@ -770,7 +765,8 @@ impl Account { ); } Ok(job) => { - let handle = self.job_executor.spawn_blocking(job); + let handle = + self.main_loop_handler.job_executor.spawn_blocking(job); self.insert_job( handle.job_id, JobRequest::Generic { @@ -816,7 +812,8 @@ impl Account { ) }) { Ok(job) => { - let handle = self.job_executor.spawn_blocking(job); + let handle = + self.main_loop_handler.job_executor.spawn_blocking(job); self.insert_job( handle.job_id, JobRequest::Generic { @@ -868,7 +865,8 @@ impl Account { ); } Ok(job) => { - let handle = self.job_executor.spawn_blocking(job); + let handle = + self.main_loop_handler.job_executor.spawn_blocking(job); self.insert_job( handle.job_id, JobRequest::Generic { @@ -908,11 +906,13 @@ impl Account { }; #[cfg(feature = "sqlite3")] if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 { - let handle = self.job_executor.spawn_blocking(crate::sqlite3::insert( - (*envelope).clone(), - self.backend.clone(), - self.name.clone(), - )); + let handle = self.main_loop_handler.job_executor.spawn_blocking( + crate::sqlite3::insert( + (*envelope).clone(), + self.backend.clone(), + self.name.clone(), + ), + ); self.insert_job( handle.job_id, JobRequest::Generic { @@ -1027,8 +1027,7 @@ impl Account { Some(format!("{} watcher exited with error", &self.name)), e.to_string(), Some(crate::types::NotificationType::Error(err.kind)), - ))) - .expect("Could not send event on main channel"); + ))); */ self.watch(); return Some(Notification( @@ -1057,24 +1056,26 @@ impl Account { .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::piped()) .spawn()?; - self.sender + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::StatusEvent( StatusEvent::DisplayMessage(format!("Running command {}", refresh_command)), - ))) - .unwrap(); - self.sender + ))); + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::Fork( crate::ForkType::Generic(child), - ))) - .unwrap(); + ))); return Ok(()); } let refresh_job = self.backend.write().unwrap().refresh(mailbox_hash); if let Ok(refresh_job) = refresh_job { let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(refresh_job) + self.main_loop_handler + .job_executor + .spawn_specialized(refresh_job) } else { - self.job_executor.spawn_blocking(refresh_job) + self.main_loop_handler + .job_executor + .spawn_blocking(refresh_job) }; self.insert_job( handle.job_id, @@ -1096,9 +1097,9 @@ impl Account { match self.backend.read().unwrap().watch() { Ok(fut) => { let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(fut) + self.main_loop_handler.job_executor.spawn_specialized(fut) } else { - self.job_executor.spawn_blocking(fut) + self.main_loop_handler.job_executor.spawn_blocking(fut) }; self.active_jobs .insert(handle.job_id, JobRequest::Watch { handle }); @@ -1107,14 +1108,13 @@ impl Account { if e.kind == ErrorKind::NotSupported || e.kind == ErrorKind::NotImplemented => { } Err(e) => { - self.sender + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::StatusEvent( StatusEvent::DisplayMessage(format!( "Account `{}` watch action returned error: {}", &self.name, e )), - ))) - .unwrap(); + ))); } } } @@ -1174,9 +1174,13 @@ impl Account { Ok(mailbox_job) => { let mailbox_job = mailbox_job.into_future(); let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(mailbox_job) + self.main_loop_handler + .job_executor + .spawn_specialized(mailbox_job) } else { - self.job_executor.spawn_blocking(mailbox_job) + self.main_loop_handler + .job_executor + .spawn_blocking(mailbox_job) }; self.insert_job( handle.job_id, @@ -1192,9 +1196,8 @@ impl Account { .and_modify(|entry| { entry.status = MailboxStatus::Failed(err); }); - self.sender - .send(ThreadEvent::UIEvent(UIEvent::StartupCheck(mailbox_hash))) - .unwrap(); + self.main_loop_handler + .send(ThreadEvent::UIEvent(UIEvent::StartupCheck(mailbox_hash))); } } } @@ -1266,9 +1269,9 @@ impl Account { .save(bytes.to_vec(), mailbox_hash, flags)?; let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(job) + self.main_loop_handler.job_executor.spawn_specialized(job) } else { - self.job_executor.spawn_blocking(job) + self.main_loop_handler.job_executor.spawn_blocking(job) }; self.insert_job( handle.job_id, @@ -1335,11 +1338,14 @@ impl Account { } #[cfg(feature = "smtp")] SendMail::Smtp(conf) => { - let handle = self.job_executor.spawn_specialized(async move { - let mut smtp_connection = - melib::smtp::SmtpConnection::new_connection(conf).await?; - smtp_connection.mail_transaction(&message, None).await - }); + let handle = self + .main_loop_handler + .job_executor + .spawn_specialized(async move { + let mut smtp_connection = + melib::smtp::SmtpConnection::new_connection(conf).await?; + smtp_connection.mail_transaction(&message, None).await + }); if complete_in_background { self.insert_job(handle.job_id, JobRequest::SendMessageBackground { handle }); return Ok(None); @@ -1357,9 +1363,9 @@ impl Account { .submit(message.into_bytes(), None, None)?; let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(job) + self.main_loop_handler.job_executor.spawn_specialized(job) } else { - self.job_executor.spawn_blocking(job) + self.main_loop_handler.job_executor.spawn_blocking(job) }; self.insert_job(handle.job_id, JobRequest::SendMessageBackground { handle }); return Ok(None); @@ -1478,9 +1484,9 @@ impl Account { .unwrap() .create_mailbox(path.to_string())?; let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(job) + self.main_loop_handler.job_executor.spawn_specialized(job) } else { - self.job_executor.spawn_blocking(job) + self.main_loop_handler.job_executor.spawn_blocking(job) }; self.insert_job(handle.job_id, JobRequest::CreateMailbox { path, handle }); Ok(()) @@ -1493,9 +1499,9 @@ impl Account { let mailbox_hash = self.mailbox_by_path(&path)?; let job = self.backend.write().unwrap().delete_mailbox(mailbox_hash)?; let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(job) + self.main_loop_handler.job_executor.spawn_specialized(job) } else { - self.job_executor.spawn_blocking(job) + self.main_loop_handler.job_executor.spawn_blocking(job) }; self.insert_job( handle.job_id, @@ -1514,9 +1520,9 @@ impl Account { .unwrap() .set_mailbox_subscription(mailbox_hash, true)?; let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(job) + self.main_loop_handler.job_executor.spawn_specialized(job) } else { - self.job_executor.spawn_blocking(job) + self.main_loop_handler.job_executor.spawn_blocking(job) }; self.insert_job( handle.job_id, @@ -1536,9 +1542,9 @@ impl Account { .unwrap() .set_mailbox_subscription(mailbox_hash, false)?; let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(job) + self.main_loop_handler.job_executor.spawn_specialized(job) } else { - self.job_executor.spawn_blocking(job) + self.main_loop_handler.job_executor.spawn_blocking(job) }; self.insert_job( handle.job_id, @@ -1587,9 +1593,13 @@ impl Account { let online_job = self.backend.read().unwrap().is_online(); if let Ok(online_job) = online_job { let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(online_job) + self.main_loop_handler + .job_executor + .spawn_specialized(online_job) } else { - self.job_executor.spawn_blocking(online_job) + self.main_loop_handler + .job_executor + .spawn_blocking(online_job) }; self.insert_job(handle.job_id, JobRequest::IsOnline { handle }); } @@ -1643,11 +1653,10 @@ impl Account { } pub fn process_event(&mut self, job_id: &JobId) -> bool { - self.sender + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::StatusEvent( StatusEvent::JobFinished(*job_id), - ))) - .unwrap(); + ))); if let Some(mut job) = self.active_jobs.remove(job_id) { match job { @@ -1655,32 +1664,36 @@ impl Account { if let Ok(Some(mailboxes)) = handle.chan.try_recv() { if let Err(err) = mailboxes.and_then(|mailboxes| self.init(mailboxes)) { if err.kind.is_authentication() { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::Notification( + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::Notification( Some(format!("{}: authentication error", &self.name)), err.to_string(), Some(crate::types::NotificationType::Error(err.kind)), - ))) - .expect("Could not send event on main channel"); + ), + )); self.is_online = Err(err); return true; } let mailboxes_job = self.backend.read().unwrap().mailboxes(); if let Ok(mailboxes_job) = mailboxes_job { let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(mailboxes_job) + self.main_loop_handler + .job_executor + .spawn_specialized(mailboxes_job) } else { - self.job_executor.spawn_blocking(mailboxes_job) + self.main_loop_handler + .job_executor + .spawn_blocking(mailboxes_job) }; self.insert_job(handle.job_id, JobRequest::Mailboxes { handle }); }; } else { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange( + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::AccountStatusChange( self.hash, Some("Loaded mailboxes.".into()), - ))) - .unwrap(); + ), + )); } } } @@ -1705,40 +1718,38 @@ impl Account { .and_modify(|entry| { entry.status = MailboxStatus::Available; }); - self.sender - .send(ThreadEvent::UIEvent(UIEvent::MailboxUpdate(( - self.hash, - mailbox_hash, - )))) - .unwrap(); + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::MailboxUpdate((self.hash, mailbox_hash)), + )); return true; } Ok(Some((Some(Err(err)), _))) => { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::Notification( + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::Notification( Some(format!("{}: could not fetch mailbox", &self.name)), err.to_string(), Some(crate::types::NotificationType::Error(err.kind)), - ))) - .expect("Could not send event on main channel"); + ), + )); self.mailbox_entries .entry(mailbox_hash) .and_modify(|entry| { entry.status = MailboxStatus::Failed(err); }); - self.sender - .send(ThreadEvent::UIEvent(UIEvent::MailboxUpdate(( - self.hash, - mailbox_hash, - )))) - .unwrap(); + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::MailboxUpdate((self.hash, mailbox_hash)), + )); return true; } Ok(Some((Some(Ok(payload)), rest))) => { let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(rest.into_future()) + self.main_loop_handler + .job_executor + .spawn_specialized(rest.into_future()) } else { - self.job_executor.spawn_blocking(rest.into_future()) + self.main_loop_handler + .job_executor + .spawn_blocking(rest.into_future()) }; self.insert_job( handle.job_id, @@ -1756,29 +1767,22 @@ impl Account { .merge(envelopes, mailbox_hash, self.sent_mailbox) { for f in updated_mailboxes { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::MailboxUpdate(( - self.hash, f, - )))) - .unwrap(); + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::MailboxUpdate((self.hash, f)), + )); } } - self.sender - .send(ThreadEvent::UIEvent(UIEvent::MailboxUpdate(( - self.hash, - mailbox_hash, - )))) - .unwrap(); + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::MailboxUpdate((self.hash, mailbox_hash)), + )); } } } JobRequest::IsOnline { ref mut handle, .. } => { if let Ok(Some(is_online)) = handle.chan.try_recv() { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange( - self.hash, None, - ))) - .unwrap(); + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::AccountStatusChange(self.hash, None), + )); if is_online.is_ok() { if self.is_online.is_err() && !self @@ -1798,9 +1802,13 @@ impl Account { let online_job = self.backend.read().unwrap().is_online(); if let Ok(online_job) = online_job { let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(online_job) + self.main_loop_handler + .job_executor + .spawn_specialized(online_job) } else { - self.job_executor.spawn_blocking(online_job) + self.main_loop_handler + .job_executor + .spawn_blocking(online_job) }; self.insert_job(handle.job_id, JobRequest::IsOnline { handle }); }; @@ -1829,11 +1837,9 @@ impl Account { .is_authentication()) { self.is_online = Ok(()); - self.sender - .send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange( - self.hash, None, - ))) - .unwrap(); + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::AccountStatusChange(self.hash, None), + )); } } Ok(Some(Err(err))) => { @@ -1841,31 +1847,32 @@ impl Account { let online_job = self.backend.read().unwrap().is_online(); if let Ok(online_job) = online_job { let handle = if self.backend_capabilities.is_async { - self.job_executor.spawn_specialized(online_job) + self.main_loop_handler + .job_executor + .spawn_specialized(online_job) } else { - self.job_executor.spawn_blocking(online_job) + self.main_loop_handler + .job_executor + .spawn_blocking(online_job) }; self.insert_job(handle.job_id, JobRequest::IsOnline { handle }); }; } self.is_online = Err(err); - self.sender - .send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange( - self.hash, None, - ))) - .unwrap(); + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::AccountStatusChange(self.hash, None), + )); } } } JobRequest::SetFlags { ref mut handle, .. } => { if let Ok(Some(Err(err))) = handle.chan.try_recv() { - self.sender + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::Notification( Some(format!("{}: could not set flag", &self.name)), err.to_string(), Some(crate::types::NotificationType::Error(err.kind)), - ))) - .expect("Could not send event on main channel"); + ))); } } JobRequest::SaveMessage { @@ -1882,7 +1889,7 @@ impl Account { "Message was stored in {} so that you can restore it manually.", file.path.display() ); - self.sender + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::Notification( Some(format!("{}: could not save message", &self.name)), format!( @@ -1890,31 +1897,28 @@ impl Account { file.path.display() ), Some(crate::types::NotificationType::Info), - ))) - .expect("Could not send event on main channel"); + ))); } } JobRequest::SendMessage => {} JobRequest::SendMessageBackground { ref mut handle, .. } => { if let Ok(Some(Err(err))) = handle.chan.try_recv() { - self.sender + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::Notification( Some("Could not send message".to_string()), err.to_string(), Some(crate::types::NotificationType::Error(err.kind)), - ))) - .expect("Could not send event on main channel"); + ))); } } JobRequest::DeleteMessages { ref mut handle, .. } => { if let Ok(Some(Err(err))) = handle.chan.try_recv() { - self.sender + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::Notification( Some(format!("{}: could not delete message", &self.name)), err.to_string(), Some(crate::types::NotificationType::Error(err.kind)), - ))) - .expect("Could not send event on main channel"); + ))); } } JobRequest::CreateMailbox { @@ -1925,24 +1929,21 @@ impl Account { if let Ok(Some(r)) = handle.chan.try_recv() { match r { Err(err) => { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::Notification( + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::Notification( Some(format!( "{}: could not create mailbox {}", &self.name, path )), err.to_string(), Some(crate::types::NotificationType::Error(err.kind)), - ))) - .expect("Could not send event on main channel"); + ), + )); } Ok((mailbox_hash, mut mailboxes)) => { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::MailboxCreate(( - self.hash, - mailbox_hash, - )))) - .unwrap(); + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::MailboxCreate((self.hash, mailbox_hash)), + )); let mut new = FileMailboxConf::default(); new.mailbox_conf.subscribe = super::ToggleFlag::InternalVal(true); new.mailbox_conf.usage = if mailboxes[&mailbox_hash].special_usage() @@ -2013,21 +2014,18 @@ impl Account { Err(_) => { /* canceled */ } Ok(None) => {} Ok(Some(Err(err))) => { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::Notification( + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::Notification( Some(format!("{}: could not delete mailbox", &self.name)), err.to_string(), Some(crate::types::NotificationType::Error(err.kind)), - ))) - .expect("Could not send event on main channel"); + ), + )); } Ok(Some(Ok(mut mailboxes))) => { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::MailboxDelete(( - self.hash, - mailbox_hash, - )))) - .unwrap(); + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::MailboxDelete((self.hash, mailbox_hash)), + )); if let Some(pos) = self.mailboxes_order.iter().position(|&h| h == mailbox_hash) { @@ -2069,13 +2067,13 @@ impl Account { ); // FIXME remove from settings as well - self.sender - .send(ThreadEvent::UIEvent(UIEvent::Notification( + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::Notification( Some(format!("{}: mailbox deleted successfully", &self.name)), String::new(), Some(crate::types::NotificationType::Info), - ))) - .expect("Could not send event on main channel"); + ), + )); } } } @@ -2085,28 +2083,28 @@ impl Account { Err(_) => { /* canceled */ } Ok(None) => {} Ok(Some(Err(err))) => { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::Notification( + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::Notification( Some(format!( "{}: could not set mailbox permissions", &self.name )), err.to_string(), Some(crate::types::NotificationType::Error(err.kind)), - ))) - .expect("Could not send event on main channel"); + ), + )); } Ok(Some(Ok(_))) => { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::Notification( + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::Notification( Some(format!( "{}: mailbox permissions set successfully", &self.name )), String::new(), Some(crate::types::NotificationType::Info), - ))) - .expect("Could not send event on main channel"); + ), + )); } } } @@ -2119,16 +2117,16 @@ impl Account { Err(_) => { /* canceled */ } Ok(None) => {} Ok(Some(Err(err))) => { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::Notification( + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::Notification( Some(format!( "{}: could not set mailbox subscription", &self.name )), err.to_string(), Some(crate::types::NotificationType::Error(err.kind)), - ))) - .expect("Could not send event on main channel"); + ), + )); } Ok(Some(Ok(()))) if self.mailbox_entries.contains_key(mailbox_hash) => { self.mailbox_entries.entry(*mailbox_hash).and_modify(|m| { @@ -2139,8 +2137,8 @@ impl Account { }; let _ = m.ref_mailbox.set_is_subscribed(*new_value); }); - self.sender - .send(ThreadEvent::UIEvent(UIEvent::Notification( + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::Notification( Some(format!( "{}: `{}` has been {}subscribed.", &self.name, @@ -2149,8 +2147,8 @@ impl Account { )), String::new(), Some(crate::types::NotificationType::Info), - ))) - .expect("Could not send event on main channel"); + ), + )); } Ok(Some(Ok(()))) => {} } @@ -2162,13 +2160,13 @@ impl Account { self.watch(); } else { //TODO: relaunch watch job with ratelimit for failure - self.sender - .send(ThreadEvent::UIEvent(UIEvent::Notification( + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::Notification( Some(format!("{}: watch thread failed", &self.name)), err.to_string(), Some(crate::types::NotificationType::Error(err.kind)), - ))) - .expect("Could not send event on main channel"); + ), + )); } } } @@ -2180,34 +2178,33 @@ impl Account { } => { match handle.chan.try_recv() { Ok(Some(Err(err))) => { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::Notification( + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::Notification( Some(format!("{}: {} failed", &self.name, name,)), err.to_string(), Some(crate::types::NotificationType::Error(err.kind)), - ))) - .expect("Could not send event on main channel"); + ), + )); } Ok(Some(Ok(()))) if on_finish.is_none() => { if log_level <= LogLevel::INFO { - self.sender - .send(ThreadEvent::UIEvent(UIEvent::Notification( + self.main_loop_handler.send(ThreadEvent::UIEvent( + UIEvent::Notification( Some(format!("{}: {} succeeded", &self.name, name,)), String::new(), Some(crate::types::NotificationType::Info), - ))) - .expect("Could not send event on main channel"); + ), + )); } } Err(_) => { /* canceled */ } Ok(Some(Ok(()))) | Ok(None) => {} } if on_finish.is_some() { - self.sender + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::Callback( on_finish.take().unwrap(), - ))) - .unwrap(); + ))); } } } @@ -2221,20 +2218,18 @@ impl Account { self.active_jobs.insert(job_id, job); self.active_job_instants .insert(std::time::Instant::now(), job_id); - self.sender + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::StatusEvent( StatusEvent::NewJob(job_id), - ))) - .unwrap(); + ))); } pub fn cancel_job(&mut self, job_id: JobId) -> Option { if let Some(req) = self.active_jobs.remove(&job_id) { - self.sender + self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::StatusEvent( StatusEvent::JobCanceled(job_id), - ))) - .unwrap(); + ))); Some(req) } else { None diff --git a/src/conf/overrides.rs b/src/conf/overrides.rs index b9c9359d..315f4878 100644 --- a/src/conf/overrides.rs +++ b/src/conf/overrides.rs @@ -27,7 +27,7 @@ use super::*; use melib::HeaderName; -# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "headers-sticky")] # [serde (default)] pub headers_sticky : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > , # [doc = " Extra headers to display, if present, in the default header preamble."] # [doc = " Default: []"] # [serde (alias = "show-extra-headers")] # [serde (default)] pub show_extra_headers : Option < Vec < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { PagerSettingsOverride { pager_context : None , pager_stop : None , headers_sticky : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None , show_extra_headers : None } } } +# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "sticky-headers" , alias = "headers-sticky" , alias = "headers_sticky")] # [serde (default)] pub sticky_headers : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > , # [doc = " Extra headers to display, if present, in the default header preamble."] # [doc = " Default: []"] # [serde (alias = "show-extra-headers")] # [serde (default)] pub show_extra_headers : Option < Vec < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { PagerSettingsOverride { pager_context : None , pager_stop : None , sticky_headers : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None , show_extra_headers : None } } } # [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ListingSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "context-lines")] # [serde (default)] pub context_lines : Option < usize > , # [doc = "Show auto-hiding scrollbar in accounts sidebar menu."] # [doc = "Default: True"] # [serde (default)] pub show_menu_scrollbar : Option < bool > , # [doc = " Datetime formatting passed verbatim to strftime(3)."] # [doc = " Default: %Y-%m-%d %T"] # [serde (alias = "datetime-fmt")] # [serde (default)] pub datetime_fmt : Option < Option < String > > , # [doc = " Show recent dates as `X {minutes,hours,days} ago`, up to 7 days."] # [doc = " Default: true"] # [serde (alias = "recent-dates")] # [serde (default)] pub recent_dates : Option < bool > , # [doc = " Show only envelopes that match this query"] # [doc = " Default: None"] # [serde (default)] pub filter : Option < Option < Query > > , # [serde (alias = "index-style")] # [serde (default)] pub index_style : Option < IndexStyle > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling_leaf : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling_leaf : Option < Option < String > > , # [doc = "Default: ' '"] # [serde (default)] pub sidebar_divider : Option < char > , # [doc = "Default: 90"] # [serde (default)] pub sidebar_ratio : Option < usize > , # [doc = " Flag to show if thread entry contains unseen mail."] # [doc = " Default: \"●\""] # [serde (default)] pub unseen_flag : Option < Option < String > > , # [doc = " Flag to show if thread has been snoozed."] # [doc = " Default: \"💤\""] # [serde (default)] pub thread_snoozed_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry has been selected."] # [doc = " Default: \"☑\u{fe0f}\""] # [serde (default)] pub selected_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry contains attachments."] # [doc = " Default: \"📎\""] # [serde (default)] pub attachment_flag : Option < Option < String > > , # [doc = " Should threads with differentiating Subjects show a list of those"] # [doc = " subjects on the entry title?"] # [doc = " Default: \"true\""] # [serde (default)] pub thread_subject_pack : Option < bool > } impl Default for ListingSettingsOverride { fn default () -> Self { ListingSettingsOverride { context_lines : None , show_menu_scrollbar : None , datetime_fmt : None , recent_dates : None , filter : None , index_style : None , sidebar_mailbox_tree_has_sibling : None , sidebar_mailbox_tree_no_sibling : None , sidebar_mailbox_tree_has_sibling_leaf : None , sidebar_mailbox_tree_no_sibling_leaf : None , sidebar_divider : None , sidebar_ratio : None , unseen_flag : None , thread_snoozed_flag : None , selected_flag : None , attachment_flag : None , thread_subject_pack : None } } } diff --git a/src/conf/pager.rs b/src/conf/pager.rs index 86e90a4e..b6d7484a 100644 --- a/src/conf/pager.rs +++ b/src/conf/pager.rs @@ -41,8 +41,14 @@ pub struct PagerSettings { /// Always show headers when scrolling. /// Default: true - #[serde(default = "true_val", alias = "headers-sticky")] - pub headers_sticky: bool, + #[serde( + default = "true_val", + alias = "sticky-headers", + /* deprecated names */ + alias = "headers-sticky", + alias = "headers_sticky" + )] + pub sticky_headers: bool, /// The height of the pager in mail view, in percent. /// Default: 80 @@ -117,7 +123,7 @@ impl Default for PagerSettings { Self { pager_context: 0, pager_stop: false, - headers_sticky: true, + sticky_headers: true, pager_ratio: 80, filter: None, html_filter: None, @@ -141,7 +147,7 @@ impl DotAddressable for PagerSettings { match *field { "pager_context" => self.pager_context.lookup(field, tail), "pager_stop" => self.pager_stop.lookup(field, tail), - "headers_sticky" => self.headers_sticky.lookup(field, tail), + "sticky_headers" => self.sticky_headers.lookup(field, tail), "pager_ratio" => self.pager_ratio.lookup(field, tail), "filter" => self.filter.lookup(field, tail), "html_filter" => self.html_filter.lookup(field, tail), diff --git a/src/conf/themes.rs b/src/conf/themes.rs index b21870b8..e1822ff1 100644 --- a/src/conf/themes.rs +++ b/src/conf/themes.rs @@ -251,7 +251,6 @@ const DEFAULT_KEYS: &[&str] = &[ "text.error", "text.highlight", "error_message", - "email_header", "highlight", "status.bar", "status.command_bar", @@ -1316,8 +1315,6 @@ impl Default for Themes { add!("text.highlight", dark = { fg: Color::Blue, bg: "theme_default", attrs: Attr::REVERSE }, light = { fg: Color::Blue, bg: "theme_default", attrs: Attr::REVERSE }); /* rest */ - add!("email_header", dark = { fg: Color::Byte(33), bg: Color::Default, attrs: Attr::DEFAULT }, light = { fg: Color::Byte(33), bg: Color::Default, attrs: Attr::DEFAULT }); - add!("highlight", dark = { fg: Color::Byte(240), bg: Color::Byte(237), attrs: Attr::BOLD }, light = { fg: Color::Byte(240), bg: Color::Byte(237), attrs: Attr::BOLD }); add!("status.bar", dark = { fg: Color::Byte(123), bg: Color::Byte(26) }, light = { fg: Color::Byte(123), bg: Color::Byte(26) }); diff --git a/src/main.rs b/src/main.rs index df9830af..e4a09945 100644 --- a/src/main.rs +++ b/src/main.rs @@ -265,11 +265,13 @@ fn run_app(opt: Opt) -> Result<()> { sender, receiver.clone(), )?; + let main_loop_handler = state.context.main_loop_handler.clone(); state.register_component(Box::new(EnvelopeView::new( wrapper, None, None, - AccountHash::default(), + None, + main_loop_handler, ))); } else { state = State::new(None, sender, receiver.clone())?; diff --git a/src/state.rs b/src/state.rs index f405065a..c49f6f96 100644 --- a/src/state.rs +++ b/src/state.rs @@ -34,10 +34,10 @@ //! for user input, observe folders for file changes etc. The relevant struct is //! [`ThreadEvent`]. -use std::{env, os::unix::io::RawFd, sync::Arc, thread}; +use std::{collections::BTreeSet, env, os::unix::io::RawFd, sync::Arc, thread}; use crossbeam::channel::{unbounded, Receiver, Sender}; -use indexmap::IndexMap; +use indexmap::{IndexMap, IndexSet}; use melib::{ backends::{AccountHash, BackendEvent, BackendEventConsumer, Backends, RefreshEvent}, UnixTimestamp, @@ -102,6 +102,21 @@ impl InputHandler { } } +#[derive(Debug, Clone)] +pub struct MainLoopHandler { + pub sender: Sender, + pub job_executor: Arc, +} + +impl MainLoopHandler { + #[inline] + pub fn send(&self, event: ThreadEvent) { + if let Err(err) = self.sender.send(event) { + log::error!("Could not send event to main loop: {}", err); + } + } +} + /// A context container for loaded settings, accounts, UI changes, etc. pub struct Context { pub accounts: IndexMap, @@ -112,10 +127,11 @@ pub struct Context { /// Events queue that components send back to the state pub replies: VecDeque, - pub sender: Sender, + pub realized: IndexMap>, + pub unrealized: IndexSet, + pub main_loop_handler: MainLoopHandler, receiver: Receiver, input_thread: InputHandler, - pub job_executor: Arc, pub children: Vec, pub temp_files: Vec, @@ -196,8 +212,10 @@ impl Context { name, account_conf, &backends, - job_executor.clone(), - sender.clone(), + MainLoopHandler { + job_executor: job_executor.clone(), + sender: sender.clone(), + }, BackendEventConsumer::new(Arc::new( move |account_hash: AccountHash, ev: BackendEvent| { sender @@ -219,8 +237,9 @@ impl Context { settings, dirty_areas: VecDeque::with_capacity(0), replies: VecDeque::with_capacity(0), + realized: IndexMap::default(), + unrealized: IndexSet::default(), temp_files: Vec::new(), - job_executor, children: vec![], input_thread: InputHandler { @@ -230,7 +249,10 @@ impl Context { control, state_tx: sender.clone(), }, - sender, + main_loop_handler: MainLoopHandler { + job_executor, + sender, + }, receiver, } } @@ -244,8 +266,9 @@ pub struct State { draw_rate_limit: RateLimit, child: Option, pub mode: UIMode, - overlay: Vec>, - components: Vec>, + overlay: IndexMap>, + components: IndexMap>, + component_tree: IndexMap, pub context: Box, timer: thread::JoinHandle<()>, @@ -337,8 +360,10 @@ impl State { n.to_string(), a_s.clone(), &backends, - job_executor.clone(), - sender.clone(), + MainLoopHandler { + job_executor: job_executor.clone(), + sender: sender.clone(), + }, BackendEventConsumer::new(Arc::new( move |account_hash: AccountHash, ev: BackendEvent| { sender @@ -388,8 +413,9 @@ impl State { }), child: None, mode: UIMode::Normal, - components: Vec::with_capacity(8), - overlay: Vec::new(), + components: IndexMap::default(), + overlay: IndexMap::default(), + component_tree: IndexMap::default(), timer, draw_rate_limit: RateLimit::new(1, 3, job_executor.clone()), display_messages: SmallVec::new(), @@ -404,8 +430,9 @@ impl State { settings, dirty_areas: VecDeque::with_capacity(5), replies: VecDeque::with_capacity(5), + realized: IndexMap::default(), + unrealized: IndexSet::default(), temp_files: Vec::new(), - job_executor, children: vec![], input_thread: InputHandler { @@ -415,7 +442,10 @@ impl State { control, state_tx: sender.clone(), }, - sender, + main_loop_handler: MainLoopHandler { + job_executor, + sender, + }, receiver, }), }; @@ -480,7 +510,7 @@ impl State { } pub fn sender(&self) -> Sender { - self.context.sender.clone() + self.context.main_loop_handler.sender.clone() } pub fn restore_input(&mut self) { @@ -759,7 +789,7 @@ impl State { ), ); copy_area(&mut self.screen.overlay_grid, &self.screen.grid, area, area); - self.overlay.get_mut(0).unwrap().draw( + self.overlay.get_index_mut(0).unwrap().1.draw( &mut self.screen.overlay_grid, area, &mut self.context, @@ -809,11 +839,12 @@ impl State { ref context, .. } = self; - components.iter_mut().all(|c| c.can_quit_cleanly(context)) + components.values_mut().all(|c| c.can_quit_cleanly(context)) } pub fn register_component(&mut self, component: Box) { - self.components.push(component); + component.realize(None, &mut self.context); + self.components.insert(component.id(), component); } /// Convert user commands to actions/method calls. @@ -885,7 +916,11 @@ impl State { } match crate::sqlite3::index(&mut self.context, account_index) { Ok(job) => { - let handle = self.context.job_executor.spawn_blocking(job); + let handle = self + .context + .main_loop_handler + .job_executor + .spawn_blocking(job); self.context.accounts[account_index].active_jobs.insert( handle.job_id, crate::conf::accounts::JobRequest::Generic { @@ -963,6 +998,7 @@ impl State { } Quit => { self.context + .main_loop_handler .sender .send(ThreadEvent::Input(( self.context.settings.shortcuts.general.quit.clone(), @@ -989,7 +1025,7 @@ impl State { UIEvent::Command(cmd) => { if let Ok(action) = parse_command(cmd.as_bytes()) { if action.needs_confirmation() { - self.overlay.push(Box::new(UIConfirmationDialog::new( + let new = Box::new(UIConfirmationDialog::new( "You sure?", vec![(true, "yes".to_string()), (false, "no".to_string())], true, @@ -1000,7 +1036,9 @@ impl State { )) })), &self.context, - ))); + )); + + self.overlay.insert(new.id(), new); } else if let Action::ReloadConfiguration = action { match Settings::new().and_then(|new_settings| { let old_accounts = self @@ -1114,6 +1152,7 @@ impl State { } UIEvent::ChangeMode(m) => { self.context + .main_loop_handler .sender .send(ThreadEvent::UIEvent(UIEvent::ChangeMode(m))) .unwrap(); @@ -1164,13 +1203,7 @@ impl State { self.display_messages_pos = self.display_messages.len() - 1; self.redraw(); } - UIEvent::ComponentKill(ref id) if self.overlay.iter().any(|c| c.id() == *id) => { - let pos = self.overlay.iter().position(|c| c.id() == *id).unwrap(); - self.overlay.remove(pos); - } - UIEvent::FinishedUIDialog(ref id, ref mut results) - if self.overlay.iter().any(|c| c.id() == *id) => - { + UIEvent::FinishedUIDialog(ref id, ref mut results) if self.overlay.contains_key(id) => { if let Some(ref mut action @ Some(_)) = results.downcast_mut::>() { self.exec_command(action.take().unwrap()); @@ -1182,7 +1215,7 @@ impl State { return; } UIEvent::GlobalUIDialog(dialog) => { - self.overlay.push(dialog); + self.overlay.insert(dialog.id(), dialog); return; } _ => {} @@ -1195,12 +1228,60 @@ impl State { } = self; /* inform each component */ - for c in overlay.iter_mut().chain(components.iter_mut()) { + for c in overlay.values_mut().chain(components.values_mut()) { if c.process_event(&mut event, context) { break; } } + while let Some((id, parent)) = self.context.realized.pop() { + match parent { + None => { + self.component_tree.insert(id, ComponentPath::new(id)); + } + Some(parent) if self.component_tree.contains_key(&parent) => { + let mut v = self.component_tree[&parent].clone(); + v.push_front(id); + if let Some(p) = v.root() { + assert_eq!( + v.resolve(&self.components[p] as &dyn Component) + .unwrap() + .id(), + id + ); + } + self.component_tree.insert(id, v); + } + Some(parent) if !self.context.realized.contains_key(&parent) => { + log::debug!( + "BUG: component_realize new_id = {:?} parent = {:?} but component_tree \ + does not include parent, skipping.", + id, + parent + ); + self.component_tree.insert(id, ComponentPath::new(id)); + } + Some(_) => { + let from_index = self.context.realized.len(); + self.context.realized.insert(id, parent); + self.context.realized.move_index(from_index, 0); + } + } + } + while let Some(id) = self.context.unrealized.pop() { + let mut to_delete = BTreeSet::new(); + for (desc, _) in self.component_tree.iter().filter(|(_, path)| { + path.parent() + .map(|p| self.context.unrealized.contains(p) || *p == id) + .unwrap_or(false) + }) { + to_delete.insert(*desc); + } + self.context.unrealized.extend(to_delete.into_iter()); + self.component_tree.remove(&id); + self.components.remove(&id); + } + if !self.context.replies.is_empty() { let replies: smallvec::SmallVec<[UIEvent; 8]> = self.context.replies.drain(0..).collect(); diff --git a/src/types.rs b/src/types.rs index 6fbc0b2e..5369d5db 100644 --- a/src/types.rs +++ b/src/types.rs @@ -38,13 +38,13 @@ mod helpers; use std::{borrow::Cow, fmt, sync::Arc}; +pub use helpers::*; use melib::{ backends::{AccountHash, BackendEvent, MailboxHash}, EnvelopeHash, RefreshEvent, ThreadHash, }; use nix::unistd::Pid; -pub use self::helpers::*; use super::{ command::Action, jobs::{JobExecutor, JobId, TimerId}, @@ -52,6 +52,8 @@ use super::{ }; use crate::components::{Component, ComponentId, ScrollUpdate}; +pub type UIMessage = Box; + #[derive(Debug)] pub enum StatusEvent { DisplayMessage(String), @@ -70,15 +72,13 @@ pub enum StatusEvent { /// between our threads to the main process. #[derive(Debug)] pub enum ThreadEvent { - /// User input. - Input((Key, Vec)), /// User input and input as raw bytes. + Input((Key, Vec)), /// A watched Mailbox has been refreshed. RefreshMailbox(Box), UIEvent(UIEvent), /// A thread has updated some of its information Pulse, - //Decode { _ }, // For gpg2 signature check JobFinished(JobId), } @@ -126,9 +126,7 @@ pub enum UIEvent { CmdInput(Key), InsertInput(Key), EmbedInput((Key, Vec)), - //Quit? Resize, - /// Force redraw. Fork(ForkType), ChangeMailbox(usize), ChangeMode(UIMode), @@ -140,7 +138,7 @@ pub enum UIEvent { MailboxDelete((AccountHash, MailboxHash)), MailboxCreate((AccountHash, MailboxHash)), AccountStatusChange(AccountHash, Option>), - ComponentKill(ComponentId), + ComponentUnrealize(ComponentId), BackendEvent(AccountHash, BackendEvent), StartupCheck(MailboxHash), RefreshEvent(Box), @@ -150,6 +148,11 @@ pub enum UIEvent { Contacts(ContactEvent), Compose(ComposeEvent), FinishedUIDialog(ComponentId, UIMessage), + IntraComm { + from: ComponentId, + to: ComponentId, + content: UIMessage, + }, Callback(CallbackFn), GlobalUIDialog(Box), Timer(TimerId), @@ -374,8 +377,6 @@ pub enum ComposeEvent { SetReceipients(Vec), } -pub type UIMessage = Box; - #[cfg(test)] mod tests { //use super::*;