diff --git a/docs/meli.conf.5 b/docs/meli.conf.5 index d6220aa1..078d324d 100644 --- a/docs/meli.conf.5 +++ b/docs/meli.conf.5 @@ -407,6 +407,11 @@ Show a different name for this mailbox in the UI Load this mailbox on startup .\" default value .Pq Em true +.It Ic collapsed Ar boolean +.Pq Em optional +Collapse this mailbox subtree in menu. +.\" default value +.Pq Em false .It Ic subscribe Ar boolean .Pq Em optional Watch this mailbox for updates @@ -691,6 +696,10 @@ Go to previous mailbox. Open selected mailbox .\" default value .Pq Em Enter +.It Ic toggle_mailbox_collapse +Toggle mailbox visibility in menu. +.\" default value +.Pq Em Space .It Ic search Search within list of e-mails. .\" default value diff --git a/src/components/mail/listing.rs b/src/components/mail/listing.rs index 0620fb40..c9685672 100644 --- a/src/components/mail/listing.rs +++ b/src/components/mail/listing.rs @@ -24,7 +24,7 @@ use crate::conf::accounts::JobRequest; use crate::types::segment_tree::SegmentTree; use melib::backends::EnvelopeHashBatch; use smallvec::SmallVec; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::convert::TryFrom; use std::ops::{Deref, DerefMut}; @@ -161,12 +161,23 @@ column_str!(struct SubjectString(String)); column_str!(struct FlagString(String)); column_str!(struct TagString(String, SmallVec<[Option; 8]>)); +#[derive(Debug)] +struct MailboxMenuEntry { + depth: usize, + indentation: u32, + has_sibling: bool, + visible: bool, + collapsed: bool, + mailbox_hash: MailboxHash, +} + #[derive(Debug)] struct AccountMenuEntry { name: String, hash: AccountHash, index: usize, - entries: SmallVec<[(usize, u32, bool, MailboxHash); 16]>, + visible: bool, + entries: SmallVec<[MailboxMenuEntry; 16]>, } pub trait MailListingTrait: ListingTrait { @@ -768,6 +779,18 @@ impl Component for Listing { if self.cursor_pos.0 == account_index { self.change_account(context); } else { + let previous_collapsed_mailboxes: BTreeSet = self.accounts + [account_index] + .entries + .iter() + .filter_map(|e| { + if e.collapsed { + Some(e.mailbox_hash) + } else { + None + } + }) + .collect::<_>(); self.accounts[account_index].entries = context.accounts[&*account_hash] .list_mailboxes() .into_iter() @@ -776,7 +799,18 @@ impl Component for Listing { .ref_mailbox .is_subscribed() }) - .map(|f| (f.depth, f.indentation, f.has_sibling, f.hash)) + .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) + }, + }) .collect::<_>(); self.set_dirty(true); self.menu_content.empty(); @@ -795,6 +829,18 @@ impl Component for Listing { .get_index_of(account_hash) .expect("Invalid account_hash in UIEventMailbox{Delete,Create}"); self.menu_content.empty(); + let previous_collapsed_mailboxes: BTreeSet = self.accounts + [account_index] + .entries + .iter() + .filter_map(|e| { + if e.collapsed { + Some(e.mailbox_hash) + } else { + None + } + }) + .collect::<_>(); self.accounts[account_index].entries = context.accounts[&*account_hash] .list_mailboxes() .into_iter() @@ -803,7 +849,14 @@ impl Component for Listing { .ref_mailbox .is_subscribed() }) - .map(|f| (f.depth, f.indentation, f.has_sibling, f.hash)) + .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), + }) .collect::<_>(); let mut fallback = 0; if let MenuEntryCursor::Mailbox(ref mut cur) = self.cursor_pos.1 { @@ -821,7 +874,7 @@ impl Component for Listing { .process_event(&mut UIEvent::VisibilityChange(false), context); self.component.set_coordinates(( self.accounts[self.cursor_pos.0].hash, - self.accounts[self.cursor_pos.0].entries[fallback].3, + self.accounts[self.cursor_pos.0].entries[fallback].mailbox_hash, )); self.component.refresh_mailbox(context, true); } @@ -840,7 +893,7 @@ impl Component for Listing { self.set_dirty(true); } UIEvent::Action(Action::ViewMailbox(ref idx)) => { - if let Some((_, _, _, mailbox_hash)) = + if let Some(MailboxMenuEntry { mailbox_hash, .. }) = self.accounts[self.cursor_pos.0].entries.get(*idx) { let account_hash = self.accounts[self.cursor_pos.0].hash; @@ -1311,6 +1364,33 @@ impl Component for Listing { ))); return true; } + UIEvent::Input(ref k) + if shortcut!( + k == shortcuts[Listing::DESCRIPTION]["toggle_mailbox_collapse"] + ) && matches!(self.menu_cursor_pos.1, MenuEntryCursor::Mailbox(_)) => + { + let target_mailbox_idx = + if let MenuEntryCursor::Mailbox(idx) = self.menu_cursor_pos.1 { + idx + } else { + return false; + }; + if let Some(target) = self.accounts[self.menu_cursor_pos.0] + .entries + .get_mut(target_mailbox_idx) + { + target.collapsed = !(target.collapsed); + self.dirty = true; + self.menu_content.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[Listing::DESCRIPTION]["open_mailbox"]) => { @@ -1371,13 +1451,22 @@ impl Component for Listing { return true; } } - (_, MenuEntryCursor::Mailbox(ref mut mailbox_idx)) => { + ( + ref account_cursor, + MenuEntryCursor::Mailbox(ref mut mailbox_idx), + ) => loop { if *mailbox_idx > 0 { *mailbox_idx -= 1; + if self.accounts[*account_cursor].entries[*mailbox_idx] + .visible + { + break; + } } else { self.menu_cursor_pos.1 = MenuEntryCursor::Status; + break; } - } + }, } amount -= 1; @@ -1407,18 +1496,24 @@ impl Component for Listing { ( ref mut account_cursor, MenuEntryCursor::Mailbox(ref mut mailbox_idx), - ) => { + ) => loop { if (*mailbox_idx + 1) < self.accounts[*account_cursor].entries.len() { *mailbox_idx += 1; + if self.accounts[*account_cursor].entries[*mailbox_idx] + .visible + { + break; + } } else if *account_cursor + 1 < self.accounts.len() { *account_cursor += 1; self.menu_cursor_pos.1 = MenuEntryCursor::Status; + break; } else { return true; } - } + }, } amount -= 1; @@ -1648,7 +1743,7 @@ impl Component for Listing { fn get_status(&self, context: &Context) -> String { let mailbox_hash = match self.cursor_pos.1 { MenuEntryCursor::Mailbox(idx) => { - if let Some((_, _, _, mailbox_hash)) = + if let Some(MailboxMenuEntry { mailbox_hash, .. }) = self.accounts[self.cursor_pos.0].entries.get(idx) { *mailbox_hash @@ -1695,17 +1790,25 @@ impl Listing { .iter() .enumerate() .map(|(i, (h, a))| { - let entries: SmallVec<[(usize, u32, bool, MailboxHash); 16]> = a + let entries: SmallVec<[MailboxMenuEntry; 16]> = a .list_mailboxes() .into_iter() .filter(|mailbox_node| a[&mailbox_node.hash].ref_mailbox.is_subscribed()) - .map(|f| (f.depth, f.indentation, f.has_sibling, f.hash)) + .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, + }) .collect::<_>(); AccountMenuEntry { name: a.name().to_string(), hash: *h, index: i, + visible: true, entries, } }) @@ -1848,6 +1951,19 @@ impl Listing { */ fn print_account(&mut self, area: Area, aidx: usize, context: &mut Context) -> usize { debug_assert!(is_valid_area!(area)); + + #[derive(Copy, Debug, Clone)] + struct Line { + visible: bool, + collapsed: bool, + depth: usize, + inc: usize, + indentation: u32, + has_sibling: bool, + mailbox_idx: MailboxHash, + count: Option, + collapsed_count: Option, + } // Each entry and its index in the account let mailboxes: HashMap = context.accounts[self.accounts[aidx].index] .mailbox_entries @@ -1865,25 +1981,47 @@ impl Listing { let must_highlight_account: bool = cursor.0 == self.accounts[aidx].index; - let mut lines: Vec<(usize, usize, u32, bool, MailboxHash, Option)> = Vec::new(); + let mut lines: Vec = Vec::new(); - for (i, &(depth, indentation, has_sibling, mailbox_hash)) in - self.accounts[aidx].entries.iter().enumerate() + for ( + i, + &MailboxMenuEntry { + depth, + indentation, + has_sibling, + mailbox_hash, + visible, + collapsed, + }, + ) in self.accounts[aidx].entries.iter().enumerate() { if mailboxes[&mailbox_hash].is_subscribed() { match context.accounts[self.accounts[aidx].index][&mailbox_hash].status { crate::conf::accounts::MailboxStatus::Failed(_) => { - lines.push((depth, i, indentation, has_sibling, mailbox_hash, None)); - } - _ => { - lines.push(( + lines.push(Line { + visible, + collapsed, depth, - i, + inc: i, indentation, has_sibling, - mailbox_hash, - mailboxes[&mailbox_hash].count().ok().map(|(v, _)| v), - )); + mailbox_idx: mailbox_hash, + count: None, + collapsed_count: None, + }); + } + _ => { + lines.push(Line { + visible, + collapsed, + depth, + inc: i, + indentation, + has_sibling, + mailbox_idx: mailbox_hash, + count: mailboxes[&mailbox_hash].count().ok().map(|(v, _)| v), + collapsed_count: None, + }); } } } @@ -1931,10 +2069,44 @@ impl Listing { let mut idx = 0; let mut branches = String::with_capacity(16); - for y in get_y(upper_left) + 1..get_y(bottom_right) { + // 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 = None; + let mut skipped_counter: usize = 0; + 'grid_loop: for y in get_y(upper_left) + 1..get_y(bottom_right) { 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.1 { MenuEntryCursor::Mailbox(c) => c == idx, @@ -1970,7 +2142,6 @@ impl Listing { ) }; - let (depth, inc, indentation, has_sibling, mailbox_idx, count) = lines[idx]; /* Calculate how many columns the mailbox index tags should occupy with right alignment, * eg. * 1 @@ -2025,7 +2196,7 @@ impl Listing { .unwrap_or(" "); let (x, _) = write_string_to_grid( - &format!("{:>width$}", inc, width = total_mailbox_no_digits), + &format!("{:>width$}", l.inc, width = total_mailbox_no_digits), &mut self.menu_content, index_att.fg, index_att.bg, @@ -2036,18 +2207,18 @@ impl Listing { { branches.clear(); branches.push_str(no_sibling_str); - let leading_zeros = indentation.leading_zeros(); + 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 indentation & o > 0 { + if l.indentation & o > 0 { branches.push_str(has_sibling_str); } else { branches.push_str(no_sibling_str); } o >>= 1; } - if depth > 0 { - if has_sibling { + if l.depth > 0 { + if l.has_sibling { branches.push_str(has_sibling_leaf_str); } else { branches.push_str(no_sibling_leaf_str); @@ -2064,7 +2235,7 @@ impl Listing { None, ); let (x, _) = write_string_to_grid( - context.accounts[self.accounts[aidx].index].mailbox_entries[&mailbox_idx].name(), + context.accounts[self.accounts[aidx].index].mailbox_entries[&l.mailbox_idx].name(), &mut self.menu_content, att.fg, att.bg, @@ -2074,14 +2245,15 @@ impl Listing { ); /* Unread message count */ - let count_string = if let Some(c) = count { - if c > 0 { - format!(" {}", c) - } else { - String::new() - } - } else { - " ...".to_string() + 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, _) = write_string_to_grid( @@ -2090,7 +2262,7 @@ impl Listing { unread_count_att.fg, unread_count_att.bg, unread_count_att.attrs - | if count.unwrap_or(0) > 0 { + | if l.count.unwrap_or(0) > 0 { Attr::BOLD } else { Attr::DEFAULT @@ -2116,12 +2288,23 @@ impl Listing { if idx == 0 { 0 } else { - idx - 1 + idx - 1 - skipped_counter } } fn change_account(&mut self, context: &mut Context) { let account_hash = context.accounts[self.cursor_pos.0].hash(); + let previous_collapsed_mailboxes: BTreeSet = self.accounts[self.cursor_pos.0] + .entries + .iter() + .filter_map(|e| { + if e.collapsed { + Some(e.mailbox_hash) + } else { + None + } + }) + .collect::<_>(); self.accounts[self.cursor_pos.0].entries = context.accounts[self.cursor_pos.0] .list_mailboxes() .into_iter() @@ -2130,12 +2313,23 @@ impl Listing { .ref_mailbox .is_subscribed() }) - .map(|f| (f.depth, f.indentation, f.has_sibling, f.hash)) + .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.0][&f.hash].conf.collapsed + } else { + previous_collapsed_mailboxes.contains(&f.hash) + }, + }) .collect::<_>(); match self.cursor_pos.1 { MenuEntryCursor::Mailbox(idx) => { /* Account might have no mailboxes yet if it's offline */ - if let Some((_, _, _, mailbox_hash)) = + if let Some(MailboxMenuEntry { mailbox_hash, .. }) = self.accounts[self.cursor_pos.0].entries.get(idx) { self.component diff --git a/src/conf.rs b/src/conf.rs index ba882e3e..2b9aaa27 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -135,6 +135,8 @@ pub struct MailUIConf { pub struct FileMailboxConf { #[serde(flatten)] pub conf_override: MailUIConf, + #[serde(default = "false_val")] + pub collapsed: bool, #[serde(flatten)] pub mailbox_conf: MailboxConf, } diff --git a/src/conf/accounts.rs b/src/conf/accounts.rs index deded7e6..072452dc 100644 --- a/src/conf/accounts.rs +++ b/src/conf/accounts.rs @@ -2389,6 +2389,6 @@ fn build_mailboxes_order( } } - rec(node, &mailbox_entries, 0, 0, false); + rec(node, &mailbox_entries, 1, 0, false); } } diff --git a/src/conf/shortcuts.rs b/src/conf/shortcuts.rs index f82aee82..d0b294e5 100644 --- a/src/conf/shortcuts.rs +++ b/src/conf/shortcuts.rs @@ -176,6 +176,7 @@ shortcut_key_values! { "listing", prev_account |> "Go to previous account." |> Key::Char('l'), prev_mailbox |> "Go to previous mailbox." |> Key::Char('K'), open_mailbox |> "Open selected mailbox" |> Key::Char('\n'), + toggle_mailbox_collapse |> "Toggle mailbox collapse in menu." |> Key::Char(' '), prev_page |> "Go to previous page." |> Key::PageUp, search |> "Search within list of e-mails." |> Key::Char('/'), refresh |> "Manually request a mailbox refresh." |> Key::F(5),