From fbf2b7dc7bf53754f27aecaeab45a88eb276110d Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Thu, 17 Sep 2020 16:49:19 +0300 Subject: [PATCH] sidebar: add customizable mailbox tree Concerns #72 --- meli.conf.5 | 81 ++++++++++++++++++++++++++++++++++ src/components/mail/listing.rs | 81 +++++++++++++++++++++++++++------- src/conf/accounts.rs | 24 ++++++++++ src/conf/listing.rs | 57 ++++++++++++++++++++++++ src/conf/overrides.rs | 16 +++++++ 5 files changed, 244 insertions(+), 15 deletions(-) diff --git a/meli.conf.5 b/meli.conf.5 index 697abd73d..a183251ef 100644 --- a/meli.conf.5 +++ b/meli.conf.5 @@ -772,7 +772,88 @@ Example: .Bd -literal filter = "not flags:seen" # show only unseen messages .Ed +.It Ic index_style Ar String +Sets the way mailboxes are displayed. +.It Ic sidebar_mailbox_tree_has_sibling Ar String +.Pq Em optional +Sets the string to print in the mailbox tree for a level where its root has a sibling. +See example below for a clear explanation and examples. +.It Ic sidebar_mailbox_tree_no_sibling Ar String +.Pq Em optional +Sets the string to print in the mailbox tree for a level where its root has no sibling. +.It Ic sidebar_mailbox_tree_has_sibling_leaf Ar String +.Pq Em optional +Sets the string to print in the mailbox tree for a leaf level where its root has a sibling. +.It Ic sidebar_mailbox_tree_no_sibling_leaf Ar String +.Pq Em optional +Sets the string to print in the mailbox tree for a leaf level where its root has no sibling. .El +.Ss Examples of sidebar mailbox tree customization +The default values + +.Bd + has_sibling = " " + no_sibling = " "; + has_sibling_leaf = " " + no_sibling_leaf = " " +.Ed + +render a mailbox tree like the following: + +.Bd -literal +0 Inbox 3 +1 Archive +2 Drafts +3 Lists +4 example-list-a +5 example-list-b +6 Sent +7 Spam +8 Trash +.Ed + +Other possible trees: + +.Bd -literal +has_sibling = " ┃" +no_sibling = " " +has_sibling_leaf = " ┣━" +no_sibling_leaf = " ┗━" +.Ed + +.Bd -literal +0 Inbox 3 +1 ┣━Archive +2 ┣━Drafts +3 ┣━Lists +4 ┃ ┣━example-list-a +5 ┃ ┗━example-list-b +6 ┣━Sent +7 ┣━Spam +8 ┗━Trash +.Ed + +A completely ASCII one: + +.Bd -literal +has_sibling = " |" +no_sibling = " " +has_sibling_leaf = " |\\_" +no_sibling_leaf = " \\_" +.Ed + +.Bd -literal +0 Inbox 3 +1 |\\_Archive +2 |\\_Drafts +3 |\\_Lists +4 | |\\_example-list-a +5 | \\_example-list-b +6 |\\_Sent +7 |\\_Spam +8 \\_Trash +.Ed + .Sh TAGS .Bl -tag -width 36n .It Ic colours Ar hash table String[Color] diff --git a/src/components/mail/listing.rs b/src/components/mail/listing.rs index c6dcbd24e..294eba8ce 100644 --- a/src/components/mail/listing.rs +++ b/src/components/mail/listing.rs @@ -136,7 +136,7 @@ struct AccountMenuEntry { name: String, hash: AccountHash, index: usize, - entries: SmallVec<[(usize, MailboxHash); 16]>, + entries: SmallVec<[(usize, u32, bool, MailboxHash); 16]>, } pub trait MailListingTrait: ListingTrait { @@ -587,7 +587,7 @@ impl Component for Listing { .ref_mailbox .is_subscribed() }) - .map(|f| (f.depth, f.hash)) + .map(|f| (f.depth, f.indentation, f.has_sibling, f.hash)) .collect::<_>(); self.set_dirty(true); } @@ -607,7 +607,7 @@ impl Component for Listing { .ref_mailbox .is_subscribed() }) - .map(|f| (f.depth, f.hash)) + .map(|f| (f.depth, f.indentation, f.has_sibling, f.hash)) .collect::<_>(); if self.cursor_pos.0 == account_index { self.cursor_pos.1 = std::cmp::min( @@ -616,7 +616,7 @@ impl Component for Listing { ); self.component.set_coordinates(( self.accounts[self.cursor_pos.0].hash, - self.accounts[self.cursor_pos.0].entries[self.cursor_pos.1].1, + self.accounts[self.cursor_pos.0].entries[self.cursor_pos.1].3, )); self.component.refresh_mailbox(context, true); } @@ -772,7 +772,7 @@ impl Component for Listing { return true; } Action::ViewMailbox(idx) => { - if let Some((_, mailbox_hash)) = + if let Some((_, _, _, mailbox_hash)) = self.accounts[self.cursor_pos.0].entries.get(*idx) { let account_hash = self.accounts[self.cursor_pos.0].hash; @@ -1195,7 +1195,7 @@ impl Component for Listing { } fn get_status(&self, context: &Context) -> String { - let mailbox_hash = if let Some((_, mailbox_hash)) = self.accounts[self.cursor_pos.0] + let mailbox_hash = if let Some((_, _, _, mailbox_hash)) = self.accounts[self.cursor_pos.0] .entries .get(self.cursor_pos.1) { @@ -1233,11 +1233,11 @@ impl Listing { .iter() .enumerate() .map(|(i, (h, a))| { - let entries: SmallVec<[(usize, MailboxHash); 16]> = a + let entries: SmallVec<[(usize, u32, bool, MailboxHash); 16]> = a .list_mailboxes() .into_iter() .filter(|mailbox_node| a[&mailbox_node.hash].ref_mailbox.is_subscribed()) - .map(|f| (f.depth, f.hash)) + .map(|f| (f.depth, f.indentation, f.has_sibling, f.hash)) .collect::<_>(); AccountMenuEntry { @@ -1321,18 +1321,20 @@ impl Listing { && self.cursor_pos.0 == a.index) || (self.focus == ListingFocus::Menu && self.menu_cursor_pos.0 == a.index); - let mut lines: Vec<(usize, usize, MailboxHash, Option)> = Vec::new(); + let mut lines: Vec<(usize, usize, u32, bool, MailboxHash, Option)> = Vec::new(); - for (i, &(depth, mailbox_hash)) in a.entries.iter().enumerate() { + for (i, &(depth, indentation, has_sibling, mailbox_hash)) in a.entries.iter().enumerate() { if mailboxes[&mailbox_hash].is_subscribed() { match context.accounts[a.index][&mailbox_hash].status { crate::conf::accounts::MailboxStatus::Failed(_) => { - lines.push((depth, i, mailbox_hash, None)); + lines.push((depth, i, indentation, has_sibling, mailbox_hash, None)); } _ => { lines.push(( depth, i, + indentation, + has_sibling, mailbox_hash, mailboxes[&mailbox_hash].count().ok().map(|(v, _)| v), )); @@ -1372,6 +1374,7 @@ impl Listing { let lines_len = lines.len(); let mut idx = 0; + let mut branches = String::with_capacity(16); for y in get_y(upper_left) + 1..get_y(bottom_right) { if idx == lines_len { @@ -1411,7 +1414,7 @@ impl Listing { ) }; - let (depth, inc, mailbox_idx, count) = lines[idx]; + 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 @@ -1429,6 +1432,33 @@ impl Listing { } ctr }; + + let has_sibling_str: &str = + account_settings!(context[a.hash].listing.sidebar_mailbox_tree_has_sibling) + .as_ref() + .map(|s| s.as_str()) + .unwrap_or(" "); + let no_sibling_str: &str = + account_settings!(context[a.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[a.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[a.hash].listing.sidebar_mailbox_tree_no_sibling_leaf) + .as_ref() + .map(|s| s.as_str()) + .unwrap_or(" "); + let (x, _) = write_string_to_grid( &format!("{:>width$}", inc, width = total_mailbox_no_digits), grid, @@ -1438,8 +1468,29 @@ impl Listing { (set_y(upper_left, y), bottom_right), None, ); + { + branches.clear(); + branches.push_str(no_sibling_str); + let mut o = 1; + let leading_zeros = indentation.leading_zeros(); + for _ in 0..(30_u32.saturating_sub(leading_zeros)) { + if indentation & o > 0 { + branches.push_str(has_sibling_str); + } else { + branches.push_str(no_sibling_str); + } + o <<= 1; + } + if depth > 0 { + if has_sibling { + branches.push_str(has_sibling_leaf_str); + } else { + branches.push_str(no_sibling_leaf_str); + } + } + } let (x, _) = write_string_to_grid( - &" ".repeat(depth + 1), + &branches, grid, att.fg, att.bg, @@ -1511,10 +1562,10 @@ impl Listing { .ref_mailbox .is_subscribed() }) - .map(|f| (f.depth, f.hash)) + .map(|f| (f.depth, f.indentation, f.has_sibling, f.hash)) .collect::<_>(); /* Account might have no mailboxes yet if it's offline */ - if let Some((_, mailbox_hash)) = self.accounts[self.cursor_pos.0] + if let Some((_, _, _, mailbox_hash)) = self.accounts[self.cursor_pos.0] .entries .get(self.cursor_pos.1) { diff --git a/src/conf/accounts.rs b/src/conf/accounts.rs index e0d0cdf58..95d642ada 100644 --- a/src/conf/accounts.rs +++ b/src/conf/accounts.rs @@ -390,6 +390,8 @@ impl Drop for Account { pub struct MailboxNode { pub hash: MailboxHash, pub depth: usize, + pub indentation: u32, + pub has_sibling: bool, pub children: Vec, } @@ -2088,6 +2090,8 @@ fn build_mailboxes_order( hash: h, children: Vec::new(), depth, + indentation: 0, + has_sibling: false, }; for &c in mailbox_entries[&h].ref_mailbox.children() { if mailbox_entries.contains_key(&c) { @@ -2151,4 +2155,24 @@ fn build_mailboxes_order( stack.extend(next.children.iter().rev().map(Some)); } } + drop(stack); + for node in tree.iter_mut() { + fn rec(node: &mut MailboxNode, depth: usize, mut indentation: u32, has_sibling: bool) { + node.indentation = indentation; + node.has_sibling = has_sibling; + let mut iter = (0..node.children.len()).peekable(); + if has_sibling { + indentation <<= 1; + indentation |= 1; + } else { + indentation <<= 1; + } + while let Some(i) = iter.next() { + let c = &mut node.children[i]; + rec(c, depth + 1, indentation, iter.peek() != None); + } + }; + + rec(node, 0, 1, false); + } } diff --git a/src/conf/listing.rs b/src/conf/listing.rs index ea86f595f..05dd889fb 100644 --- a/src/conf/listing.rs +++ b/src/conf/listing.rs @@ -24,6 +24,31 @@ use melib::search::Query; use melib::{MeliError, Result}; /// Settings for mail listings +/// +/// +/// Tree decoration examples: +/// +///```no_run +///const HAS_SIBLING: &'static str = " ┃"; +///const NO_SIBLING: &'static str = " "; +///const HAS_SIBLING_LEAF: &'static str = " ┣━"; +///const NO_SIBLING_LEAF: &'static str = " ┗━"; +/// +///const HAS_SIBLING: &'static str = " |"; +///const NO_SIBLING: &'static str = " "; +///const HAS_SIBLING_LEAF: &'static str = " |\\_"; +///const NO_SIBLING_LEAF: &'static str = " \\_"; +/// +///const HAS_SIBLING: &'static str = " "; +///const NO_SIBLING: &'static str = " "; +///const HAS_SIBLING_LEAF: &'static str = " "; +///const NO_SIBLING_LEAF: &'static str = " "; +/// +///const HAS_SIBLING: &'static str = " │"; +///const NO_SIBLING: &'static str = " "; +///const HAS_SIBLING_LEAF: &'static str = " ├─"; +///const NO_SIBLING_LEAF: &'static str = " ╰─"; +///``` #[derive(Debug, Deserialize, Clone, Serialize)] #[serde(deny_unknown_fields)] pub struct ListingSettings { @@ -49,6 +74,22 @@ pub struct ListingSettings { #[serde(default, alias = "index-style")] pub index_style: IndexStyle, + + ///Default: " " + #[serde(default = "none")] + pub sidebar_mailbox_tree_has_sibling: Option, + + ///Default: " " + #[serde(default)] + pub sidebar_mailbox_tree_no_sibling: Option, + + ///Default: " " + #[serde(default)] + pub sidebar_mailbox_tree_has_sibling_leaf: Option, + + ///Default: " " + #[serde(default)] + pub sidebar_mailbox_tree_no_sibling_leaf: Option, } impl Default for ListingSettings { @@ -59,6 +100,10 @@ impl Default for ListingSettings { recent_dates: true, filter: None, index_style: IndexStyle::default(), + sidebar_mailbox_tree_has_sibling: None, + sidebar_mailbox_tree_no_sibling: None, + sidebar_mailbox_tree_has_sibling_leaf: None, + sidebar_mailbox_tree_no_sibling_leaf: None, } } } @@ -74,6 +119,18 @@ impl DotAddressable for ListingSettings { "recent_dates" => self.recent_dates.lookup(field, tail), "filter" => self.filter.lookup(field, tail), "index_style" => self.index_style.lookup(field, tail), + "sidebar_mailbox_tree_has_sibling" => { + self.sidebar_mailbox_tree_has_sibling.lookup(field, tail) + } + "sidebar_mailbox_tree_no_sibling" => { + self.sidebar_mailbox_tree_no_sibling.lookup(field, tail) + } + "sidebar_mailbox_tree_has_sibling_leaf" => self + .sidebar_mailbox_tree_has_sibling_leaf + .lookup(field, tail), + "sidebar_mailbox_tree_no_sibling_leaf" => self + .sidebar_mailbox_tree_no_sibling_leaf + .lookup(field, tail), other => Err(MeliError::new(format!( "{} has no field named {}", parent_field, other diff --git a/src/conf/overrides.rs b/src/conf/overrides.rs index 45d30274c..c6e80d937 100644 --- a/src/conf/overrides.rs +++ b/src/conf/overrides.rs @@ -119,6 +119,18 @@ pub struct ListingSettingsOverride { #[serde(alias = "index-style")] #[serde(default)] pub index_style: Option, + #[doc = "Default: \" \""] + #[serde(default)] + pub sidebar_mailbox_tree_has_sibling: Option>, + #[doc = "Default: \" \""] + #[serde(default)] + pub sidebar_mailbox_tree_no_sibling: Option>, + #[doc = "Default: \" \""] + #[serde(default)] + pub sidebar_mailbox_tree_has_sibling_leaf: Option>, + #[doc = "Default: \" \""] + #[serde(default)] + pub sidebar_mailbox_tree_no_sibling_leaf: Option>, } impl Default for ListingSettingsOverride { fn default() -> Self { @@ -128,6 +140,10 @@ impl Default for ListingSettingsOverride { recent_dates: None, filter: None, index_style: None, + sidebar_mailbox_tree_has_sibling: None, + sidebar_mailbox_tree_no_sibling: None, + sidebar_mailbox_tree_has_sibling_leaf: None, + sidebar_mailbox_tree_no_sibling_leaf: None, } } }