summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorManos Pitsidianakis <el13635@mail.ntua.gr>2019-06-24 23:57:35 +0300
committerManos Pitsidianakis <el13635@mail.ntua.gr>2019-06-25 10:37:18 +0300
commitf4f3b87f849dcbccaf723c3a424f6f33a8a6201f (patch)
treecdae74a98c7db8c3f16019e5d3d8b5f67142c77c
parent8c97336307737aa121bd57d09cd0daaa477c7913 (diff)
ui: add selections to CompactListing
Select multiple entries by pressing 'v'. Set read/unread and delete actions are then performed on the selected entries.
-rw-r--r--melib/src/backends/maildir.rs3
-rw-r--r--melib/src/email.rs12
-rw-r--r--ui/src/components/mail/listing/compact.rs181
3 files changed, 122 insertions, 74 deletions
diff --git a/melib/src/backends/maildir.rs b/melib/src/backends/maildir.rs
index 2f6aa198..eb71d988 100644
--- a/melib/src/backends/maildir.rs
+++ b/melib/src/backends/maildir.rs
@@ -137,9 +137,6 @@ impl<'a> BackendOp for MaildirOp {
+ 3;
let mut new_name: String = path[..idx].to_string();
let mut flags = self.fetch_flags();
- if !(flags & f).is_empty() {
- return Ok(());
- }
flags.toggle(f);
if !(flags & Flag::DRAFT).is_empty() {
new_name.push('D');
diff --git a/melib/src/email.rs b/melib/src/email.rs
index 90de2071..06630b97 100644
--- a/melib/src/email.rs
+++ b/melib/src/email.rs
@@ -779,10 +779,18 @@ impl Envelope {
self.flags
}
pub fn set_seen(&mut self, operation: Box<BackendOp>) -> Result<()> {
- self.set_flag(Flag::SEEN, operation)
+ if !self.flags.contains(Flag::SEEN) {
+ self.set_flag(Flag::SEEN, operation)
+ } else {
+ Ok(())
+ }
}
pub fn set_unseen(&mut self, operation: Box<BackendOp>) -> Result<()> {
- self.set_flag(Flag::SEEN, operation)
+ if self.flags.contains(Flag::SEEN) {
+ self.set_flag(Flag::SEEN, operation)
+ } else {
+ Ok(())
+ }
}
pub fn is_seen(&self) -> bool {
self.flags.contains(Flag::SEEN)
diff --git a/ui/src/components/mail/listing/compact.rs b/ui/src/components/mail/listing/compact.rs
index 7c656856..785ad833 100644
--- a/ui/src/components/mail/listing/compact.rs
+++ b/ui/src/components/mail/listing/compact.rs
@@ -86,6 +86,7 @@ pub struct CompactListing {
filter_term: String,
filtered_selection: Vec<EnvelopeHash>,
filtered_order: FnvHashMap<EnvelopeHash, usize>,
+ selection: FnvHashMap<EnvelopeHash, bool>,
/// If we must redraw on next redraw event
dirty: bool,
/// If `self.view` exists or not.
@@ -106,29 +107,12 @@ impl ListingTrait for CompactListing {
self.unfocused = false;
}
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) {
+ if self.length == 0 {
+ return;
+ }
+ let i = self.get_envelope_under_cursor(idx, context);
+ let account = &context.accounts[self.cursor_pos.0];
let is_seen = {
- if self.length == 0 {
- return;
- }
- let account = &context.accounts[self.cursor_pos.0];
- let mailbox = account[self.cursor_pos.1].as_ref().unwrap();
- let threads = &account.collection.threads[&mailbox.folder.hash()];
- let i = if self.filtered_selection.is_empty() {
- let thread_node = threads.root_set(idx);
- let thread_node = &threads.thread_nodes()[&thread_node];
- if let Some(i) = thread_node.message() {
- i
- } else {
- let mut iter_ptr = thread_node.children()[0];
- while threads.thread_nodes()[&iter_ptr].message().is_none() {
- iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0];
- }
- threads.thread_nodes()[&iter_ptr].message().unwrap()
- }
- } else {
- self.filtered_selection[idx]
- };
-
let root_envelope: &Envelope = &account.get_env(&i);
root_envelope.is_seen()
};
@@ -138,8 +122,12 @@ impl ListingTrait for CompactListing {
} else {
Color::Default
};
- let bg_color = if self.cursor_pos.2 == idx {
+ let bg_color = if self.cursor_pos.2 == idx && self.selection[&i] {
+ Color::Byte(246 | 210)
+ } else if self.cursor_pos.2 == idx {
Color::Byte(246)
+ } else if self.selection[&i] {
+ Color::Byte(210)
} else if !is_seen {
Color::Byte(251)
} else if idx % 2 == 0 {
@@ -370,6 +358,7 @@ impl ListingTrait for CompactListing {
);
}
+ /* TODO: highlight selected entries */
self.highlight_line(
grid,
(
@@ -394,6 +383,9 @@ impl ListingTrait for CompactListing {
fn filter(&mut self, filter_term: &str, context: &Context) {
self.filtered_order.clear();
self.filtered_selection.clear();
+ for v in self.selection.values_mut() {
+ *v = false;
+ }
self.filter_term.clear();
for (i, h) in self.order.keys().enumerate() {
@@ -402,11 +394,13 @@ impl ListingTrait for CompactListing {
if envelope.subject().contains(&filter_term) {
self.filtered_selection.push(*h);
self.filtered_order.insert(*h, i);
+ self.selection.insert(*h, false);
continue;
}
if envelope.field_from_to_string().contains(&filter_term) {
self.filtered_selection.push(*h);
self.filtered_order.insert(*h, i);
+ self.selection.insert(*h, false);
continue;
}
let op = account.operation(*h);
@@ -416,6 +410,7 @@ impl ListingTrait for CompactListing {
if body_text.contains(&filter_term) {
self.filtered_selection.push(*h);
self.filtered_order.insert(*h, i);
+ self.selection.insert(*h, false);
}
}
if !self.filtered_selection.is_empty() {
@@ -464,6 +459,7 @@ impl CompactListing {
filter_term: String::new(),
filtered_selection: Vec::new(),
filtered_order: FnvHashMap::default(),
+ selection: FnvHashMap::default(),
row_updates: StackVec::new(),
data_columns: DataColumns::default(),
dirty: true,
@@ -570,6 +566,7 @@ impl CompactListing {
let threads = &account.collection.threads[&mailbox.folder.hash()];
self.order.clear();
+ self.selection.clear();
self.length = 0;
let mut rows = Vec::with_capacity(1024);
let mut min_width = (0, 0, 0, 0, 0);
@@ -613,7 +610,14 @@ impl CompactListing {
min_width.4 = cmp::max(min_width.4, strings.4.grapheme_width()); /* subject */
rows.push(strings);
self.order.insert(i, idx);
+ self.selection.insert(i, false);
}
+ let CompactListing {
+ ref mut selection,
+ ref order,
+ ..
+ } = self;
+ selection.retain(|e, _| order.contains_key(e));
/* index column */
self.data_columns.columns[0] =
@@ -738,8 +742,6 @@ impl CompactListing {
}
(false, false) => {}
}
-
- self.order.insert(i, idx);
}
if self.length == 0 {
let message = format!("Folder `{}` is empty.", mailbox.folder.name());
@@ -825,7 +827,9 @@ impl CompactListing {
} else {
Color::Default
};
- let bg_color = if !envelope.is_seen() {
+ let bg_color = if self.selection[&envelope_hash] {
+ Color::Byte(210)
+ } else if !envelope.is_seen() {
Color::Byte(251)
} else if idx % 2 == 0 {
Color::Byte(236)
@@ -907,6 +911,29 @@ impl CompactListing {
}
}
}
+ fn get_envelope_under_cursor(&self, cursor: usize, context: &Context) -> EnvelopeHash {
+ let account = &context.accounts[self.cursor_pos.0];
+ let folder_hash = account[self.cursor_pos.1]
+ .as_ref()
+ .map(|m| m.folder.hash())
+ .unwrap();
+ let threads = &account.collection.threads[&folder_hash];
+ if self.filtered_selection.is_empty() {
+ let thread_node = threads.root_set(cursor);
+ let thread_node = &threads.thread_nodes()[&thread_node];
+ if let Some(i) = thread_node.message() {
+ i
+ } else {
+ let mut iter_ptr = thread_node.children()[0];
+ while threads.thread_nodes()[&iter_ptr].message().is_none() {
+ iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0];
+ }
+ threads.thread_nodes()[&iter_ptr].message().unwrap()
+ }
+ } else {
+ self.filtered_selection[self.cursor_pos.2]
+ }
+ }
}
impl Component for CompactListing {
@@ -1041,6 +1068,10 @@ impl Component for CompactListing {
self.dirty = true;
return true;
}
+ UIEvent::Input(ref key) if !self.unfocused && *key == shortcuts["select_entry"] => {
+ let env_hash = self.get_envelope_under_cursor(self.cursor_pos.2, context);
+ self.selection.entry(env_hash).and_modify(|e| *e = !*e);
+ }
UIEvent::MailboxUpdate((ref idxa, ref idxf))
if (*idxa, *idxf)
== (
@@ -1062,6 +1093,8 @@ impl Component for CompactListing {
UIEvent::EnvelopeRename(ref old_hash, ref new_hash) => {
if let Some(row) = self.order.remove(old_hash) {
self.order.insert(*new_hash, row);
+ let selection_status = self.selection.remove(old_hash).unwrap();
+ self.selection.insert(*new_hash, selection_status);
self.highlight_line(
&mut CellBuffer::default(),
((0, row), (MAX_COLS - 1, row)),
@@ -1116,14 +1149,15 @@ impl Component for CompactListing {
return true;
}
Action::ToggleThreadSnooze => {
+ let i = self.get_envelope_under_cursor(self.cursor_pos.2, context);
let account = &mut context.accounts[self.cursor_pos.0];
+ let thread_hash = account.get_env(&i).thread();
let folder_hash = account[self.cursor_pos.1]
.as_ref()
.map(|m| m.folder.hash())
.unwrap();
let threads = account.collection.threads.entry(folder_hash).or_default();
- let thread_group =
- threads.thread_nodes()[&threads.root_set(self.cursor_pos.2)].thread_group();
+ let thread_group = threads.thread_nodes()[&thread_hash].thread_group();
let thread_group = threads.find(thread_group);
/*let i = if let Some(i) = threads.thread_nodes[&thread_group].message() {
i
@@ -1148,65 +1182,66 @@ impl Component for CompactListing {
Action::Listing(a @ SetRead)
| Action::Listing(a @ SetUnread)
| Action::Listing(a @ Delete) => {
- let account = &mut context.accounts[self.cursor_pos.0];
- let folder_hash = account[self.cursor_pos.1]
- .as_ref()
- .map(|m| m.folder.hash())
- .unwrap();
- let threads = &account.collection.threads[&folder_hash];
- let i = if self.filtered_selection.is_empty() {
- let thread_node = threads.root_set(self.cursor_pos.2);
- let thread_node = &threads.thread_nodes()[&thread_node];
- if let Some(i) = thread_node.message() {
- i
- } else {
- let mut iter_ptr = thread_node.children()[0];
- while threads.thread_nodes()[&iter_ptr].message().is_none() {
- iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0];
- }
- threads.thread_nodes()[&iter_ptr].message().unwrap()
- }
+ /* Iterate over selection if exists, else only over the envelope under the
+ * cursor. Using two iterators allows chaining them which results into a Chain
+ * type. We can't conditonally select either a slice iterator or a Map iterator
+ * because of the type system */
+ let is_selection_empty =
+ self.selection.values().cloned().any(std::convert::identity);
+ let i = [self.get_envelope_under_cursor(self.cursor_pos.2, context)];
+ let cursor_iter;
+ let sel_iter = if is_selection_empty {
+ cursor_iter = None;
+ Some(self.selection.iter().filter(|(_, v)| **v).map(|(k, _)| k))
} else {
- self.filtered_selection[self.cursor_pos.2]
+ cursor_iter = Some(i.iter());
+ None
};
- if !account.contains_key(i) {
- /* The envelope has been renamed or removed, so wait for the appropriate event to
- * arrive */
- return true;
- }
- match a {
- SetRead => {
- let (hash, is_seen) = {
- let envelope: &Envelope = &account.get_env(&i);
- (envelope.hash(), envelope.is_seen())
- };
- if !is_seen {
+ let iter = sel_iter
+ .into_iter()
+ .flatten()
+ .chain(cursor_iter.into_iter().flatten());
+ for &i in iter {
+ let account = &mut context.accounts[self.cursor_pos.0];
+ if !account.contains_key(i) {
+ /* The envelope has been renamed or removed, so wait for the appropriate event to
+ * arrive */
+ continue;
+ }
+ match a {
+ SetRead => {
+ let hash = account.get_env(&i).hash();
let op = account.operation(hash);
let envelope: &mut Envelope = &mut account.get_env_mut(&i);
envelope.set_seen(op).unwrap();
+ self.row_updates.push(i);
}
- }
- SetUnread => {
- let (hash, is_seen) = {
- let envelope: &Envelope = &account.get_env(&i);
- (envelope.hash(), envelope.is_seen())
- };
- if is_seen {
+ SetUnread => {
+ let hash = account.get_env(&i).hash();
let op = account.operation(hash);
let envelope: &mut Envelope = &mut account.get_env_mut(&i);
envelope.set_unseen(op).unwrap();
+ self.row_updates.push(i);
}
+ Delete => { /* do nothing */ }
+ _ => unreachable!(),
}
- Delete => { /* do nothing */ }
- _ => unreachable!(),
}
+ self.dirty = true;
+ for v in self.selection.values_mut() {
+ *v = false;
+ }
+ return true;
}
_ => {}
},
- UIEvent::Input(Key::Esc) => {
+ UIEvent::Input(Key::Esc) if !self.unfocused && !self.filtered_selection.is_empty() => {
self.filter_term.clear();
self.filtered_selection.clear();
+ for v in self.selection.values_mut() {
+ *v = false;
+ }
self.filtered_order.clear();
self.refresh_mailbox(context);
return true;
@@ -1273,6 +1308,14 @@ impl Component for CompactListing {
Key::Char('i')
},
),
+ (
+ "select_entry",
+ if let Some(key) = config_map.get("select_entry") {
+ (*key).clone()
+ } else {
+ Key::Char('v')
+ },
+ ),
]
.iter()
.cloned()