/* * meli - ui crate. * * 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 . */ use super::*; mod conversations; pub use self::conversations::*; mod compact; pub use self::compact::*; mod thread; pub use self::thread::*; mod plain; pub use self::plain::*; #[derive(Debug, Default, Clone)] pub struct DataColumns { pub columns: [CellBuffer; 12], pub widths: [usize; 12], // widths of columns calculated in first draw and after size changes } #[derive(Debug)] struct AccountMenuEntry { name: String, // Index in the config account vector. index: usize, } pub trait MailListingTrait: ListingTrait { fn perform_action( &mut self, context: &mut Context, thread_hash: ThreadHash, a: &ListingAction, ) { let account = &mut context.accounts[self.coordinates().0]; let mut envs_to_set: StackVec = StackVec::new(); let folder_hash = account[self.coordinates().1].unwrap().folder.hash(); { let mut stack = StackVec::new(); stack.push(thread_hash); while let Some(thread_iter) = stack.pop() { { let threads = account.collection.threads.get_mut(&folder_hash).unwrap(); threads .thread_nodes .entry(thread_iter) .and_modify(|t| t.set_has_unseen(false)); } let threads = &account.collection.threads[&folder_hash]; if let Some(env_hash) = threads[&thread_iter].message() { if !account.contains_key(env_hash) { /* The envelope has been renamed or removed, so wait for the appropriate event to * arrive */ continue; } envs_to_set.push(env_hash); } for c in 0..threads[&thread_iter].children().len() { let c = threads[&thread_iter].children()[c]; stack.push(c); } } } for env_hash in envs_to_set { let op = account.operation(env_hash); let mut envelope: EnvelopeRefMut = account.collection.get_env_mut(env_hash); match a { ListingAction::SetSeen => { if let Err(e) = envelope.set_seen(op) { context.replies.push_back(UIEvent::StatusEvent( StatusEvent::DisplayMessage(e.to_string()), )); } self.row_updates().push(thread_hash); } ListingAction::SetUnseen => { if let Err(e) = envelope.set_unseen(op) { context.replies.push_back(UIEvent::StatusEvent( StatusEvent::DisplayMessage(e.to_string()), )); } } ListingAction::Delete => { /* do nothing */ continue; } ListingAction::Tag(Remove(ref tag_str)) => { use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; let h = { let mut hasher = DefaultHasher::new(); hasher.write(tag_str.as_bytes()); hasher.finish() }; let backend_lck = account.backend.write().unwrap(); if let Some(t) = backend_lck.tags() { let mut tags_lck = t.write().unwrap(); if !tags_lck.contains_key(&h) { tags_lck.insert(h, tag_str.to_string()); } if let Some(pos) = envelope.labels().iter().position(|&el| el == h) { envelope.labels_mut().remove(pos); } } else { return; } } ListingAction::Tag(Add(ref tag_str)) => { use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; let h = { let mut hasher = DefaultHasher::new(); hasher.write(tag_str.as_bytes()); hasher.finish() }; let backend_lck = account.backend.write().unwrap(); if let Some(t) = backend_lck.tags() { let mut tags_lck = t.write().unwrap(); if !tags_lck.contains_key(&h) { tags_lck.insert(h, tag_str.to_string()); } envelope.labels_mut().push(h); } else { return; } } _ => unreachable!(), } self.row_updates().push(thread_hash); drop(envelope); } } fn row_updates(&mut self) -> &mut StackVec; fn update_line(&mut self, context: &Context, thread_hash: ThreadHash); } pub trait ListingTrait: Component { fn coordinates(&self) -> (usize, usize); fn set_coordinates(&mut self, _: (usize, usize)); 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: &str, _context: &Context) {} fn set_movement(&mut self, mvm: PageMovement); } #[derive(Debug)] pub enum ListingComponent { Plain(PlainListing), Threaded(ThreadListing), Compact(CompactListing), Conversations(ConversationsListing), } use crate::ListingComponent::*; impl core::ops::Deref for ListingComponent { type Target = dyn MailListingTrait; fn deref(&self) -> &Self::Target { match &self { Compact(ref l) => l, Plain(ref l) => l, Threaded(ref l) => l, Conversations(ref l) => l, } } } impl core::ops::DerefMut for ListingComponent { fn deref_mut(&mut self) -> &mut (dyn MailListingTrait + 'static) { match self { Compact(l) => l, Plain(l) => l, Threaded(l) => l, Conversations(l) => l, } } } impl ListingComponent { fn set_style(&mut self, new_style: IndexStyle) { match new_style { IndexStyle::Plain => { if let Plain(_) = self { return; } let mut new_l = PlainListing::default(); let coors = self.coordinates(); new_l.set_coordinates((coors.0, coors.1)); *self = Plain(new_l); } IndexStyle::Threaded => { if let Threaded(_) = self { return; } let mut new_l = ThreadListing::default(); let coors = self.coordinates(); new_l.set_coordinates((coors.0, coors.1)); *self = Threaded(new_l); } IndexStyle::Compact => { if let Compact(_) = self { return; } let mut new_l = CompactListing::default(); let coors = self.coordinates(); new_l.set_coordinates((coors.0, coors.1)); *self = Compact(new_l); } IndexStyle::Conversations => { if let Conversations(_) = self { return; } let mut new_l = ConversationsListing::default(); let coors = self.coordinates(); new_l.set_coordinates((coors.0, coors.1)); *self = Conversations(new_l); } } } } #[derive(Debug)] pub struct Listing { component: ListingComponent, accounts: Vec, dirty: bool, visible: bool, cursor_pos: (usize, usize), id: ComponentId, show_divider: bool, menu_visibility: bool, cmd_buf: String, /// This is the width of the right container to the entire width. ratio: usize, // right/(container width) * 100 } impl fmt::Display for Listing { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.component { Compact(ref l) => write!(f, "{}", l), Plain(ref l) => write!(f, "{}", l), Threaded(ref l) => write!(f, "{}", l), Conversations(ref l) => write!(f, "{}", l), } } } impl Component for Listing { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { for i in 0..context.accounts.len() { context.is_online(i); } if !self.is_dirty() { return; } if !is_valid_area!(area) { return; } let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); let total_cols = get_x(bottom_right) - get_x(upper_left); let right_component_width = if self.menu_visibility { (self.ratio * total_cols) / 100 } else { total_cols }; let mid = get_x(bottom_right) - right_component_width; if self.dirty && mid != get_x(upper_left) { if self.show_divider { for i in get_y(upper_left)..=get_y(bottom_right) { grid[(mid, i)].set_ch(VERT_BOUNDARY); grid[(mid, i)].set_fg(Color::Default); grid[(mid, i)].set_bg(Color::Default); } } else { for i in get_y(upper_left)..=get_y(bottom_right) { grid[(mid, i)].set_fg(Color::Default); grid[(mid, i)].set_bg(Color::Default); } } context .dirty_areas .push_back(((mid, get_y(upper_left)), (mid, get_y(bottom_right)))); } if right_component_width == total_cols { if !context.is_online(self.cursor_pos.0) { clear_area(grid, area); write_string_to_grid( "offline", grid, Color::Byte(243), Color::Default, Attr::Default, area, None, ); context.dirty_areas.push_back(area); return; } self.component.draw(grid, area, context); } else if right_component_width == 0 { self.draw_menu(grid, area, context); } else { self.draw_menu(grid, (upper_left, (mid, get_y(bottom_right))), context); if !context.is_online(self.cursor_pos.0) { clear_area(grid, (set_x(upper_left, mid + 1), bottom_right)); write_string_to_grid( "offline", grid, Color::Byte(243), Color::Default, Attr::Default, (set_x(upper_left, mid + 1), bottom_right), None, ); context.dirty_areas.push_back(area); return; } self.component .draw(grid, (set_x(upper_left, mid + 1), bottom_right), context); } self.dirty = false; } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { if self.component.process_event(event, context) { return true; } let shortcuts = self.get_shortcuts(context); match *event { UIEvent::Input(ref k) if shortcut!(k == shortcuts[Listing::DESCRIPTION]["next_folder"]) || shortcut!(k == shortcuts[Listing::DESCRIPTION]["prev_folder"]) => { let amount = if self.cmd_buf.is_empty() { 1 } else if let Ok(amount) = self.cmd_buf.parse::() { self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); amount } else { self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); return true; }; let folder_length = context.accounts[self.cursor_pos.0].len(); match k { k if shortcut!(k == shortcuts[Listing::DESCRIPTION]["next_folder"]) && folder_length > 0 => { if self.cursor_pos.1 + amount < folder_length { self.cursor_pos.1 += amount; self.component .set_coordinates((self.cursor_pos.0, self.cursor_pos.1)); self.set_dirty(); } else { return true; } } k if shortcut!(k == shortcuts[Listing::DESCRIPTION]["prev_folder"]) => { if self.cursor_pos.1 >= amount { self.cursor_pos.1 -= amount; self.component .set_coordinates((self.cursor_pos.0, self.cursor_pos.1)); self.set_dirty(); } else { return true; } } _ => return false, } let folder_hash = context.accounts[self.cursor_pos.0].folders_order[self.cursor_pos.1]; /* Check if per-folder configuration overrides general configuration */ if let Some(index_style) = context .accounts .get(self.cursor_pos.0) .and_then(|account| account.folder_confs(folder_hash).conf_override.index_style) { self.component.set_style(index_style); } else if let Some(index_style) = context .accounts .get(self.cursor_pos.0) .and_then(|account| Some(account.settings.conf.index_style())) { self.component.set_style(index_style); } context .replies .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( self.get_status(context).unwrap(), ))); return true; } UIEvent::Input(ref k) if shortcut!(k == shortcuts[Listing::DESCRIPTION]["next_account"]) || shortcut!(k == shortcuts[Listing::DESCRIPTION]["prev_account"]) => { let amount = if self.cmd_buf.is_empty() { 1 } else if let Ok(amount) = self.cmd_buf.parse::() { self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); amount } else { self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); return true; }; match k { k if shortcut!(k == shortcuts[Listing::DESCRIPTION]["next_account"]) => { if self.cursor_pos.0 + amount < self.accounts.len() { self.cursor_pos = (self.cursor_pos.0 + amount, 0); self.component.set_coordinates((self.cursor_pos.0, 0)); self.set_dirty(); } else { return true; } } k if shortcut!(k == shortcuts[Listing::DESCRIPTION]["prev_account"]) => { if self.cursor_pos.0 >= amount { self.cursor_pos = (self.cursor_pos.0 - amount, 0); self.component.set_coordinates((self.cursor_pos.0, 0)); self.set_dirty(); } else { return true; } } _ => return false, } /* Account might have no folders yet if it's offline */ if let Some(&folder_hash) = context.accounts[self.cursor_pos.0] .folders_order .get(self.cursor_pos.1) { /* Check if per-folder configuration overrides general configuration */ if let Some(index_style) = context.accounts.get(self.cursor_pos.0).and_then(|account| { account.folder_confs(folder_hash).conf_override.index_style }) { self.component.set_style(index_style); } else if let Some(index_style) = context .accounts .get(self.cursor_pos.0) .and_then(|account| Some(account.settings.conf.index_style())) { self.component.set_style(index_style); } } context .replies .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( self.get_status(context).unwrap(), ))); return true; } UIEvent::Action(ref action) => match action { Action::Listing(ListingAction::SetPlain) => { self.component.set_style(IndexStyle::Plain); return true; } Action::Listing(ListingAction::SetThreaded) => { self.component.set_style(IndexStyle::Threaded); return true; } Action::Listing(ListingAction::SetCompact) => { self.component.set_style(IndexStyle::Compact); return true; } Action::Listing(ListingAction::SetConversations) => { self.component.set_style(IndexStyle::Conversations); return true; } _ => {} }, UIEvent::RefreshMailbox((idxa, folder_hash)) => { self.cursor_pos = ( idxa, context.accounts[idxa] .folders_order .iter() .position(|&h| h == folder_hash) .unwrap_or(0), ); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( self.get_status(context).unwrap(), ))); self.dirty = true; } UIEvent::ChangeMode(UIMode::Normal) => { self.dirty = true; } UIEvent::Resize => { self.set_dirty(); } UIEvent::Input(Key::Up) => { let amount = if self.cmd_buf.is_empty() { 1 } else if let Ok(amount) = self.cmd_buf.parse::() { self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); amount } else { self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); return true; }; self.component.set_movement(PageMovement::Up(amount)); return true; } UIEvent::Input(Key::Down) => { let amount = if self.cmd_buf.is_empty() { 1 } else if let Ok(amount) = self.cmd_buf.parse::() { self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); amount } else { self.cmd_buf.clear(); 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[Listing::DESCRIPTION]["prev_page"]) => { let mult = if self.cmd_buf.is_empty() { 1 } else if let Ok(mult) = self.cmd_buf.parse::() { self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); mult } else { self.cmd_buf.clear(); 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[Listing::DESCRIPTION]["next_page"]) => { let mult = if self.cmd_buf.is_empty() { 1 } else if let Ok(mult) = self.cmd_buf.parse::() { self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); mult } else { self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); return true; }; self.component.set_movement(PageMovement::PageDown(mult)); return true; } UIEvent::Input(ref key) if *key == Key::Home => { self.component.set_movement(PageMovement::Home); return true; } UIEvent::Input(ref key) if *key == Key::End => { self.component.set_movement(PageMovement::End); return true; } UIEvent::Input(ref k) if shortcut!(k == shortcuts[Listing::DESCRIPTION]["toggle_menu_visibility"]) => { self.menu_visibility = !self.menu_visibility; self.set_dirty(); } UIEvent::Input(ref k) if shortcut!(k == shortcuts[Listing::DESCRIPTION]["new_mail"]) => { context .replies .push_back(UIEvent::Action(Tab(NewDraft(self.cursor_pos.0, None)))); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Listing::DESCRIPTION]["search"]) => { context .replies .push_back(UIEvent::ExInput(Key::Paste("filter ".to_string()))); context .replies .push_back(UIEvent::ChangeMode(UIMode::Execute)); return true; } UIEvent::Input(ref key) if shortcut!(key == shortcuts[Listing::DESCRIPTION]["set_seen"]) => { let mut event = UIEvent::Action(Action::Listing(ListingAction::SetSeen)); if self.component.process_event(&mut event, context) { return true; } } UIEvent::StartupCheck(_) => { self.dirty = true; context .replies .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( self.get_status(context).unwrap(), ))); } UIEvent::MailboxUpdate(_) => { self.dirty = true; context .replies .push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus( self.get_status(context).unwrap(), ))); } UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) => { self.cmd_buf.clear(); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); return true; } UIEvent::Input(Key::Char(c)) if c >= '0' && c <= '9' => { self.cmd_buf.push(c); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::BufSet( self.cmd_buf.clone(), ))); return true; } _ => {} } false } fn is_dirty(&self) -> bool { self.dirty || self.component.is_dirty() } fn set_dirty(&mut self) { self.dirty = true; self.component.set_dirty(); } fn get_shortcuts(&self, context: &Context) -> ShortcutMaps { let mut map = self.component.get_shortcuts(context); let config_map = context.settings.shortcuts.listing.key_values(); map.insert(Listing::DESCRIPTION, config_map); map } fn id(&self) -> ComponentId { self.component.id() } fn set_id(&mut self, id: ComponentId) { self.component.set_id(id); } fn get_status(&self, context: &Context) -> Option { Some({ let folder_hash = if let Some(h) = context.accounts[self.cursor_pos.0] .folders_order .get(self.cursor_pos.1) { *h } else { return Some(String::new()); }; if !context.accounts[self.cursor_pos.0].folders[&folder_hash].is_available() { return Some(String::new()); } let account = &context.accounts[self.cursor_pos.0]; let m = if account[self.cursor_pos.1].is_available() { account[self.cursor_pos.1].unwrap() } else { return Some(String::new()); }; let envelopes = account.collection.envelopes.clone(); let envelopes = envelopes.read().unwrap(); format!( "Mailbox: {}, Messages: {}, New: {}", m.folder.name(), m.envelopes.len(), m.envelopes .iter() .map(|h| &envelopes[&h]) .filter(|e| !e.is_seen()) .count() ) }) } } impl From for ListingComponent { fn from(index_style: IndexStyle) -> Self { match index_style { IndexStyle::Plain => Plain(Default::default()), IndexStyle::Threaded => Threaded(Default::default()), IndexStyle::Compact => Compact(Default::default()), IndexStyle::Conversations => Conversations(Default::default()), } } } impl Listing { const DESCRIPTION: &'static str = "listing"; pub fn new(accounts: &[Account]) -> Self { let account_entries = accounts .iter() .enumerate() .map(|(i, a)| AccountMenuEntry { name: a.name().to_string(), index: i, }) .collect(); /* Check if per-folder configuration overrides general configuration */ let component = if let Some(index_style) = accounts.get(0).and_then(|account| { account.folders_order.get(0).and_then(|folder_hash| { account.folder_confs(*folder_hash).conf_override.index_style }) }) { ListingComponent::from(index_style) } else if let Some(index_style) = accounts .get(0) .and_then(|account| Some(account.settings.conf.index_style())) { ListingComponent::from(index_style) } else { Conversations(Default::default()) }; Listing { component, accounts: account_entries, visible: true, dirty: true, cursor_pos: (0, 0), id: ComponentId::new_v4(), show_divider: false, menu_visibility: true, ratio: 90, cmd_buf: String::with_capacity(4), } } fn draw_menu(&mut self, grid: &mut CellBuffer, mut area: Area, context: &mut Context) { if !self.is_dirty() { return; } clear_area(grid, area); /* visually divide menu and listing */ area = (area.0, pos_dec(area.1, (1, 0))); let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); self.dirty = false; let mut y = get_y(upper_left); for a in &self.accounts { y += self.print_account(grid, (set_y(upper_left, y), bottom_right), &a, context); y += 3; } context.dirty_areas.push_back(area); } /* * Print a single account in the menu area. */ fn print_account( &self, grid: &mut CellBuffer, area: Area, a: &AccountMenuEntry, context: &mut Context, ) -> usize { if !is_valid_area!(area) { debug!("BUG: invalid area in print_account"); } // Each entry and its index in the account let entries: FnvHashMap = context.accounts[a.index] .list_folders() .into_iter() .map(|f| (f.hash(), f)) .collect(); let folders_order: FnvHashMap = context.accounts[a.index] .folders_order() .iter() .enumerate() .map(|(i, &fh)| (fh, i)) .collect(); let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); let must_highlight_account: bool = self.cursor_pos.0 == a.index; let mut inc = 0; let mut depth = 0; let mut lines: Vec<(usize, usize, FolderHash, Option)> = Vec::new(); /* Gather the folder tree structure in `lines` recursively */ fn print( folder_idx: FolderHash, depth: &mut usize, inc: &mut usize, entries: &FnvHashMap, folders_order: &FnvHashMap, lines: &mut Vec<(usize, usize, FolderHash, Option)>, index: usize, //account index context: &mut Context, ) { match context.accounts[index].status(entries[&folder_idx].hash()) { Ok(_) => { let account = &context.accounts[index]; let count = account[entries[&folder_idx].hash()] .unwrap() .envelopes .iter() .filter_map(|&h| { if account.collection.get_env(h).is_seen() { None } else { Some(()) } }) .count(); lines.push((*depth, *inc, folder_idx, Some(count))); } Err(_) => { lines.push((*depth, *inc, folder_idx, None)); } } *inc += 1; let mut children: Vec = entries[&folder_idx].children().to_vec(); children .sort_unstable_by(|a, b| folders_order[a].partial_cmp(&folders_order[b]).unwrap()); *depth += 1; for child in children { print( child, depth, inc, entries, folders_order, lines, index, context, ); } *depth -= 1; } let mut keys = entries.keys().cloned().collect::>(); keys.sort_unstable_by(|a, b| folders_order[a].partial_cmp(&folders_order[b]).unwrap()); /* Start with roots */ for f in keys { if entries[&f].parent().is_none() { print( f, &mut depth, &mut inc, &entries, &folders_order, &mut lines, a.index, context, ); } } /* Print account name first */ write_string_to_grid( &a.name, grid, Color::Default, Color::Default, Attr::Bold, area, None, ); if lines.is_empty() { write_string_to_grid( "offline", grid, Color::Byte(243), Color::Default, Attr::Default, (pos_inc(upper_left, (0, 1)), bottom_right), None, ); return 0; } let lines_len = lines.len(); let mut idx = 0; for y in get_y(upper_left) + 1..get_y(bottom_right) { if idx == lines_len { break; } let (fg_color, bg_color) = if must_highlight_account { if self.cursor_pos.1 == idx { (Color::Byte(233), Color::Byte(15)) } else { (Color::Byte(15), Color::Byte(233)) } } else { (Color::Default, Color::Default) }; let (depth, inc, folder_idx, count) = lines[idx]; /* Calculate how many columns the folder index tags should occupy with right alignment, * eg. * 1 * 2 * ... * 9 * 10 */ let total_folder_no_digits = { let mut len = lines_len; let mut ctr = 1; while len > 9 { ctr += 1; len /= 10; } ctr }; let (x, _) = write_string_to_grid( &format!("{:>width$}", inc, width = total_folder_no_digits), grid, Color::Byte(243), bg_color, Attr::Default, (set_y(upper_left, y), bottom_right), None, ); let (x, _) = write_string_to_grid( &" ".repeat(depth + 1), grid, fg_color, bg_color, Attr::Default, ((x, y), bottom_right), None, ); let (x, _) = write_string_to_grid( entries[&folder_idx].name(), grid, fg_color, bg_color, Attr::Default, ((x, y), bottom_right), None, ); /* Unread message count */ let count_string = if let Some(c) = count { if c > 0 { format!(" {}", c) } else { String::new() } } else { " ...".to_string() }; let (x, _) = write_string_to_grid( &count_string, grid, fg_color, bg_color, if count.unwrap_or(0) > 0 { Attr::Bold } else { Attr::Default }, ( ( /* Hide part of folder name if need be to fit the message count */ std::cmp::min(x, get_x(bottom_right).saturating_sub(count_string.len())), y, ), bottom_right, ), None, ); change_colors(grid, ((x, y), set_y(bottom_right, y)), fg_color, bg_color); idx += 1; } if idx == 0 { 0 } else { idx - 1 } } }