mail/listing.rs: move mail view to listing parent component

Instead of having a different widget to view mail in for each Listing
(plain, threaded, compact, etc) use a single widget in the listing's
parent type.

This will help with making the listing logic more modular in future
refactors to allow all combinations of listing/mail view/ thread view
positions and layouts.
pull/227/head
Manos Pitsidianakis 12 months ago
parent 5c9b3fb044
commit 575509f1ed
No known key found for this signature in database
GPG Key ID: 7729C7707F7E09D0

@ -173,8 +173,6 @@ theme_default
.It .It
error_message error_message
.It .It
email_header
.It
highlight highlight
.It .It
status.bar status.bar

@ -1120,7 +1120,7 @@ Play sound file in notifications if possible.
.Sh PAGER .Sh PAGER
Default values are shown in parentheses. Default values are shown in parentheses.
.Bl -tag -width 36n .Bl -tag -width 36n
.It Ic headers_sticky Ar boolean .It Ic sticky_headers Ar boolean
.Pq Em optional .Pq Em optional
Always show headers when scrolling. Always show headers when scrolling.
.\" default value .\" default value

@ -91,7 +91,7 @@
#[pager] #[pager]
#filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email" #filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email"
#pager_context = 0 # default, optional #pager_context = 0 # default, optional
#headers_sticky = true # default, optional #sticky_headers = true # default, optional
# #
#[notifications] #[notifications]
#script = "notify-send" #script = "notify-send"

