sidebar: add customizable mailbox tree

Concerns #72
memfd
Manos Pitsidianakis 2020-09-17 16:49:19 +03:00
parent 10a3430233
commit fbf2b7dc7b
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
5 changed files with 244 additions and 15 deletions

View File

@ -772,7 +772,88 @@ Example:
.Bd -literal .Bd -literal
filter = "not flags:seen" # show only unseen messages filter = "not flags:seen" # show only unseen messages
.Ed .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 .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 .Sh TAGS
.Bl -tag -width 36n .Bl -tag -width 36n
.It Ic colours Ar hash table String[Color] .It Ic colours Ar hash table String[Color]

View File

@ -136,7 +136,7 @@ struct AccountMenuEntry {
name: String, name: String,
hash: AccountHash, hash: AccountHash,
index: usize, index: usize,
entries: SmallVec<[(usize, MailboxHash); 16]>, entries: SmallVec<[(usize, u32, bool, MailboxHash); 16]>,
} }
pub trait MailListingTrait: ListingTrait { pub trait MailListingTrait: ListingTrait {
@ -587,7 +587,7 @@ impl Component for Listing {
.ref_mailbox .ref_mailbox
.is_subscribed() .is_subscribed()
}) })
.map(|f| (f.depth, f.hash)) .map(|f| (f.depth, f.indentation, f.has_sibling, f.hash))
.collect::<_>(); .collect::<_>();
self.set_dirty(true); self.set_dirty(true);
} }
@ -607,7 +607,7 @@ impl Component for Listing {
.ref_mailbox .ref_mailbox
.is_subscribed() .is_subscribed()
}) })
.map(|f| (f.depth, f.hash)) .map(|f| (f.depth, f.indentation, f.has_sibling, f.hash))
.collect::<_>(); .collect::<_>();
if self.cursor_pos.0 == account_index { if self.cursor_pos.0 == account_index {
self.cursor_pos.1 = std::cmp::min( self.cursor_pos.1 = std::cmp::min(
@ -616,7 +616,7 @@ impl Component for Listing {
); );
self.component.set_coordinates(( self.component.set_coordinates((
self.accounts[self.cursor_pos.0].hash, 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); self.component.refresh_mailbox(context, true);
} }
@ -772,7 +772,7 @@ impl Component for Listing {
return true; return true;
} }
Action::ViewMailbox(idx) => { Action::ViewMailbox(idx) => {
if let Some((_, mailbox_hash)) = if let Some((_, _, _, mailbox_hash)) =
self.accounts[self.cursor_pos.0].entries.get(*idx) self.accounts[self.cursor_pos.0].entries.get(*idx)
{ {
let account_hash = self.accounts[self.cursor_pos.0].hash; 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 { 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 .entries
.get(self.cursor_pos.1) .get(self.cursor_pos.1)
{ {
@ -1233,11 +1233,11 @@ impl Listing {
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, (h, a))| { .map(|(i, (h, a))| {
let entries: SmallVec<[(usize, MailboxHash); 16]> = a let entries: SmallVec<[(usize, u32, bool, MailboxHash); 16]> = a
.list_mailboxes() .list_mailboxes()
.into_iter() .into_iter()
.filter(|mailbox_node| a[&mailbox_node.hash].ref_mailbox.is_subscribed()) .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::<_>(); .collect::<_>();
AccountMenuEntry { AccountMenuEntry {
@ -1321,18 +1321,20 @@ impl Listing {
&& self.cursor_pos.0 == a.index) && self.cursor_pos.0 == a.index)
|| (self.focus == ListingFocus::Menu && self.menu_cursor_pos.0 == a.index); || (self.focus == ListingFocus::Menu && self.menu_cursor_pos.0 == a.index);
let mut lines: Vec<(usize, usize, MailboxHash, Option<usize>)> = Vec::new(); let mut lines: Vec<(usize, usize, u32, bool, MailboxHash, Option<usize>)> = 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() { if mailboxes[&mailbox_hash].is_subscribed() {
match context.accounts[a.index][&mailbox_hash].status { match context.accounts[a.index][&mailbox_hash].status {
crate::conf::accounts::MailboxStatus::Failed(_) => { crate::conf::accounts::MailboxStatus::Failed(_) => {
lines.push((depth, i, mailbox_hash, None)); lines.push((depth, i, indentation, has_sibling, mailbox_hash, None));
} }
_ => { _ => {
lines.push(( lines.push((
depth, depth,
i, i,
indentation,
has_sibling,
mailbox_hash, mailbox_hash,
mailboxes[&mailbox_hash].count().ok().map(|(v, _)| v), mailboxes[&mailbox_hash].count().ok().map(|(v, _)| v),
)); ));
@ -1372,6 +1374,7 @@ impl Listing {
let lines_len = lines.len(); let lines_len = lines.len();
let mut idx = 0; let mut idx = 0;
let mut branches = String::with_capacity(16);
for y in get_y(upper_left) + 1..get_y(bottom_right) { for y in get_y(upper_left) + 1..get_y(bottom_right) {
if idx == lines_len { 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, /* Calculate how many columns the mailbox index tags should occupy with right alignment,
* eg. * eg.
* 1 * 1
@ -1429,6 +1432,33 @@ impl Listing {
} }
ctr 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( let (x, _) = write_string_to_grid(
&format!("{:>width$}", inc, width = total_mailbox_no_digits), &format!("{:>width$}", inc, width = total_mailbox_no_digits),
grid, grid,
@ -1438,8 +1468,29 @@ impl Listing {
(set_y(upper_left, y), bottom_right), (set_y(upper_left, y), bottom_right),
None, 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( let (x, _) = write_string_to_grid(
&" ".repeat(depth + 1), &branches,
grid, grid,
att.fg, att.fg,
att.bg, att.bg,
@ -1511,10 +1562,10 @@ impl Listing {
.ref_mailbox .ref_mailbox
.is_subscribed() .is_subscribed()
}) })
.map(|f| (f.depth, f.hash)) .map(|f| (f.depth, f.indentation, f.has_sibling, f.hash))
.collect::<_>(); .collect::<_>();
/* Account might have no mailboxes yet if it's offline */ /* 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 .entries
.get(self.cursor_pos.1) .get(self.cursor_pos.1)
{ {

View File

@ -390,6 +390,8 @@ impl Drop for Account {
pub struct MailboxNode { pub struct MailboxNode {
pub hash: MailboxHash, pub hash: MailboxHash,
pub depth: usize, pub depth: usize,
pub indentation: u32,
pub has_sibling: bool,
pub children: Vec<MailboxNode>, pub children: Vec<MailboxNode>,
} }
@ -2088,6 +2090,8 @@ fn build_mailboxes_order(
hash: h, hash: h,
children: Vec::new(), children: Vec::new(),
depth, depth,
indentation: 0,
has_sibling: false,
}; };
for &c in mailbox_entries[&h].ref_mailbox.children() { for &c in mailbox_entries[&h].ref_mailbox.children() {
if mailbox_entries.contains_key(&c) { if mailbox_entries.contains_key(&c) {
@ -2151,4 +2155,24 @@ fn build_mailboxes_order(
stack.extend(next.children.iter().rev().map(Some)); 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);
}
} }

View File

@ -24,6 +24,31 @@ use melib::search::Query;
use melib::{MeliError, Result}; use melib::{MeliError, Result};
/// Settings for mail listings /// 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)] #[derive(Debug, Deserialize, Clone, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct ListingSettings { pub struct ListingSettings {
@ -49,6 +74,22 @@ pub struct ListingSettings {
#[serde(default, alias = "index-style")] #[serde(default, alias = "index-style")]
pub index_style: IndexStyle, pub index_style: IndexStyle,
///Default: " "
#[serde(default = "none")]
pub sidebar_mailbox_tree_has_sibling: Option<String>,
///Default: " "
#[serde(default)]
pub sidebar_mailbox_tree_no_sibling: Option<String>,
///Default: " "
#[serde(default)]
pub sidebar_mailbox_tree_has_sibling_leaf: Option<String>,
///Default: " "
#[serde(default)]
pub sidebar_mailbox_tree_no_sibling_leaf: Option<String>,
} }
impl Default for ListingSettings { impl Default for ListingSettings {
@ -59,6 +100,10 @@ impl Default for ListingSettings {
recent_dates: true, recent_dates: true,
filter: None, filter: None,
index_style: IndexStyle::default(), 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), "recent_dates" => self.recent_dates.lookup(field, tail),
"filter" => self.filter.lookup(field, tail), "filter" => self.filter.lookup(field, tail),
"index_style" => self.index_style.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!( other => Err(MeliError::new(format!(
"{} has no field named {}", "{} has no field named {}",
parent_field, other parent_field, other

View File

@ -119,6 +119,18 @@ pub struct ListingSettingsOverride {
#[serde(alias = "index-style")] #[serde(alias = "index-style")]
#[serde(default)] #[serde(default)]
pub index_style: Option<IndexStyle>, pub index_style: Option<IndexStyle>,
#[doc = "Default: \" \""]
#[serde(default)]
pub sidebar_mailbox_tree_has_sibling: Option<Option<String>>,
#[doc = "Default: \" \""]
#[serde(default)]
pub sidebar_mailbox_tree_no_sibling: Option<Option<String>>,
#[doc = "Default: \" \""]
#[serde(default)]
pub sidebar_mailbox_tree_has_sibling_leaf: Option<Option<String>>,
#[doc = "Default: \" \""]
#[serde(default)]
pub sidebar_mailbox_tree_no_sibling_leaf: Option<Option<String>>,
} }
impl Default for ListingSettingsOverride { impl Default for ListingSettingsOverride {
fn default() -> Self { fn default() -> Self {
@ -128,6 +140,10 @@ impl Default for ListingSettingsOverride {
recent_dates: None, recent_dates: None,
filter: None, filter: None,
index_style: 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,
} }
} }
} }