/* * 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::*; //use melib::mailbox::backends::BackendOp; const MAX_COLS: usize = 500; /// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the `Envelope` content in a /// `ThreadView`. #[derive(Debug)] pub struct CompactListing { /// (x, y, z): x is accounts, y is folders, z is index inside a folder. cursor_pos: (usize, usize, usize), new_cursor_pos: (usize, usize, usize), length: usize, sort: (SortField, SortOrder), subsort: (SortField, SortOrder), /// Cache current view. content: CellBuffer, /// If we must redraw on next redraw event dirty: bool, /// If `self.view` exists or not. unfocused: bool, view: Option, } impl Default for CompactListing { fn default() -> Self { Self::new() } } impl fmt::Display for CompactListing { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "mail") } } impl CompactListing { /// Helper function to format entry strings for CompactListing */ /* TODO: Make this configurable */ fn make_entry_string(e: &Envelope, len: usize, idx: usize) -> String { if len > 1 { format!( "{} {} {:.85} ({})", idx, &CompactListing::format_date(e), e.subject(), len ) } else { format!( "{} {} {:.85}", idx, &CompactListing::format_date(e), e.subject(), ) } } pub fn new() -> Self { let content = CellBuffer::new(0, 0, Cell::with_char(' ')); CompactListing { cursor_pos: (0, 1, 0), new_cursor_pos: (0, 0, 0), length: 0, sort: (Default::default(), Default::default()), subsort: (Default::default(), Default::default()), content, dirty: true, unfocused: false, view: None, } } /// Fill the `self.content` `CellBuffer` with the contents of the account folder the user has /// chosen. fn refresh_mailbox(&mut self, context: &mut Context) { self.dirty = true; self.cursor_pos.2 = 0; self.new_cursor_pos.2 = 0; self.cursor_pos.1 = self.new_cursor_pos.1; self.cursor_pos.0 = self.new_cursor_pos.0; // Inform State that we changed the current folder view. context.replies.push_back(UIEvent { id: 0, event_type: UIEventType::RefreshMailbox((self.cursor_pos.0, self.cursor_pos.1)), }); // Get mailbox as a reference. // loop { // TODO: Show progress visually if context.accounts[self.cursor_pos.0] .status(self.cursor_pos.1) .is_ok() { break; } } let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] .as_ref() .unwrap(); self.length = mailbox.threads.root_len(); self.content = CellBuffer::new(MAX_COLS, self.length + 1, Cell::with_char(' ')); if self.length == 0 { write_string_to_grid( &format!("Folder `{}` is empty.", mailbox.folder.name()), &mut self.content, Color::Default, Color::Default, ((0, 0), (MAX_COLS - 1, 0)), true, ); return; } let threads = &mailbox.threads; threads.sort_by(self.sort, self.subsort, &mailbox.collection); for (idx, (t, len, has_unseen)) in threads.root_set_iter().enumerate() { let container = &threads.containers()[t]; let i = if let Some(i) = container.message() { i } else { threads.containers()[container.first_child().unwrap()] .message() .unwrap() }; let root_envelope: &Envelope = &mailbox.collection[i]; let fg_color = if has_unseen { Color::Byte(0) } else { Color::Default }; let bg_color = if has_unseen { Color::Byte(251) } else if idx % 2 == 0 { Color::Byte(236) } else { Color::Default }; let (x, _) = write_string_to_grid( &CompactListing::make_entry_string(root_envelope, len, idx), &mut self.content, fg_color, bg_color, ((0, idx), (MAX_COLS - 1, idx)), false, ); for x in x..MAX_COLS { self.content[(x, idx)].set_ch(' '); self.content[(x, idx)].set_bg(bg_color); } } } fn highlight_line(&self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) { if idx == self.cursor_pos.2 { let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] .as_ref() .unwrap(); let threads = &mailbox.threads; let container = threads.root_set()[idx]; let container = &threads.containers()[container]; let i = if let Some(i) = container.message() { i } else { threads.containers()[container.first_child().unwrap()] .message() .unwrap() }; let root_envelope: &Envelope = &mailbox.collection[i]; let fg_color = if !root_envelope.is_seen() { Color::Byte(0) } else { Color::Default }; let bg_color = if self.cursor_pos.2 == idx { Color::Byte(246) } else if !root_envelope.is_seen() { Color::Byte(251) } else if idx % 2 == 0 { Color::Byte(236) } else { Color::Default }; change_colors(grid, area, fg_color, bg_color); return; } let (width, height) = self.content.size(); copy_area( grid, &self.content, area, ((0, idx), (width - 1, height - 1)), ); } /// Draw the list of `Envelope`s. fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { if self.cursor_pos.1 != self.new_cursor_pos.1 { self.refresh_mailbox(context); } let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); if self.length == 0 { clear_area(grid, area); copy_area(grid, &self.content, area, ((0, 0), (MAX_COLS - 1, 0))); context.dirty_areas.push_back(area); return; } let rows = get_y(bottom_right) - get_y(upper_left) + 1; let prev_page_no = (self.cursor_pos.2).wrapping_div(rows); let page_no = (self.new_cursor_pos.2).wrapping_div(rows); let top_idx = page_no * rows; /* If cursor position has changed, remove the highlight from the previous position and * apply it in the new one. */ if self.cursor_pos.2 != self.new_cursor_pos.2 && prev_page_no == page_no { let old_cursor_pos = self.cursor_pos; self.cursor_pos = self.new_cursor_pos; for idx in &[old_cursor_pos.2, self.new_cursor_pos.2] { if *idx >= self.length { continue; //bounds check } let new_area = ( set_y(upper_left, get_y(upper_left) + (*idx % rows)), set_y(bottom_right, get_y(upper_left) + (*idx % rows)), ); self.highlight_line(grid, new_area, *idx, context); context.dirty_areas.push_back(new_area); } return; } else if self.cursor_pos != self.new_cursor_pos { self.cursor_pos = self.new_cursor_pos; } /* Page_no has changed, so draw new page */ copy_area( grid, &self.content, area, ((0, top_idx), (MAX_COLS - 1, self.length)), ); self.highlight_line( grid, ( set_y(upper_left, get_y(upper_left) + (self.cursor_pos.2 % rows)), set_y(bottom_right, get_y(upper_left) + (self.cursor_pos.2 % rows)), ), self.cursor_pos.2, context, ); context.dirty_areas.push_back(area); } fn format_date(envelope: &Envelope) -> String { let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(envelope.date()); let now: std::time::Duration = std::time::SystemTime::now().duration_since(d).unwrap(); match now.as_secs() { n if n < 10 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(8)), n if n < 24 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(7)), n if n < 4 * 24 * 60 * 60 => { format!("{} days ago{}", n / (24 * 60 * 60), " ".repeat(9)) } _ => envelope.datetime().format("%Y-%m-%d %H:%M:%S").to_string(), } } } impl Component for CompactListing { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { if !self.unfocused { if !self.is_dirty() { return; } self.dirty = false; /* Draw the entire list */ self.draw_list(grid, area, context); } else { if self.length == 0 && self.dirty { clear_area(grid, area); context.dirty_areas.push_back(area); return; } /* Render the mail body in a pager */ if !self.dirty { if let Some(v) = self.view.as_mut() { v.draw(grid, area, context); } return; } self.view = Some(ThreadView::new(self.cursor_pos, None, context)); self.view.as_mut().unwrap().draw(grid, area, context); self.dirty = false; } } fn process_event(&mut self, event: &UIEvent, context: &mut Context) -> bool { if let Some(ref mut v) = self.view { if v.process_event(event, context) { return true; } } match event.event_type { UIEventType::Input(Key::Up) => { if self.cursor_pos.2 > 0 { self.new_cursor_pos.2 = self.new_cursor_pos.2.saturating_sub(1); self.dirty = true; } return true; } UIEventType::Input(Key::Down) => { if self.length > 0 && self.new_cursor_pos.2 < self.length - 1 { self.new_cursor_pos.2 += 1; self.dirty = true; } return true; } UIEventType::Input(Key::Char('\n')) if !self.unfocused => { self.unfocused = true; self.dirty = true; return true; } UIEventType::Input(Key::Char('i')) if self.unfocused => { self.unfocused = false; self.dirty = true; self.view = None; return true; } UIEventType::Input(Key::Char(k @ 'J')) | UIEventType::Input(Key::Char(k @ 'K')) => { let folder_length = context.accounts[self.cursor_pos.0].len(); let accounts_length = context.accounts.len(); match k { 'J' if folder_length > 0 => { if self.new_cursor_pos.1 < folder_length - 1 { self.new_cursor_pos.1 = self.cursor_pos.1 + 1; self.dirty = true; self.refresh_mailbox(context); } else if accounts_length > 0 && self.new_cursor_pos.0 < accounts_length - 1 { self.new_cursor_pos.0 = self.cursor_pos.0 + 1; self.new_cursor_pos.1 = 0; self.dirty = true; self.refresh_mailbox(context); } } 'K' => { if self.cursor_pos.1 > 0 { self.new_cursor_pos.1 = self.cursor_pos.1 - 1; self.dirty = true; self.refresh_mailbox(context); } else if self.cursor_pos.0 > 0 { self.new_cursor_pos.0 = self.cursor_pos.0 - 1; self.new_cursor_pos.1 = 0; self.dirty = true; self.refresh_mailbox(context); } } _ => {} } return true; } UIEventType::Input(Key::Char(k @ 'h')) | UIEventType::Input(Key::Char(k @ 'l')) => { let accounts_length = context.accounts.len(); match k { 'h' if accounts_length > 0 && self.new_cursor_pos.0 < accounts_length - 1 => { self.new_cursor_pos.0 = self.cursor_pos.0 + 1; self.new_cursor_pos.1 = 0; self.dirty = true; self.refresh_mailbox(context); } 'l' if self.cursor_pos.0 > 0 => { self.new_cursor_pos.0 = self.cursor_pos.0 - 1; self.new_cursor_pos.1 = 0; self.dirty = true; self.refresh_mailbox(context); } _ => {} } return true; } UIEventType::RefreshMailbox(_) => { self.dirty = true; self.view = None; } UIEventType::MailboxUpdate((ref idxa, ref idxf)) => { if *idxa == self.new_cursor_pos.0 && *idxf == self.new_cursor_pos.1 { self.dirty = true; self.refresh_mailbox(context); } } UIEventType::ChangeMode(UIMode::Normal) => { self.dirty = true; } UIEventType::Resize => { self.dirty = true; } UIEventType::Action(ref action) => match action { Action::ViewMailbox(idx) => { self.new_cursor_pos.1 = *idx; self.dirty = true; self.refresh_mailbox(context); return true; } Action::SubSort(field, order) => { eprintln!("SubSort {:?} , {:?}", field, order); self.subsort = (*field, *order); self.dirty = true; self.refresh_mailbox(context); return true; } Action::Sort(field, order) => { eprintln!("Sort {:?} , {:?}", field, order); self.sort = (*field, *order); self.dirty = true; self.refresh_mailbox(context); return true; } _ => {} }, _ => {} } false } fn is_dirty(&self) -> bool { self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false) } fn set_dirty(&mut self) { if let Some(p) = self.view.as_mut() { p.set_dirty(); } self.dirty = true; } }