@ -32,6 +32,7 @@ use crate::{
melib::text_processing::{TextProcessing, Truncate}, melib::text_processing::{TextProcessing, Truncate},
terminal::boundaries::*, terminal::boundaries::*,
}; };
use smallvec::SmallVec;
pub mod mail; pub mod mail;
pub use crate::mail::*; pub use crate::mail::*;
@ -163,6 +164,31 @@ pub trait Component: Display + Debug + Send + Sync {
fn status(&self, _context: &Context) -> String { fn status(&self, _context: &Context) -> String {
String::new() String::new()
} }
fn attributes(&self) -> &'static ComponentAttr {
&ComponentAttr::DEFAULT
}
fn children(&self) -> IndexMap<ComponentId, &dyn Component> {
IndexMap::default()
}
fn children_mut(&mut self) -> IndexMap<ComponentId, &mut dyn Component> {
IndexMap::default()
}
fn realize(&self, parent: Option<ComponentId>, 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<dyn Component> { impl Component for Box<dyn Component> {
@ -205,4 +231,89 @@ impl Component for Box<dyn Component> {
fn status(&self, context: &Context) -> String { fn status(&self, context: &Context) -> String {
(**self).status(context) (**self).status(context)
} }
fn attributes(&self) -> &'static ComponentAttr {
(**self).attributes()
}
fn children(&self) -> IndexMap<ComponentId, &dyn Component> {
(**self).children()
}
fn children_mut(&mut self) -> IndexMap<ComponentId, &mut dyn Component> {
(**self).children_mut()
}
fn realize(&self, parent: Option<ComponentId>, 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()
}
} }

@ -221,10 +221,10 @@ impl Component for ContactManager {
context.replies.push_back(UIEvent::StatusEvent( context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage("Saved.".into()), StatusEvent::DisplayMessage("Saved.".into()),
)); ));
context.replies.push_back(UIEvent::ComponentKill(self.id)); self.unrealize(context);
} }
Some(false) => { Some(false) => {
context.replies.push_back(UIEvent::ComponentKill(self.id)); self.unrealize(context);
} }
} }
self.set_dirty(true); self.set_dirty(true);
@ -237,7 +237,7 @@ impl Component for ContactManager {
ViewMode::ReadOnly => { ViewMode::ReadOnly => {
if let &mut UIEvent::Input(Key::Esc) = event { if let &mut UIEvent::Input(Key::Esc) = event {
if self.can_quit_cleanly(context) { if self.can_quit_cleanly(context) {
context.replies.push_back(UIEvent::ComponentKill(self.id)); self.unrealize(context);
} }
return true; return true;
} }

@ -899,7 +899,9 @@ impl Component for ContactList {
} }
} else { } else {
match event { 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.mode = ViewMode::List;
self.view.take(); self.view.take();
self.set_dirty(true); self.set_dirty(true);

@ -1158,7 +1158,7 @@ impl Component for Composer {
Flag::SEEN, Flag::SEEN,
) { ) {
Ok(job) => { Ok(job) => {
let handle = context.job_executor.spawn_blocking(job); let handle = context.main_loop_handler.job_executor.spawn_blocking(job);
context context
.replies .replies
.push_back(UIEvent::StatusEvent(StatusEvent::NewJob( .push_back(UIEvent::StatusEvent(StatusEvent::NewJob(
@ -1208,26 +1208,29 @@ impl Component for Composer {
self.set_dirty(true); self.set_dirty(true);
return 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.mode = ViewMode::Edit;
self.set_dirty(true); self.set_dirty(true);
} }
(ViewMode::SelectRecipients(ref dialog), UIEvent::ComponentKill(ref id)) (ViewMode::SelectRecipients(ref dialog), UIEvent::ComponentUnrealize(ref id))
if *id == dialog.id() => if *id == dialog.id() =>
{ {
self.mode = ViewMode::Edit; self.mode = ViewMode::Edit;
self.set_dirty(true); self.set_dirty(true);
} }
(ViewMode::Discard(_, ref dialog), UIEvent::ComponentKill(ref id)) (ViewMode::Discard(_, ref dialog), UIEvent::ComponentUnrealize(ref id))
if *id == dialog.id() => if *id == dialog.id() =>
{ {
self.mode = ViewMode::Edit; self.mode = ViewMode::Edit;
self.set_dirty(true); self.set_dirty(true);
} }
#[cfg(feature = "gpgme")] #[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.mode = ViewMode::Edit;
self.set_dirty(true); self.set_dirty(true);
return true; return true;
@ -2315,7 +2318,7 @@ pub fn send_draft_async(
) -> Result<Pin<Box<dyn Future<Output = Result<()>> + Send>>> { ) -> Result<Pin<Box<dyn Future<Output = Result<()>> + Send>>> {
let store_sent_mail = *account_settings!(context[account_hash].composing.store_sent_mail); 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 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")] #[cfg(feature = "gpgme")]
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
let mut filters_stack: Vec< let mut filters_stack: Vec<

@ -68,7 +68,10 @@ impl KeySelection {
ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?; ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?;
} }
let job = ctx.keylist(secret, Some(pattern.clone()))?; 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); let mut progress_spinner = ProgressSpinner::new(8, context);
progress_spinner.start(); progress_spinner.start();
Ok(KeySelection::LoadingKeys { Ok(KeySelection::LoadingKeys {

@ -55,6 +55,7 @@ pub const DEFAULT_SNOOZED_FLAG: &str = "💤";
pub struct RowsState<T> { pub struct RowsState<T> {
pub selection: HashMap<EnvelopeHash, bool>, pub selection: HashMap<EnvelopeHash, bool>,
pub row_updates: SmallVec<[EnvelopeHash; 8]>, pub row_updates: SmallVec<[EnvelopeHash; 8]>,
/// FIXME: env vec should have at least one element guaranteed
pub thread_to_env: HashMap<ThreadHash, SmallVec<[EnvelopeHash; 8]>>, pub thread_to_env: HashMap<ThreadHash, SmallVec<[EnvelopeHash; 8]>>,
pub env_to_thread: HashMap<EnvelopeHash, ThreadHash>, pub env_to_thread: HashMap<EnvelopeHash, ThreadHash>,
pub thread_order: HashMap<ThreadHash, usize>, pub thread_order: HashMap<ThreadHash, usize>,
@ -412,6 +413,20 @@ struct AccountMenuEntry {
} }
pub trait MailListingTrait: ListingTrait { 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( fn perform_action(
&mut self, &mut self,
context: &mut Context, context: &mut Context,
@ -450,7 +465,10 @@ pub trait MailListingTrait: ListingTrait {
)); ));
} }
Ok(fut) => { Ok(fut) => {
let handle = account.job_executor.spawn_specialized(fut); let handle = account
.main_loop_handler
.job_executor
.spawn_specialized(fut);
account account
.insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle }); .insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle });
} }
@ -469,7 +487,10 @@ pub trait MailListingTrait: ListingTrait {
)); ));
} }
Ok(fut) => { Ok(fut) => {
let handle = account.job_executor.spawn_specialized(fut); let handle = account
.main_loop_handler
.job_executor
.spawn_specialized(fut);
account account
.insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle }); .insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle });
} }
@ -488,7 +509,10 @@ pub trait MailListingTrait: ListingTrait {
)); ));
} }
Ok(fut) => { Ok(fut) => {
let handle = account.job_executor.spawn_specialized(fut); let handle = account
.main_loop_handler
.job_executor
.spawn_specialized(fut);
account account
.insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle }); .insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle });
} }
@ -507,7 +531,10 @@ pub trait MailListingTrait: ListingTrait {
)); ));
} }
Ok(fut) => { Ok(fut) => {
let handle = account.job_executor.spawn_specialized(fut); let handle = account
.main_loop_handler
.job_executor
.spawn_specialized(fut);
account account
.insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle }); .insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle });
} }
@ -526,7 +553,10 @@ pub trait MailListingTrait: ListingTrait {
)); ));
} }
Ok(fut) => { Ok(fut) => {
let handle = account.job_executor.spawn_specialized(fut); let handle = account
.main_loop_handler
.job_executor
.spawn_specialized(fut);
account.insert_job( account.insert_job(
handle.job_id, handle.job_id,
JobRequest::DeleteMessages { env_hashes, handle }, JobRequest::DeleteMessages { env_hashes, handle },
@ -551,7 +581,10 @@ pub trait MailListingTrait: ListingTrait {
)); ));
} }
Ok(fut) => { Ok(fut) => {
let handle = account.job_executor.spawn_specialized(fut); let handle = account
.main_loop_handler
.job_executor
.spawn_specialized(fut);
account.insert_job( account.insert_job(
handle.job_id, handle.job_id,
JobRequest::Generic { JobRequest::Generic {
@ -588,7 +621,10 @@ pub trait MailListingTrait: ListingTrait {
)); ));
} }
Ok(fut) => { Ok(fut) => {
let handle = account.job_executor.spawn_specialized(fut); let handle = account
.main_loop_handler
.job_executor
.spawn_specialized(fut);
account.insert_job( account.insert_job(
handle.job_id, handle.job_id,
JobRequest::Generic { JobRequest::Generic {
@ -667,7 +703,7 @@ pub trait MailListingTrait: ListingTrait {
let _ = sender.send(r); let _ = sender.send(r);
Ok(()) 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(); let path = path.to_path_buf();
account.insert_job( account.insert_job(
handle.job_id, handle.job_id,
@ -743,12 +779,27 @@ pub trait ListingTrait: Component {
) { ) {
} }
fn unfocused(&self) -> bool; fn unfocused(&self) -> bool;
fn view_area(&self) -> Option<Area>;
fn set_modifier_active(&mut self, _new_val: bool); fn set_modifier_active(&mut self, _new_val: bool);
fn set_modifier_command(&mut self, _new_val: Option<Modifier>); fn set_modifier_command(&mut self, _new_val: Option<Modifier>);
fn modifier_command(&self) -> Option<Modifier>; fn modifier_command(&self) -> Option<Modifier>;
fn set_movement(&mut self, mvm: PageMovement); fn set_movement(&mut self, mvm: PageMovement);
fn focus(&self) -> Focus; fn focus(&self) -> Focus;
fn set_focus(&mut self, new_value: Focus, context: &mut Context); 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)] #[derive(Debug)]
@ -788,32 +839,13 @@ impl core::ops::DerefMut for ListingComponent {
} }
impl ListingComponent { impl ListingComponent {
fn set_style(&mut self, new_style: IndexStyle) { fn id(&self) -> ComponentId {
match new_style { match self {
IndexStyle::Plain => { Compact(l) => l.as_component().id(),
if let Plain(_) = self { Plain(l) => l.as_component().id(),
return; Threaded(l) => l.as_component().id(),
} Conversations(l) => l.as_component().id(),
*self = Plain(PlainListing::new(self.coordinates())); Offline(l) => l.as_component().id(),
}
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()));
}
} }
} }
} }
@ -862,6 +894,7 @@ pub struct Listing {
prev_ratio: usize, prev_ratio: usize,
menu_width: WidgetWidth, menu_width: WidgetWidth,
focus: ListingFocus, focus: ListingFocus,
view: Box<ThreadView>,
} }
impl fmt::Display for Listing { impl fmt::Display for Listing {
@ -930,14 +963,20 @@ impl Component for Listing {
if context.is_online(account_hash).is_err() if context.is_online(account_hash).is_err()
&& !matches!(self.component, ListingComponent::Offline(_)) && !matches!(self.component, ListingComponent::Offline(_))
{ {
self.component.unrealize(context);
self.component = self.component =
Offline(OfflineListing::new((account_hash, MailboxHash::default()))); Offline(OfflineListing::new((account_hash, MailboxHash::default())));
self.component.realize(self.id().into(), context);
} }
if let Some(s) = self.status.as_mut() { if let Some(s) = self.status.as_mut() {
s.draw(grid, area, context); s.draw(grid, area, context);
} else { } else {
self.component.draw(grid, area, context); 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 { } else if right_component_width == 0 {
self.draw_menu(grid, area, context); self.draw_menu(grid, area, context);
@ -950,14 +989,20 @@ impl Component for Listing {
if context.is_online(account_hash).is_err() if context.is_online(account_hash).is_err()
&& !matches!(self.component, ListingComponent::Offline(_)) && !matches!(self.component, ListingComponent::Offline(_))
{ {
self.component.unrealize(context);
self.component = self.component =
Offline(OfflineListing::new((account_hash, MailboxHash::default()))); Offline(OfflineListing::new((account_hash, MailboxHash::default())));
self.component.realize(self.id().into(), context);
} }
if let Some(s) = self.status.as_mut() { if let Some(s) = self.status.as_mut() {
s.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context); s.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context);
} else { } else {
self.component let area = (set_x(upper_left, mid + 1), bottom_right);
.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context); 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; self.dirty = false;
@ -1132,9 +1177,73 @@ impl Component for Listing {
} }
return true; return true;
} }
UIEvent::IntraComm {
from,
to,
ref content,
} if (*from, *to) == (self.component.id(), self.id()) => {
match content.downcast_ref::<ListingMessage>().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::<ListingMessage>().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 self.focus == ListingFocus::Mailbox && self.status.is_some() {
if let Some(s) = self.status.as_mut() { if let Some(s) = self.status.as_mut() {
if s.process_event(event, context) { if s.process_event(event, context) {
@ -1142,11 +1251,12 @@ impl Component for Listing {
} }
} }
} }
if self.focus == ListingFocus::Mailbox if self.focus == ListingFocus::Mailbox && self.status.is_none() {
&& self.status.is_none() if self.component.unfocused() && self.view.process_event(event, context) {
&& self.component.process_event(event, context) return true;
{ } else if self.component.process_event(event, context) {
return true; return true;
}
} }
let shortcuts = self.shortcuts(context); let shortcuts = self.shortcuts(context);
@ -1336,19 +1446,19 @@ impl Component for Listing {
match event { match event {
UIEvent::Action(ref action) => match action { UIEvent::Action(ref action) => match action {
Action::Listing(ListingAction::SetPlain) => { Action::Listing(ListingAction::SetPlain) => {
self.component.set_style(IndexStyle::Plain); self.set_style(IndexStyle::Plain, context);
return true; return true;
} }
Action::Listing(ListingAction::SetThreaded) => { Action::Listing(ListingAction::SetThreaded) => {
self.component.set_style(IndexStyle::Threaded); self.set_style(IndexStyle::Threaded, context);
return true; return true;
} }
Action::Listing(ListingAction::SetCompact) => { Action::Listing(ListingAction::SetCompact) => {
self.component.set_style(IndexStyle::Compact); self.set_style(IndexStyle::Compact, context);
return true; return true;
} }
Action::Listing(ListingAction::SetConversations) => { Action::Listing(ListingAction::SetConversations) => {
self.component.set_style(IndexStyle::Conversations); self.set_style(IndexStyle::Conversations, context);
return true; return true;
} }
Action::Listing(ListingAction::Import(file_path, mailbox_path)) => { Action::Listing(ListingAction::Import(file_path, mailbox_path)) => {
@ -1952,6 +2062,11 @@ impl Component for Listing {
.as_ref() .as_ref()
.map(Component::is_dirty) .map(Component::is_dirty)
.unwrap_or_else(|| self.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) { fn set_dirty(&mut self, value: bool) {
@ -1960,6 +2075,9 @@ impl Component for Listing {
s.set_dirty(value); s.set_dirty(value);
} else { } else {
self.component.set_dirty(value); 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(); let mut config_map = context.settings.shortcuts.listing.key_values();
if self.focus != ListingFocus::Menu { if self.focus != ListingFocus::Menu {
config_map.remove("open_mailbox"); config_map.remove("open_mailbox");
if self.component.unfocused() {
map.extend(self.view.shortcuts(context).into_iter());
}
} }
map.insert(Shortcuts::LISTING, config_map); map.insert(Shortcuts::LISTING, config_map);
@ -1979,7 +2100,7 @@ impl Component for Listing {
} }
fn id(&self) -> ComponentId { fn id(&self) -> ComponentId {
self.component.id() self.id
} }
fn status(&self, context: &Context) -> String { fn status(&self, context: &Context) -> String {
@ -2022,6 +2143,38 @@ impl Component for Listing {
MailboxStatus::Failed(_) | MailboxStatus::None => account[&mailbox_hash].status(), MailboxStatus::Failed(_) | MailboxStatus::None => account[&mailbox_hash].status(),
} }
} }
fn children(&self) -> IndexMap<ComponentId, &dyn Component> {
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<ComponentId, &mut dyn Component> {
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 { impl Listing {
@ -2059,18 +2212,23 @@ impl Listing {
first_account_hash, first_account_hash,
MailboxHash::default(), MailboxHash::default(),
))), ))),
view: Box::new(ThreadView::default()),
accounts: account_entries, accounts: account_entries,
status: None, status: None,
dirty: true, dirty: true,
cursor_pos: (0, MenuEntryCursor::Mailbox(0)), cursor_pos: (0, MenuEntryCursor::Mailbox(0)),
menu_cursor_pos: (0, MenuEntryCursor::Mailbox(0)), menu_cursor_pos: (0, MenuEntryCursor::Mailbox(0)),
menu_content: CellBuffer::new_with_context(0, 0, None, context), 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_secs(0),
std::time::Duration::from_millis(1200), std::time::Duration::from_millis(1200),
), ),
show_menu_scrollbar: ShowMenuScrollbar::Never, 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"), theme_default: conf::value(context, "theme_default"),
id: ComponentId::default(), id: ComponentId::default(),
sidebar_divider: *account_settings!( sidebar_divider: *account_settings!(
@ -2084,6 +2242,7 @@ impl Listing {
focus: ListingFocus::Mailbox, focus: ListingFocus::Mailbox,
cmd_buf: String::with_capacity(4), cmd_buf: String::with_capacity(4),
}; };
ret.component.realize(ret.id().into(), context);
ret.change_account(context); ret.change_account(context);
ret ret
} }
@ -2580,10 +2739,12 @@ impl Listing {
let index_style = let index_style =
mailbox_settings!(context[account_hash][mailbox_hash].listing.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(_)) { } else if !matches!(self.component, ListingComponent::Offline(_)) {
self.component.unrealize(context);
self.component = self.component =
Offline(OfflineListing::new((account_hash, MailboxHash::default()))); Offline(OfflineListing::new((account_hash, MailboxHash::default())));
self.component.realize(self.id().into(), context);
} }
self.status = None; self.status = None;
context context
@ -2622,4 +2783,64 @@ impl Listing {
fn is_menu_visible(&self) -> bool { fn is_menu_visible(&self) -> bool {
!matches!(self.component.focus(), Focus::EntryFullscreen) && self.menu_visibility !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,
} }

@ -185,12 +185,13 @@ pub struct CompactListing {
force_draw: bool, force_draw: bool,
/// If `self.view` exists or not. /// If `self.view` exists or not.
focus: Focus, focus: Focus,
view: Box<ThreadView>,
color_cache: ColorCache, color_cache: ColorCache,
movement: Option<PageMovement>, movement: Option<PageMovement>,
modifier_active: bool, modifier_active: bool,
modifier_command: Option<Modifier>, modifier_command: Option<Modifier>,
view_area: Option<Area>,
parent: ComponentId,
id: ComponentId, id: ComponentId,
} }
@ -287,6 +288,7 @@ impl MailListingTrait for CompactListing {
self.sort, self.sort,
&context.accounts[&self.cursor_pos.0].collection.envelopes, &context.accounts[&self.cursor_pos.0].collection.envelopes,
); );
drop(threads);
self.redraw_threads_list( self.redraw_threads_list(
context, context,
@ -294,10 +296,22 @@ impl MailListingTrait for CompactListing {
); );
if !force && old_cursor_pos == self.new_cursor_pos { 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() { } else if self.unfocused() {
if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) { if let Some((thread_hash, env_hash)) = self
self.view = Box::new(ThreadView::new(self.new_cursor_pos, thread, None, context)); .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)) { fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0); self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.focus = Focus::None; self.focus = Focus::None;
self.view = Box::<ThreadView>::default();
self.filtered_selection.clear(); self.filtered_selection.clear();
self.filtered_order.clear(); self.filtered_order.clear();
self.filter_term.clear(); self.filter_term.clear();
@ -812,6 +825,10 @@ impl ListingTrait for CompactListing {
); );
} }
fn view_area(&self) -> Option<Area> {
self.view_area
}
fn unfocused(&self) -> bool { fn unfocused(&self) -> bool {
!matches!(self.focus, Focus::None) !matches!(self.focus, Focus::None)
} }
@ -836,8 +853,6 @@ impl ListingTrait for CompactListing {
fn set_focus(&mut self, new_value: Focus, context: &mut Context) { fn set_focus(&mut self, new_value: Focus, context: &mut Context) {
match new_value { match new_value {
Focus::None => { Focus::None => {
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true; self.dirty = true;
/* If self.rows.row_updates is not empty and we exit a thread, the row_update /* 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. * events will be performed but the list will not be drawn.
@ -848,13 +863,17 @@ impl ListingTrait for CompactListing {
Focus::Entry => { Focus::Entry => {
self.force_draw = true; self.force_draw = true;
self.dirty = true; self.dirty = true;
self.view.set_dirty(true);
} }
Focus::EntryFullscreen => { Focus::EntryFullscreen => {
self.view.set_dirty(true); self.dirty = true;
} }
} }
self.focus = new_value; self.focus = new_value;
self.kick_parent(
self.parent,
ListingMessage::FocusUpdate { new_value },
context,
);
} }
fn focus(&self) -> Focus { fn focus(&self) -> Focus {
@ -870,7 +889,7 @@ impl fmt::Display for CompactListing {
impl CompactListing { impl CompactListing {
pub const DESCRIPTION: &'static str = "compact listing"; pub const DESCRIPTION: &'static str = "compact listing";
pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box<Self> { pub fn new(parent: ComponentId, coordinates: (AccountHash, MailboxHash)) -> Box<Self> {
Box::new(CompactListing { Box::new(CompactListing {
cursor_pos: (coordinates.0, MailboxHash::default(), 0), cursor_pos: (coordinates.0, MailboxHash::default(), 0),
new_cursor_pos: (coordinates.0, coordinates.1, 0), new_cursor_pos: (coordinates.0, coordinates.1, 0),
@ -889,11 +908,12 @@ impl CompactListing {
rows: RowsState::default(), rows: RowsState::default(),
dirty: true, dirty: true,
force_draw: true, force_draw: true,
view: Box::<ThreadView>::default(),
color_cache: ColorCache::default(), color_cache: ColorCache::default(),
movement: None, movement: None,
modifier_active: false, modifier_active: false,
modifier_command: None, modifier_command: None,
view_area: None,
parent,
id: ComponentId::default(), id: ComponentId::default(),
}) })
} }
@ -1440,12 +1460,13 @@ impl CompactListing {
impl Component for CompactListing { impl Component for CompactListing {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { 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; return;
} }
if matches!(self.focus, Focus::EntryFullscreen) { if !self.is_dirty() {
return self.view.draw(grid, area, context); return;
} }
if !self.unfocused() { if !self.unfocused() {
@ -1681,7 +1702,7 @@ impl Component for CompactListing {
return; return;
} }
self.view.draw(grid, area, context); self.view_area = area.into();
} }
self.dirty = false; 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 { if self.length > 0 {
match *event { match *event {
UIEvent::Input(ref k) UIEvent::Input(ref k)
@ -1722,9 +1739,21 @@ impl Component for CompactListing {
&& (shortcut!(k == shortcuts[Shortcuts::LISTING]["open_entry"]) && (shortcut!(k == shortcuts[Shortcuts::LISTING]["open_entry"])
|| shortcut!(k == shortcuts[Shortcuts::LISTING]["focus_right"])) => || shortcut!(k == shortcuts[Shortcuts::LISTING]["focus_right"])) =>
{ {
if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) { if let Some((thread_hash, env_hash)) = self
self.view = .get_thread_under_cursor(self.cursor_pos.2)
Box::new(ThreadView::new(self.cursor_pos, thread, None, context)); .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); self.set_focus(Focus::Entry, context);
} }
return true; return true;
@ -1837,7 +1866,7 @@ impl Component for CompactListing {
self.refresh_mailbox(context, false); self.refresh_mailbox(context, false);
self.set_dirty(true); 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 account = &context.accounts[&self.cursor_pos.0];
let threads = account.collection.get_threads(self.cursor_pos.1); let threads = account.collection.get_threads(self.cursor_pos.1);
if !account.collection.contains_key(new_hash) { if !account.collection.contains_key(new_hash) {
@ -1855,13 +1884,8 @@ impl Component for CompactListing {
} }
self.set_dirty(true); 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) { if self.rows.thread_order.contains_key(thread_hash) {
self.refresh_mailbox(context, false); self.refresh_mailbox(context, false);
self.set_dirty(true); self.set_dirty(true);
@ -1885,11 +1909,6 @@ impl Component for CompactListing {
} }
self.set_dirty(true); self.set_dirty(true);
if self.unfocused() {
self.view
.process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context);
}
} }
UIEvent::ChangeMode(UIMode::Normal) => { UIEvent::ChangeMode(UIMode::Normal) => {
self.set_dirty(true); self.set_dirty(true);
@ -1926,6 +1945,7 @@ impl Component for CompactListing {
) { ) {
Ok(job) => { Ok(job) => {
let handle = context.accounts[&self.cursor_pos.0] let handle = context.accounts[&self.cursor_pos.0]
.main_loop_handler
.job_executor .job_executor
.spawn_specialized(job); .spawn_specialized(job);
self.search_job = Some((filter_term.to_string(), handle)); self.search_job = Some((filter_term.to_string(), handle));
@ -1948,6 +1968,7 @@ impl Component for CompactListing {
) { ) {
Ok(job) => { Ok(job) => {
let mut handle = context.accounts[&self.cursor_pos.0] let mut handle = context.accounts[&self.cursor_pos.0]
.main_loop_handler
.job_executor .job_executor
.spawn_specialized(job); .spawn_specialized(job);
if let Ok(Some(search_result)) = try_recv_timeout!(&mut handle.chan) { 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 { fn is_dirty(&self) -> bool {
match self.focus { match self.focus {
Focus::None => self.dirty, Focus::None => self.dirty,
Focus::Entry => self.dirty || self.view.is_dirty(), Focus::Entry => self.dirty,
Focus::EntryFullscreen => self.view.is_dirty(), Focus::EntryFullscreen => false,
} }
} }
fn set_dirty(&mut self, value: bool) { fn set_dirty(&mut self, value: bool) {
self.dirty = value; self.dirty = value;
if self.unfocused() {
self.view.set_dirty(value);
}
} }
fn shortcuts(&self, context: &Context) -> ShortcutMaps { fn shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map = if self.unfocused() { let mut map = ShortcutMaps::default();
self.view.shortcuts(context)
} else {
ShortcutMaps::default()
};
map.insert( map.insert(
Shortcuts::LISTING, Shortcuts::LISTING,

@ -113,14 +113,15 @@ pub struct ConversationsListing {
/// If we must redraw on next redraw event /// If we must redraw on next redraw event
dirty: bool, dirty: bool,
force_draw: bool, force_draw: bool,
/// If `self.view` exists or not. /// If `self.view` is visible or not.
focus: Focus, focus: Focus,
view: ThreadView,
color_cache: ColorCache, color_cache: ColorCache,
movement: Option<PageMovement>, movement: Option<PageMovement>,
modifier_active: bool, modifier_active: bool,
modifier_command: Option<Modifier>, modifier_command: Option<Modifier>,
view_area: Option<Area>,
parent: ComponentId,
id: ComponentId, id: ComponentId,
} }
@ -204,6 +205,7 @@ impl MailListingTrait for ConversationsListing {
self.sort, self.sort,
&context.accounts[&self.cursor_pos.0].collection.envelopes, &context.accounts[&self.cursor_pos.0].collection.envelopes,
); );
drop(threads);
self.redraw_threads_list( self.redraw_threads_list(
context, 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 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() { } else if self.unfocused() {
if let Some(thread_group) = self.get_thread_under_cursor(self.cursor_pos.2) { if let Some((thread_hash, env_hash)) = self
self.view = ThreadView::new(self.new_cursor_pos, thread_group, None, context); .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)) { fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0); self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.focus = Focus::None; self.focus = Focus::None;
self.view = ThreadView::default();
self.filtered_selection.clear(); self.filtered_selection.clear();
self.filtered_order.clear(); self.filtered_order.clear();
self.filter_term.clear(); self.filter_term.clear();
@ -556,6 +569,10 @@ impl ListingTrait for ConversationsListing {
); );
} }
fn view_area(&self) -> Option<Area> {
self.view_area
}
fn unfocused(&self) -> bool { fn unfocused(&self) -> bool {
!matches!(self.focus, Focus::None) !matches!(self.focus, Focus::None)
} }
@ -580,8 +597,6 @@ impl ListingTrait for ConversationsListing {
fn set_focus(&mut self, new_value: Focus, context: &mut Context) { fn set_focus(&mut self, new_value: Focus, context: &mut Context) {
match new_value { match new_value {
Focus::None => { Focus::None => {
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true; self.dirty = true;
/* If self.rows.row_updates is not empty and we exit a thread, the row_update /* 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. * events will be performed but the list will not be drawn.
@ -592,13 +607,15 @@ impl ListingTrait for ConversationsListing {
Focus::Entry => { Focus::Entry => {
self.force_draw = true; self.force_draw = true;
self.dirty = true; self.dirty = true;
self.view.set_dirty(true);
}
Focus::EntryFullscreen => {
self.view.set_dirty(true);
} }
Focus::EntryFullscreen => {}
} }
self.focus = new_value; self.focus = new_value;
self.kick_parent(
self.parent,
ListingMessage::FocusUpdate { new_value },
context,
);
} }
fn focus(&self) -> Focus { fn focus(&self) -> Focus {
@ -615,7 +632,7 @@ impl fmt::Display for ConversationsListing {
impl ConversationsListing { impl ConversationsListing {
//const PADDING_CHAR: char = ' '; //░'; //const PADDING_CHAR: char = ' '; //░';
pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box<Self> { pub fn new(parent: ComponentId, coordinates: (AccountHash, MailboxHash)) -> Box<Self> {
Box::new(Self { Box::new(Self {
cursor_pos: (coordinates.0, MailboxHash::default(), 0), cursor_pos: (coordinates.0, MailboxHash::default(), 0),
new_cursor_pos: (coordinates.0, coordinates.1, 0), new_cursor_pos: (coordinates.0, coordinates.1, 0),
@ -631,11 +648,12 @@ impl ConversationsListing {
dirty: true, dirty: true,
force_draw: true, force_draw: true,
focus: Focus::None, focus: Focus::None,
view: ThreadView::default(),
color_cache: ColorCache::default(), color_cache: ColorCache::default(),
movement: None, movement: None,
modifier_active: false, modifier_active: false,
modifier_command: None, modifier_command: None,
view_area: None,
parent,
id: ComponentId::default(), id: ComponentId::default(),
}) })
} }
@ -969,12 +987,13 @@ impl ConversationsListing {
impl Component for ConversationsListing { impl Component for ConversationsListing {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { 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; return;
} }
if matches!(self.focus, Focus::EntryFullscreen) { if !self.is_dirty() {
return self.view.draw(grid, area, context); return;
} }
let (upper_left, bottom_right) = area; let (upper_left, bottom_right) = area;
@ -1228,7 +1247,7 @@ impl Component for ConversationsListing {
); );
clear_area(grid, gap_area, self.color_cache.theme_default); clear_area(grid, gap_area, self.color_cache.theme_default);
context.dirty_areas.push_back(gap_area); context.dirty_areas.push_back(gap_area);
self.view.draw(grid, entry_area, context); self.view_area = entry_area.into();
} }
self.dirty = false; 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 { if self.length > 0 {
match *event { match *event {
UIEvent::Input(ref k) UIEvent::Input(ref k)
@ -1269,8 +1284,21 @@ impl Component for ConversationsListing {
&& (shortcut!(k == shortcuts[Shortcuts::LISTING]["open_entry"]) && (shortcut!(k == shortcuts[Shortcuts::LISTING]["open_entry"])
|| shortcut!(k == shortcuts[Shortcuts::LISTING]["focus_right"])) => || shortcut!(k == shortcuts[Shortcuts::LISTING]["focus_right"])) =>
{ {
if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) { if let Some((thread_hash, env_hash)) = self
self.view = ThreadView::new(self.cursor_pos, thread, None, context); .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); self.set_focus(Focus::Entry, context);
} }
return true; return true;
@ -1336,13 +1364,6 @@ impl Component for ConversationsListing {
} }
self.set_dirty(true); 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 _env_hash, ref thread_hash) => {
if self.rows.thread_order.contains_key(thread_hash) { if self.rows.thread_order.contains_key(thread_hash) {
@ -1368,11 +1389,6 @@ impl Component for ConversationsListing {
} }
self.set_dirty(true); self.set_dirty(true);
if self.unfocused() {
self.view
.process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context);
}
} }
UIEvent::Action(ref action) => match action { UIEvent::Action(ref action) => match action {
Action::SubSort(field, order) if !self.unfocused() => { Action::SubSort(field, order) if !self.unfocused() => {
@ -1464,6 +1480,7 @@ impl Component for ConversationsListing {
) { ) {
Ok(job) => { Ok(job) => {
let handle = context.accounts[&self.cursor_pos.0] let handle = context.accounts[&self.cursor_pos.0]
.main_loop_handler
.job_executor .job_executor
.spawn_specialized(job); .spawn_specialized(job);
self.search_job = Some((filter_term.to_string(), handle)); self.search_job = Some((filter_term.to_string(), handle));
@ -1533,24 +1550,17 @@ impl Component for ConversationsListing {
fn is_dirty(&self) -> bool { fn is_dirty(&self) -> bool {
match self.focus { match self.focus {
Focus::None => self.dirty, Focus::None => self.dirty,
Focus::Entry => self.dirty || self.view.is_dirty(), Focus::Entry => self.dirty,
Focus::EntryFullscreen => self.view.is_dirty(), Focus::EntryFullscreen => false,
} }
} }
fn set_dirty(&mut self, value: bool) { fn set_dirty(&mut self, value: bool) {
if self.unfocused() {
self.view.set_dirty(value);
}
self.dirty = value; self.dirty = value;
} }
fn shortcuts(&self, context: &Context) -> ShortcutMaps { fn shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map = if self.unfocused() { let mut map = ShortcutMaps::default();
self.view.shortcuts(context)
} else {
ShortcutMaps::default()
};
map.insert( map.insert(
Shortcuts::LISTING, Shortcuts::LISTING,

@ -82,6 +82,10 @@ impl ListingTrait for OfflineListing {
fn draw_list(&mut self, _: &mut CellBuffer, _: Area, _: &mut Context) {} fn draw_list(&mut self, _: &mut CellBuffer, _: Area, _: &mut Context) {}
fn view_area(&self) -> Option<Area> {
None
}
fn unfocused(&self) -> bool { fn unfocused(&self) -> bool {
false false
} }

@ -141,13 +141,14 @@ pub struct PlainListing {
/// If we must redraw on next redraw event /// If we must redraw on next redraw event
dirty: bool, dirty: bool,
force_draw: bool, force_draw: bool,
/// If `self.view` exists or not. /// If view is visible or not.
focus: Focus, focus: Focus,
view: MailView,
color_cache: ColorCache, color_cache: ColorCache,
movement: Option<PageMovement>, movement: Option<PageMovement>,
modifier_active: bool, modifier_active: bool,
modifier_command: Option<Modifier>, modifier_command: Option<Modifier>,
view_area: Option<Area>,
parent: ComponentId,
id: ComponentId, id: ComponentId,
} }
@ -261,11 +262,21 @@ impl MailListingTrait for PlainListing {
drop(env_lck); drop(env_lck);
if let Some(env_hash) = self.get_env_under_cursor(self.cursor_pos.2) { 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 { 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() { } 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)) { fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0); self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.focus = Focus::None; self.focus = Focus::None;
self.view = MailView::default();
self.filtered_selection.clear(); self.filtered_selection.clear();
self.filtered_order.clear(); self.filtered_order.clear();
self.filter_term.clear(); self.filter_term.clear();
@ -544,6 +554,10 @@ impl ListingTrait for PlainListing {
); );
} }
fn view_area(&self) -> Option<Area> {
self.view_area
}
fn unfocused(&self) -> bool { fn unfocused(&self) -> bool {
!matches!(self.focus, Focus::None) !matches!(self.focus, Focus::None)
} }
@ -568,8 +582,7 @@ impl ListingTrait for PlainListing {
fn set_focus(&mut self, new_value: Focus, context: &mut Context) { fn set_focus(&mut self, new_value: Focus, context: &mut Context) {
match new_value { match new_value {
Focus::None => { Focus::None => {
self.view //self.view .process_event(&mut UIEvent::VisibilityChange(false), context);
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true; self.dirty = true;
/* If self.rows.row_updates is not empty and we exit a thread, the row_update /* 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. * events will be performed but the list will not be drawn.
@ -578,20 +591,33 @@ impl ListingTrait for PlainListing {
self.force_draw = true; self.force_draw = true;
} }
Focus::Entry => { Focus::Entry => {
if let Some(env_hash) = self.get_env_under_cursor(self.cursor_pos.2) { if let Some((thread_hash, env_hash)) = self
let temp = (self.cursor_pos.0, self.cursor_pos.1, env_hash); .get_env_under_cursor(self.cursor_pos.2)
self.view = MailView::new(temp, None, None, context); .map(|env_hash| (self.rows.env_to_thread[&env_hash], env_hash))
{
self.force_draw = true; self.force_draw = true;
self.dirty = 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 => { Focus::EntryFullscreen => {
self.dirty = true; self.dirty = true;
self.view.set_dirty(true);
} }
} }
self.focus = new_value; self.focus = new_value;
self.kick_parent(
self.parent,
ListingMessage::FocusUpdate { new_value },
context,
);
} }
fn focus(&self) -> Focus { fn focus(&self) -> Focus {
@ -606,7 +632,7 @@ impl fmt::Display for PlainListing {
} }
impl PlainListing { impl PlainListing {
pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box<Self> { pub fn new(parent: ComponentId, coordinates: (AccountHash, MailboxHash)) -> Box<Self> {
Box::new(PlainListing { Box::new(PlainListing {
cursor_pos: (AccountHash::default(), MailboxHash::default(), 0), cursor_pos: (AccountHash::default(), MailboxHash::default(), 0),
new_cursor_pos: (coordinates.0, coordinates.1, 0), new_cursor_pos: (coordinates.0, coordinates.1, 0),
@ -623,11 +649,12 @@ impl PlainListing {
dirty: true, dirty: true,
force_draw: true, force_draw: true,
focus: Focus::None, focus: Focus::None,
view: MailView::default(),
color_cache: ColorCache::default(), color_cache: ColorCache::default(),
movement: None, movement: None,
modifier_active: false, modifier_active: false,
modifier_command: None, modifier_command: None,
view_area: None,
parent,
id: ComponentId::default(), id: ComponentId::default(),
}) })
} }
@ -1097,12 +1124,13 @@ impl PlainListing {
impl Component for PlainListing { impl Component for PlainListing {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { 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; return;
} }
if matches!(self.focus, Focus::EntryFullscreen) { if !self.is_dirty() {
return self.view.draw(grid, area, context); return;
} }
if matches!(self.focus, Focus::None) { if matches!(self.focus, Focus::None) {
@ -1340,7 +1368,7 @@ impl Component for PlainListing {
return; return;
} }
self.view.draw(grid, area, context); self.view_area = area.into();
} }
self.dirty = false; 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 { if self.length > 0 {
match *event { match *event {
UIEvent::Input(ref k) UIEvent::Input(ref k)
@ -1481,11 +1505,6 @@ impl Component for PlainListing {
} }
self.set_dirty(true); self.set_dirty(true);
if self.unfocused() {
self.view
.process_event(&mut UIEvent::EnvelopeRename(*old_hash, *new_hash), context);
}
} }
UIEvent::EnvelopeUpdate(ref env_hash) => { UIEvent::EnvelopeUpdate(ref env_hash) => {
let account = &context.accounts[&self.cursor_pos.0]; let account = &context.accounts[&self.cursor_pos.0];
@ -1500,11 +1519,6 @@ impl Component for PlainListing {
self.rows.row_updates.push(*env_hash); self.rows.row_updates.push(*env_hash);
self.set_dirty(true); self.set_dirty(true);
if self.unfocused() {
self.view
.process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context);
}
} }
UIEvent::ChangeMode(UIMode::Normal) => { UIEvent::ChangeMode(UIMode::Normal) => {
self.set_dirty(true); self.set_dirty(true);
@ -1539,6 +1553,7 @@ impl Component for PlainListing {
) { ) {
Ok(job) => { Ok(job) => {
let handle = context.accounts[&self.cursor_pos.0] let handle = context.accounts[&self.cursor_pos.0]
.main_loop_handler
.job_executor .job_executor
.spawn_specialized(job); .spawn_specialized(job);
self.search_job = Some((filter_term.to_string(), handle)); self.search_job = Some((filter_term.to_string(), handle));
@ -1583,23 +1598,16 @@ impl Component for PlainListing {
fn is_dirty(&self) -> bool { fn is_dirty(&self) -> bool {
match self.focus { match self.focus {
Focus::None => self.dirty, Focus::None => self.dirty,
Focus::Entry | Focus::EntryFullscreen => self.view.is_dirty(), Focus::Entry | Focus::EntryFullscreen => false,
} }
} }
fn set_dirty(&mut self, value: bool) { fn set_dirty(&mut self, value: bool) {
self.dirty = value; self.dirty = value;
if self.unfocused() {
self.view.set_dirty(value);
}
} }
fn shortcuts(&self, context: &Context) -> ShortcutMaps { fn shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map = if self.unfocused() { let mut map = ShortcutMaps::default();
self.view.shortcuts(context)
} else {
ShortcutMaps::default()
};
map.insert( map.insert(
Shortcuts::LISTING, Shortcuts::LISTING,

@ -102,8 +102,8 @@ macro_rules! row_attr {
}}; }};
} }
/// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the /// A list of all mail ([`Envelope`](melib::Envelope)s) in a `Mailbox`. On `\n` it opens the
/// `Envelope` content in a `MailView`. /// [`Envelope`](melib::Envelope) content in a [`MailView`].
#[derive(Debug)] #[derive(Debug)]
pub struct ThreadListing { pub struct ThreadListing {
/// (x, y, z): x is accounts, y is mailboxes, z is index inside a mailbox. /// (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 /// If we must redraw on next redraw event
dirty: bool, dirty: bool,
force_draw: bool, force_draw: bool,
/// If `self.view` is focused or not. /// If `self.view` is visible or not.
focus: Focus, focus: Focus,
initialised: bool, initialized: bool,
view: Option<Box<MailView>>,
modifier_active: bool, modifier_active: bool,
modifier_command: Option<Modifier>, modifier_command: Option<Modifier>,
movement: Option<PageMovement>, movement: Option<PageMovement>,
view_area: Option<Area>,
parent: ComponentId,
id: ComponentId, id: ComponentId,
} }
@ -171,6 +172,7 @@ impl MailListingTrait for ThreadListing {
/// mailbox the user has chosen. /// mailbox the user has chosen.
fn refresh_mailbox(&mut self, context: &mut Context, _force: bool) { fn refresh_mailbox(&mut self, context: &mut Context, _force: bool) {
self.set_dirty(true); self.set_dirty(true);
self.initialized = true;
if !(self.cursor_pos.0 == self.new_cursor_pos.0 if !(self.cursor_pos.0 == self.new_cursor_pos.0
&& self.cursor_pos.1 == self.new_cursor_pos.1) && 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)) { fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0); self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.focus = Focus::None; self.focus = Focus::None;
self.view = None;
self.rows.clear(); self.rows.clear();
self.initialised = false; self.initialized = false;
} }
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { 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); self.refresh_mailbox(context, false);
} }
@ -608,6 +611,10 @@ impl ListingTrait for ThreadListing {
let _account = &context.accounts[&self.cursor_pos.0]; let _account = &context.accounts[&self.cursor_pos.0];
} }
fn view_area(&self) -> Option<Area> {
self.view_area
}
fn unfocused(&self) -> bool { fn unfocused(&self) -> bool {
!matches!(self.focus, Focus::None) !matches!(self.focus, Focus::None)
} }
@ -632,7 +639,6 @@ impl ListingTrait for ThreadListing {
fn set_focus(&mut self, new_value: Focus, context: &mut Context) { fn set_focus(&mut self, new_value: Focus, context: &mut Context) {
match new_value { match new_value {
Focus::None => { Focus::None => {
self.view = None;
self.dirty = true; self.dirty = true;
/* If self.rows.row_updates is not empty and we exit a thread, the row_update /* 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. * events will be performed but the list will not be drawn.
@ -641,29 +647,34 @@ impl ListingTrait for ThreadListing {
self.force_draw = true; self.force_draw = true;
} }
Focus::Entry => { 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.force_draw = true;
self.dirty = 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 { self.kick_parent(
s.set_dirty(true); self.parent,
} ListingMessage::OpenEntryUnderCursor {
thread_hash,
env_hash,
show_thread: false,
},
context,
);
} }
} }
Focus::EntryFullscreen => { Focus::EntryFullscreen => {
if let Some(ref mut s) = self.view { self.dirty = true;
s.set_dirty(true);
}
} }
} }
self.focus = new_value; self.focus = new_value;
self.kick_parent(
self.parent,
ListingMessage::FocusUpdate { new_value },
context,
);
} }
fn focus(&self) -> Focus { fn focus(&self) -> Focus {
@ -678,26 +689,31 @@ impl fmt::Display for ThreadListing {
} }
impl ThreadListing { impl ThreadListing {
pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box<Self> { pub fn new(
parent: ComponentId,
coordinates: (AccountHash, MailboxHash),
context: &mut Context,
) -> Box<Self> {
Box::new(ThreadListing { Box::new(ThreadListing {
cursor_pos: (coordinates.0, MailboxHash::default(), 0), cursor_pos: (coordinates.0, MailboxHash::default(), 0),
new_cursor_pos: (coordinates.0, coordinates.1, 0), new_cursor_pos: (coordinates.0, coordinates.1, 0),
length: 0, length: 0,
sort: (Default::default(), Default::default()), sort: (Default::default(), Default::default()),
subsort: (Default::default(), Default::default()), subsort: (Default::default(), Default::default()),
color_cache: ColorCache::default(), color_cache: ColorCache::new(context, IndexStyle::Threaded),
data_columns: DataColumns::default(), data_columns: DataColumns::default(),
rows: RowsState::default(), rows: RowsState::default(),
search_job: None,
dirty: true, dirty: true,
force_draw: true, force_draw: true,
focus: Focus::None, focus: Focus::None,
view: None, initialized: false,
initialised: false,
movement: None, movement: None,
modifier_active: false, modifier_active: false,
modifier_command: None, modifier_command: None,
view_area: None,
parent,
id: ComponentId::default(), id: ComponentId::default(),
search_job: None,
}) })
} }
@ -1026,6 +1042,11 @@ impl ThreadListing {
impl Component for ThreadListing { impl Component for ThreadListing {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { 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 (upper_left, bottom_right) = area;
let rows = get_y(bottom_right) - get_y(upper_left) + 1; let rows = get_y(bottom_right) - get_y(upper_left) + 1;
@ -1210,12 +1231,6 @@ impl Component for ThreadListing {
return; return;
} }
if matches!(self.focus, Focus::EntryFullscreen) {
if let Some(v) = self.view.as_mut() {
return v.draw(grid, area, context);
}
}
if !self.unfocused() { if !self.unfocused() {
self.dirty = false; self.dirty = false;
/* Draw the entire list */ /* Draw the entire list */
@ -1290,27 +1305,7 @@ impl Component for ThreadListing {
.push_back((set_y(upper_left, mid), set_y(bottom_right, mid))); .push_back((set_y(upper_left, mid), set_y(bottom_right, mid)));
} }
if !self.dirty { self.view_area = (set_y(upper_left, mid + 1), bottom_right).into();
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.dirty = false; 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 { match *event {
UIEvent::ConfigReload { old_settings: _ } => { UIEvent::ConfigReload { old_settings: _ } => {
self.color_cache = ColorCache::new(context, IndexStyle::Threaded); self.color_cache = ColorCache::new(context, IndexStyle::Threaded);
@ -1411,15 +1400,6 @@ impl Component for ThreadListing {
} }
self.set_dirty(true); 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, _) => { UIEvent::EnvelopeRemove(ref env_hash, _) => {
if self.rows.contains_env(*env_hash) { if self.rows.contains_env(*env_hash) {
@ -1437,12 +1417,6 @@ impl Component for ThreadListing {
} }
self.set_dirty(true); 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) => { UIEvent::ChangeMode(UIMode::Normal) => {
self.set_dirty(true); self.set_dirty(true);
@ -1500,6 +1474,7 @@ impl Component for ThreadListing {
) { ) {
Ok(job) => { Ok(job) => {
let handle = context.accounts[&self.cursor_pos.0] let handle = context.accounts[&self.cursor_pos.0]
.main_loop_handler
.job_executor .job_executor
.spawn_specialized(job); .spawn_specialized(job);
self.search_job = Some((filter_term.to_string(), handle)); self.search_job = Some((filter_term.to_string(), handle));
@ -1547,27 +1522,17 @@ impl Component for ThreadListing {
fn is_dirty(&self) -> bool { fn is_dirty(&self) -> bool {
match self.focus { match self.focus {
Focus::None => self.dirty, Focus::None => self.dirty,
Focus::Entry => self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false), Focus::Entry => self.dirty,
Focus::EntryFullscreen => self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false), Focus::EntryFullscreen => false,
} }
} }
fn set_dirty(&mut self, value: bool) { fn set_dirty(&mut self, value: bool) {
if let Some(p) = self.view.as_mut() {
p.set_dirty(value);
};
self.dirty = value; self.dirty = value;
} }
fn shortcuts(&self, context: &Context) -> ShortcutMaps { fn shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map = if self.unfocused() { let mut map = ShortcutMaps::default();
self.view
.as_ref()
.map(|p| p.shortcuts(context))
.unwrap_or_default()
} else {
ShortcutMaps::default()
};
map.insert( map.insert(
Shortcuts::LISTING, Shortcuts::LISTING,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -24,6 +24,8 @@ use std::{
process::{Command, Stdio}, process::{Command, Stdio},
}; };
use melib::xdg_utils::query_default_app;
use super::*; use super::*;
#[derive(Debug)] #[derive(Debug)]

@ -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 <http://www.gnu.org/licenses/>.
*/
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<PendingReplyAction>,
},
LoadingBody {
handle: JoinHandle<Result<Vec<u8>>>,
pending_action: Option<PendingReplyAction>,
},
Error {
err: Error,
},
Loaded {
bytes: Vec<u8>,
env: Box<Envelope>,
env_view: Box<EnvelopeView>,
stack: Vec<Box<dyn Component>>,
},
}
impl MailViewState {
pub fn load_bytes(self_: &mut MailView, bytes: Vec<u8>, 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,
}
}
}

@ -37,6 +37,15 @@ struct ThreadEntry {
hidden: bool, hidden: bool,
heading: String, heading: String,
timestamp: UnixTimestamp, timestamp: UnixTimestamp,
mailview: Box<MailView>,
}
#[derive(Debug, Default, Copy, Clone)]
pub enum ThreadViewFocus {
#[default]
None,
Thread,
MailView,
} }
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -46,11 +55,9 @@ pub struct ThreadView {
expanded_pos: usize, expanded_pos: usize,
new_expanded_pos: usize, new_expanded_pos: usize,
reversed: bool, reversed: bool,
coordinates: (AccountHash, MailboxHash, usize), coordinates: (AccountHash, MailboxHash, EnvelopeHash),
thread_group: ThreadHash, thread_group: ThreadHash,
mailview: MailView, focus: ThreadViewFocus,
show_mailview: bool,
show_thread: bool,
entries: Vec<ThreadEntry>, entries: Vec<ThreadEntry>,
visible_entries: Vec<Vec<usize>>, visible_entries: Vec<Vec<usize>>,
indentation_colors: [ThemeAttribute; 6], indentation_colors: [ThemeAttribute; 6],
@ -64,24 +71,24 @@ pub struct ThreadView {
impl ThreadView { impl ThreadView {
/* /*
* coordinates: (account index, mailbox_hash, root set thread_node index) * @coordinates: (account index, mailbox_hash, root set thread_node index)
* expanded_hash: optional position of expanded entry when we render the * @expanded_hash: optional position of expanded entry when we render the
* threadview. Default expanded message is the last one. * ThreadView.
* context: current context * default: expanded message is the last one.
* @context: current context
*/ */
pub fn new( pub fn new(
coordinates: (AccountHash, MailboxHash, usize), coordinates: (AccountHash, MailboxHash, EnvelopeHash),
thread_group: ThreadHash, thread_group: ThreadHash,
expanded_hash: Option<ThreadNodeHash>, expanded_hash: Option<EnvelopeHash>,
context: &Context, focus: Option<ThreadViewFocus>,
context: &mut Context,
) -> Self { ) -> Self {
let mut view = ThreadView { let mut view = ThreadView {
reversed: false, reversed: false,
coordinates, coordinates,
thread_group, thread_group,
mailview: MailView::default(), focus: focus.unwrap_or_default(),
show_mailview: true,
show_thread: true,
entries: Vec::new(), entries: Vec::new(),
cursor_pos: 1, cursor_pos: 1,
new_cursor_pos: 0, new_cursor_pos: 0,
@ -103,7 +110,7 @@ impl ThreadView {
view view
} }
pub fn update(&mut self, context: &Context) { pub fn update(&mut self, context: &mut Context) {
if self.entries.is_empty() { if self.entries.is_empty() {
return; return;
} }
@ -122,7 +129,7 @@ impl ThreadView {
None 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); self.initiate(expanded_hash, context);
let mut old_cursor = 0; let mut old_cursor = 0;
@ -165,18 +172,25 @@ impl ThreadView {
self.set_dirty(true); self.set_dirty(true);
} }
fn initiate(&mut self, expanded_hash: Option<ThreadNodeHash>, context: &Context) { fn initiate(&mut self, expanded_hash: Option<EnvelopeHash>, context: &mut Context) {
#[inline(always)] #[inline(always)]
fn make_entry( fn make_entry(
i: (usize, ThreadNodeHash, usize), i: (usize, ThreadNodeHash, usize),
account_hash: AccountHash,
mailbox_hash: MailboxHash,
msg_hash: EnvelopeHash, msg_hash: EnvelopeHash,
seen: bool, seen: bool,
timestamp: UnixTimestamp, timestamp: UnixTimestamp,
context: &mut Context,
) -> ThreadEntry { ) -> ThreadEntry {
let (ind, _, _) = i; let (ind, _, _) = i;
ThreadEntry { ThreadEntry {
index: i, index: i,
indentation: ind, indentation: ind,
mailview: Box::new(MailView::new(
Some((account_hash, mailbox_hash, msg_hash)),
context,
)),
msg_hash, msg_hash,
seen, seen,
dirty: true, dirty: true,
@ -186,36 +200,43 @@ impl ThreadView {
} }
} }
let account = &context.accounts[&self.coordinates.0]; let collection = context.accounts[&self.coordinates.0].collection.clone();
let threads = account.collection.get_threads(self.coordinates.1); let threads = collection.get_threads(self.coordinates.1);
if !threads.groups.contains_key(&self.thread_group) { if !threads.groups.contains_key(&self.thread_group) {
return; return;
} }
let (account_hash, mailbox_hash, _) = self.coordinates;
let thread_iter = threads.thread_group_iter(self.thread_group); let thread_iter = threads.thread_group_iter(self.thread_group);
self.entries.clear(); self.entries.clear();
for (line, (ind, thread_node_hash)) in thread_iter.enumerate() { 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 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( make_entry(
(ind, thread_node_hash, line), (ind, thread_node_hash, line),
account_hash,
mailbox_hash,
msg_hash, msg_hash,
env_ref.is_seen(), is_seen,
env_ref.timestamp, timestamp,
context,
) )
} else { } else {
continue; continue;
}; };
self.entries.push(entry);
match expanded_hash { 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.new_expanded_pos = self.entries.len().saturating_sub(1);
self.expanded_pos = self.new_expanded_pos + 1; self.expanded_pos = self.new_expanded_pos + 1;
} }
_ => {} _ => {}
} }
self.entries.push(entry);
} }
if expanded_hash.is_none() { if expanded_hash.is_none() {
self.new_expanded_pos = self self.new_expanded_pos = self
@ -712,18 +733,21 @@ impl ThreadView {
.set_bg(theme_default.bg); .set_bg(theme_default.bg);
} }
match (self.show_mailview, self.show_thread) { match self.focus {
(true, true) => { ThreadViewFocus::None => {
self.draw_list( self.draw_list(
grid, grid,
(set_y(upper_left, y), set_x(bottom_right, mid - 1)), (set_y(upper_left, y), set_x(bottom_right, mid - 1)),
context, context,
); );
let upper_left = (mid + 1, get_y(upper_left) + y - 1); let upper_left = (mid + 1, get_y(upper_left) + y - 1);
self.mailview self.entries[self.new_expanded_pos].mailview.draw(
.draw(grid, (upper_left, bottom_right), context); grid,
(upper_left, bottom_right),
context,
);
} }
(false, true) => { ThreadViewFocus::Thread => {
clear_area( clear_area(
grid, grid,
((mid + 1, get_y(upper_left) + y - 1), bottom_right), ((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); self.draw_list(grid, (set_y(upper_left, y), bottom_right), context);
} }
(_, false) => { ThreadViewFocus::MailView => {
self.mailview.draw(grid, area, context); self.entries[self.new_expanded_pos]
.mailview
.draw(grid, area, context);
} }
} }
} }
@ -820,8 +846,8 @@ impl ThreadView {
); );
let (width, height) = self.content.size(); let (width, height) = self.content.size();
match (self.show_mailview, self.show_thread) { match self.focus {
(true, true) => { ThreadViewFocus::None => {
let area = (set_y(upper_left, y), set_y(bottom_right, mid)); let area = (set_y(upper_left, y), set_y(bottom_right, mid));
let upper_left = upper_left!(area); let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area); let bottom_right = bottom_right!(area);
@ -841,7 +867,7 @@ impl ThreadView {
); );
context.dirty_areas.push_back(area); context.dirty_areas.push_back(area);
} }
(false, true) => { ThreadViewFocus::Thread => {
let area = (set_y(upper_left, y), bottom_right); let area = (set_y(upper_left, y), bottom_right);
let upper_left = upper_left!(area); let upper_left = upper_left!(area);
@ -859,11 +885,11 @@ impl ThreadView {
); );
context.dirty_areas.push_back(area); context.dirty_areas.push_back(area);
} }
(_, false) => { /* show only envelope */ } ThreadViewFocus::MailView => { /* show only envelope */ }
} }
match (self.show_mailview, self.show_thread) { match self.focus {
(true, true) => { ThreadViewFocus::None => {
let area = (set_y(upper_left, mid), set_y(bottom_right, mid)); let area = (set_y(upper_left, mid), set_y(bottom_right, mid));
context.dirty_areas.push_back(area); context.dirty_areas.push_back(area);
for x in get_x(upper_left)..=get_x(bottom_right) { 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)); let area = (set_y(upper_left, y), set_y(bottom_right, mid - 1));
self.draw_list(grid, area, context); self.draw_list(grid, area, context);
self.mailview self.entries[self.new_expanded_pos].mailview.draw(
.draw(grid, (set_y(upper_left, mid + 1), bottom_right), context); grid,
(set_y(upper_left, mid + 1), bottom_right),
context,
);
} }
(false, true) => { ThreadViewFocus::Thread => {
self.dirty = true; self.dirty = true;
self.draw_list(grid, (set_y(upper_left, y), bottom_right), context); self.draw_list(grid, (set_y(upper_left, y), bottom_right), context);
} }
(_, false) => { ThreadViewFocus::MailView => {
self.mailview.draw(grid, area, context); 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 user has selected another mail to view, change to it */
if self.new_expanded_pos != self.expanded_pos { if self.new_expanded_pos != self.expanded_pos {
self.expanded_pos = self.new_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 { 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 { } else if total_cols >= self.content.size().0 + 74 {
self.draw_vert(grid, area, context); self.draw_vert(grid, area, context);
} else { } else {
@ -998,7 +1025,13 @@ impl Component for ThreadView {
return true; 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; return true;
} }
@ -1035,34 +1068,45 @@ impl Component for ThreadView {
self.movement = Some(PageMovement::PageDown(1)); self.movement = Some(PageMovement::PageDown(1));
self.dirty = true; 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.movement = Some(PageMovement::Home);
self.dirty = true; 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.movement = Some(PageMovement::End);
self.dirty = true; 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 { if self.entries.len() < 2 {
return true; return true;
} }
self.new_expanded_pos = self.current_pos(); 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); self.set_dirty(true);
return true; return true;
} }
UIEvent::Input(ref key) UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["toggle_mailview"]) => 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); self.set_dirty(true);
return true; return true;
} }
UIEvent::Input(ref key) UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["toggle_threadview"]) => 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); self.set_dirty(true);
return true; return true;
} }
@ -1070,7 +1114,7 @@ impl Component for ThreadView {
if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["reverse_thread_order"]) => if shortcut!(key == shortcuts[Shortcuts::THREAD_VIEW]["reverse_thread_order"]) =>
{ {
self.reversed = !self.reversed; 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.initiate(Some(expanded_hash), context);
self.dirty = true; self.dirty = true;
return true; return true;
@ -1107,7 +1151,7 @@ impl Component for ThreadView {
self.dirty = true; self.dirty = true;
return true; return true;
} }
UIEvent::Resize => { UIEvent::Resize | UIEvent::VisibilityChange(true) => {
self.set_dirty(true); self.set_dirty(true);
} }
UIEvent::EnvelopeRename(ref old_hash, ref new_hash) => { UIEvent::EnvelopeRename(ref old_hash, ref new_hash) => {
@ -1116,31 +1160,35 @@ impl Component for ThreadView {
if e.msg_hash == *old_hash { if e.msg_hash == *old_hash {
e.msg_hash = *new_hash; e.msg_hash = *new_hash;
let seen: bool = account.collection.get_env(*new_hash).is_seen(); let seen: bool = account.collection.get_env(*new_hash).is_seen();
if seen != e.seen {
self.dirty = true;
}
e.seen = seen; 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) => { UIEvent::EnvelopeUpdate(ref env_hash) => {
let account = &context.accounts[&self.coordinates.0]; let account = &context.accounts[&self.coordinates.0];
for e in self.entries.iter_mut() { for e in self.entries.iter_mut() {
if e.msg_hash == *env_hash { if e.msg_hash == *env_hash {
let seen: bool = account.collection.get_env(*env_hash).is_seen(); let seen: bool = account.collection.get_env(*env_hash).is_seen();
if seen != e.seen {
self.dirty = true;
}
e.seen = seen; 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; return true;
} }
} }
@ -1149,20 +1197,41 @@ impl Component for ThreadView {
} }
fn is_dirty(&self) -> bool { 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) { fn set_dirty(&mut self, value: bool) {
self.dirty = value; self.dirty = value;
self.mailview.set_dirty(value); self.entries[self.new_expanded_pos]
.mailview
.set_dirty(value);
} }
fn shortcuts(&self, context: &Context) -> ShortcutMaps { 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( map.insert(
Shortcuts::THREAD_VIEW, 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 map

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String>,
pub html_filter: Option<String>,
pub url_launcher: Option<String>,
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<String>,
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<UIDialog<Option<Charset>>>),
Forced(Charset),
}
impl Into<Option<Charset>> for &ForceCharset {
fn into(self) -> Option<Charset> {
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<Attachment>,
shown_display: usize,
display: Vec<AttachmentDisplay>,
},
InlineText {
inner: Box<Attachment>,
comment: Option<String>,
text: String,
},
InlineOther {
inner: Box<Attachment>,
},
Attachment {
inner: Box<Attachment>,
},
SignedPending {
inner: Box<Attachment>,
display: Vec<AttachmentDisplay>,
handle: JoinHandle<Result<()>>,
job_id: JobId,
},
SignedFailed {
inner: Box<Attachment>,
display: Vec<AttachmentDisplay>,
error: Error,
},
SignedUnverified {
inner: Box<Attachment>,
display: Vec<AttachmentDisplay>,
},
SignedVerified {
inner: Box<Attachment>,
display: Vec<AttachmentDisplay>,
description: String,
},
EncryptedPending {
inner: Box<Attachment>,
handle: JoinHandle<Result<(DecryptionMetadata, Vec<u8>)>>,
},
EncryptedFailed {
inner: Box<Attachment>,
error: Error,
},
EncryptedSuccess {
inner: Box<Attachment>,
plaintext: Box<Attachment>,
plaintext_display: Vec<AttachmentDisplay>,
description: String,
},
}

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
);
}
}

@ -50,7 +50,11 @@ mod dbus {
impl DbusNotifications { impl DbusNotifications {
pub fn new(context: &Context) -> Self { pub fn new(context: &Context) -> Self {
DbusNotifications { 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(), id: ComponentId::default(),
} }
} }

@ -795,6 +795,57 @@ impl Component for StatusBar {
fn can_quit_cleanly(&mut self, context: &Context) -> bool { fn can_quit_cleanly(&mut self, context: &Context) -> bool {
self.container.can_quit_cleanly(context) self.container.can_quit_cleanly(context)
} }
fn attributes(&self) -> &'static ComponentAttr {
&ComponentAttr::CONTAINER
}
fn children(&self) -> IndexMap<ComponentId, &dyn Component> {
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<ComponentId, &mut dyn Component> {
IndexMap::default()
}
fn realize(&self, parent: Option<ComponentId>, 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)] #[derive(Debug)]
@ -837,6 +888,7 @@ impl Tabbed {
.extend(ret.shortcuts(context).into_iter()); .extend(ret.shortcuts(context).into_iter());
ret ret
} }
fn draw_tabs(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { fn draw_tabs(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
let upper_left = upper_left!(area); let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area); let bottom_right = bottom_right!(area);
@ -911,7 +963,8 @@ impl Tabbed {
context.dirty_areas.push_back(area); context.dirty_areas.push_back(area);
} }
pub fn add_component(&mut self, new: Box<dyn Component>) { pub fn add_component(&mut self, new: Box<dyn Component>, context: &mut Context) {
new.realize(self.id().into(), context);
self.children.push(new); self.children.push(new);
} }
} }
@ -1386,7 +1439,7 @@ impl Component for Tabbed {
return true; return true;
} }
UIEvent::Action(Tab(New(ref mut e))) if e.is_some() => { 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] self.children[self.cursor_pos]
.process_event(&mut UIEvent::VisibilityChange(false), context); .process_event(&mut UIEvent::VisibilityChange(false), context);
self.cursor_pos = self.children.len() - 1; 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) { if let Some(c_idx) = self.children.iter().position(|x| x.id() == *id) {
self.children[c_idx] self.children[c_idx]
.process_event(&mut UIEvent::VisibilityChange(false), context); .process_event(&mut UIEvent::VisibilityChange(false), context);
self.children[c_idx].unrealize(context);
self.children.remove(c_idx); self.children.remove(c_idx);
self.cursor_pos = 0; self.cursor_pos = 0;
self.set_dirty(true); self.set_dirty(true);
@ -1555,6 +1609,41 @@ impl Component for Tabbed {
} }
true true
} }
fn attributes(&self) -> &'static ComponentAttr {
&ComponentAttr::CONTAINER
}
fn children(&self) -> IndexMap<ComponentId, &dyn Component> {
let mut ret = IndexMap::default();
for c in &self.children {
ret.insert(c.id(), c as _);
}
ret
}
fn children_mut(&mut self) -> IndexMap<ComponentId, &mut dyn Component> {
IndexMap::default()
}
fn realize(&self, parent: Option<ComponentId>, 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)] #[derive(Debug, Clone, PartialEq, Eq)]

@ -124,7 +124,7 @@ impl<T: 'static + PartialEq + Debug + Clone + Sync + Send> Component for UIDialo
self.done = true; self.done = true;
if let Some(event) = self.done() { if let Some(event) = self.done() {
context.replies.push_back(event); context.replies.push_back(event);
context.replies.push_back(UIEvent::ComponentKill(self.id)); self.unrealize(context);
} }
return true; return true;
} }
@ -160,7 +160,7 @@ impl<T: 'static + PartialEq + Debug + Clone + Sync + Send> Component for UIDialo
self.done = true; self.done = true;
if let Some(event) = self.done() { if let Some(event) = self.done() {
context.replies.push_back(event); context.replies.push_back(event);
context.replies.push_back(UIEvent::ComponentKill(self.id)); self.unrealize(context);
} }
return true; return true;
} }
@ -171,7 +171,7 @@ impl<T: 'static + PartialEq + Debug + Clone + Sync + Send> Component for UIDialo
self.done = true; self.done = true;
if let Some(event) = self.done() { if let Some(event) = self.done() {
context.replies.push_back(event); context.replies.push_back(event);
context.replies.push_back(UIEvent::ComponentKill(self.id)); self.unrealize(context);
} }
return true; return true;
} }
@ -182,7 +182,7 @@ impl<T: 'static + PartialEq + Debug + Clone + Sync + Send> Component for UIDialo
self.done = true; self.done = true;
if let Some(event) = self.done() { if let Some(event) = self.done() {
context.replies.push_back(event); context.replies.push_back(event);
context.replies.push_back(UIEvent::ComponentKill(self.id)); self.unrealize(context);
} }
return true; return true;
} }
@ -450,7 +450,7 @@ impl Component for UIConfirmationDialog {
self.done = true; self.done = true;
if let Some(event) = self.done() { if let Some(event) = self.done() {
context.replies.push_back(event); context.replies.push_back(event);
context.replies.push_back(UIEvent::ComponentKill(self.id)); self.unrealize(context);
} }
return true; return true;
} }
@ -486,7 +486,7 @@ impl Component for UIConfirmationDialog {
self.done = true; self.done = true;
if let Some(event) = self.done() { if let Some(event) = self.done() {
context.replies.push_back(event); context.replies.push_back(event);
context.replies.push_back(UIEvent::ComponentKill(self.id)); self.unrealize(context);
} }
return true; return true;
} }
@ -497,7 +497,7 @@ impl Component for UIConfirmationDialog {
self.done = true; self.done = true;
if let Some(event) = self.done() { if let Some(event) = self.done() {
context.replies.push_back(event); context.replies.push_back(event);
context.replies.push_back(UIEvent::ComponentKill(self.id)); self.unrealize(context);
} }
return true; return true;
} }
@ -508,7 +508,7 @@ impl Component for UIConfirmationDialog {
self.done = true; self.done = true;
if let Some(event) = self.done() { if let Some(event) = self.done() {
context.replies.push_back(event); context.replies.push_back(event);
context.replies.push_back(UIEvent::ComponentKill(self.id)); self.unrealize(context);
} }
return true; return true;
} }

@ -41,6 +41,12 @@ pub struct Pager {
colors: ThemeAttribute, colors: ThemeAttribute,
initialised: bool, initialised: bool,
show_scrollbar: 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<CellBuffer>)>, filtered_content: Option<(String, Result<CellBuffer>)>,
text_lines: Vec<String>, text_lines: Vec<String>,
line_breaker: LineBreakText, line_breaker: LineBreakText,
@ -525,9 +531,6 @@ impl Component for Pager {
clear_area(grid, area, crate::conf::value(context, "theme_default")); clear_area(grid, area, crate::conf::value(context, "theme_default"));
let (mut cols, mut rows) = (width!(area), height!(area)); 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() { let (has_more_lines, (width, height)) = if self.filtered_content.is_some() {
(false, (self.width, self.height)) (false, (self.width, self.height))
} else { } else {
@ -536,6 +539,14 @@ impl Component for Pager {
(self.line_breaker.width().unwrap_or(cols), self.height), (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 { if self.show_scrollbar && rows < height {
cols -= 1; cols -= 1;
rows -= 1; rows -= 1;
@ -655,56 +666,60 @@ impl Component for Pager {
self.set_dirty(true); self.set_dirty(true);
} }
UIEvent::Input(ref key) 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.movement = Some(PageMovement::Up(1));
self.dirty = true; self.dirty = true;
return true; return true;
} }
UIEvent::Input(ref key) 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.movement = Some(PageMovement::Down(1));
self.dirty = true; self.dirty = true;
return true; return true;
} }
UIEvent::Input(ref key) 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; self.dirty = true;
return true; return true;
} }
UIEvent::Input(ref key) 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; self.dirty = true;
return true; return true;
} }
UIEvent::Input(ref key) UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::PAGER]["page_up"]) => {
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) => self.movement = Some(PageMovement::PageUp(1));
{
self.movement = Some(PageMovement::Left(1));
self.dirty = true; self.dirty = true;
return true; return true;
} }
UIEvent::Input(ref key) 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; self.dirty = true;
return true; return true;
} }
UIEvent::Input(ref key) if shortcut!(key == shortcuts[Shortcuts::PAGER]["page_up"]) => { UIEvent::Input(ref key)
self.movement = Some(PageMovement::PageUp(1)); if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) =>
{
self.movement = Some(PageMovement::Home);
self.dirty = true; self.dirty = true;
return true; return true;
} }
UIEvent::Input(ref key) 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; self.dirty = true;
return true; return true;
} }
@ -802,7 +817,10 @@ impl Component for Pager {
} }
UIEvent::Resize => { UIEvent::Resize => {
self.initialised = false; self.initialised = false;
self.dirty = true; self.set_dirty(true);
}
UIEvent::VisibilityChange(true) => {
self.set_dirty(true);
} }
UIEvent::VisibilityChange(false) => { UIEvent::VisibilityChange(false) => {
context context

@ -1122,6 +1122,7 @@ impl ProgressSpinner {
.unwrap_or(0); .unwrap_or(0);
let interval = Self::KINDS[kind].0; let interval = Self::KINDS[kind].0;
let timer = context let timer = context
.main_loop_handler
.job_executor .job_executor
.clone() .clone()
.create_timer(interval, interval); .create_timer(interval, interval);

@ -37,7 +37,6 @@ use std::{
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
use crossbeam::channel::Sender;
use futures::{ use futures::{
future::FutureExt, future::FutureExt,
stream::{Stream, StreamExt}, stream::{Stream, StreamExt},
@ -57,9 +56,9 @@ use smallvec::SmallVec;
use super::{AccountConf, FileMailboxConf}; use super::{AccountConf, FileMailboxConf};
use crate::{ use crate::{
command::actions::AccountAction, command::actions::AccountAction,
jobs::{JobExecutor, JobId, JoinHandle}, jobs::{JobId, JoinHandle},
types::UIEvent::{self, EnvelopeRemove, EnvelopeRename, EnvelopeUpdate, Notification}, types::UIEvent::{self, EnvelopeRemove, EnvelopeRename, EnvelopeUpdate, Notification},
StatusEvent, ThreadEvent, MainLoopHandler, StatusEvent, ThreadEvent,
}; };
#[macro_export] #[macro_export]
@ -177,10 +176,9 @@ pub struct Account {
pub settings: AccountConf, pub settings: AccountConf,
pub backend: Arc<RwLock<Box<dyn MailBackend>>>, pub backend: Arc<RwLock<Box<dyn MailBackend>>>,
pub job_executor: Arc<JobExecutor>, pub main_loop_handler: MainLoopHandler,
pub active_jobs: HashMap<JobId, JobRequest>, pub active_jobs: HashMap<JobId, JobRequest>,
pub active_job_instants: BTreeMap<std::time::Instant, JobId>, pub active_job_instants: BTreeMap<std::time::Instant, JobId>,
pub sender: Sender<ThreadEvent>,
pub event_queue: VecDeque<(MailboxHash, RefreshEvent)>, pub event_queue: VecDeque<(MailboxHash, RefreshEvent)>,
pub backend_capabilities: MailBackendCapabilities, pub backend_capabilities: MailBackendCapabilities,
} }
@ -434,8 +432,7 @@ impl Account {
name: String, name: String,
mut settings: AccountConf, mut settings: AccountConf,
map: &Backends, map: &Backends,
job_executor: Arc<JobExecutor>, main_loop_handler: MainLoopHandler,
sender: Sender<ThreadEvent>,
event_consumer: BackendEventConsumer, event_consumer: BackendEventConsumer,
) -> Result<Self> { ) -> Result<Self> {
let s = settings.clone(); let s = settings.clone();
@ -490,18 +487,20 @@ impl Account {
if let Ok(mailboxes_job) = backend.mailboxes() { if let Ok(mailboxes_job) = backend.mailboxes() {
if let Ok(online_job) = backend.is_online() { if let Ok(online_job) = backend.is_online() {
let handle = if backend.capabilities().is_async { 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 { } 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; let job_id = handle.job_id;
active_jobs.insert(job_id, JobRequest::Mailboxes { handle }); active_jobs.insert(job_id, JobRequest::Mailboxes { handle });
active_job_instants.insert(std::time::Instant::now(), job_id); active_job_instants.insert(std::time::Instant::now(), job_id);
sender main_loop_handler.send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( StatusEvent::NewJob(job_id),
StatusEvent::NewJob(job_id), )));
)))
.unwrap();
} }
} }
@ -509,15 +508,13 @@ impl Account {
if settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 { if settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
let db_path = match crate::sqlite3::db_path() { let db_path = match crate::sqlite3::db_path() {
Err(err) => { Err(err) => {
sender main_loop_handler.send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( StatusEvent::DisplayMessage(format!(
StatusEvent::DisplayMessage(format!( "Error with setting up an sqlite3 search database for account \
"Error with setting up an sqlite3 search database for account \
`{}`: {}", `{}`: {}",
name, err name, err
)), )),
))) )));
.unwrap();
None None
} }
Ok(path) => Some(path), Ok(path) => Some(path),
@ -529,11 +526,9 @@ impl Account {
one will be created.", one will be created.",
name name
); );
sender main_loop_handler.send(ThreadEvent::UIEvent(UIEvent::Action(
.send(ThreadEvent::UIEvent(UIEvent::Action( (name.clone(), AccountAction::ReIndex).into(),
(name.clone(), AccountAction::ReIndex).into(), )));
)))
.unwrap();
} }
} }
} }
@ -553,8 +548,7 @@ impl Account {
sent_mailbox: Default::default(), sent_mailbox: Default::default(),
collection: backend.collection(), collection: backend.collection(),
settings, settings,
sender, main_loop_handler,
job_executor,
active_jobs, active_jobs,
active_job_instants, active_job_instants,
event_queue: VecDeque::with_capacity(8), event_queue: VecDeque::with_capacity(8),
@ -643,15 +637,14 @@ impl Account {
&self.name, &self.name,
missing_mailbox, missing_mailbox,
); );
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( .send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!( StatusEvent::DisplayMessage(format!(
"Account `{}` mailbox `{}` configured but not present in account's \ "Account `{}` mailbox `{}` configured but not present in account's \
mailboxes. Is it misspelled?", mailboxes. Is it misspelled?",
&self.name, missing_mailbox, &self.name, missing_mailbox,
)), )),
))) )));
.unwrap();
} }
if !mailbox_conf_hash_set.is_empty() { if !mailbox_conf_hash_set.is_empty() {
let mut mailbox_comma_sep_list_string = mailbox_entries let mut mailbox_comma_sep_list_string = mailbox_entries
@ -671,14 +664,13 @@ impl Account {
&self.name, &self.name,
mailbox_comma_sep_list_string, mailbox_comma_sep_list_string,
); );
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( .send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!( StatusEvent::DisplayMessage(format!(
"Account `{}` has the following mailboxes: [{}]", "Account `{}` has the following mailboxes: [{}]",
&self.name, mailbox_comma_sep_list_string, &self.name, mailbox_comma_sep_list_string,
)), )),
))) )));
.unwrap();
} }
let mut tree: Vec<MailboxNode> = Vec::new(); let mut tree: Vec<MailboxNode> = Vec::new();
@ -697,16 +689,19 @@ impl Account {
if let Ok(mailbox_job) = self.backend.write().unwrap().fetch(*h) { if let Ok(mailbox_job) = self.backend.write().unwrap().fetch(*h) {
let mailbox_job = mailbox_job.into_future(); let mailbox_job = mailbox_job.into_future();
let handle = if self.backend_capabilities.is_async { 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 { } else {
self.job_executor.spawn_blocking(mailbox_job) self.main_loop_handler
.job_executor
.spawn_blocking(mailbox_job)
}; };
let job_id = handle.job_id; let job_id = handle.job_id;
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( .send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::NewJob(job_id), StatusEvent::NewJob(job_id),
))) )));
.unwrap();
self.active_jobs.insert( self.active_jobs.insert(
job_id, job_id,
JobRequest::Fetch { JobRequest::Fetch {
@ -770,7 +765,8 @@ impl Account {
); );
} }
Ok(job) => { Ok(job) => {
let handle = self.job_executor.spawn_blocking(job); let handle =
self.main_loop_handler.job_executor.spawn_blocking(job);
self.insert_job( self.insert_job(
handle.job_id, handle.job_id,
JobRequest::Generic { JobRequest::Generic {
@ -816,7 +812,8 @@ impl Account {
) )
}) { }) {
Ok(job) => { Ok(job) => {
let handle = self.job_executor.spawn_blocking(job); let handle =
self.main_loop_handler.job_executor.spawn_blocking(job);
self.insert_job( self.insert_job(
handle.job_id, handle.job_id,
JobRequest::Generic { JobRequest::Generic {
@ -868,7 +865,8 @@ impl Account {
); );
} }
Ok(job) => { Ok(job) => {
let handle = self.job_executor.spawn_blocking(job); let handle =
self.main_loop_handler.job_executor.spawn_blocking(job);
self.insert_job( self.insert_job(
handle.job_id, handle.job_id,
JobRequest::Generic { JobRequest::Generic {
@ -908,11 +906,13 @@ impl Account {
}; };
#[cfg(feature = "sqlite3")] #[cfg(feature = "sqlite3")]
if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 { if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
let handle = self.job_executor.spawn_blocking(crate::sqlite3::insert( let handle = self.main_loop_handler.job_executor.spawn_blocking(
(*envelope).clone(), crate::sqlite3::insert(
self.backend.clone(), (*envelope).clone(),
self.name.clone(), self.backend.clone(),
)); self.name.clone(),
),
);
self.insert_job( self.insert_job(
handle.job_id, handle.job_id,
JobRequest::Generic { JobRequest::Generic {
@ -1027,8 +1027,7 @@ impl Account {
Some(format!("{} watcher exited with error", &self.name)), Some(format!("{} watcher exited with error", &self.name)),
e.to_string(), e.to_string(),
Some(crate::types::NotificationType::Error(err.kind)), Some(crate::types::NotificationType::Error(err.kind)),
))) )));
.expect("Could not send event on main channel");
*/ */
self.watch(); self.watch();
return Some(Notification( return Some(Notification(
@ -1057,24 +1056,26 @@ impl Account {
.stdout(std::process::Stdio::null()) .stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped())
.spawn()?; .spawn()?;
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( .send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!("Running command {}", refresh_command)), StatusEvent::DisplayMessage(format!("Running command {}", refresh_command)),
))) )));
.unwrap(); self.main_loop_handler
self.sender
.send(ThreadEvent::UIEvent(UIEvent::Fork( .send(ThreadEvent::UIEvent(UIEvent::Fork(
crate::ForkType::Generic(child), crate::ForkType::Generic(child),
))) )));
.unwrap();
return Ok(()); return Ok(());
} }
let refresh_job = self.backend.write().unwrap().refresh(mailbox_hash); let refresh_job = self.backend.write().unwrap().refresh(mailbox_hash);
if let Ok(refresh_job) = refresh_job { if let Ok(refresh_job) = refresh_job {
let handle = if self.backend_capabilities.is_async { 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 { } else {
self.job_executor.spawn_blocking(refresh_job) self.main_loop_handler
.job_executor
.spawn_blocking(refresh_job)
}; };
self.insert_job( self.insert_job(
handle.job_id, handle.job_id,
@ -1096,9 +1097,9 @@ impl Account {
match self.backend.read().unwrap().watch() { match self.backend.read().unwrap().watch() {
Ok(fut) => { Ok(fut) => {
let handle = if self.backend_capabilities.is_async { let handle = if self.backend_capabilities.is_async {
self.job_executor.spawn_specialized(fut) self.main_loop_handler.job_executor.spawn_specialized(fut)
} else { } else {
self.job_executor.spawn_blocking(fut) self.main_loop_handler.job_executor.spawn_blocking(fut)
}; };
self.active_jobs self.active_jobs
.insert(handle.job_id, JobRequest::Watch { handle }); .insert(handle.job_id, JobRequest::Watch { handle });
@ -1107,14 +1108,13 @@ impl Account {
if e.kind == ErrorKind::NotSupported || e.kind == ErrorKind::NotImplemented => { if e.kind == ErrorKind::NotSupported || e.kind == ErrorKind::NotImplemented => {
} }
Err(e) => { Err(e) => {
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( .send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!( StatusEvent::DisplayMessage(format!(
"Account `{}` watch action returned error: {}", "Account `{}` watch action returned error: {}",
&self.name, e &self.name, e
)), )),
))) )));
.unwrap();
} }
} }
} }
@ -1174,9 +1174,13 @@ impl Account {
Ok(mailbox_job) => { Ok(mailbox_job) => {
let mailbox_job = mailbox_job.into_future(); let mailbox_job = mailbox_job.into_future();
let handle = if self.backend_capabilities.is_async { 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 { } else {
self.job_executor.spawn_blocking(mailbox_job) self.main_loop_handler
.job_executor
.spawn_blocking(mailbox_job)
}; };
self.insert_job( self.insert_job(
handle.job_id, handle.job_id,
@ -1192,9 +1196,8 @@ impl Account {
.and_modify(|entry| { .and_modify(|entry| {
entry.status = MailboxStatus::Failed(err); entry.status = MailboxStatus::Failed(err);
}); });
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::StartupCheck(mailbox_hash))) .send(ThreadEvent::UIEvent(UIEvent::StartupCheck(mailbox_hash)));
.unwrap();
} }
} }
} }
@ -1266,9 +1269,9 @@ impl Account {
.save(bytes.to_vec(), mailbox_hash, flags)?; .save(bytes.to_vec(), mailbox_hash, flags)?;
let handle = if self.backend_capabilities.is_async { let handle = if self.backend_capabilities.is_async {
self.job_executor.spawn_specialized(job) self.main_loop_handler.job_executor.spawn_specialized(job)
} else { } else {
self.job_executor.spawn_blocking(job) self.main_loop_handler.job_executor.spawn_blocking(job)
}; };
self.insert_job( self.insert_job(
handle.job_id, handle.job_id,
@ -1335,11 +1338,14 @@ impl Account {
} }
#[cfg(feature = "smtp")] #[cfg(feature = "smtp")]
SendMail::Smtp(conf) => { SendMail::Smtp(conf) => {
let handle = self.job_executor.spawn_specialized(async move { let handle = self
let mut smtp_connection = .main_loop_handler
melib::smtp::SmtpConnection::new_connection(conf).await?; .job_executor
smtp_connection.mail_transaction(&message, None).await .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 { if complete_in_background {
self.insert_job(handle.job_id, JobRequest::SendMessageBackground { handle }); self.insert_job(handle.job_id, JobRequest::SendMessageBackground { handle });
return Ok(None); return Ok(None);
@ -1357,9 +1363,9 @@ impl Account {
.submit(message.into_bytes(), None, None)?; .submit(message.into_bytes(), None, None)?;
let handle = if self.backend_capabilities.is_async { let handle = if self.backend_capabilities.is_async {
self.job_executor.spawn_specialized(job) self.main_loop_handler.job_executor.spawn_specialized(job)
} else { } 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 }); self.insert_job(handle.job_id, JobRequest::SendMessageBackground { handle });
return Ok(None); return Ok(None);
@ -1478,9 +1484,9 @@ impl Account {
.unwrap() .unwrap()
.create_mailbox(path.to_string())?; .create_mailbox(path.to_string())?;
let handle = if self.backend_capabilities.is_async { let handle = if self.backend_capabilities.is_async {
self.job_executor.spawn_specialized(job) self.main_loop_handler.job_executor.spawn_specialized(job)
} else { } 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 }); self.insert_job(handle.job_id, JobRequest::CreateMailbox { path, handle });
Ok(()) Ok(())
@ -1493,9 +1499,9 @@ impl Account {
let mailbox_hash = self.mailbox_by_path(&path)?; let mailbox_hash = self.mailbox_by_path(&path)?;
let job = self.backend.write().unwrap().delete_mailbox(mailbox_hash)?; let job = self.backend.write().unwrap().delete_mailbox(mailbox_hash)?;
let handle = if self.backend_capabilities.is_async { let handle = if self.backend_capabilities.is_async {
self.job_executor.spawn_specialized(job) self.main_loop_handler.job_executor.spawn_specialized(job)
} else { } else {
self.job_executor.spawn_blocking(job) self.main_loop_handler.job_executor.spawn_blocking(job)
}; };
self.insert_job( self.insert_job(
handle.job_id, handle.job_id,
@ -1514,9 +1520,9 @@ impl Account {
.unwrap() .unwrap()
.set_mailbox_subscription(mailbox_hash, true)?; .set_mailbox_subscription(mailbox_hash, true)?;
let handle = if self.backend_capabilities.is_async { let handle = if self.backend_capabilities.is_async {
self.job_executor.spawn_specialized(job) self.main_loop_handler.job_executor.spawn_specialized(job)
} else { } else {
self.job_executor.spawn_blocking(job) self.main_loop_handler.job_executor.spawn_blocking(job)
}; };
self.insert_job( self.insert_job(
handle.job_id, handle.job_id,
@ -1536,9 +1542,9 @@ impl Account {
.unwrap() .unwrap()
.set_mailbox_subscription(mailbox_hash, false)?; .set_mailbox_subscription(mailbox_hash, false)?;
let handle = if self.backend_capabilities.is_async { let handle = if self.backend_capabilities.is_async {
self.job_executor.spawn_specialized(job) self.main_loop_handler.job_executor.spawn_specialized(job)
} else { } else {
self.job_executor.spawn_blocking(job) self.main_loop_handler.job_executor.spawn_blocking(job)
}; };
self.insert_job( self.insert_job(
handle.job_id, handle.job_id,
@ -1587,9 +1593,13 @@ impl Account {
let online_job = self.backend.read().unwrap().is_online(); let online_job = self.backend.read().unwrap().is_online();
if let Ok(online_job) = online_job { if let Ok(online_job) = online_job {
let handle = if self.backend_capabilities.is_async { 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 { } 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.insert_job(handle.job_id, JobRequest::IsOnline { handle });
} }
@ -1643,11 +1653,10 @@ impl Account {
} }
pub fn process_event(&mut self, job_id: &JobId) -> bool { pub fn process_event(&mut self, job_id: &JobId) -> bool {
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( .send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::JobFinished(*job_id), StatusEvent::JobFinished(*job_id),
))) )));
.unwrap();
if let Some(mut job) = self.active_jobs.remove(job_id) { if let Some(mut job) = self.active_jobs.remove(job_id) {
match job { match job {
@ -1655,32 +1664,36 @@ impl Account {
if let Ok(Some(mailboxes)) = handle.chan.try_recv() { if let Ok(Some(mailboxes)) = handle.chan.try_recv() {
if let Err(err) = mailboxes.and_then(|mailboxes| self.init(mailboxes)) { if let Err(err) = mailboxes.and_then(|mailboxes| self.init(mailboxes)) {
if err.kind.is_authentication() { if err.kind.is_authentication() {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::Notification( UIEvent::Notification(
Some(format!("{}: authentication error", &self.name)), Some(format!("{}: authentication error", &self.name)),
err.to_string(), err.to_string(),
Some(crate::types::NotificationType::Error(err.kind)), Some(crate::types::NotificationType::Error(err.kind)),
))) ),
.expect("Could not send event on main channel"); ));
self.is_online = Err(err); self.is_online = Err(err);
return true; return true;
} }
let mailboxes_job = self.backend.read().unwrap().mailboxes(); let mailboxes_job = self.backend.read().unwrap().mailboxes();
if let Ok(mailboxes_job) = mailboxes_job { if let Ok(mailboxes_job) = mailboxes_job {
let handle = if self.backend_capabilities.is_async { 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 { } 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 }); self.insert_job(handle.job_id, JobRequest::Mailboxes { handle });
}; };
} else { } else {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange( UIEvent::AccountStatusChange(
self.hash, self.hash,
Some("Loaded mailboxes.".into()), Some("Loaded mailboxes.".into()),
))) ),
.unwrap(); ));
} }
} }
} }
@ -1705,40 +1718,38 @@ impl Account {
.and_modify(|entry| { .and_modify(|entry| {
entry.status = MailboxStatus::Available; entry.status = MailboxStatus::Available;
}); });
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::MailboxUpdate(( UIEvent::MailboxUpdate((self.hash, mailbox_hash)),
self.hash, ));
mailbox_hash,
))))
.unwrap();
return true; return true;
} }
Ok(Some((Some(Err(err)), _))) => { Ok(Some((Some(Err(err)), _))) => {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::Notification( UIEvent::Notification(
Some(format!("{}: could not fetch mailbox", &self.name)), Some(format!("{}: could not fetch mailbox", &self.name)),
err.to_string(), err.to_string(),
Some(crate::types::NotificationType::Error(err.kind)), Some(crate::types::NotificationType::Error(err.kind)),
))) ),
.expect("Could not send event on main channel"); ));
self.mailbox_entries self.mailbox_entries
.entry(mailbox_hash) .entry(mailbox_hash)
.and_modify(|entry| { .and_modify(|entry| {
entry.status = MailboxStatus::Failed(err); entry.status = MailboxStatus::Failed(err);
}); });
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::MailboxUpdate(( UIEvent::MailboxUpdate((self.hash, mailbox_hash)),
self.hash, ));
mailbox_hash,
))))
.unwrap();
return true; return true;
} }
Ok(Some((Some(Ok(payload)), rest))) => { Ok(Some((Some(Ok(payload)), rest))) => {
let handle = if self.backend_capabilities.is_async { 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 { } else {
self.job_executor.spawn_blocking(rest.into_future()) self.main_loop_handler
.job_executor
.spawn_blocking(rest.into_future())
}; };
self.insert_job( self.insert_job(
handle.job_id, handle.job_id,
@ -1756,29 +1767,22 @@ impl Account {
.merge(envelopes, mailbox_hash, self.sent_mailbox) .merge(envelopes, mailbox_hash, self.sent_mailbox)
{ {
for f in updated_mailboxes { for f in updated_mailboxes {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::MailboxUpdate(( UIEvent::MailboxUpdate((self.hash, f)),
self.hash, f, ));
))))
.unwrap();
} }
} }
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::MailboxUpdate(( UIEvent::MailboxUpdate((self.hash, mailbox_hash)),
self.hash, ));
mailbox_hash,
))))
.unwrap();
} }
} }
} }
JobRequest::IsOnline { ref mut handle, .. } => { JobRequest::IsOnline { ref mut handle, .. } => {
if let Ok(Some(is_online)) = handle.chan.try_recv() { if let Ok(Some(is_online)) = handle.chan.try_recv() {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange( UIEvent::AccountStatusChange(self.hash, None),
self.hash, None, ));
)))
.unwrap();
if is_online.is_ok() { if is_online.is_ok() {
if self.is_online.is_err() if self.is_online.is_err()
&& !self && !self
@ -1798,9 +1802,13 @@ impl Account {
let online_job = self.backend.read().unwrap().is_online(); let online_job = self.backend.read().unwrap().is_online();
if let Ok(online_job) = online_job { if let Ok(online_job) = online_job {
let handle = if self.backend_capabilities.is_async { 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 { } 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.insert_job(handle.job_id, JobRequest::IsOnline { handle });
}; };
@ -1829,11 +1837,9 @@ impl Account {
.is_authentication()) .is_authentication())
{ {
self.is_online = Ok(()); self.is_online = Ok(());
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange( UIEvent::AccountStatusChange(self.hash, None),
self.hash, None, ));
)))
.unwrap();
} }
} }
Ok(Some(Err(err))) => { Ok(Some(Err(err))) => {
@ -1841,31 +1847,32 @@ impl Account {
let online_job = self.backend.read().unwrap().is_online(); let online_job = self.backend.read().unwrap().is_online();
if let Ok(online_job) = online_job { if let Ok(online_job) = online_job {
let handle = if self.backend_capabilities.is_async { 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 { } 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.insert_job(handle.job_id, JobRequest::IsOnline { handle });
}; };
} }
self.is_online = Err(err); self.is_online = Err(err);
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange( UIEvent::AccountStatusChange(self.hash, None),
self.hash, None, ));
)))
.unwrap();
} }
} }
} }
JobRequest::SetFlags { ref mut handle, .. } => { JobRequest::SetFlags { ref mut handle, .. } => {
if let Ok(Some(Err(err))) = handle.chan.try_recv() { if let Ok(Some(Err(err))) = handle.chan.try_recv() {
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::Notification( .send(ThreadEvent::UIEvent(UIEvent::Notification(
Some(format!("{}: could not set flag", &self.name)), Some(format!("{}: could not set flag", &self.name)),
err.to_string(), err.to_string(),
Some(crate::types::NotificationType::Error(err.kind)), Some(crate::types::NotificationType::Error(err.kind)),
))) )));
.expect("Could not send event on main channel");
} }
} }
JobRequest::SaveMessage { JobRequest::SaveMessage {
@ -1882,7 +1889,7 @@ impl Account {
"Message was stored in {} so that you can restore it manually.", "Message was stored in {} so that you can restore it manually.",
file.path.display() file.path.display()
); );
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::Notification( .send(ThreadEvent::UIEvent(UIEvent::Notification(
Some(format!("{}: could not save message", &self.name)), Some(format!("{}: could not save message", &self.name)),
format!( format!(
@ -1890,31 +1897,28 @@ impl Account {
file.path.display() file.path.display()
), ),
Some(crate::types::NotificationType::Info), Some(crate::types::NotificationType::Info),
))) )));
.expect("Could not send event on main channel");
} }
} }
JobRequest::SendMessage => {} JobRequest::SendMessage => {}
JobRequest::SendMessageBackground { ref mut handle, .. } => { JobRequest::SendMessageBackground { ref mut handle, .. } => {
if let Ok(Some(Err(err))) = handle.chan.try_recv() { if let Ok(Some(Err(err))) = handle.chan.try_recv() {
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::Notification( .send(ThreadEvent::UIEvent(UIEvent::Notification(
Some("Could not send message".to_string()), Some("Could not send message".to_string()),
err.to_string(), err.to_string(),
Some(crate::types::NotificationType::Error(err.kind)), Some(crate::types::NotificationType::Error(err.kind)),
))) )));
.expect("Could not send event on main channel");
} }
} }
JobRequest::DeleteMessages { ref mut handle, .. } => { JobRequest::DeleteMessages { ref mut handle, .. } => {
if let Ok(Some(Err(err))) = handle.chan.try_recv() { if let Ok(Some(Err(err))) = handle.chan.try_recv() {
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::Notification( .send(ThreadEvent::UIEvent(UIEvent::Notification(
Some(format!("{}: could not delete message", &self.name)), Some(format!("{}: could not delete message", &self.name)),
err.to_string(), err.to_string(),
Some(crate::types::NotificationType::Error(err.kind)), Some(crate::types::NotificationType::Error(err.kind)),
))) )));
.expect("Could not send event on main channel");
} }
} }
JobRequest::CreateMailbox { JobRequest::CreateMailbox {
@ -1925,24 +1929,21 @@ impl Account {
if let Ok(Some(r)) = handle.chan.try_recv() { if let Ok(Some(r)) = handle.chan.try_recv() {
match r { match r {
Err(err) => { Err(err) => {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::Notification( UIEvent::Notification(
Some(format!( Some(format!(
"{}: could not create mailbox {}", "{}: could not create mailbox {}",
&self.name, path &self.name, path
)), )),
err.to_string(), err.to_string(),
Some(crate::types::NotificationType::Error(err.kind)), Some(crate::types::NotificationType::Error(err.kind)),
))) ),
.expect("Could not send event on main channel"); ));
} }
Ok((mailbox_hash, mut mailboxes)) => { Ok((mailbox_hash, mut mailboxes)) => {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::MailboxCreate(( UIEvent::MailboxCreate((self.hash, mailbox_hash)),
self.hash, ));
mailbox_hash,
))))
.unwrap();
let mut new = FileMailboxConf::default(); let mut new = FileMailboxConf::default();
new.mailbox_conf.subscribe = super::ToggleFlag::InternalVal(true); new.mailbox_conf.subscribe = super::ToggleFlag::InternalVal(true);
new.mailbox_conf.usage = if mailboxes[&mailbox_hash].special_usage() new.mailbox_conf.usage = if mailboxes[&mailbox_hash].special_usage()
@ -2013,21 +2014,18 @@ impl Account {
Err(_) => { /* canceled */ } Err(_) => { /* canceled */ }
Ok(None) => {} Ok(None) => {}
Ok(Some(Err(err))) => { Ok(Some(Err(err))) => {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::Notification( UIEvent::Notification(
Some(format!("{}: could not delete mailbox", &self.name)), Some(format!("{}: could not delete mailbox", &self.name)),
err.to_string(), err.to_string(),
Some(crate::types::NotificationType::Error(err.kind)), Some(crate::types::NotificationType::Error(err.kind)),
))) ),
.expect("Could not send event on main channel"); ));
} }
Ok(Some(Ok(mut mailboxes))) => { Ok(Some(Ok(mut mailboxes))) => {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::MailboxDelete(( UIEvent::MailboxDelete((self.hash, mailbox_hash)),
self.hash, ));
mailbox_hash,
))))
.unwrap();
if let Some(pos) = if let Some(pos) =
self.mailboxes_order.iter().position(|&h| h == mailbox_hash) self.mailboxes_order.iter().position(|&h| h == mailbox_hash)
{ {
@ -2069,13 +2067,13 @@ impl Account {
); );
// FIXME remove from settings as well // FIXME remove from settings as well
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::Notification( UIEvent::Notification(
Some(format!("{}: mailbox deleted successfully", &self.name)), Some(format!("{}: mailbox deleted successfully", &self.name)),
String::new(), String::new(),
Some(crate::types::NotificationType::Info), Some(crate::types::NotificationType::Info),
))) ),
.expect("Could not send event on main channel"); ));
} }
} }
} }
@ -2085,28 +2083,28 @@ impl Account {
Err(_) => { /* canceled */ } Err(_) => { /* canceled */ }
Ok(None) => {} Ok(None) => {}
Ok(Some(Err(err))) => { Ok(Some(Err(err))) => {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::Notification( UIEvent::Notification(
Some(format!( Some(format!(
"{}: could not set mailbox permissions", "{}: could not set mailbox permissions",
&self.name &self.name
)), )),
err.to_string(), err.to_string(),
Some(crate::types::NotificationType::Error(err.kind)), Some(crate::types::NotificationType::Error(err.kind)),
))) ),
.expect("Could not send event on main channel"); ));
} }
Ok(Some(Ok(_))) => { Ok(Some(Ok(_))) => {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::Notification( UIEvent::Notification(
Some(format!( Some(format!(
"{}: mailbox permissions set successfully", "{}: mailbox permissions set successfully",
&self.name &self.name
)), )),
String::new(), String::new(),
Some(crate::types::NotificationType::Info), Some(crate::types::NotificationType::Info),
))) ),
.expect("Could not send event on main channel"); ));
} }
} }
} }
@ -2119,16 +2117,16 @@ impl Account {
Err(_) => { /* canceled */ } Err(_) => { /* canceled */ }
Ok(None) => {} Ok(None) => {}
Ok(Some(Err(err))) => { Ok(Some(Err(err))) => {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::Notification( UIEvent::Notification(
Some(format!( Some(format!(
"{}: could not set mailbox subscription", "{}: could not set mailbox subscription",
&self.name &self.name
)), )),
err.to_string(), err.to_string(),
Some(crate::types::NotificationType::Error(err.kind)), 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) => { Ok(Some(Ok(()))) if self.mailbox_entries.contains_key(mailbox_hash) => {
self.mailbox_entries.entry(*mailbox_hash).and_modify(|m| { self.mailbox_entries.entry(*mailbox_hash).and_modify(|m| {
@ -2139,8 +2137,8 @@ impl Account {
}; };
let _ = m.ref_mailbox.set_is_subscribed(*new_value); let _ = m.ref_mailbox.set_is_subscribed(*new_value);
}); });
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::Notification( UIEvent::Notification(
Some(format!( Some(format!(
"{}: `{}` has been {}subscribed.", "{}: `{}` has been {}subscribed.",
&self.name, &self.name,
@ -2149,8 +2147,8 @@ impl Account {
)), )),
String::new(), String::new(),
Some(crate::types::NotificationType::Info), Some(crate::types::NotificationType::Info),
))) ),
.expect("Could not send event on main channel"); ));
} }
Ok(Some(Ok(()))) => {} Ok(Some(Ok(()))) => {}
} }
@ -2162,13 +2160,13 @@ impl Account {
self.watch(); self.watch();
} else { } else {
//TODO: relaunch watch job with ratelimit for failure //TODO: relaunch watch job with ratelimit for failure
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::Notification( UIEvent::Notification(
Some(format!("{}: watch thread failed", &self.name)), Some(format!("{}: watch thread failed", &self.name)),
err.to_string(), err.to_string(),
Some(crate::types::NotificationType::Error(err.kind)), 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() { match handle.chan.try_recv() {
Ok(Some(Err(err))) => { Ok(Some(Err(err))) => {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::Notification( UIEvent::Notification(
Some(format!("{}: {} failed", &self.name, name,)), Some(format!("{}: {} failed", &self.name, name,)),
err.to_string(), err.to_string(),
Some(crate::types::NotificationType::Error(err.kind)), Some(crate::types::NotificationType::Error(err.kind)),
))) ),
.expect("Could not send event on main channel"); ));
} }
Ok(Some(Ok(()))) if on_finish.is_none() => { Ok(Some(Ok(()))) if on_finish.is_none() => {
if log_level <= LogLevel::INFO { if log_level <= LogLevel::INFO {
self.sender self.main_loop_handler.send(ThreadEvent::UIEvent(
.send(ThreadEvent::UIEvent(UIEvent::Notification( UIEvent::Notification(
Some(format!("{}: {} succeeded", &self.name, name,)), Some(format!("{}: {} succeeded", &self.name, name,)),
String::new(), String::new(),
Some(crate::types::NotificationType::Info), Some(crate::types::NotificationType::Info),
))) ),
.expect("Could not send event on main channel"); ));
} }
} }
Err(_) => { /* canceled */ } Err(_) => { /* canceled */ }
Ok(Some(Ok(()))) | Ok(None) => {} Ok(Some(Ok(()))) | Ok(None) => {}
} }
if on_finish.is_some() { if on_finish.is_some() {
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::Callback( .send(ThreadEvent::UIEvent(UIEvent::Callback(
on_finish.take().unwrap(), on_finish.take().unwrap(),
))) )));
.unwrap();
} }
} }
} }
@ -2221,20 +2218,18 @@ impl Account {
self.active_jobs.insert(job_id, job); self.active_jobs.insert(job_id, job);
self.active_job_instants self.active_job_instants
.insert(std::time::Instant::now(), job_id); .insert(std::time::Instant::now(), job_id);
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( .send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::NewJob(job_id), StatusEvent::NewJob(job_id),
))) )));
.unwrap();
} }
pub fn cancel_job(&mut self, job_id: JobId) -> Option<JobRequest> { pub fn cancel_job(&mut self, job_id: JobId) -> Option<JobRequest> {
if let Some(req) = self.active_jobs.remove(&job_id) { if let Some(req) = self.active_jobs.remove(&job_id) {
self.sender self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( .send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::JobCanceled(job_id), StatusEvent::JobCanceled(job_id),
))) )));
.unwrap();
Some(req) Some(req)
} else { } else {
None None

@ -27,7 +27,7 @@
use super::*; use super::*;
use melib::HeaderName; 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 } } } # [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 } } }

