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 2023-06-14 12:24:20 +03:00
parent 5c9b3fb044
commit 575509f1ed
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
33 changed files with 3387 additions and 3214 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -899,7 +899,9 @@ impl Component for ContactList {
}
} else {
match event {
UIEvent::ComponentKill(ref kill_id) if self.mode == ViewMode::View(*kill_id) => {
UIEvent::ComponentUnrealize(ref kill_id)
if self.mode == ViewMode::View(*kill_id) =>
{
self.mode = ViewMode::List;
self.view.take();
self.set_dirty(true);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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,
}
}
}

View File

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

View File

@ -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,
},
}

View File

@ -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)
);
}
}

View File

@ -50,7 +50,11 @@ mod dbus {
impl DbusNotifications {
pub fn new(context: &Context) -> Self {
DbusNotifications {
rate_limit: RateLimit::new(1000, 1000, context.job_executor.clone()),
rate_limit: RateLimit::new(
1000,
1000,
context.main_loop_handler.job_executor.clone(),
),
id: ComponentId::default(),
}
}

View File

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

View File

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

View File

@ -41,6 +41,12 @@ pub struct Pager {
colors: ThemeAttribute,
initialised: bool,
show_scrollbar: bool,
/// At the last draw, were the visible columns plus horizontal cursor less than total width?
/// Used to decide whether to accept `scroll_right` key events.
cols_lt_width: bool,
/// At the last draw, were the visible rows plus vertical cursor less than total height?
/// Used to decide whether to accept `scroll_down` key events.
rows_lt_height: bool,
filtered_content: Option<(String, Result<CellBuffer>)>,
text_lines: Vec<String>,
line_breaker: LineBreakText,
@ -525,9 +531,6 @@ impl Component for Pager {
clear_area(grid, area, crate::conf::value(context, "theme_default"));
let (mut cols, mut rows) = (width!(area), height!(area));
if cols < 2 || rows < 2 {
return;
}
let (has_more_lines, (width, height)) = if self.filtered_content.is_some() {
(false, (self.width, self.height))
} else {
@ -536,6 +539,14 @@ impl Component for Pager {
(self.line_breaker.width().unwrap_or(cols), self.height),
)
};
self.cols_lt_width = cols + self.cursor.0 < width;
self.rows_lt_height = rows + self.cursor.1 < height;
if cols < 2 || rows < 2 {
return;
}
if self.show_scrollbar && rows < height {
cols -= 1;
rows -= 1;
@ -655,42 +666,32 @@ impl Component for Pager {
self.set_dirty(true);
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_up"]) =>
if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_up"])
&& self.cursor.1 > 0 =>
{
self.movement = Some(PageMovement::Up(1));
self.dirty = true;
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_down"]) =>
if shortcut!(key == shortcuts[Shortcuts::PAGER]["scroll_down"])
&& self.rows_lt_height =>
{
self.movement = Some(PageMovement::Down(1));
self.dirty = true;
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) =>
{
self.movement = Some(PageMovement::Home);
self.dirty = true;
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) =>
{
self.movement = Some(PageMovement::End);
self.dirty = true;
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) =>
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"])
&& self.cursor.0 > 0 =>
{
self.movement = Some(PageMovement::Left(1));
self.dirty = true;
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) =>
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"])
&& dbg!(self.cols_lt_width) =>
{
self.movement = Some(PageMovement::Right(1));
self.dirty = true;
@ -708,6 +709,20 @@ impl Component for Pager {
self.dirty = true;
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) =>
{
self.movement = Some(PageMovement::Home);
self.dirty = true;
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) =>
{
self.movement = Some(PageMovement::End);
self.dirty = true;
return true;
}
UIEvent::ChangeMode(UIMode::Normal) => {
self.dirty = true;
}
@ -802,7 +817,10 @@ impl Component for Pager {
}
UIEvent::Resize => {
self.initialised = false;
self.dirty = true;
self.set_dirty(true);
}
UIEvent::VisibilityChange(true) => {
self.set_dirty(true);
}
UIEvent::VisibilityChange(false) => {
context

View File

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

View File

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

View File

@ -27,7 +27,7 @@
use super::*;
use melib::HeaderName;
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "headers-sticky")] # [serde (default)] pub headers_sticky : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > , # [doc = " Extra headers to display, if present, in the default header preamble."] # [doc = " Default: []"] # [serde (alias = "show-extra-headers")] # [serde (default)] pub show_extra_headers : Option < Vec < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { PagerSettingsOverride { pager_context : None , pager_stop : None , headers_sticky : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None , show_extra_headers : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "sticky-headers" , alias = "headers-sticky" , alias = "headers_sticky")] # [serde (default)] pub sticky_headers : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > , # [doc = " Extra headers to display, if present, in the default header preamble."] # [doc = " Default: []"] # [serde (alias = "show-extra-headers")] # [serde (default)] pub show_extra_headers : Option < Vec < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { PagerSettingsOverride { pager_context : None , pager_stop : None , sticky_headers : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None , show_extra_headers : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ListingSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "context-lines")] # [serde (default)] pub context_lines : Option < usize > , # [doc = "Show auto-hiding scrollbar in accounts sidebar menu."] # [doc = "Default: True"] # [serde (default)] pub show_menu_scrollbar : Option < bool > , # [doc = " Datetime formatting passed verbatim to strftime(3)."] # [doc = " Default: %Y-%m-%d %T"] # [serde (alias = "datetime-fmt")] # [serde (default)] pub datetime_fmt : Option < Option < String > > , # [doc = " Show recent dates as `X {minutes,hours,days} ago`, up to 7 days."] # [doc = " Default: true"] # [serde (alias = "recent-dates")] # [serde (default)] pub recent_dates : Option < bool > , # [doc = " Show only envelopes that match this query"] # [doc = " Default: None"] # [serde (default)] pub filter : Option < Option < Query > > , # [serde (alias = "index-style")] # [serde (default)] pub index_style : Option < IndexStyle > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling_leaf : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling_leaf : Option < Option < String > > , # [doc = "Default: ' '"] # [serde (default)] pub sidebar_divider : Option < char > , # [doc = "Default: 90"] # [serde (default)] pub sidebar_ratio : Option < usize > , # [doc = " Flag to show if thread entry contains unseen mail."] # [doc = " Default: \"\""] # [serde (default)] pub unseen_flag : Option < Option < String > > , # [doc = " Flag to show if thread has been snoozed."] # [doc = " Default: \"💤\""] # [serde (default)] pub thread_snoozed_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry has been selected."] # [doc = " Default: \"\u{fe0f}\""] # [serde (default)] pub selected_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry contains attachments."] # [doc = " Default: \"📎\""] # [serde (default)] pub attachment_flag : Option < Option < String > > , # [doc = " Should threads with differentiating Subjects show a list of those"] # [doc = " subjects on the entry title?"] # [doc = " Default: \"true\""] # [serde (default)] pub thread_subject_pack : Option < bool > } impl Default for ListingSettingsOverride { fn default () -> Self { ListingSettingsOverride { context_lines : None , show_menu_scrollbar : None , datetime_fmt : None , recent_dates : None , filter : None , index_style : None , sidebar_mailbox_tree_has_sibling : None , sidebar_mailbox_tree_no_sibling : None , sidebar_mailbox_tree_has_sibling_leaf : None , sidebar_mailbox_tree_no_sibling_leaf : None , sidebar_divider : None , sidebar_ratio : None , unseen_flag : None , thread_snoozed_flag : None , selected_flag : None , attachment_flag : None , thread_subject_pack : None } } }

View File

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

View File

@ -251,7 +251,6 @@ const DEFAULT_KEYS: &[&str] = &[
"text.error",
"text.highlight",
"error_message",
"email_header",
"highlight",
"status.bar",
"status.command_bar",
@ -1316,8 +1315,6 @@ impl Default for Themes {
add!("text.highlight", dark = { fg: Color::Blue, bg: "theme_default", attrs: Attr::REVERSE }, light = { fg: Color::Blue, bg: "theme_default", attrs: Attr::REVERSE });
/* rest */
add!("email_header", dark = { fg: Color::Byte(33), bg: Color::Default, attrs: Attr::DEFAULT }, light = { fg: Color::Byte(33), bg: Color::Default, attrs: Attr::DEFAULT });
add!("highlight", dark = { fg: Color::Byte(240), bg: Color::Byte(237), attrs: Attr::BOLD }, light = { fg: Color::Byte(240), bg: Color::Byte(237), attrs: Attr::BOLD });
add!("status.bar", dark = { fg: Color::Byte(123), bg: Color::Byte(26) }, light = { fg: Color::Byte(123), bg: Color::Byte(26) });

View File

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

View File

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

View File

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