meli/meli/src/mail/listing.rs

3469 lines
143 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

/*
* 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::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
convert::TryFrom,
fs::File,
future::Future,
io::{BufWriter, Write},
ops::{Deref, DerefMut},
pin::Pin,
};
use futures::future::try_join_all;
use melib::{
backends::EnvelopeHashBatch, mbox::MboxMetadata, utils::datetime, Flag, FlagOp, UnixTimestamp,
};
use smallvec::SmallVec;
use super::*;
use crate::{
accounts::{JobRequest, MailboxStatus},
components::ExtendShortcutsMaps,
};
pub const DEFAULT_ATTACHMENT_FLAG: &str = concat!("📎", emoji_text_presentation_selector!());
pub const DEFAULT_SELECTED_FLAG: &str = concat!("☑️", emoji_text_presentation_selector!());
pub const DEFAULT_UNSEEN_FLAG: &str = concat!("", emoji_text_presentation_selector!());
pub const DEFAULT_SNOOZED_FLAG: &str = concat!("💤", emoji_text_presentation_selector!());
pub const DEFAULT_HIGHLIGHT_SELF_FLAG: &str = concat!("", emoji_text_presentation_selector!());
#[derive(Debug, Default)]
pub struct RowsState<T> {
pub selection: HashMap<EnvelopeHash, bool>,
pub row_updates: SmallVec<[EnvelopeHash; 8]>,
// [ref: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>,
pub env_order: HashMap<EnvelopeHash, usize>,
#[allow(clippy::type_complexity)]
pub entries: Vec<(T, EntryStrings)>,
pub all_threads: HashSet<ThreadHash>,
pub all_envelopes: HashSet<EnvelopeHash>,
pub row_attr_cache: HashMap<usize, ThemeAttribute>,
}
impl<T> RowsState<T> {
#[inline(always)]
pub fn clear(&mut self) {
self.selection.clear();
self.row_updates.clear();
self.thread_to_env.clear();
self.env_to_thread.clear();
self.thread_order.clear();
self.env_order.clear();
self.entries.clear();
self.all_threads.clear();
self.all_envelopes.clear();
self.row_attr_cache.clear();
}
#[inline(always)]
pub fn is_thread_selected(&self, thread: ThreadHash) -> bool {
debug_assert!(self.all_threads.contains(&thread));
debug_assert!(self.thread_order.contains_key(&thread));
debug_assert!(self.thread_to_env.contains_key(&thread));
self.thread_to_env
.get(&thread)
.iter()
.flat_map(|v| v.iter())
.any(|env_hash| self.selection[env_hash])
}
#[inline(always)]
pub fn insert_thread(
&mut self,
thread: ThreadHash,
metadata: T,
mut env_hashes: SmallVec<[EnvelopeHash; 8]>,
entry_strings: EntryStrings,
) {
env_hashes.dedup();
env_hashes.retain(|h| !self.all_envelopes.contains(h));
if env_hashes.is_empty() {
return;
}
let index = self.entries.len();
for &env_hash in &env_hashes {
self.selection.insert(env_hash, false);
self.env_to_thread.insert(env_hash, thread);
self.env_order.insert(env_hash, index);
self.all_envelopes.insert(env_hash);
}
if !self.all_threads.contains(&thread) {
self.thread_order.insert(thread, index);
self.all_threads.insert(thread);
self.thread_to_env.insert(thread, env_hashes);
} else {
self.thread_to_env
.entry(thread)
.or_default()
.extend_from_slice(&env_hashes);
}
self.entries.push((metadata, entry_strings));
}
#[inline(always)]
pub fn row_update_add_thread(&mut self, thread: ThreadHash) {
let env_hashes = self.thread_to_env.entry(thread).or_default().clone();
for env_hash in env_hashes {
self.row_updates.push(env_hash);
}
}
#[inline(always)]
pub fn row_update_add_envelope(&mut self, env_hash: EnvelopeHash) {
self.row_updates.push(env_hash);
}
#[inline(always)]
pub fn contains_thread(&self, thread: ThreadHash) -> bool {
debug_assert_eq!(
self.all_threads.contains(&thread),
self.thread_order.contains_key(&thread)
);
debug_assert_eq!(
self.thread_order.contains_key(&thread),
self.thread_to_env.contains_key(&thread)
);
self.thread_order.contains_key(&thread)
}
#[inline(always)]
pub fn contains_env(&self, env_hash: EnvelopeHash) -> bool {
self.all_envelopes.contains(&env_hash)
}
#[inline(always)]
pub fn update_selection_with_thread(
&mut self,
thread: ThreadHash,
mut cl: impl FnMut(&mut bool),
) {
let env_hashes = self.thread_to_env.entry(thread).or_default().clone();
for env_hash in env_hashes {
self.selection.entry(env_hash).and_modify(&mut cl);
self.row_updates.push(env_hash);
}
}
#[inline(always)]
pub fn update_selection_with_env(
&mut self,
env_hash: EnvelopeHash,
mut cl: impl FnMut(&mut bool),
) {
self.selection.entry(env_hash).and_modify(&mut cl);
self.row_updates.push(env_hash);
}
#[inline(always)]
pub fn len(&self) -> usize {
self.entries.len()
}
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[inline(always)]
pub fn clear_selection(&mut self) {
for (k, v) in self.selection.iter_mut() {
if *v {
*v = false;
self.row_updates.push(*k);
}
}
}
pub fn rename_env(&mut self, old_hash: EnvelopeHash, new_hash: EnvelopeHash) {
self.row_updates.push(new_hash);
if let Some(row) = self.env_order.remove(&old_hash) {
self.env_order.insert(new_hash, row);
}
if let Some(thread) = self.env_to_thread.remove(&old_hash) {
self.thread_to_env
.entry(thread)
.or_default()
.retain(|h| *h != old_hash);
self.thread_to_env.entry(thread).or_default().push(new_hash);
}
let selection_status = self.selection.remove(&old_hash).unwrap_or(false);
self.selection.insert(new_hash, selection_status);
self.all_envelopes.remove(&old_hash);
self.all_envelopes.insert(old_hash);
}
}
mod conversations;
pub use self::conversations::*;
mod compact;
pub use self::compact::*;
mod thread;
pub use self::thread::*;
mod plain;
pub use self::plain::*;
mod offline;
pub use self::offline::*;
#[derive(Clone, Copy, Debug)]
pub enum Focus {
None,
Entry,
EntryFullscreen,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Modifier {
#[default]
SymmetricDifference,
Union,
Difference,
Intersection,
}
#[derive(Debug, Default)]
/// Save theme colors to avoid looking them up again and again from settings
pub struct ColorCache {
pub theme_default: ThemeAttribute,
pub unseen: ThemeAttribute,
pub highlighted: ThemeAttribute,
pub selected: ThemeAttribute,
pub highlighted_selected: ThemeAttribute,
pub even: ThemeAttribute,
pub odd: ThemeAttribute,
pub even_unseen: ThemeAttribute,
pub even_highlighted: ThemeAttribute,
pub even_selected: ThemeAttribute,
pub odd_unseen: ThemeAttribute,
pub odd_highlighted: ThemeAttribute,
pub odd_selected: ThemeAttribute,
pub even_highlighted_selected: ThemeAttribute,
pub odd_highlighted_selected: ThemeAttribute,
pub tag_default: ThemeAttribute,
// Conversations
pub subject: ThemeAttribute,
pub from: ThemeAttribute,
pub date: ThemeAttribute,
}
impl ColorCache {
pub fn new(context: &Context, style: IndexStyle) -> Self {
let mut ret = match style {
IndexStyle::Plain => Self {
even: crate::conf::value(context, "mail.listing.plain.even"),
odd: crate::conf::value(context, "mail.listing.plain.odd"),
even_unseen: crate::conf::value(context, "mail.listing.plain.even_unseen"),
odd_unseen: crate::conf::value(context, "mail.listing.plain.odd_unseen"),
even_highlighted: crate::conf::value(
context,
"mail.listing.plain.even_highlighted",
),
odd_highlighted: crate::conf::value(context, "mail.listing.plain.odd_highlighted"),
odd_highlighted_selected: crate::conf::value(
context,
"mail.listing.plain.odd_highlighted_selected",
),
even_selected: crate::conf::value(context, "mail.listing.plain.even_selected"),
even_highlighted_selected: crate::conf::value(
context,
"mail.listing.plain.even_highlighted_selected",
),
odd_selected: crate::conf::value(context, "mail.listing.plain.odd_selected"),
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
theme_default: crate::conf::value(context, "theme_default"),
..Self::default()
},
IndexStyle::Threaded => Self {
even_unseen: crate::conf::value(context, "mail.listing.plain.even_unseen"),
even_selected: crate::conf::value(context, "mail.listing.plain.even_selected"),
even_highlighted: crate::conf::value(
context,
"mail.listing.plain.even_highlighted",
),
even_highlighted_selected: crate::conf::value(
context,
"mail.listing.plain.even_highlighted_selected",
),
odd_unseen: crate::conf::value(context, "mail.listing.plain.odd_unseen"),
odd_selected: crate::conf::value(context, "mail.listing.plain.odd_selected"),
odd_highlighted: crate::conf::value(context, "mail.listing.plain.odd_highlighted"),
odd_highlighted_selected: crate::conf::value(
context,
"mail.listing.plain.odd_highlighted_selected",
),
even: crate::conf::value(context, "mail.listing.plain.even"),
odd: crate::conf::value(context, "mail.listing.plain.odd"),
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
theme_default: crate::conf::value(context, "theme_default"),
..Self::default()
},
IndexStyle::Compact => Self {
even_unseen: crate::conf::value(context, "mail.listing.compact.even_unseen"),
even_selected: crate::conf::value(context, "mail.listing.compact.even_selected"),
even_highlighted: crate::conf::value(
context,
"mail.listing.compact.even_highlighted",
),
even_highlighted_selected: crate::conf::value(
context,
"mail.listing.compact.even_highlighted_selected",
),
odd_unseen: crate::conf::value(context, "mail.listing.compact.odd_unseen"),
odd_selected: crate::conf::value(context, "mail.listing.compact.odd_selected"),
odd_highlighted: crate::conf::value(
context,
"mail.listing.compact.odd_highlighted",
),
odd_highlighted_selected: crate::conf::value(
context,
"mail.listing.compact.odd_highlighted_selected",
),
even: crate::conf::value(context, "mail.listing.compact.even"),
odd: crate::conf::value(context, "mail.listing.compact.odd"),
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
theme_default: crate::conf::value(context, "theme_default"),
..Self::default()
},
IndexStyle::Conversations => Self {
theme_default: crate::conf::value(context, "mail.listing.conversations"),
subject: crate::conf::value(context, "mail.listing.conversations.subject"),
from: crate::conf::value(context, "mail.listing.conversations.from"),
date: crate::conf::value(context, "mail.listing.conversations.date"),
selected: crate::conf::value(context, "mail.listing.conversations.selected"),
unseen: crate::conf::value(context, "mail.listing.conversations.unseen"),
highlighted: crate::conf::value(context, "mail.listing.conversations.highlighted"),
highlighted_selected: crate::conf::value(
context,
"mail.listing.conversations.highlighted_selected",
),
tag_default: crate::conf::value(context, "mail.listing.tag_default"),
..Self::default()
},
};
if !context.settings.terminal.use_color() {
ret.highlighted.attrs |= Attr::REVERSE;
ret.tag_default.attrs |= Attr::REVERSE;
ret.even_highlighted.attrs |= Attr::REVERSE;
ret.odd_highlighted.attrs |= Attr::REVERSE;
ret.even_highlighted_selected.attrs |= Attr::REVERSE | Attr::DIM;
ret.odd_highlighted_selected.attrs |= Attr::REVERSE | Attr::DIM;
}
ret
}
}
#[derive(Debug)]
pub struct EntryStrings {
pub date: DateString,
pub subject: SubjectString,
pub flag: FlagString,
pub from: FromString,
pub tags: TagString,
pub highlight_self: bool,
}
#[macro_export]
macro_rules! digits_of_num {
($num:expr) => {{
const GUESS: [usize; 65] = [
1, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 8, 8,
8, 9, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15,
15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 18, 19,
];
const TENS: [usize; 20] = [
1,
10,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000,
1000000000,
10000000000,
100000000000,
1000000000000,
10000000000000,
100000000000000,
1000000000000000,
10000000000000000,
100000000000000000,
1000000000000000000,
10000000000000000000,
];
const SIZE_IN_BITS: usize = std::mem::size_of::<usize>() * 8;
let leading_zeros = $num.leading_zeros() as usize;
let base_two_digits: usize = SIZE_IN_BITS - leading_zeros;
let x = GUESS[base_two_digits];
x + if $num >= TENS[x] { 1 } else { 0 }
}};
}
macro_rules! column_str {
(
struct $name:ident($($t:ty),+)) => {
#[derive(Debug)]
pub struct $name($(pub $t),+);
impl Deref for $name {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for $name {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
};
}
column_str!(struct DateString(String));
column_str!(struct FromString(String));
column_str!(struct SubjectString(String));
column_str!(struct FlagString(String));
column_str!(struct TagString(String, SmallVec<[Option<Color>; 8]>));
impl FlagString {
pub(self) fn new(
flags: Flag,
is_selected: bool,
is_snoozed: bool,
is_unseen: bool,
has_attachments: bool,
context: &Context,
coordinates: (AccountHash, MailboxHash),
) -> Self {
Self(format!(
"{flag_passed}{flag_replied}{flag_seen}{flag_trashed}{flag_draft}{flag_flagged} \
{selected}{snoozed}{unseen}{attachments}{whitespace}",
flag_passed = Some("P")
.filter(|_| flags.contains(Flag::PASSED))
.unwrap_or_default(),
flag_replied = Some("R")
.filter(|_| flags.contains(Flag::REPLIED))
.unwrap_or_default(),
flag_seen = Some("S")
.filter(|_| flags.contains(Flag::SEEN))
.unwrap_or_default(),
flag_trashed = Some("T")
.filter(|_| flags.contains(Flag::TRASHED))
.unwrap_or_default(),
flag_draft = Some("D")
.filter(|_| flags.contains(Flag::DRAFT))
.unwrap_or_default(),
flag_flagged = Some("F")
.filter(|_| flags.contains(Flag::FLAGGED))
.unwrap_or_default(),
selected = if is_selected {
mailbox_settings!(context[coordinates.0][&coordinates.1].listing.selected_flag)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(DEFAULT_SELECTED_FLAG)
} else {
""
},
snoozed = if is_snoozed {
mailbox_settings!(
context[coordinates.0][&coordinates.1]
.listing
.thread_snoozed_flag
)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(DEFAULT_SNOOZED_FLAG)
} else {
""
},
unseen = if is_unseen {
mailbox_settings!(context[coordinates.0][&coordinates.1].listing.unseen_flag)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(DEFAULT_UNSEEN_FLAG)
} else {
""
},
attachments = if has_attachments {
mailbox_settings!(
context[coordinates.0][&coordinates.1]
.listing
.attachment_flag
)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(DEFAULT_ATTACHMENT_FLAG)
} else {
""
},
whitespace = if is_selected || is_unseen || is_snoozed || has_attachments {
" "
} else {
""
},
))
}
}
#[derive(Clone, Copy, Debug)]
struct MailboxMenuEntry {
depth: usize,
indentation: u32,
has_sibling: bool,
visible: bool,
collapsed: bool,
mailbox_hash: MailboxHash,
index_style: Option<IndexStyle>,
}
#[derive(Debug)]
struct AccountMenuEntry {
name: String,
hash: AccountHash,
index: usize,
entries: SmallVec<[MailboxMenuEntry; 16]>,
}
impl AccountMenuEntry {
fn entry_by_hash(&self, needle: MailboxHash) -> Option<usize> {
self.entries.iter().enumerate().find_map(|(i, e)| {
if e.mailbox_hash == needle {
Some(i)
} else {
None
}
})
}
}
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,
envs_to_set: SmallVec<[EnvelopeHash; 8]>,
a: &ListingAction,
) {
fn inner(
context: &mut Context,
envs_to_set: SmallVec<[EnvelopeHash; 8]>,
account_hash: AccountHash,
mailbox_hash: MailboxHash,
a: &ListingAction,
) {
let env_hashes = if let Ok(batch) = EnvelopeHashBatch::try_from(envs_to_set.as_slice())
{
batch
} else {
return;
};
let account = &mut context.accounts[&account_hash];
match a {
ListingAction::Flag(FlagAction::Set(Flag::SEEN)) | ListingAction::SetSeen => {
if let Err(err) = account.set_flags(
env_hashes,
mailbox_hash,
smallvec::smallvec![FlagOp::Set(Flag::SEEN)],
) {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
}
}
ListingAction::Flag(FlagAction::Unset(Flag::SEEN)) | ListingAction::SetUnseen => {
if let Err(err) = account.set_flags(
env_hashes,
mailbox_hash,
smallvec::smallvec![FlagOp::UnSet(Flag::SEEN)],
) {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
}
}
ListingAction::Flag(FlagAction::Set(flag)) => {
if let Err(err) = account.set_flags(
env_hashes,
mailbox_hash,
smallvec::smallvec![FlagOp::Set(*flag)],
) {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
}
}
ListingAction::Flag(FlagAction::Unset(flag)) => {
if let Err(err) = account.set_flags(
env_hashes,
mailbox_hash,
smallvec::smallvec![FlagOp::UnSet(*flag)],
) {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
}
}
ListingAction::Tag(TagAction::Add(ref tag_str)) => {
if let Err(err) = account.set_flags(
env_hashes,
mailbox_hash,
smallvec::smallvec![FlagOp::SetTag(tag_str.into())],
) {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
}
}
ListingAction::Tag(TagAction::Remove(ref tag_str)) => {
if let Err(err) = account.set_flags(
env_hashes,
mailbox_hash,
smallvec::smallvec![FlagOp::UnSetTag(tag_str.into())],
) {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
}
}
ListingAction::Delete => {
let job = account
.backend
.write()
.unwrap()
.delete_messages(env_hashes.clone(), mailbox_hash);
match job {
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
}
Ok(fut) => {
let handle = account
.main_loop_handler
.job_executor
.spawn_specialized("delete".into(), fut);
account.insert_job(
handle.job_id,
JobRequest::DeleteMessages { env_hashes, handle },
);
}
}
}
ListingAction::CopyTo(ref mailbox_path) => {
match account.mailbox_by_path(mailbox_path).and_then(
|destination_mailbox_hash| {
account.backend.write().unwrap().copy_messages(
env_hashes,
mailbox_hash,
destination_mailbox_hash,
/* move? */ false,
)
},
) {
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
}
Ok(fut) => {
let handle = account
.main_loop_handler
.job_executor
.spawn_specialized("copy_to_mailbox".into(), fut);
account.insert_job(
handle.job_id,
JobRequest::Generic {
name: "message copying".into(),
handle,
on_finish: None,
log_level: LogLevel::INFO,
},
);
}
}
}
ListingAction::CopyToOtherAccount(ref _account_name, ref _mailbox_path) => {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(
"Copying to another account is currently unimplemented".into(),
)));
}
ListingAction::MoveTo(ref mailbox_path) => {
match account.mailbox_by_path(mailbox_path).and_then(
|destination_mailbox_hash| {
account.backend.write().unwrap().copy_messages(
env_hashes,
mailbox_hash,
destination_mailbox_hash,
/* move? */ true,
)
},
) {
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
}
Ok(fut) => {
let handle = account
.main_loop_handler
.job_executor
.spawn_specialized("move_to_mailbox".into(), fut);
account.insert_job(
handle.job_id,
JobRequest::Generic {
name: "message moving".into(),
handle,
on_finish: None,
log_level: LogLevel::INFO,
},
);
}
}
}
ListingAction::ExportMbox(format, ref path) => {
let futures: Result<Vec<_>> = envs_to_set
.iter()
.map(|&env_hash| {
account.operation(env_hash).and_then(|mut op| op.as_bytes())
})
.collect::<Result<Vec<_>>>();
let mut path = path.to_path_buf();
if path.is_relative() {
path = context.current_dir().join(&path);
}
let account = &mut context.accounts[&account_hash];
let format = (*format).unwrap_or_default();
let collection = account.collection.clone();
let (sender, mut receiver) = crate::jobs::oneshot::channel();
let fut: Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>> =
Box::pin(async move {
let cl = async move {
// fully capture variables.
let _ = (&envs_to_set, &collection);
let bytes: Vec<Vec<u8>> = try_join_all(futures?).await?;
let envs: Vec<_> = envs_to_set
.iter()
.map(|&env_hash| collection.get_env(env_hash))
.collect();
if path.is_dir() {
if envs.len() == 1 {
path.push(format!("{}.mbox", envs[0].message_id_raw()));
} else {
let now = datetime::timestamp_to_string(
datetime::now(),
Some(datetime::formats::RFC3339_DATETIME),
false,
);
path.push(format!(
"{}-{}-{}_envelopes.mbox",
now,
envs[0].message_id_raw(),
envs.len(),
));
}
}
let mut file = BufWriter::new(File::create(&path)?);
let mut iter = envs.iter().zip(bytes);
let tags_lck = collection.tag_index.read().unwrap();
if let Some((env, ref bytes)) = iter.next() {
let tags: Vec<&str> = env
.tags()
.iter()
.filter_map(|h| tags_lck.get(h).map(|s| s.as_str()))
.collect();
format.append(
&mut file,
bytes.as_slice(),
env.from().first(),
Some(env.date()),
(env.flags(), tags),
MboxMetadata::CClient,
true,
false,
)?;
}
for (env, bytes) in iter {
let tags: Vec<&str> = env
.tags()
.iter()
.filter_map(|h| tags_lck.get(h).map(|s| s.as_str()))
.collect();
format.append(
&mut file,
bytes.as_slice(),
env.from().first(),
Some(env.date()),
(env.flags(), tags),
MboxMetadata::CClient,
false,
false,
)?;
}
file.flush()?;
Ok(path)
};
let r: Result<PathBuf> = cl.await;
let _ = sender.send(r);
Ok(())
});
let handle = account
.main_loop_handler
.job_executor
.spawn_blocking("exporting mbox".into(), fut);
account.insert_job(
handle.job_id,
JobRequest::Generic {
name: "exporting mbox".into(),
handle,
on_finish: Some(CallbackFn(Box::new(move |context: &mut Context| {
context.replies.push_back(match receiver.try_recv() {
Err(_) | Ok(None) => UIEvent::Notification {
title: Some("Could not export mbox".into()),
source: None,
body: "Job was canceled.".into(),
kind: Some(NotificationType::Info),
},
Ok(Some(Err(err))) => UIEvent::Notification {
title: Some("Could not export mbox".into()),
source: None,
body: err.to_string().into(),
kind: Some(NotificationType::Error(err.kind)),
},
Ok(Some(Ok(path))) => UIEvent::Notification {
title: Some("Successfully exported mbox".into()),
source: None,
body: format!("Wrote to file {}", path.display()).into(),
kind: Some(NotificationType::Info),
},
});
}))),
log_level: LogLevel::INFO,
},
);
}
ListingAction::MoveToOtherAccount(ref _account_name, ref _mailbox_path) => {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(
"Moving to another account is currently unimplemented".into(),
)));
}
_ => unreachable!(),
}
}
let account_hash = self.coordinates().0;
let mailbox_hash = self.coordinates().1;
/*{
let threads_lck = account.collection.get_threads(mailbox_hash);
for thread_hash in thread_hashes {
for (_, h) in threads_lck.thread_iter(thread_hash) {
envs_to_set.push(threads_lck.thread_nodes()[&h].message().unwrap());
}
self.row_updates().push(thread_hash);
}
}
*/
inner(context, envs_to_set, account_hash, mailbox_hash, a);
self.set_dirty(true);
}
fn row_updates(&mut self) -> &mut SmallVec<[EnvelopeHash; 8]>;
fn selection(&mut self) -> &mut HashMap<EnvelopeHash, bool>;
fn get_focused_items(&self, _context: &Context) -> SmallVec<[EnvelopeHash; 8]>;
fn redraw_threads_list(
&mut self,
context: &Context,
items: Box<dyn Iterator<Item = ThreadHash>>,
);
fn redraw_envelope_list(
&mut self,
_context: &Context,
_items: Box<dyn Iterator<Item = EnvelopeHash>>,
) {
}
/// Use `force` when there have been changes in the mailbox or account lists
/// in `context`
fn refresh_mailbox(&mut self, context: &mut Context, force: bool);
}
pub trait ListingTrait: Component {
fn coordinates(&self) -> (AccountHash, MailboxHash);
fn set_coordinates(&mut self, _: (AccountHash, MailboxHash));
fn next_entry(&mut self, context: &mut Context);
fn prev_entry(&mut self, context: &mut Context);
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context);
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context);
fn filter(
&mut self,
_filter_term: String,
_results: SmallVec<[EnvelopeHash; 512]>,
_context: &Context,
) {
}
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),
});
}
fn format_date(&self, context: &Context, epoch: UnixTimestamp) -> String {
let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(epoch);
let now: std::time::Duration = std::time::SystemTime::now()
.duration_since(d)
.unwrap_or_else(|_| std::time::Duration::new(std::u64::MAX, 0));
match now.as_secs() {
n if context.settings.listing.recent_dates && n < 60 * 60 => format!(
"{} minute{} ago",
n / (60),
if n / 60 == 1 { "" } else { "s" }
),
n if context.settings.listing.recent_dates && n < 24 * 60 * 60 => format!(
"{} hour{} ago",
n / (60 * 60),
if n / (60 * 60) == 1 { "" } else { "s" }
),
n if context.settings.listing.recent_dates && n < 7 * 24 * 60 * 60 => format!(
"{} day{} ago",
n / (24 * 60 * 60),
if n / (24 * 60 * 60) == 1 { "" } else { "s" }
),
_ => melib::utils::datetime::timestamp_to_string(
epoch,
context
.settings
.listing
.datetime_fmt
.as_deref()
.or(Some("%Y-%m-%d %T")),
false,
),
}
}
}
#[derive(Debug)]
pub enum ListingComponent {
Compact(Box<CompactListing>),
Conversations(Box<ConversationsListing>),
Offline(Box<OfflineListing>),
Plain(Box<PlainListing>),
Threaded(Box<ThreadListing>),
}
use crate::ListingComponent::*;
impl std::ops::Deref for ListingComponent {
type Target = dyn MailListingTrait;
fn deref(&self) -> &Self::Target {
match &self {
Compact(ref l) => l.as_ref(),
Conversations(ref l) => l.as_ref(),
Offline(ref l) => l.as_ref(),
Plain(ref l) => l.as_ref(),
Threaded(ref l) => l.as_ref(),
}
}
}
impl std::ops::DerefMut for ListingComponent {
fn deref_mut(&mut self) -> &mut (dyn MailListingTrait + 'static) {
match self {
Compact(l) => l.as_mut(),
Conversations(l) => l.as_mut(),
Offline(l) => l.as_mut(),
Plain(l) => l.as_mut(),
Threaded(l) => l.as_mut(),
}
}
}
impl ListingComponent {
fn id(&self) -> ComponentId {
match self {
Compact(l) => l.as_component().id(),
Conversations(l) => l.as_component().id(),
Offline(l) => l.as_component().id(),
Plain(l) => l.as_component().id(),
Threaded(l) => l.as_component().id(),
}
}
}
#[derive(Debug, Eq, PartialEq)]
enum ListingFocus {
Menu,
Mailbox,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct CursorPos {
account: usize,
menu: MenuEntryCursor,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MenuEntryCursor {
Status,
Mailbox(usize),
}
impl std::ops::Sub<MenuEntryCursor> for isize {
type Output = Self;
fn sub(self, other: MenuEntryCursor) -> Self {
if let MenuEntryCursor::Mailbox(v) = other {
v as Self - self
} else {
self - 1
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ShowMenuScrollbar {
Never,
True,
False,
}
#[derive(Debug)]
pub struct Listing {
component: ListingComponent,
accounts: Vec<AccountMenuEntry>,
status: Option<AccountStatus>,
dirty: bool,
cursor_pos: CursorPos,
menu_cursor_pos: CursorPos,
menu: Screen<Virtual>,
menu_scrollbar_show_timer: crate::jobs::Timer,
show_menu_scrollbar: ShowMenuScrollbar,
startup_checks_rate: RateLimit,
id: ComponentId,
theme_default: ThemeAttribute,
sidebar_divider: char,
sidebar_divider_theme: ThemeAttribute,
menu_visibility: bool,
cmd_buf: String,
/// This is the width of the right container to the entire width.
ratio: usize, // right/(container width) * 100
prev_ratio: usize,
menu_width: WidgetWidth,
focus: ListingFocus,
view: Option<Box<ThreadView>>,
}
impl std::fmt::Display for Listing {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.component {
Compact(ref l) => write!(f, "{}", l),
Conversations(ref l) => write!(f, "{}", l),
Offline(ref l) => write!(f, "{}", l),
Plain(ref l) => write!(f, "{}", l),
Threaded(ref l) => write!(f, "{}", l),
}
}
}
impl Component for Listing {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if !self.is_dirty() {
return;
}
let total_cols = area.width();
let right_component_width = if self.is_menu_visible() {
if self.focus == ListingFocus::Menu {
(self.ratio * total_cols) / 100
} else {
match self.menu_width {
WidgetWidth::Set(ref mut v) | WidgetWidth::Hold(ref mut v) => {
if *v == 0 {
*v = 1;
} else if *v >= total_cols {
*v = total_cols.saturating_sub(2);
}
total_cols.saturating_sub(*v)
}
WidgetWidth::Unset => {
self.menu_width =
WidgetWidth::Set(total_cols - ((self.ratio * total_cols) / 100));
(self.ratio * total_cols) / 100
}
}
}
} else {
total_cols
};
let mid = area.width().saturating_sub(right_component_width);
if self.dirty && mid != 0 {
let divider_area = area.nth_col(mid);
for row in grid.bounds_iter(divider_area) {
for c in row {
grid[c]
.set_ch(self.sidebar_divider)
.set_fg(self.sidebar_divider_theme.fg)
.set_bg(self.sidebar_divider_theme.bg)
.set_attrs(self.sidebar_divider_theme.attrs);
}
}
context.dirty_areas.push_back(divider_area);
}
let account_hash = self.accounts[self.cursor_pos.account].hash;
if right_component_width == total_cols {
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
.process_event(&mut UIEvent::VisibilityChange(true), context);
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() {
if let Some(ref mut view) = self.view {
view.draw(grid, self.component.view_area().unwrap_or(area), context);
}
}
}
} else if right_component_width == 0 {
self.draw_menu(grid, area, context);
} else {
self.draw_menu(grid, area.take_cols(mid), context);
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
.process_event(&mut UIEvent::VisibilityChange(true), context);
self.component.realize(self.id().into(), context);
}
if let Some(s) = self.status.as_mut() {
s.draw(grid, area.skip_cols(mid + 1), context);
} else {
let area = area.skip_cols(mid + 1);
self.component.draw(grid, area, context);
if self.component.unfocused() {
if let Some(ref mut view) = self.view {
view.draw(grid, self.component.view_area().unwrap_or(area), context);
}
}
}
}
self.dirty = false;
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
match event {
UIEvent::ConfigReload { old_settings: _ } => {
self.theme_default = crate::conf::value(context, "theme_default");
let account_hash = context.accounts[self.cursor_pos.account].hash();
self.sidebar_divider =
*account_settings!(context[account_hash].listing.sidebar_divider);
self.sidebar_divider_theme = conf::value(context, "mail.sidebar_divider");
self.menu.grid_mut().empty();
self.set_dirty(true);
}
UIEvent::Timer(n) if *n == self.menu_scrollbar_show_timer.id() => {
if self.show_menu_scrollbar == ShowMenuScrollbar::True {
self.show_menu_scrollbar = ShowMenuScrollbar::False;
self.set_dirty(true);
self.menu.grid_mut().empty();
}
return true;
}
UIEvent::StartupCheck(ref f)
if self.component.coordinates().1 == *f && !self.startup_checks_rate.tick() =>
{
return false;
}
UIEvent::Timer(n) if *n == self.startup_checks_rate.id() => {
if self.startup_checks_rate.active {
self.startup_checks_rate.reset();
return self.process_event(
&mut UIEvent::StartupCheck(self.component.coordinates().1),
context,
);
}
}
UIEvent::AccountStatusChange(account_hash, msg) => {
let account_index: usize = context
.accounts
.get_index_of(account_hash)
.expect("Invalid account_hash in UIEventMailbox{Delete,Create}");
if self.cursor_pos.account == account_index {
self.change_account(context);
} else {
let previous_collapsed_mailboxes: BTreeSet<MailboxHash> = self.accounts
[account_index]
.entries
.iter()
.filter_map(|e| {
if e.collapsed {
Some(e.mailbox_hash)
} else {
None
}
})
.collect::<_>();
let previous_index_styles: BTreeMap<MailboxHash, IndexStyle> = self.accounts
[account_index]
.entries
.iter()
.filter_map(|e| Some((e.mailbox_hash, e.index_style?)))
.collect::<_>();
self.accounts[account_index].entries = context.accounts[&*account_hash]
.list_mailboxes()
.into_iter()
.filter(|mailbox_node| {
context.accounts[&*account_hash][&mailbox_node.hash]
.ref_mailbox
.is_subscribed()
})
.map(|f| MailboxMenuEntry {
depth: f.depth,
indentation: f.indentation,
has_sibling: f.has_sibling,
mailbox_hash: f.hash,
visible: true,
collapsed: if previous_collapsed_mailboxes.is_empty() {
context.accounts[&*account_hash][&f.hash].conf.collapsed
} else {
previous_collapsed_mailboxes.contains(&f.hash)
},
index_style: previous_index_styles.get(&f.hash).copied(),
})
.collect::<_>();
self.menu.grid_mut().empty();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(match msg {
Some(msg) => format!("{} {}", self.status(context), msg),
None => self.status(context),
})));
}
}
UIEvent::MailboxDelete((account_hash, mailbox_hash))
| UIEvent::MailboxCreate((account_hash, mailbox_hash)) => {
let account_index = context
.accounts
.get_index_of(account_hash)
.expect("Invalid account_hash in UIEventMailbox{Delete,Create}");
self.menu.grid_mut().empty();
let previous_collapsed_mailboxes: BTreeSet<MailboxHash> = self.accounts
[account_index]
.entries
.iter()
.filter_map(|e| {
if e.collapsed {
Some(e.mailbox_hash)
} else {
None
}
})
.collect::<_>();
let previous_index_styles: BTreeMap<MailboxHash, IndexStyle> = self.accounts
[account_index]
.entries
.iter()
.filter_map(|e| Some((e.mailbox_hash, e.index_style?)))
.collect::<_>();
self.accounts[account_index].entries = context.accounts[&*account_hash]
.list_mailboxes()
.into_iter()
.filter(|mailbox_node| {
context.accounts[&*account_hash][&mailbox_node.hash]
.ref_mailbox
.is_subscribed()
})
.map(|f| MailboxMenuEntry {
depth: f.depth,
indentation: f.indentation,
has_sibling: f.has_sibling,
mailbox_hash: f.hash,
visible: true,
collapsed: previous_collapsed_mailboxes.contains(&f.hash),
index_style: previous_index_styles.get(&f.hash).copied(),
})
.collect::<_>();
let mut fallback = 0;
if let MenuEntryCursor::Mailbox(ref mut cur) = self.cursor_pos.menu {
*cur = std::cmp::min(
self.accounts[self.cursor_pos.account]
.entries
.len()
.saturating_sub(1),
*cur,
);
fallback = *cur;
}
if self.component.coordinates() == (*account_hash, *mailbox_hash) {
self.component
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.component.set_coordinates((
self.accounts[self.cursor_pos.account].hash,
self.accounts[self.cursor_pos.account].entries[fallback].mailbox_hash,
));
self.component.refresh_mailbox(context, true);
self.component
.process_event(&mut UIEvent::VisibilityChange(true), context);
}
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.status(context),
)));
self.set_dirty(true);
return true;
}
UIEvent::ChangeMode(UIMode::Normal) => {
self.set_dirty(true);
}
UIEvent::Resize => {
self.set_dirty(true);
}
UIEvent::Action(Action::ViewMailbox(ref idx)) => {
if let Some(MailboxMenuEntry { mailbox_hash, .. }) =
self.accounts[self.cursor_pos.account].entries.get(*idx)
{
let account_hash = self.accounts[self.cursor_pos.account].hash;
self.cursor_pos.menu = MenuEntryCursor::Mailbox(*idx);
self.status = None;
self.component
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.component
.set_coordinates((account_hash, *mailbox_hash));
self.component
.process_event(&mut UIEvent::VisibilityChange(true), context);
self.menu.grid_mut().empty();
self.set_dirty(true);
}
return true;
}
UIEvent::IntraComm {
from,
to,
ref content,
} if (*from, *to) == (self.component.id(), self.id()) => {
match content.downcast_ref::<ListingMessage>().copied() {
None => {}
Some(ListingMessage::FocusUpdate { new_value }) => {
if let Some(ref mut view) = self.view {
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) => {
if let Some(ref mut view) = self.view {
view.set_dirty(true);
}
}
Some(ListingMessage::OpenEntryUnderCursor {
env_hash,
thread_hash,
show_thread,
go_to_first_unread,
}) => {
let (a, m) = self.component.coordinates();
if let Some(view) = self.view.take() {
view.unrealize(context);
}
self.view = Some(Box::new(ThreadView::new(
(a, m, env_hash),
thread_hash,
Some(env_hash),
go_to_first_unread,
if show_thread {
None
} else {
Some(ThreadViewFocus::MailView)
},
context,
)));
}
}
return true;
}
#[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>().copied()
);
log::debug!(
"BUG component is {} and self id is {}",
self.component.id(),
self.id()
);
}
}
_ => {}
}
if self.component.unfocused()
&& self
.view
.as_mut()
.map(|v| v.process_event(event, context))
.unwrap_or(false)
{
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) {
return true;
}
}
}
if (self.focus == ListingFocus::Mailbox && self.status.is_none())
&& ((self.component.unfocused()
&& self
.view
.as_mut()
.map(|v| v.process_event(event, context))
.unwrap_or(false))
|| self.component.process_event(event, context))
{
return true;
}
let shortcuts = {
let mut m = self.shortcuts(context);
m.insert(
Shortcuts::GENERAL,
context.settings.shortcuts.general.key_values(),
);
m
};
if self.focus == ListingFocus::Mailbox {
match *event {
UIEvent::Input(Key::Mouse(MouseEvent::Press(MouseButton::Left, x, _y)))
if self.is_menu_visible() =>
{
match self.menu_width {
WidgetWidth::Hold(wx) | WidgetWidth::Set(wx)
if wx + 1 == usize::from(x) =>
{
self.menu_width = WidgetWidth::Hold(wx - 1);
}
WidgetWidth::Set(_) => return false,
WidgetWidth::Hold(x) => {
self.menu_width = WidgetWidth::Set(x);
}
WidgetWidth::Unset => return false,
}
self.set_dirty(true);
return true;
}
UIEvent::Input(Key::Mouse(MouseEvent::Hold(x, _y))) if self.is_menu_visible() => {
match self.menu_width {
WidgetWidth::Hold(ref mut hx) => {
*hx = usize::from(x).saturating_sub(1);
}
_ => return false,
}
self.set_dirty(true);
return true;
}
UIEvent::Input(Key::Mouse(MouseEvent::Release(x, _y)))
if self.is_menu_visible() =>
{
match self.menu_width {
WidgetWidth::Hold(_) => {
self.menu_width = WidgetWidth::Set(usize::from(x).saturating_sub(1));
}
_ => return false,
}
self.set_dirty(true);
return true;
}
UIEvent::Input(ref k)
if self.is_menu_visible()
&& shortcut!(k == shortcuts[Shortcuts::LISTING]["focus_left"]) =>
{
self.focus = ListingFocus::Menu;
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
self.menu_scrollbar_show_timer.rearm();
self.show_menu_scrollbar = ShowMenuScrollbar::True;
}
self.prev_ratio = self.ratio;
self.ratio = 50;
self.set_dirty(true);
}
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Shortcuts::LISTING]["next_mailbox"])
|| shortcut!(k == shortcuts[Shortcuts::LISTING]["prev_mailbox"]) =>
{
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
let target = match k {
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["next_mailbox"]) => {
match self.cursor_pos.menu {
MenuEntryCursor::Status => amount.saturating_sub(1),
MenuEntryCursor::Mailbox(idx) => idx + amount,
}
}
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["prev_mailbox"]) => {
match self.cursor_pos.menu {
MenuEntryCursor::Status => {
return true;
}
MenuEntryCursor::Mailbox(idx) => {
if idx >= amount {
idx - amount
} else {
return true;
}
}
}
}
_ => return true,
};
if self.accounts[self.cursor_pos.account]
.entries
.get(target)
.is_some()
{
self.cursor_pos.menu = MenuEntryCursor::Mailbox(target)
} else {
return true;
}
self.change_account(context);
return true;
}
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Shortcuts::LISTING]["next_account"])
|| shortcut!(k == shortcuts[Shortcuts::LISTING]["prev_account"]) =>
{
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
match k {
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["next_account"]) => {
if self.cursor_pos.account + amount < self.accounts.len() {
self.cursor_pos.account += amount;
let _new_val = self.cursor_pos.account;
self.cursor_pos.menu = if let Some(idx) = context.accounts[_new_val]
.default_mailbox()
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
{
MenuEntryCursor::Mailbox(idx)
} else {
MenuEntryCursor::Status
};
} else {
return true;
}
}
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["prev_account"]) => {
if self.cursor_pos.account >= amount {
self.cursor_pos.account -= amount;
let _new_val = self.cursor_pos.account;
self.cursor_pos.menu = if let Some(idx) = context.accounts[_new_val]
.default_mailbox()
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
{
MenuEntryCursor::Mailbox(idx)
} else {
MenuEntryCursor::Status
};
} else {
return true;
}
}
_ => return false,
}
self.change_account(context);
return true;
}
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Shortcuts::LISTING]["toggle_menu_visibility"]) =>
{
self.menu_visibility = !self.menu_visibility;
self.set_dirty(true);
}
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Shortcuts::LISTING]["increase_sidebar"]) =>
{
self.ratio = self.ratio.saturating_sub(2);
self.prev_ratio = self.prev_ratio.saturating_sub(2);
self.menu_width = WidgetWidth::Unset;
self.set_dirty(true);
}
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Shortcuts::LISTING]["decrease_sidebar"]) =>
{
self.ratio += 2;
self.ratio = std::cmp::min(100, self.ratio);
self.prev_ratio += 2;
self.prev_ratio = std::cmp::min(100, self.prev_ratio);
self.menu_width = WidgetWidth::Unset;
self.set_dirty(true);
}
_ => {}
}
if self.status.is_none() {
match event {
UIEvent::Action(ref action) => match action {
Action::Listing(ListingAction::SetPlain) => {
self.set_index_style(IndexStyle::Plain, context);
return true;
}
Action::Listing(ListingAction::SetThreaded) => {
self.set_index_style(IndexStyle::Threaded, context);
return true;
}
Action::Listing(ListingAction::SetCompact) => {
self.set_index_style(IndexStyle::Compact, context);
return true;
}
Action::Listing(ListingAction::SetConversations) => {
self.set_index_style(IndexStyle::Conversations, context);
return true;
}
Action::Listing(ListingAction::Import(file_path, mailbox_path)) => {
let account = &mut context.accounts[self.cursor_pos.account];
if let Err(err) = account
.mailbox_by_path(mailbox_path)
.and_then(|mailbox_hash| {
Ok((
std::fs::read(file_path).chain_err_summary(|| {
format!("Could not read {}", file_path.display())
})?,
mailbox_hash,
))
})
.and_then(|(bytes, mailbox_hash)| {
account.save(&bytes, mailbox_hash, None)
})
{
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
}
return true;
}
Action::Listing(a @ ListingAction::SetSeen)
| Action::Listing(a @ ListingAction::SetUnseen)
| Action::Listing(a @ ListingAction::Delete)
| Action::Listing(a @ ListingAction::CopyTo(_))
| Action::Listing(a @ ListingAction::MoveTo(_))
| Action::Listing(a @ ListingAction::CopyToOtherAccount(_, _))
| Action::Listing(a @ ListingAction::MoveToOtherAccount(_, _))
| Action::Listing(a @ ListingAction::ExportMbox(_, _))
| Action::Listing(a @ ListingAction::Flag(_))
| Action::Listing(a @ ListingAction::Tag(_)) => {
let focused = self.component.get_focused_items(context);
self.component.perform_action(context, focused, a);
let should_be_unselected: bool = matches!(
a,
ListingAction::Delete
| ListingAction::MoveTo(_)
| ListingAction::MoveToOtherAccount(_, _)
);
let mut row_updates: SmallVec<[EnvelopeHash; 8]> = SmallVec::new();
for (k, v) in self.component.selection().iter_mut() {
if *v {
*v = !should_be_unselected;
row_updates.push(*k);
}
}
self.component.row_updates().extend(row_updates);
return true;
}
Action::Listing(ListingAction::ClearSelection) => {
// Clear selection.
let row_updates: SmallVec<[EnvelopeHash; 8]> =
self.component.get_focused_items(context);
for h in &row_updates {
if let Some(val) = self.component.selection().get_mut(h) {
*val = false;
}
}
self.component.row_updates().extend(row_updates);
self.component.set_dirty(true);
return true;
}
_ => {}
},
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::LISTING]["scroll_up"]) =>
{
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
self.component.set_movement(PageMovement::Up(amount));
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::LISTING]["scroll_down"]) =>
{
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
self.component.set_movement(PageMovement::Down(amount));
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) =>
{
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
self.component.set_movement(PageMovement::Right(amount));
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) =>
{
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
self.component.set_movement(PageMovement::Left(amount));
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::LISTING]["prev_page"]) =>
{
let mult = if self.cmd_buf.is_empty() {
1
} else if let Ok(mult) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
mult
} else {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
self.component.set_movement(PageMovement::PageUp(mult));
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::LISTING]["next_page"]) =>
{
let mult = if self.cmd_buf.is_empty() {
1
} else if let Ok(mult) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
mult
} else {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
self.component.set_movement(PageMovement::PageDown(mult));
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) =>
{
self.component.set_movement(PageMovement::Home);
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) =>
{
self.component.set_movement(PageMovement::End);
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::LISTING]["search"]) =>
{
context
.replies
.push_back(UIEvent::CmdInput(Key::Paste("search ".to_string())));
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Command));
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::LISTING]["set_seen"]) =>
{
let mut event = UIEvent::Action(Action::Listing(ListingAction::SetSeen));
if self.process_event(&mut event, context) {
return true;
}
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::LISTING]["refresh"]) =>
{
let account = &mut context.accounts[self.cursor_pos.account];
if let MenuEntryCursor::Mailbox(idx) = self.cursor_pos.menu {
if let Some(&mailbox_hash) = account.mailboxes_order.get(idx) {
if let Err(err) = account.refresh(mailbox_hash) {
context.replies.push_back(UIEvent::Notification {
title: Some("Could not refresh.".into()),
source: None,
body: err.to_string().into(),
kind: Some(NotificationType::Error(err.kind)),
});
}
}
}
return true;
}
UIEvent::Input(ref key)
if !self.component.unfocused()
&& shortcut!(
key == shortcuts[Shortcuts::LISTING]["union_modifier"]
)
&& self.component.modifier_command().is_some() =>
{
self.component.set_modifier_command(Some(Modifier::Union));
return true;
}
UIEvent::Input(ref key)
if !self.component.unfocused()
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["diff_modifier"])
&& self.component.modifier_command().is_some() =>
{
self.component
.set_modifier_command(Some(Modifier::Difference));
return true;
}
UIEvent::Input(ref key)
if !self.component.unfocused()
&& shortcut!(
key == shortcuts[Shortcuts::LISTING]["intersection_modifier"]
)
&& self.component.modifier_command().is_some() =>
{
self.component
.set_modifier_command(Some(Modifier::Intersection));
return true;
}
UIEvent::Input(ref key)
if self.component.unfocused()
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["next_entry"]) =>
{
self.component.next_entry(context);
return true;
}
UIEvent::Input(ref key)
if self.component.unfocused()
&& shortcut!(
key == shortcuts[Shortcuts::LISTING]["previous_entry"]
) =>
{
self.component.prev_entry(context);
return true;
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
if !self.component.unfocused() =>
{
// Clear selection.
let row_updates: SmallVec<[EnvelopeHash; 8]> =
self.component.get_focused_items(context);
for h in &row_updates {
if let Some(val) = self.component.selection().get_mut(h) {
*val = false;
}
}
self.component.row_updates().extend(row_updates);
self.component.set_dirty(true);
return true;
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
if !self.cmd_buf.is_empty() =>
{
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
}
UIEvent::Input(Key::Char(c)) if c.is_ascii_digit() => {
self.cmd_buf.push(*c);
self.component.set_modifier_active(true);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
self.cmd_buf.clone(),
)));
return true;
}
_ => {}
}
}
} else if self.focus == ListingFocus::Menu {
match *event {
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Shortcuts::LISTING]["focus_right"]) =>
{
self.focus = ListingFocus::Mailbox;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
self.ratio = self.prev_ratio;
self.set_dirty(true);
return true;
}
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Shortcuts::LISTING]["open_mailbox"])
&& self.menu_cursor_pos.menu == MenuEntryCursor::Status =>
{
self.cursor_pos = self.menu_cursor_pos;
self.change_account(context);
self.set_dirty(true);
self.focus = ListingFocus::Mailbox;
self.ratio = self.prev_ratio;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
return true;
}
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Shortcuts::LISTING]["toggle_mailbox_collapse"])
&& matches!(self.menu_cursor_pos.menu, MenuEntryCursor::Mailbox(_)) =>
{
let target_mailbox_idx =
if let MenuEntryCursor::Mailbox(idx) = self.menu_cursor_pos.menu {
idx
} else {
return false;
};
if let Some(target) = self.accounts[self.menu_cursor_pos.account]
.entries
.get_mut(target_mailbox_idx)
{
target.collapsed = !(target.collapsed);
self.dirty = true;
self.menu.grid_mut().empty();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
return true;
}
return false;
}
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Shortcuts::LISTING]["open_mailbox"]) =>
{
self.cursor_pos = self.menu_cursor_pos;
self.change_account(context);
self.focus = ListingFocus::Mailbox;
self.ratio = self.prev_ratio;
self.set_dirty(true);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.status(context),
)));
return true;
}
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Shortcuts::LISTING]["scroll_up"])
|| shortcut!(k == shortcuts[Shortcuts::LISTING]["scroll_down"]) =>
{
let mut amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
if shortcut!(k == shortcuts[Shortcuts::LISTING]["scroll_up"]) {
while amount > 0 {
match self.menu_cursor_pos {
CursorPos {
ref mut account,
menu: MenuEntryCursor::Status,
} => {
if *account > 0 {
*account -= 1;
self.menu_cursor_pos.menu =
if self.accounts[*account].entries.is_empty() {
MenuEntryCursor::Status
} else {
MenuEntryCursor::Mailbox(
self.accounts[*account]
.entries
.len()
.saturating_sub(1),
)
};
} else {
return true;
}
}
CursorPos {
ref account,
menu: MenuEntryCursor::Mailbox(ref mut mailbox_idx),
} => loop {
if *mailbox_idx > 0 {
*mailbox_idx -= 1;
if self.accounts[*account].entries[*mailbox_idx].visible {
break;
}
} else {
self.menu_cursor_pos.menu = MenuEntryCursor::Status;
break;
}
},
}
amount -= 1;
}
} else if shortcut!(k == shortcuts[Shortcuts::LISTING]["scroll_down"]) {
while amount > 0 {
match self.menu_cursor_pos {
// If current account has mailboxes, go to first mailbox
CursorPos {
ref account,
ref mut menu,
} if !self.accounts[*account].entries.is_empty()
&& *menu == MenuEntryCursor::Status =>
{
if let Some(idx) = context.accounts[*account]
.default_mailbox()
.and_then(|h| self.accounts[*account].entry_by_hash(h))
{
*menu = MenuEntryCursor::Mailbox(idx);
}
}
// If current account has no mailboxes, go to next account
CursorPos {
ref mut account,
ref mut menu,
} if *account + 1 < self.accounts.len()
&& *menu == MenuEntryCursor::Status =>
{
*account += 1;
*menu = MenuEntryCursor::Status;
}
// If current account has no mailboxes and there is no next account,
// return true
CursorPos {
menu: MenuEntryCursor::Status,
..
} => {
return true;
}
CursorPos {
ref mut account,
menu: MenuEntryCursor::Mailbox(ref mut mailbox_idx),
} => loop {
if (*mailbox_idx + 1) < self.accounts[*account].entries.len() {
*mailbox_idx += 1;
if self.accounts[*account].entries[*mailbox_idx].visible {
break;
}
} else if *account + 1 < self.accounts.len() {
*account += 1;
self.menu_cursor_pos.menu = MenuEntryCursor::Status;
break;
} else {
return true;
}
},
}
amount -= 1;
}
}
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
self.menu_scrollbar_show_timer.rearm();
self.show_menu_scrollbar = ShowMenuScrollbar::True;
}
self.menu.grid_mut().empty();
self.set_dirty(true);
return true;
}
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Shortcuts::LISTING]["next_mailbox"])
|| shortcut!(k == shortcuts[Shortcuts::LISTING]["prev_mailbox"]) =>
{
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
let target = match k {
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["next_mailbox"]) => {
match self.menu_cursor_pos.menu {
MenuEntryCursor::Status => amount.saturating_sub(1),
MenuEntryCursor::Mailbox(idx) => idx + amount,
}
}
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["prev_mailbox"]) => {
match self.menu_cursor_pos.menu {
MenuEntryCursor::Status => {
return true;
}
MenuEntryCursor::Mailbox(idx) => {
if idx >= amount {
idx - amount
} else {
return true;
}
}
}
}
_ => return true,
};
if self.accounts[self.menu_cursor_pos.account]
.entries
.get(target)
.is_some()
{
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(target)
} else {
return true;
}
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
self.menu_scrollbar_show_timer.rearm();
self.show_menu_scrollbar = ShowMenuScrollbar::True;
}
self.menu.grid_mut().empty();
return true;
}
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Shortcuts::LISTING]["next_account"])
|| shortcut!(k == shortcuts[Shortcuts::LISTING]["prev_account"])
|| shortcut!(k == shortcuts[Shortcuts::LISTING]["next_page"])
|| shortcut!(k == shortcuts[Shortcuts::LISTING]["prev_page"]) =>
{
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
match k {
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["next_account"])
|| shortcut!(k == shortcuts[Shortcuts::LISTING]["next_page"]) =>
{
if self.menu_cursor_pos.account + amount >= self.accounts.len() {
// Go to last mailbox.
self.menu_cursor_pos.menu = if self.accounts
[self.menu_cursor_pos.account]
.entries
.is_empty()
{
MenuEntryCursor::Status
} else {
MenuEntryCursor::Mailbox(
self.accounts[self.menu_cursor_pos.account]
.entries
.len()
.saturating_sub(1),
)
};
} else if self.menu_cursor_pos.account + amount < self.accounts.len() {
self.menu_cursor_pos.account += amount;
let _new_val = self.menu_cursor_pos.account;
self.menu_cursor_pos.menu = if let Some(idx) = context.accounts
[_new_val]
.default_mailbox()
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
{
MenuEntryCursor::Mailbox(idx)
} else {
MenuEntryCursor::Status
};
} else {
return true;
}
}
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["prev_account"])
|| shortcut!(k == shortcuts[Shortcuts::LISTING]["prev_page"]) =>
{
if self.menu_cursor_pos.account >= amount {
self.menu_cursor_pos.account -= amount;
let _new_val = self.menu_cursor_pos.account;
self.menu_cursor_pos.menu = if let Some(idx) = context.accounts
[_new_val]
.default_mailbox()
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
{
MenuEntryCursor::Mailbox(idx)
} else {
MenuEntryCursor::Status
};
} else {
return true;
}
}
_ => return false,
}
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
self.menu_scrollbar_show_timer.rearm();
self.show_menu_scrollbar = ShowMenuScrollbar::True;
}
self.menu.grid_mut().empty();
self.set_dirty(true);
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) =>
{
if matches!(
self.menu_cursor_pos,
CursorPos {
account: 0,
menu: MenuEntryCursor::Mailbox(0)
}
) {
// Can't go anywhere upwards, we're on top already.
return true;
}
match (
self.menu_cursor_pos.menu,
context.accounts[self.menu_cursor_pos.account]
.default_mailbox()
.and_then(|h| {
self.accounts[self.menu_cursor_pos.account].entry_by_hash(h)
}),
) {
(MenuEntryCursor::Mailbox(0), _) => {
self.menu_cursor_pos.account = 0;
}
(MenuEntryCursor::Mailbox(_), Some(v)) => {
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(v);
}
_ => return true,
}
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
self.menu_scrollbar_show_timer.rearm();
self.show_menu_scrollbar = ShowMenuScrollbar::True;
}
self.menu.grid_mut().empty();
self.set_dirty(true);
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) =>
{
let CursorPos {
ref mut account,
ref mut menu,
} = self.menu_cursor_pos;
if matches!(
(*account, *menu),
(a, MenuEntryCursor::Mailbox(
i
)) if a == self.accounts.len().saturating_sub(1) && i ==
self.accounts[*account].entries.len().saturating_sub(1)
) {
// Do nothing, this is the End.
// "Father?"
// "Yes, son?"
// "I want to kill you"
// "Come on, baby"
return true;
} else if matches!(
*menu,
MenuEntryCursor::Mailbox(
i
) if i ==
self.accounts[*account].entries.len().saturating_sub(1)
) {
*account = self.accounts.len().saturating_sub(1);
*menu = if let Some(idx) = context.accounts[*account]
.default_mailbox()
.and_then(|h| self.accounts[*account].entry_by_hash(h))
{
MenuEntryCursor::Mailbox(idx)
} else {
MenuEntryCursor::Status
};
} else if !self.accounts[*account].entries.is_empty() {
*menu = MenuEntryCursor::Mailbox(
self.accounts[*account].entries.len().saturating_sub(1),
);
} else {
*menu = MenuEntryCursor::Status;
}
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
self.menu_scrollbar_show_timer.rearm();
self.show_menu_scrollbar = ShowMenuScrollbar::True;
}
self.menu.grid_mut().empty();
self.set_dirty(true);
return true;
}
_ => {}
}
}
match *event {
UIEvent::Input(ref k) if shortcut!(k == shortcuts[Shortcuts::LISTING]["new_mail"]) => {
let account_hash = context.accounts[self.cursor_pos.account].hash();
let composer = Composer::with_account(account_hash, context);
context
.replies
.push_back(UIEvent::Action(Tab(New(Some(Box::new(composer))))));
return true;
}
UIEvent::Action(Action::Tab(ManageMailboxes)) => {
let account_pos = self.cursor_pos.account;
let mgr = MailboxManager::new(context, account_pos);
context
.replies
.push_back(UIEvent::Action(Tab(New(Some(Box::new(mgr))))));
return true;
}
UIEvent::Action(Action::Tab(ManageJobs)) => {
let mgr = JobManager::new(context);
context
.replies
.push_back(UIEvent::Action(Tab(New(Some(Box::new(mgr))))));
return true;
}
UIEvent::Action(Action::Compose(ComposeAction::Mailto(ref mailto))) => {
let account_hash = context.accounts[self.cursor_pos.account].hash();
let mut composer = Composer::with_account(account_hash, context);
composer.set_draft(mailto.into(), context);
context
.replies
.push_back(UIEvent::Action(Tab(New(Some(Box::new(composer))))));
return true;
}
UIEvent::StartupCheck(_)
| UIEvent::MailboxUpdate(_)
| UIEvent::EnvelopeUpdate(_)
| UIEvent::EnvelopeRename(_, _)
| UIEvent::EnvelopeRemove(_, _) => {
self.dirty = true;
// clear menu to force redraw
self.menu.grid_mut().empty();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.status(context),
)));
}
UIEvent::Input(Key::Backspace) if !self.cmd_buf.is_empty() => {
self.cmd_buf.pop();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
self.cmd_buf.clone(),
)));
return true;
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) if !self.cmd_buf.is_empty() => {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
}
UIEvent::Input(Key::Char(c)) if c.is_ascii_digit() => {
self.cmd_buf.push(c);
self.component.set_modifier_active(true);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
self.cmd_buf.clone(),
)));
return true;
}
UIEvent::Input(ref key)
if context
.settings
.shortcuts
.listing
.commands
.iter()
.any(|cmd| {
if cmd.shortcut == *key {
for cmd in &cmd.command {
context.replies.push_back(UIEvent::Command(cmd.to_string()));
}
return true;
}
false
}) =>
{
return true;
}
_ => {}
}
false
}
fn is_dirty(&self) -> bool {
self.dirty
|| self
.status
.as_ref()
.map(Component::is_dirty)
.unwrap_or_else(|| self.component.is_dirty())
|| if self.component.unfocused() {
self.view.as_ref().map(|v| v.is_dirty()).unwrap_or(false)
} else {
self.component.is_dirty()
}
}
fn set_dirty(&mut self, value: bool) {
self.dirty = value;
if let Some(s) = self.status.as_mut() {
s.set_dirty(value);
} else {
self.component.set_dirty(value);
if self.component.unfocused() {
if let Some(ref mut view) = self.view {
view.set_dirty(value);
}
}
}
}
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map = ShortcutMaps::default();
if self.focus != ListingFocus::Menu && self.component.unfocused() {
if let Some(ref view) = self.view {
map.extend_shortcuts(view.shortcuts(context));
}
}
map.extend_shortcuts(if let Some(s) = self.status.as_ref() {
s.shortcuts(context)
} else {
self.component.shortcuts(context)
});
let mut config_map = context.settings.shortcuts.listing.key_values();
if self.focus != ListingFocus::Menu {
config_map.remove("open_mailbox");
}
map.insert(Shortcuts::LISTING, config_map);
map
}
fn id(&self) -> ComponentId {
self.id
}
fn status(&self, context: &Context) -> String {
let mailbox_hash = match self.cursor_pos.menu {
MenuEntryCursor::Mailbox(idx) => {
if let Some(MailboxMenuEntry { mailbox_hash, .. }) =
self.accounts[self.cursor_pos.account].entries.get(idx)
{
*mailbox_hash
} else {
return String::new();
}
}
MenuEntryCursor::Status => {
return format!("{} status", &self.accounts[self.cursor_pos.account].name)
}
};
let account = &context.accounts[self.cursor_pos.account];
match account[&mailbox_hash].status {
MailboxStatus::Available | MailboxStatus::Parsing(_, _) => {
let (unseen, total) = account[&mailbox_hash]
.ref_mailbox
.count()
.ok()
.unwrap_or((0, 0));
format!(
"Mailbox: {}, Messages: {}, New: {}{}",
account[&mailbox_hash].name(),
total,
unseen,
if account[&mailbox_hash].status.is_parsing() {
"(Loading...)"
} else {
""
}
)
}
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(),
Conversations(l) => l.as_component(),
Offline(l) => l.as_component(),
Plain(l) => l.as_component(),
Threaded(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(),
Conversations(l) => l.as_component_mut(),
Offline(l) => l.as_component_mut(),
Plain(l) => l.as_component_mut(),
Threaded(l) => l.as_component_mut(),
},
);
ret
}
}
impl Listing {
pub fn new(context: &mut Context) -> Self {
let account_entries: Vec<AccountMenuEntry> = context
.accounts
.iter()
.enumerate()
.map(|(i, (h, a))| {
let entries: SmallVec<[MailboxMenuEntry; 16]> = a
.list_mailboxes()
.into_iter()
.filter(|mailbox_node| a[&mailbox_node.hash].ref_mailbox.is_subscribed())
.map(|f| MailboxMenuEntry {
depth: f.depth,
indentation: f.indentation,
has_sibling: f.has_sibling,
mailbox_hash: f.hash,
visible: true,
collapsed: a[&f.hash].conf.collapsed,
index_style: a[&f.hash].conf.conf_override().listing.index_style,
})
.collect::<_>();
AccountMenuEntry {
name: a.name().to_string(),
hash: *h,
index: i,
entries,
}
})
.collect();
let first_account_hash = account_entries[0].hash;
let mut ret = Self {
component: Offline(OfflineListing::new((
first_account_hash,
MailboxHash::default(),
))),
view: None,
accounts: account_entries,
status: None,
dirty: true,
cursor_pos: CursorPos {
account: 0,
menu: MenuEntryCursor::Mailbox(0),
},
menu_cursor_pos: CursorPos {
account: 0,
menu: MenuEntryCursor::Mailbox(0),
},
menu: Screen::<Virtual>::new(),
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.main_loop_handler.job_executor.clone(),
),
theme_default: conf::value(context, "theme_default"),
id: ComponentId::default(),
sidebar_divider: *account_settings!(
context[first_account_hash].listing.sidebar_divider
),
sidebar_divider_theme: conf::value(context, "mail.sidebar_divider"),
menu_visibility: !*account_settings!(
context[first_account_hash].listing.hide_sidebar_on_launch
),
ratio: *account_settings!(context[first_account_hash].listing.sidebar_ratio),
prev_ratio: *account_settings!(context[first_account_hash].listing.sidebar_ratio),
menu_width: WidgetWidth::Unset,
focus: ListingFocus::Mailbox,
cmd_buf: String::with_capacity(4),
};
ret.component.realize(ret.id().into(), context);
ret.change_account(context);
ret
}
fn draw_menu(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
grid.clear_area(area, self.theme_default);
let total_height: usize = 3 * (self.accounts.len())
+ self
.accounts
.iter()
.map(|entry| entry.entries.len() + 1)
.sum::<usize>();
let min_width: usize = area.width();
let (width, height) = self.menu.grid().size();
let cursor = match self.focus {
ListingFocus::Mailbox => self.cursor_pos,
ListingFocus::Menu => self.menu_cursor_pos,
};
if min_width > width || height < total_height || self.dirty {
let _ = self.menu.resize(min_width, total_height);
let mut y = 0;
for a in 0..self.accounts.len() {
let menu_area = self.menu.area().skip_rows(y);
y += self.print_account(menu_area, a, context);
y += 3;
}
}
let rows = area.height();
const SCROLLING_CONTEXT: usize = 3;
let y_offset = (cursor.account)
+ self
.accounts
.iter()
.take(cursor.account)
.map(|entry| entry.entries.len() + 1)
.sum::<usize>()
+ match cursor.menu {
MenuEntryCursor::Status => 0,
MenuEntryCursor::Mailbox(idx) => idx + 1,
}
+ SCROLLING_CONTEXT;
let skip_offset = if y_offset <= rows {
0
} else {
rows * y_offset.wrapping_div(rows).saturating_sub(1) + y_offset.wrapping_rem(rows)
};
grid.copy_area(
self.menu.grid(),
area,
self.menu
.area()
.skip_rows(skip_offset.min((self.menu.area().height() - 1).saturating_sub(rows)))
.take_rows((skip_offset + rows).min(self.menu.area().height() - 1)),
);
if self.show_menu_scrollbar == ShowMenuScrollbar::True && total_height > rows {
if self.focus == ListingFocus::Menu {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::Update {
id: self.id,
context: ScrollContext {
shown_lines: skip_offset + rows,
total_lines: total_height,
has_more_lines: false,
},
},
)));
}
ScrollBar::default().set_show_arrows(true).draw(
grid,
area.nth_col(area.width().saturating_sub(1)),
context,
// position
skip_offset,
// visible_rows
rows,
// length
total_height,
);
} else if total_height < rows {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
context.dirty_areas.push_back(area);
}
/// Print a single account in the menu area.
fn print_account(&mut self, mut area: Area, aidx: usize, context: &Context) -> usize {
let account_y = self.menu.area().height() - area.height();
#[derive(Clone, Copy, Debug)]
struct Line {
collapsed: bool,
depth: usize,
inc: isize,
indentation: u32,
has_sibling: bool,
mailbox_idx: MailboxHash,
count: Option<usize>,
collapsed_count: Option<usize>,
}
// Each entry and its index in the account
let mailboxes: HashMap<MailboxHash, Mailbox> = context.accounts[self.accounts[aidx].index]
.mailbox_entries
.iter()
.map(|(&hash, entry)| (hash, entry.ref_mailbox.clone()))
.collect();
let cursor = match self.focus {
ListingFocus::Mailbox => self.cursor_pos,
ListingFocus::Menu => self.menu_cursor_pos,
};
let must_highlight_account: bool = cursor.account == self.accounts[aidx].index;
let mut lines: Vec<Line> = Vec::new();
for (
i,
&MailboxMenuEntry {
depth,
indentation,
has_sibling,
mailbox_hash,
visible: _,
collapsed,
index_style: _,
},
) in self.accounts[aidx].entries.iter().enumerate()
{
if mailboxes[&mailbox_hash].is_subscribed() {
match context.accounts[self.accounts[aidx].index][&mailbox_hash].status {
MailboxStatus::Failed(_) => {
lines.push(Line {
collapsed,
depth,
inc: i as isize,
indentation,
has_sibling,
mailbox_idx: mailbox_hash,
count: None,
collapsed_count: None,
});
}
_ => {
lines.push(Line {
collapsed,
depth,
inc: i as isize,
indentation,
has_sibling,
mailbox_idx: mailbox_hash,
count: mailboxes[&mailbox_hash].count().ok().map(|(v, _)| v),
collapsed_count: None,
});
}
}
}
}
let account_attrs = if must_highlight_account {
if cursor.menu == MenuEntryCursor::Status {
let mut v = crate::conf::value(context, "mail.sidebar_highlighted");
if !context.settings.terminal.use_color() {
v.attrs |= Attr::REVERSE;
}
v
} else {
crate::conf::value(context, "mail.sidebar_highlighted_account_name")
}
} else {
crate::conf::value(context, "mail.sidebar_account_name")
};
// Print account name first
self.menu.grid_mut().write_string(
&self.accounts[aidx].name,
account_attrs.fg,
account_attrs.bg,
account_attrs.attrs,
area,
None,
);
area = self.menu.area().skip_rows(account_y);
if lines.is_empty() {
self.menu.grid_mut().write_string(
"offline",
crate::conf::value(context, "error_message").fg,
account_attrs.bg,
account_attrs.attrs,
area.skip_rows(1),
None,
);
return 0;
}
area = self.menu.area().skip_rows(account_y);
let lines_len = lines.len();
let mut idx = 0;
let mut branches = String::with_capacity(16);
// What depth to skip if a mailbox is toggled to collapse
// The value should be the collapsed mailbox's indentation, so that its children
// are not visible.
let mut skip: Option<usize> = None;
let mut skipped_counter: usize = 0;
'grid_loop: for y in 0..area.height() {
if idx == lines_len {
break;
}
let mut l = lines[idx];
while let Some(p) = skip {
if l.depth > p {
self.accounts[aidx].entries[idx].visible = false;
idx += 1;
skipped_counter += 1;
if idx >= lines.len() {
break 'grid_loop;
}
l = lines[idx];
} else {
skip = None;
}
}
self.accounts[aidx].entries[idx].visible = true;
if l.collapsed {
skip = Some(l.depth);
// Calculate total unseen from hidden children mailboxes
let mut idx = idx + 1;
let mut counter = 0;
while idx < lines.len() {
if lines[idx].depth <= l.depth {
break;
}
counter += lines[idx].count.unwrap_or(0);
idx += 1;
}
l.collapsed_count = Some(counter);
}
let (att, index_att, unread_count_att) = if must_highlight_account {
if match cursor.menu {
MenuEntryCursor::Mailbox(c) => c == idx,
_ => false,
} {
let mut ret = (
crate::conf::value(context, "mail.sidebar_highlighted"),
crate::conf::value(context, "mail.sidebar_highlighted_index"),
crate::conf::value(context, "mail.sidebar_highlighted_unread_count"),
);
if !context.settings.terminal.use_color() {
ret.0.attrs |= Attr::REVERSE;
ret.1.attrs |= Attr::REVERSE;
ret.2.attrs |= Attr::REVERSE;
}
ret
} else {
(
crate::conf::value(context, "mail.sidebar_highlighted_account"),
crate::conf::value(context, "mail.sidebar_highlighted_account_index"),
crate::conf::value(
context,
"mail.sidebar_highlighted_account_unread_count",
),
)
}
} else {
(
crate::conf::value(context, "mail.sidebar"),
crate::conf::value(context, "mail.sidebar_index"),
crate::conf::value(context, "mail.sidebar_unread_count"),
)
};
// Calculate how many columns the mailbox index tags should occupy with right
// alignment, eg.
// 1
// 2
// ...
// 9
// 10
let total_mailbox_no_digits = {
let mut len = lines_len;
let mut ctr = 1;
while len > 9 {
ctr += 1;
len /= 10;
}
ctr
};
let has_sibling_str: &str = account_settings!(
context[self.accounts[aidx].hash]
.listing
.sidebar_mailbox_tree_has_sibling
)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(" ");
let no_sibling_str: &str = account_settings!(
context[self.accounts[aidx].hash]
.listing
.sidebar_mailbox_tree_no_sibling
)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(" ");
let has_sibling_leaf_str: &str = account_settings!(
context[self.accounts[aidx].hash]
.listing
.sidebar_mailbox_tree_has_sibling_leaf
)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(" ");
let no_sibling_leaf_str: &str = account_settings!(
context[self.accounts[aidx].hash]
.listing
.sidebar_mailbox_tree_no_sibling_leaf
)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(" ");
let (x, _) = self.menu.grid_mut().write_string(
&if *account_settings!(
context[self.accounts[aidx].hash]
.listing
.relative_menu_indices
) && must_highlight_account
{
format!(
"{:>width$}",
(l.inc - cursor.menu).abs(),
width = total_mailbox_no_digits
)
} else {
format!("{:>width$}", l.inc, width = total_mailbox_no_digits)
},
index_att.fg,
index_att.bg,
index_att.attrs,
area.nth_row(y + 1),
None,
);
area = self.menu.area().skip_rows(account_y);
{
branches.clear();
branches.push_str(no_sibling_str);
let leading_zeros = l.indentation.leading_zeros();
let mut o = 1_u32.wrapping_shl(31_u32.saturating_sub(leading_zeros));
for _ in 0..(32_u32.saturating_sub(leading_zeros)) {
if l.indentation & o > 0 {
branches.push_str(has_sibling_str);
} else {
branches.push_str(no_sibling_str);
}
o >>= 1;
}
if l.depth > 0 {
if l.has_sibling {
branches.push_str(has_sibling_leaf_str);
} else {
branches.push_str(no_sibling_leaf_str);
}
}
}
let x = self
.menu
.grid_mut()
.write_string(
&branches,
att.fg,
att.bg,
att.attrs,
area.nth_row(y + 1).skip_cols(x),
None,
)
.0
+ x;
area = self.menu.area().skip_rows(account_y);
let x = self
.menu
.grid_mut()
.write_string(
context.accounts[self.accounts[aidx].index].mailbox_entries[&l.mailbox_idx]
.name(),
att.fg,
att.bg,
att.attrs,
area.nth_row(y + 1).skip_cols(x),
None,
)
.0
+ x;
area = self.menu.area().skip_rows(account_y);
// Unread message count
let count_string = match (l.count, l.collapsed_count) {
(None, None) => " ...".to_string(),
(Some(0), None) => String::new(),
(Some(0), Some(0)) | (None, Some(0)) => " v".to_string(),
(Some(0), Some(coll)) => format!(" ({}) v", coll),
(Some(c), Some(0)) => format!(" {} v", c),
(Some(c), Some(coll)) => format!(" {} ({}) v", c, coll),
(Some(c), None) => format!(" {}", c),
(None, Some(coll)) => format!(" ({}) v", coll),
};
let x = self
.menu
.grid_mut()
.write_string(
&count_string,
unread_count_att.fg,
unread_count_att.bg,
unread_count_att.attrs
| if l.count.unwrap_or(0) > 0 {
Attr::BOLD
} else {
Attr::DEFAULT
},
area.nth_row(y + 1)
.skip_cols(x.min(area.width().saturating_sub(count_string.len()))),
None,
)
.0
+ x.min(area.width().saturating_sub(count_string.len()));
area = self.menu.area().skip_rows(account_y);
for c in self.menu.grid_mut().row_iter(area, x..area.width(), y + 1) {
self.menu.grid_mut()[c]
.set_fg(att.fg)
.set_bg(att.bg)
.set_attrs(att.attrs);
}
idx += 1;
}
if idx == 0 {
0
} else {
idx - 1 - skipped_counter
}
}
fn change_account(&mut self, context: &mut Context) {
let account_hash = context.accounts[self.cursor_pos.account].hash();
let previous_collapsed_mailboxes: BTreeSet<MailboxHash> = self.accounts
[self.cursor_pos.account]
.entries
.iter()
.filter_map(|e| {
if e.collapsed {
Some(e.mailbox_hash)
} else {
None
}
})
.collect::<_>();
let previous_index_styles: BTreeMap<MailboxHash, IndexStyle> = self.accounts
[self.cursor_pos.account]
.entries
.iter()
.filter_map(|e| Some((e.mailbox_hash, e.index_style?)))
.collect::<_>();
self.accounts[self.cursor_pos.account].entries = context.accounts[self.cursor_pos.account]
.list_mailboxes()
.into_iter()
.filter(|mailbox_node| {
context.accounts[self.cursor_pos.account][&mailbox_node.hash]
.ref_mailbox
.is_subscribed()
})
.map(|f| MailboxMenuEntry {
depth: f.depth,
indentation: f.indentation,
has_sibling: f.has_sibling,
mailbox_hash: f.hash,
visible: true,
collapsed: if previous_collapsed_mailboxes.is_empty() {
context.accounts[self.cursor_pos.account][&f.hash]
.conf
.collapsed
} else {
previous_collapsed_mailboxes.contains(&f.hash)
},
index_style: previous_index_styles.get(&f.hash).copied(),
})
.collect::<_>();
match self.cursor_pos.menu {
MenuEntryCursor::Mailbox(idx) => {
// Account might have no mailboxes yet if it's offline
if let Some(MailboxMenuEntry {
mailbox_hash,
index_style,
..
}) = self.accounts[self.cursor_pos.account].entries.get(idx)
{
self.component
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.component
.set_coordinates((account_hash, *mailbox_hash));
self.component.refresh_mailbox(context, true);
// Check if per-mailbox configuration overrides general configuration
let index_style_override =
*mailbox_settings!(context[account_hash][mailbox_hash].listing.index_style);
self.set_index_style(index_style.unwrap_or(index_style_override), 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.component
.process_event(&mut UIEvent::VisibilityChange(true), context);
self.status = None;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.status(context),
)));
}
MenuEntryCursor::Status if context.is_online(account_hash).is_ok() => {
self.open_status(self.cursor_pos.account, context);
}
MenuEntryCursor::Status => {
self.component.unrealize(context);
self.component =
Offline(OfflineListing::new((account_hash, MailboxHash::default())));
self.component.realize(self.id().into(), context);
self.component
.process_event(&mut UIEvent::VisibilityChange(true), context);
self.status = None;
self.cursor_pos.menu = MenuEntryCursor::Mailbox(0);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.status(context),
)));
}
}
self.sidebar_divider = *account_settings!(context[account_hash].listing.sidebar_divider);
self.set_dirty(true);
self.menu_cursor_pos = self.cursor_pos;
// clear menu to force redraw
self.menu.grid_mut().empty();
if *account_settings!(context[account_hash].listing.show_menu_scrollbar) {
self.show_menu_scrollbar = ShowMenuScrollbar::True;
self.menu_scrollbar_show_timer.rearm();
} else {
self.show_menu_scrollbar = ShowMenuScrollbar::Never;
}
}
fn open_status(&mut self, account_idx: usize, context: &mut Context) {
self.status = Some(AccountStatus::new(account_idx, self.theme_default));
self.menu.grid_mut().empty();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.status(context),
)));
}
fn is_menu_visible(&self) -> bool {
!matches!(self.component.focus(), Focus::EntryFullscreen) && self.menu_visibility
}
fn set_index_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)),
)
}
};
if let MenuEntryCursor::Mailbox(idx) = self.cursor_pos.menu {
if let Some(mbox_entry) = self.accounts[self.cursor_pos.account].entries.get_mut(idx) {
mbox_entry.index_style = Some(new_style);
}
}
self.component
.process_event(&mut UIEvent::VisibilityChange(true), context);
old.unrealize(context);
self.component.realize(self.id.into(), context);
}
}
#[derive(Clone, Copy, Debug)]
pub enum ListingMessage {
FocusUpdate {
new_value: Focus,
},
OpenEntryUnderCursor {
env_hash: EnvelopeHash,
thread_hash: ThreadHash,
show_thread: bool,
go_to_first_unread: bool,
},
UpdateView,
}