@ -41,8 +41,14 @@ pub struct PagerSettings {
/// Always show headers when scrolling. /// Always show headers when scrolling.
/// Default: true /// Default: true
#[serde(default = "true_val", alias = "headers-sticky")] #[serde(
pub headers_sticky: bool, 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. /// The height of the pager in mail view, in percent.
/// Default: 80 /// Default: 80
@ -117,7 +123,7 @@ impl Default for PagerSettings {
Self { Self {
pager_context: 0, pager_context: 0,
pager_stop: false, pager_stop: false,
headers_sticky: true, sticky_headers: true,
pager_ratio: 80, pager_ratio: 80,
filter: None, filter: None,
html_filter: None, html_filter: None,
@ -141,7 +147,7 @@ impl DotAddressable for PagerSettings {
match *field { match *field {
"pager_context" => self.pager_context.lookup(field, tail), "pager_context" => self.pager_context.lookup(field, tail),
"pager_stop" => self.pager_stop.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), "pager_ratio" => self.pager_ratio.lookup(field, tail),
"filter" => self.filter.lookup(field, tail), "filter" => self.filter.lookup(field, tail),
"html_filter" => self.html_filter.lookup(field, tail), "html_filter" => self.html_filter.lookup(field, tail),

@ -251,7 +251,6 @@ const DEFAULT_KEYS: &[&str] = &[
"text.error", "text.error",
"text.highlight", "text.highlight",
"error_message", "error_message",
"email_header",
"highlight", "highlight",
"status.bar", "status.bar",
"status.command_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 }); add!("text.highlight", dark = { fg: Color::Blue, bg: "theme_default", attrs: Attr::REVERSE }, light = { fg: Color::Blue, bg: "theme_default", attrs: Attr::REVERSE });
/* rest */ /* 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!("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) }); add!("status.bar", dark = { fg: Color::Byte(123), bg: Color::Byte(26) }, light = { fg: Color::Byte(123), bg: Color::Byte(26) });

@ -265,11 +265,13 @@ fn run_app(opt: Opt) -> Result<()> {
sender, sender,
receiver.clone(), receiver.clone(),
)?; )?;
let main_loop_handler = state.context.main_loop_handler.clone();
state.register_component(Box::new(EnvelopeView::new( state.register_component(Box::new(EnvelopeView::new(
wrapper, wrapper,
None, None,
None, None,
AccountHash::default(), None,
main_loop_handler,
))); )));
} else { } else {
state = State::new(None, sender, receiver.clone())?; state = State::new(None, sender, receiver.clone())?;

@ -34,10 +34,10 @@
//! for user input, observe folders for file changes etc. The relevant struct is //! for user input, observe folders for file changes etc. The relevant struct is
//! [`ThreadEvent`]. //! [`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 crossbeam::channel::{unbounded, Receiver, Sender};
use indexmap::IndexMap; use indexmap::{IndexMap, IndexSet};
use melib::{ use melib::{
backends::{AccountHash, BackendEvent, BackendEventConsumer, Backends, RefreshEvent}, backends::{AccountHash, BackendEvent, BackendEventConsumer, Backends, RefreshEvent},
UnixTimestamp, UnixTimestamp,
@ -102,6 +102,21 @@ impl InputHandler {
} }
} }
#[derive(Debug, Clone)]
pub struct MainLoopHandler {
pub sender: Sender<ThreadEvent>,
pub job_executor: Arc<JobExecutor>,
}
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. /// A context container for loaded settings, accounts, UI changes, etc.
pub struct Context { pub struct Context {
pub accounts: IndexMap<AccountHash, Account>, pub accounts: IndexMap<AccountHash, Account>,
@ -112,10 +127,11 @@ pub struct Context {
/// Events queue that components send back to the state /// Events queue that components send back to the state
pub replies: VecDeque<UIEvent>, pub replies: VecDeque<UIEvent>,
pub sender: Sender<ThreadEvent>, pub realized: IndexMap<ComponentId, Option<ComponentId>>,
pub unrealized: IndexSet<ComponentId>,
pub main_loop_handler: MainLoopHandler,
receiver: Receiver<ThreadEvent>, receiver: Receiver<ThreadEvent>,
input_thread: InputHandler, input_thread: InputHandler,
pub job_executor: Arc<JobExecutor>,
pub children: Vec<std::process::Child>, pub children: Vec<std::process::Child>,
pub temp_files: Vec<File>, pub temp_files: Vec<File>,
@ -196,8 +212,10 @@ impl Context {
name, name,
account_conf, account_conf,
&backends, &backends,
job_executor.clone(), MainLoopHandler {
sender.clone(), job_executor: job_executor.clone(),
sender: sender.clone(),
},
BackendEventConsumer::new(Arc::new( BackendEventConsumer::new(Arc::new(
move |account_hash: AccountHash, ev: BackendEvent| { move |account_hash: AccountHash, ev: BackendEvent| {
sender sender
@ -219,8 +237,9 @@ impl Context {
settings, settings,
dirty_areas: VecDeque::with_capacity(0), dirty_areas: VecDeque::with_capacity(0),
replies: VecDeque::with_capacity(0), replies: VecDeque::with_capacity(0),
realized: IndexMap::default(),
unrealized: IndexSet::default(),
temp_files: Vec::new(), temp_files: Vec::new(),
job_executor,
children: vec![], children: vec![],
input_thread: InputHandler { input_thread: InputHandler {
@ -230,7 +249,10 @@ impl Context {
control, control,
state_tx: sender.clone(), state_tx: sender.clone(),
}, },
sender, main_loop_handler: MainLoopHandler {
job_executor,
sender,
},
receiver, receiver,
} }
} }
@ -244,8 +266,9 @@ pub struct State {
draw_rate_limit: RateLimit, draw_rate_limit: RateLimit,
child: Option<ForkType>, child: Option<ForkType>,
pub mode: UIMode, pub mode: UIMode,
overlay: Vec<Box<dyn Component>>, overlay: IndexMap<ComponentId, Box<dyn Component>>,
components: Vec<Box<dyn Component>>, components: IndexMap<ComponentId, Box<dyn Component>>,
component_tree: IndexMap<ComponentId, ComponentPath>,
pub context: Box<Context>, pub context: Box<Context>,
timer: thread::JoinHandle<()>, timer: thread::JoinHandle<()>,
@ -337,8 +360,10 @@ impl State {
n.to_string(), n.to_string(),
a_s.clone(), a_s.clone(),
&backends, &backends,
job_executor.clone(), MainLoopHandler {
sender.clone(), job_executor: job_executor.clone(),
sender: sender.clone(),
},
BackendEventConsumer::new(Arc::new( BackendEventConsumer::new(Arc::new(
move |account_hash: AccountHash, ev: BackendEvent| { move |account_hash: AccountHash, ev: BackendEvent| {
sender sender
@ -388,8 +413,9 @@ impl State {
}), }),
child: None, child: None,
mode: UIMode::Normal, mode: UIMode::Normal,
components: Vec::with_capacity(8), components: IndexMap::default(),
overlay: Vec::new(), overlay: IndexMap::default(),
component_tree: IndexMap::default(),
timer, timer,
draw_rate_limit: RateLimit::new(1, 3, job_executor.clone()), draw_rate_limit: RateLimit::new(1, 3, job_executor.clone()),
display_messages: SmallVec::new(), display_messages: SmallVec::new(),
@ -404,8 +430,9 @@ impl State {
settings, settings,
dirty_areas: VecDeque::with_capacity(5), dirty_areas: VecDeque::with_capacity(5),
replies: VecDeque::with_capacity(5), replies: VecDeque::with_capacity(5),
realized: IndexMap::default(),
unrealized: IndexSet::default(),
temp_files: Vec::new(), temp_files: Vec::new(),
job_executor,
children: vec![], children: vec![],
input_thread: InputHandler { input_thread: InputHandler {
@ -415,7 +442,10 @@ impl State {
control, control,
state_tx: sender.clone(), state_tx: sender.clone(),
}, },
sender, main_loop_handler: MainLoopHandler {
job_executor,
sender,
},
receiver, receiver,
}), }),
}; };
@ -480,7 +510,7 @@ impl State {
} }
pub fn sender(&self) -> Sender<ThreadEvent> { pub fn sender(&self) -> Sender<ThreadEvent> {
self.context.sender.clone() self.context.main_loop_handler.sender.clone()
} }
pub fn restore_input(&mut self) { pub fn restore_input(&mut self) {
@ -759,7 +789,7 @@ impl State {
), ),
); );
copy_area(&mut self.screen.overlay_grid, &self.screen.grid, area, area); 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, &mut self.screen.overlay_grid,
area, area,
&mut self.context, &mut self.context,
@ -809,11 +839,12 @@ impl State {
ref context, ref context,
.. ..
} = self; } = 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<dyn Component>) { pub fn register_component(&mut self, component: Box<dyn Component>) {
self.components.push(component); component.realize(None, &mut self.context);
self.components.insert(component.id(), component);
} }
/// Convert user commands to actions/method calls. /// Convert user commands to actions/method calls.
@ -885,7 +916,11 @@ impl State {
} }
match crate::sqlite3::index(&mut self.context, account_index) { match crate::sqlite3::index(&mut self.context, account_index) {
Ok(job) => { 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( self.context.accounts[account_index].active_jobs.insert(
handle.job_id, handle.job_id,
crate::conf::accounts::JobRequest::Generic { crate::conf::accounts::JobRequest::Generic {
@ -963,6 +998,7 @@ impl State {
} }
Quit => { Quit => {
self.context self.context
.main_loop_handler
.sender .sender
.send(ThreadEvent::Input(( .send(ThreadEvent::Input((
self.context.settings.shortcuts.general.quit.clone(), self.context.settings.shortcuts.general.quit.clone(),
@ -989,7 +1025,7 @@ impl State {
UIEvent::Command(cmd) => { UIEvent::Command(cmd) => {
if let Ok(action) = parse_command(cmd.as_bytes()) { if let Ok(action) = parse_command(cmd.as_bytes()) {
if action.needs_confirmation() { if action.needs_confirmation() {
self.overlay.push(Box::new(UIConfirmationDialog::new( let new = Box::new(UIConfirmationDialog::new(
"You sure?", "You sure?",
vec![(true, "yes".to_string()), (false, "no".to_string())], vec![(true, "yes".to_string()), (false, "no".to_string())],
true, true,
@ -1000,7 +1036,9 @@ impl State {
)) ))
})), })),
&self.context, &self.context,
))); ));
self.overlay.insert(new.id(), new);
} else if let Action::ReloadConfiguration = action { } else if let Action::ReloadConfiguration = action {
match Settings::new().and_then(|new_settings| { match Settings::new().and_then(|new_settings| {
let old_accounts = self let old_accounts = self
@ -1114,6 +1152,7 @@ impl State {
} }
UIEvent::ChangeMode(m) => { UIEvent::ChangeMode(m) => {
self.context self.context
.main_loop_handler
.sender .sender
.send(ThreadEvent::UIEvent(UIEvent::ChangeMode(m))) .send(ThreadEvent::UIEvent(UIEvent::ChangeMode(m)))
.unwrap(); .unwrap();
@ -1164,13 +1203,7 @@ impl State {
self.display_messages_pos = self.display_messages.len() - 1; self.display_messages_pos = self.display_messages.len() - 1;
self.redraw(); self.redraw();
} }
UIEvent::ComponentKill(ref id) if self.overlay.iter().any(|c| c.id() == *id) => { UIEvent::FinishedUIDialog(ref id, ref mut results) if self.overlay.contains_key(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) =>
{
if let Some(ref mut action @ Some(_)) = results.downcast_mut::<Option<Action>>() { if let Some(ref mut action @ Some(_)) = results.downcast_mut::<Option<Action>>() {
self.exec_command(action.take().unwrap()); self.exec_command(action.take().unwrap());
@ -1182,7 +1215,7 @@ impl State {
return; return;
} }
UIEvent::GlobalUIDialog(dialog) => { UIEvent::GlobalUIDialog(dialog) => {
self.overlay.push(dialog); self.overlay.insert(dialog.id(), dialog);
return; return;
} }
_ => {} _ => {}
@ -1195,12 +1228,60 @@ impl State {
} = self; } = self;
/* inform each component */ /* 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) { if c.process_event(&mut event, context) {
break; 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() { if !self.context.replies.is_empty() {
let replies: smallvec::SmallVec<[UIEvent; 8]> = let replies: smallvec::SmallVec<[UIEvent; 8]> =
self.context.replies.drain(0..).collect(); self.context.replies.drain(0..).collect();

@ -38,13 +38,13 @@ mod helpers;
use std::{borrow::Cow, fmt, sync::Arc}; use std::{borrow::Cow, fmt, sync::Arc};
pub use helpers::*;
use melib::{ use melib::{
backends::{AccountHash, BackendEvent, MailboxHash}, backends::{AccountHash, BackendEvent, MailboxHash},
EnvelopeHash, RefreshEvent, ThreadHash, EnvelopeHash, RefreshEvent, ThreadHash,
}; };
use nix::unistd::Pid; use nix::unistd::Pid;
pub use self::helpers::*;
use super::{ use super::{
command::Action, command::Action,
jobs::{JobExecutor, JobId, TimerId}, jobs::{JobExecutor, JobId, TimerId},
@ -52,6 +52,8 @@ use super::{
}; };
use crate::components::{Component, ComponentId, ScrollUpdate}; use crate::components::{Component, ComponentId, ScrollUpdate};
pub type UIMessage = Box<dyn 'static + std::any::Any + Send + Sync>;
#[derive(Debug)] #[derive(Debug)]
pub enum StatusEvent { pub enum StatusEvent {
DisplayMessage(String), DisplayMessage(String),
@ -70,15 +72,13 @@ pub enum StatusEvent {
/// between our threads to the main process. /// between our threads to the main process.
#[derive(Debug)] #[derive(Debug)]
pub enum ThreadEvent { pub enum ThreadEvent {
/// User input.
Input((Key, Vec<u8>)),
/// User input and input as raw bytes. /// User input and input as raw bytes.
Input((Key, Vec<u8>)),
/// A watched Mailbox has been refreshed. /// A watched Mailbox has been refreshed.
RefreshMailbox(Box<RefreshEvent>), RefreshMailbox(Box<RefreshEvent>),
UIEvent(UIEvent), UIEvent(UIEvent),
/// A thread has updated some of its information /// A thread has updated some of its information
Pulse, Pulse,
//Decode { _ }, // For gpg2 signature check
JobFinished(JobId), JobFinished(JobId),
} }
@ -126,9 +126,7 @@ pub enum UIEvent {
CmdInput(Key), CmdInput(Key),
InsertInput(Key), InsertInput(Key),
EmbedInput((Key, Vec<u8>)), EmbedInput((Key, Vec<u8>)),
//Quit?
Resize, Resize,
/// Force redraw.
Fork(ForkType), Fork(ForkType),
ChangeMailbox(usize), ChangeMailbox(usize),
ChangeMode(UIMode), ChangeMode(UIMode),
@ -140,7 +138,7 @@ pub enum UIEvent {
MailboxDelete((AccountHash, MailboxHash)), MailboxDelete((AccountHash, MailboxHash)),
MailboxCreate((AccountHash, MailboxHash)), MailboxCreate((AccountHash, MailboxHash)),
AccountStatusChange(AccountHash, Option<Cow<'static, str>>), AccountStatusChange(AccountHash, Option<Cow<'static, str>>),
ComponentKill(ComponentId), ComponentUnrealize(ComponentId),
BackendEvent(AccountHash, BackendEvent), BackendEvent(AccountHash, BackendEvent),
StartupCheck(MailboxHash), StartupCheck(MailboxHash),
RefreshEvent(Box<RefreshEvent>), RefreshEvent(Box<RefreshEvent>),
@ -150,6 +148,11 @@ pub enum UIEvent {
Contacts(ContactEvent), Contacts(ContactEvent),
Compose(ComposeEvent), Compose(ComposeEvent),
FinishedUIDialog(ComponentId, UIMessage), FinishedUIDialog(ComponentId, UIMessage),
IntraComm {
from: ComponentId,
to: ComponentId,
content: UIMessage,
},
Callback(CallbackFn), Callback(CallbackFn),
GlobalUIDialog(Box<dyn Component>), GlobalUIDialog(Box<dyn Component>),
Timer(TimerId), Timer(TimerId),
@ -374,8 +377,6 @@ pub enum ComposeEvent {
SetReceipients(Vec<melib::Address>), SetReceipients(Vec<melib::Address>),
} }
pub type UIMessage = Box<dyn 'static + std::any::Any + Send + Sync>;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
//use super::*; //use super::*;

Loading…
Cancel
Save