diff --git a/melib/src/mailbox/email/mod.rs b/melib/src/mailbox/email/mod.rs index 66f54bea8..e45fa08d5 100644 --- a/melib/src/mailbox/email/mod.rs +++ b/melib/src/mailbox/email/mod.rs @@ -630,8 +630,7 @@ impl Envelope { s.refs.push(new_ref); } None => { - let mut v = Vec::new(); - v.push(new_ref); + let v = vec![new_ref]; self.references = Some(References { raw: "".into(), refs: v, @@ -645,10 +644,9 @@ impl Envelope { s.raw = new_val.into(); } None => { - let v = Vec::new(); self.references = Some(References { raw: new_val.into(), - refs: v, + refs: Vec::new(), }); } } diff --git a/melib/src/mailbox/mod.rs b/melib/src/mailbox/mod.rs index 010e4aa92..fcc42192d 100644 --- a/melib/src/mailbox/mod.rs +++ b/melib/src/mailbox/mod.rs @@ -83,6 +83,15 @@ impl Mailbox { pub fn len(&self) -> usize { self.collection.len() } + pub fn thread_to_mail_mut(&mut self, i: usize) -> &mut Envelope { + self.collection + .envelopes + .entry(self.collection.threads.thread_to_mail(i)) + .or_default() + } + pub fn thread_to_mail(&self, i: usize) -> &Envelope { + &self.collection.envelopes[&self.collection.threads.thread_to_mail(i)] + } pub fn threaded_mail(&self, i: usize) -> EnvelopeHash { self.collection.threads.thread_to_mail(i) } diff --git a/melib/src/mailbox/thread.rs b/melib/src/mailbox/thread.rs index b1e02680a..3972b660c 100644 --- a/melib/src/mailbox/thread.rs +++ b/melib/src/mailbox/thread.rs @@ -151,6 +151,10 @@ impl ThreadNode { pub fn children(&self) -> &[usize] { &self.children } + + pub fn indentation(&self) -> usize { + self.indentation + } } #[derive(Clone, Debug, Default)] @@ -194,7 +198,7 @@ impl Threads { if !thread_nodes[*v].has_message() && thread_nodes[*v].children.len() == 1 { /* Do not promote the children if doing so would promote them to the root set * -- unless there is only one child, in which case, do. */ - root_set.push(*v); + root_set.push(thread_nodes[*v].children[0]); continue 'root_set; } @@ -334,7 +338,13 @@ fn link_threads( /* The index of the reference we are currently examining, start from current message */ let mut ref_ptr = t_idx; + let mut iasf = 0; for &refn in v.references().iter().rev() { + if iasf == 1 { + /*FIXME: Skips anything other than direct parents */ + continue; + } + iasf += 1; let r_id = String::from_utf8_lossy(refn.raw()).into(); let parent_id = if message_ids.contains_key(&r_id) { let parent_id = message_ids[&r_id]; @@ -368,7 +378,6 @@ fn link_threads( if thread_nodes[ref_ptr].parent.is_none() { thread_nodes[ref_ptr].parent = Some(thread_nodes.len() - 1); } - /* Can't avoid copy here since we have different lifetimes */ message_ids.insert(r_id, thread_nodes.len() - 1); thread_nodes.len() - 1 }; @@ -401,6 +410,7 @@ impl Index for Threads { .expect("thread index out of bounds") } } + fn node_build( idx: usize, thread_nodes: &mut Vec, diff --git a/src/bin.rs b/src/bin.rs index 1c963c13b..d97af8dd4 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -62,7 +62,7 @@ fn main() { /* Register some reasonably useful interfaces */ let menu = Entity::from(Box::new(AccountMenu::new(&state.context.accounts))); - let listing = PlainListing::new(); + let listing = listing::Listing::default(); let b = Entity::from(Box::new(listing)); let tabs = Box::new(Tabbed::new(vec![Box::new(VSplit::new(menu, b, 90, true))])); let window = Entity::from(tabs); diff --git a/ui/src/components/mail/listing/mod.rs b/ui/src/components/mail/listing/mod.rs index be1a2ef8c..d600d33af 100644 --- a/ui/src/components/mail/listing/mod.rs +++ b/ui/src/components/mail/listing/mod.rs @@ -24,621 +24,75 @@ use super::*; mod compact; pub use self::compact::*; -const MAX_COLS: usize = 500; +mod thread; +pub use self::thread::*; + +mod plain; +pub use self::plain::*; -/// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the `Envelope` content in a -/// `MailView`. #[derive(Debug)] -pub struct PlainListing { - /// (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, - local_collection: Vec, - 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, +pub enum Listing { + Plain(PlainListing), + Threaded(ThreadListing), + Compact(CompactListing), } -impl Default for PlainListing { - fn default() -> Self { - Self::new() - } -} - -impl fmt::Display for PlainListing { +impl fmt::Display for Listing { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "mail") - } -} - -impl PlainListing { - /// Helper function to format entry strings for PlainListing */ - /* TODO: Make this configurable */ - fn make_entry_string(e: &Envelope, idx: usize) -> String { - format!( - "{} {} {}", - idx, - &e.datetime().format("%Y-%m-%d %H:%M:%S").to_string(), - e.subject() - ) - } - - pub fn new() -> Self { - let content = CellBuffer::new(0, 0, Cell::with_char(' ')); - PlainListing { - cursor_pos: (0, 1, 0), - new_cursor_pos: (0, 0, 0), - length: 0, - local_collection: Vec::new(), - 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; - - let threaded = context.accounts[self.cursor_pos.0] - .runtime_settings - .conf() - .threaded(); - // 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.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; - } - - // Populate `CellBuffer` with every entry. - let mut idx = 0; - for y in 0..=self.length { - if idx >= self.length { - /* No more entries left, so fill the rest of the area with empty space */ - clear_area(&mut self.content, ((0, y), (MAX_COLS - 1, self.length))); - break; - } - /* Write an entire line for each envelope entry. */ - self.local_collection = mailbox.collection.keys().map(|v| *v).collect(); - let sort = self.sort; - self.local_collection.sort_by(|a, b| match sort { - (SortField::Date, SortOrder::Desc) => { - let ma = &mailbox.collection[a]; - let mb = &mailbox.collection[b]; - mb.date().cmp(&ma.date()) - } - (SortField::Date, SortOrder::Asc) => { - let ma = &mailbox.collection[a]; - let mb = &mailbox.collection[b]; - ma.date().cmp(&mb.date()) - } - (SortField::Subject, SortOrder::Desc) => { - let ma = &mailbox.collection[a]; - let mb = &mailbox.collection[b]; - ma.subject().cmp(&mb.subject()) - } - (SortField::Subject, SortOrder::Asc) => { - let ma = &mailbox.collection[a]; - let mb = &mailbox.collection[b]; - mb.subject().cmp(&ma.subject()) - } - }); - let envelope: &Envelope = &mailbox.collection[&self.local_collection[idx]]; - - let fg_color = if !envelope.is_seen() { - Color::Byte(0) - } else { - Color::Default - }; - let bg_color = if !envelope.is_seen() { - Color::Byte(251) - } else if idx % 2 == 0 { - Color::Byte(236) - } else { - Color::Default - }; - let (x, y) = write_string_to_grid( - &PlainListing::make_entry_string(envelope, idx), - &mut self.content, - fg_color, - bg_color, - ((0, y), (MAX_COLS - 1, y)), - false, - ); - - for x in x..MAX_COLS { - self.content[(x, y)].set_ch(' '); - self.content[(x, y)].set_bg(bg_color); - } - - idx += 1; - } - } - - fn highlight_line_self(&mut self, idx: usize, context: &Context) { - let threaded = context.accounts[self.cursor_pos.0] - .runtime_settings - .conf() - .threaded(); - let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] - .as_ref() - .unwrap(); - let envelope: &Envelope = &mailbox.collection[&self.local_collection[idx]]; - - let fg_color = if !envelope.is_seen() { - Color::Byte(0) - } else { - Color::Default - }; - let bg_color = if !envelope.is_seen() { - Color::Byte(251) - } else if idx % 2 == 0 { - Color::Byte(236) - } else { - Color::Default - }; - change_colors( - &mut self.content, - ((0, idx), (MAX_COLS - 1, idx)), - fg_color, - bg_color, - ); - } - - fn highlight_line(&self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) { - let threaded = context.accounts[self.cursor_pos.0] - .runtime_settings - .conf() - .threaded(); - let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] - .as_ref() - .unwrap(); - let envelope: &Envelope = &mailbox.collection[&self.local_collection[idx]]; - - let fg_color = if !envelope.is_seen() { - Color::Byte(0) - } else { - Color::Default - }; - let bg_color = if self.cursor_pos.2 == idx { - Color::Byte(246) - } else if !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); - } - - /// 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 make_thread_entry( - envelope: &Envelope, - idx: usize, - indent: usize, - node_idx: usize, - nodes: &Threads, - indentations: &[bool], - idx_width: usize, - //op: Box, - ) -> String { - let has_sibling = nodes.has_sibling(node_idx); - let has_parent = nodes[node_idx].has_parent(); - let show_subject = nodes[node_idx].show_subject(); - - let mut s = format!( - "{}{}{} ", - idx, - " ".repeat(idx_width + 2 - (idx.to_string().chars().count())), - PlainListing::format_date(&envelope) - ); - for i in 0..indent { - if indentations.len() > i && indentations[i] { - s.push('│'); - } else { - s.push(' '); - } - if i > 0 { - s.push(' '); - } - } - if indent > 0 { - if has_sibling && has_parent { - s.push('├'); - } else if has_sibling { - s.push('┬'); - } else { - s.push('└'); - } - s.push('─'); - s.push('>'); - } - - if show_subject { - s.push_str(&format!("{:.85}", envelope.subject())); - } - /* TODO Very slow since we have to build all attachments - let attach_count = envelope.body(op).count_attachments(); - if attach_count > 1 { - s.push_str(&format!(" {}∞ ", attach_count - 1)); - } - */ - s - } - 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(), + match self { + Listing::Compact(l) => write!(f, "{}", l), + Listing::Plain(l) => write!(f, "{}", l), + Listing::Threaded(l) => write!(f, "{}", l), } } } -impl Component for PlainListing { +impl Default for Listing { + fn default() -> Self { + Listing::Compact(Default::default()) + } +} + +impl Component for Listing { 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 { - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - if self.length == 0 && self.dirty { - clear_area(grid, area); - context.dirty_areas.push_back(area); - } - - /* Render the mail body in a pager, basically copy what HSplit does */ - let total_rows = get_y(bottom_right) - get_y(upper_left); - let pager_ratio = context.runtime_settings.pager.pager_ratio; - let bottom_entity_rows = (pager_ratio * total_rows) / 100; - - if bottom_entity_rows > total_rows { - clear_area(grid, area); - context.dirty_areas.push_back(area); - return; - } - /* Mark message as read */ - let idx = self.cursor_pos.2; - let must_highlight = { - if self.length == 0 { - false - } else { - let threaded = context.accounts[self.cursor_pos.0] - .runtime_settings - .conf() - .threaded(); - let account = &mut context.accounts[self.cursor_pos.0]; - let (hash, is_seen) = { - let mailbox = &mut account[self.cursor_pos.1].as_mut().unwrap(); - let envelope: &mut Envelope = &mut mailbox - .collection - .entry(self.local_collection[idx]) - .or_default(); - (envelope.hash(), envelope.is_seen()) - }; - if !is_seen { - let op = { - let backend = &account.backend; - backend.operation(hash) - }; - let mailbox = &mut account[self.cursor_pos.1].as_mut().unwrap(); - let envelope: &mut Envelope = &mut mailbox - .collection - .entry(self.local_collection[idx]) - .or_default(); - envelope.set_seen(op).unwrap(); - true - } else { - false - } - } - }; - if must_highlight { - self.highlight_line_self(idx, context); - } - let mid = get_y(upper_left) + total_rows - bottom_entity_rows; - self.draw_list( - grid, - ( - upper_left, - (get_x(bottom_right), get_y(upper_left) + mid - 1), - ), - context, - ); - if self.length == 0 { - self.dirty = false; - return; - } - { - /* TODO: Move the box drawing business in separate functions */ - if get_x(upper_left) > 0 && grid[(get_x(upper_left) - 1, mid)].ch() == VERT_BOUNDARY - { - grid[(get_x(upper_left) - 1, mid)].set_ch(LIGHT_VERTICAL_AND_RIGHT); - } - - for i in get_x(upper_left)..=get_x(bottom_right) { - grid[(i, mid)].set_ch('─'); - } - context - .dirty_areas - .push_back((set_y(upper_left, mid), set_y(bottom_right, mid))); - } - // TODO: Make headers view configurable - - if !self.dirty { - if let Some(v) = self.view.as_mut() { - v.draw(grid, (set_y(upper_left, mid + 1), bottom_right), context); - } - return; - } - { - let threaded = context.accounts[self.cursor_pos.0] - .runtime_settings - .conf() - .threaded(); - let account = &context.accounts[self.cursor_pos.0]; - let mailbox = &account[self.cursor_pos.1].as_ref().unwrap(); - let mut coordinates = self.cursor_pos; - let coordinates = ( - coordinates.0, - coordinates.1, - self.local_collection[self.cursor_pos.2], - ); - self.view = Some(MailView::new(coordinates, None, None)); - } - self.view.as_mut().unwrap().draw( - grid, - (set_y(upper_left, mid + 1), bottom_right), - context, - ); - self.dirty = false; + match self { + Listing::Compact(l) => l.draw(grid, area, context), + Listing::Plain(l) => l.draw(grid, area, context), + Listing::Threaded(l) => l.draw(grid, area, context), } } 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; - } + if match self { + Listing::Plain(l) => l.process_event(event, context), + Listing::Compact(l) => l.process_event(event, context), + Listing::Threaded(l) => l.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 -= 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('m')) if !self.unfocused => { - context.replies.push_back(UIEvent { - id: 0, - event_type: UIEventType::Action(Tab(NewDraft)), - }); - 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::Resize => self.set_dirty(), UIEventType::Action(ref action) => match action { - Action::Listing(ListingAction::ToggleThreaded) => { - context.accounts[self.cursor_pos.0] - .runtime_settings - .conf_mut() - .toggle_threaded(); - self.refresh_mailbox(context); - self.dirty = true; + Action::Listing(ListingAction::SetPlain) => { + if let Listing::Plain(_) = self { + return true; + } + *self = Listing::Plain(PlainListing::default()); return true; } - Action::ViewMailbox(idx) => { - self.new_cursor_pos.1 = *idx; - self.dirty = true; - self.refresh_mailbox(context); + Action::Listing(ListingAction::SetThreaded) => { + if let Listing::Threaded(_) = self { + return true; + } + self.set_dirty(); + *self = Listing::Threaded(ThreadListing::default()); 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); + Action::Listing(ListingAction::SetCompact) => { + if let Listing::Compact(_) = self { + return true; + } + *self = Listing::Compact(CompactListing::default()); return true; } _ => {} @@ -648,12 +102,17 @@ impl Component for PlainListing { false } fn is_dirty(&self) -> bool { - self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false) + match self { + Listing::Compact(l) => l.is_dirty(), + Listing::Plain(l) => l.is_dirty(), + Listing::Threaded(l) => l.is_dirty(), + } } fn set_dirty(&mut self) { - if let Some(p) = self.view.as_mut() { - p.set_dirty(); - }; - self.dirty = true; + match self { + Listing::Compact(l) => l.set_dirty(), + Listing::Plain(l) => l.set_dirty(), + Listing::Threaded(l) => l.set_dirty(), + } } } diff --git a/ui/src/components/mail/listing/plain.rs b/ui/src/components/mail/listing/plain.rs new file mode 100644 index 000000000..42369fc61 --- /dev/null +++ b/ui/src/components/mail/listing/plain.rs @@ -0,0 +1,559 @@ +/* + * 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::*; + +const MAX_COLS: usize = 500; + +/// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the `Envelope` content in a +/// `MailView`. +#[derive(Debug)] +pub struct PlainListing { + /// (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, + local_collection: Vec, + 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 PlainListing { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Display for PlainListing { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "mail") + } +} + +impl PlainListing { + /// Helper function to format entry strings for PlainListing */ + /* TODO: Make this configurable */ + fn make_entry_string(e: &Envelope, idx: usize) -> String { + format!( + "{} {} {}", + idx, + &e.datetime().format("%Y-%m-%d %H:%M:%S").to_string(), + e.subject() + ) + } + + pub fn new() -> Self { + let content = CellBuffer::new(0, 0, Cell::with_char(' ')); + PlainListing { + cursor_pos: (0, 1, 0), + new_cursor_pos: (0, 0, 0), + length: 0, + local_collection: Vec::new(), + 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.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; + } + + // Populate `CellBuffer` with every entry. + let mut idx = 0; + for y in 0..=self.length { + if idx >= self.length { + /* No more entries left, so fill the rest of the area with empty space */ + clear_area(&mut self.content, ((0, y), (MAX_COLS - 1, self.length))); + break; + } + /* Write an entire line for each envelope entry. */ + self.local_collection = mailbox.collection.keys().map(|v| *v).collect(); + let sort = self.sort; + self.local_collection.sort_by(|a, b| match sort { + (SortField::Date, SortOrder::Desc) => { + let ma = &mailbox.collection[a]; + let mb = &mailbox.collection[b]; + mb.date().cmp(&ma.date()) + } + (SortField::Date, SortOrder::Asc) => { + let ma = &mailbox.collection[a]; + let mb = &mailbox.collection[b]; + ma.date().cmp(&mb.date()) + } + (SortField::Subject, SortOrder::Desc) => { + let ma = &mailbox.collection[a]; + let mb = &mailbox.collection[b]; + ma.subject().cmp(&mb.subject()) + } + (SortField::Subject, SortOrder::Asc) => { + let ma = &mailbox.collection[a]; + let mb = &mailbox.collection[b]; + mb.subject().cmp(&ma.subject()) + } + }); + let envelope: &Envelope = &mailbox.collection[&self.local_collection[idx]]; + + let fg_color = if !envelope.is_seen() { + Color::Byte(0) + } else { + Color::Default + }; + let bg_color = if !envelope.is_seen() { + Color::Byte(251) + } else if idx % 2 == 0 { + Color::Byte(236) + } else { + Color::Default + }; + let (x, y) = write_string_to_grid( + &PlainListing::make_entry_string(envelope, idx), + &mut self.content, + fg_color, + bg_color, + ((0, y), (MAX_COLS - 1, y)), + false, + ); + + for x in x..MAX_COLS { + self.content[(x, y)].set_ch(' '); + self.content[(x, y)].set_bg(bg_color); + } + + idx += 1; + } + } + + fn highlight_line_self(&mut self, idx: usize, context: &Context) { + let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] + .as_ref() + .unwrap(); + let envelope: &Envelope = &mailbox.collection[&self.local_collection[idx]]; + + let fg_color = if !envelope.is_seen() { + Color::Byte(0) + } else { + Color::Default + }; + let bg_color = if !envelope.is_seen() { + Color::Byte(251) + } else if idx % 2 == 0 { + Color::Byte(236) + } else { + Color::Default + }; + change_colors( + &mut self.content, + ((0, idx), (MAX_COLS - 1, idx)), + fg_color, + bg_color, + ); + } + + fn highlight_line(&self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) { + let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] + .as_ref() + .unwrap(); + let envelope: &Envelope = &mailbox.collection[&self.local_collection[idx]]; + + let fg_color = if !envelope.is_seen() { + Color::Byte(0) + } else { + Color::Default + }; + let bg_color = if self.cursor_pos.2 == idx { + Color::Byte(246) + } else if !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); + } + + /// 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); + } +} + +impl Component for PlainListing { + 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 { + let upper_left = upper_left!(area); + let bottom_right = bottom_right!(area); + if self.length == 0 && self.dirty { + clear_area(grid, area); + context.dirty_areas.push_back(area); + } + + /* Render the mail body in a pager, basically copy what HSplit does */ + let total_rows = get_y(bottom_right) - get_y(upper_left); + let pager_ratio = context.runtime_settings.pager.pager_ratio; + let bottom_entity_rows = (pager_ratio * total_rows) / 100; + + if bottom_entity_rows > total_rows { + clear_area(grid, area); + context.dirty_areas.push_back(area); + return; + } + /* Mark message as read */ + let idx = self.cursor_pos.2; + let must_highlight = { + if self.length == 0 { + false + } else { + let account = &mut context.accounts[self.cursor_pos.0]; + let (hash, is_seen) = { + let mailbox = &mut account[self.cursor_pos.1].as_mut().unwrap(); + let envelope: &mut Envelope = &mut mailbox + .collection + .entry(self.local_collection[idx]) + .or_default(); + (envelope.hash(), envelope.is_seen()) + }; + if !is_seen { + let op = { + let backend = &account.backend; + backend.operation(hash) + }; + let mailbox = &mut account[self.cursor_pos.1].as_mut().unwrap(); + let envelope: &mut Envelope = &mut mailbox + .collection + .entry(self.local_collection[idx]) + .or_default(); + envelope.set_seen(op).unwrap(); + true + } else { + false + } + } + }; + if must_highlight { + self.highlight_line_self(idx, context); + } + let mid = get_y(upper_left) + total_rows - bottom_entity_rows; + self.draw_list( + grid, + ( + upper_left, + (get_x(bottom_right), get_y(upper_left) + mid - 1), + ), + context, + ); + if self.length == 0 { + self.dirty = false; + return; + } + { + /* TODO: Move the box drawing business in separate functions */ + if get_x(upper_left) > 0 && grid[(get_x(upper_left) - 1, mid)].ch() == VERT_BOUNDARY + { + grid[(get_x(upper_left) - 1, mid)].set_ch(LIGHT_VERTICAL_AND_RIGHT); + } + + for i in get_x(upper_left)..=get_x(bottom_right) { + grid[(i, mid)].set_ch('─'); + } + context + .dirty_areas + .push_back((set_y(upper_left, mid), set_y(bottom_right, mid))); + } + // TODO: Make headers view configurable + + if !self.dirty { + if let Some(v) = self.view.as_mut() { + v.draw(grid, (set_y(upper_left, mid + 1), bottom_right), context); + } + return; + } + { + let coordinates = self.cursor_pos; + let coordinates = ( + coordinates.0, + coordinates.1, + self.local_collection[self.cursor_pos.2], + ); + self.view = Some(MailView::new(coordinates, None, None)); + } + self.view.as_mut().unwrap().draw( + grid, + (set_y(upper_left, mid + 1), bottom_right), + 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 -= 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('m')) if !self.unfocused => { + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::Action(Tab(NewDraft)), + }); + 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; + } +} diff --git a/ui/src/components/mail/listing/thread.rs b/ui/src/components/mail/listing/thread.rs new file mode 100644 index 000000000..228e98cc1 --- /dev/null +++ b/ui/src/components/mail/listing/thread.rs @@ -0,0 +1,630 @@ +/* + * 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::*; + +const MAX_COLS: usize = 500; + +/// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the `Envelope` content in a +/// `MailView`. +#[derive(Debug)] +pub struct ThreadListing { + /// (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 ThreadListing { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Display for ThreadListing { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "mail") + } +} + +impl ThreadListing { + pub fn new() -> Self { + let content = CellBuffer::new(0, 0, Cell::with_char(' ')); + ThreadListing { + 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.collection.threads.root_set().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 mut indentations: Vec = Vec::with_capacity(6); + let mut thread_idx = 0; // needed for alternate thread colors + /* Draw threaded view. */ + let threads = &mailbox.collection.threads; + threads.sort_by(self.sort, self.subsort, &mailbox.collection); + let thread_nodes: &Vec = &threads.thread_nodes(); + let mut iter = threads.root_set().into_iter().peekable(); + let len = threads.root_set().len().to_string().chars().count(); + /* This is just a desugared for loop so that we can use .peek() */ + let mut idx = 0; + while let Some(i) = iter.next() { + let thread_node = &thread_nodes[*i]; + + if !thread_node.has_message() { + continue; + } + + let indentation = thread_node.indentation(); + + if indentation == 0 { + thread_idx += 1; + } + + match iter.peek() { + Some(&x) if thread_nodes[*x].indentation() == indentation => { + indentations.pop(); + indentations.push(true); + } + _ => { + indentations.pop(); + indentations.push(false); + } + } + if threads.has_sibling(*i) { + indentations.pop(); + indentations.push(true); + } + let envelope: &Envelope = &mailbox.collection[&thread_node.message().unwrap()]; + let fg_color = if !envelope.is_seen() { + Color::Byte(0) + } else { + Color::Default + }; + let bg_color = if !envelope.is_seen() { + Color::Byte(251) + } else if thread_idx % 2 == 0 { + Color::Byte(236) + } else { + Color::Default + }; + let (x, _) = write_string_to_grid( + &ThreadListing::make_thread_entry( + envelope, + idx, + indentation, + *i, + threads, + &indentations, + len, + // context.accounts[self.cursor_pos.0].backend.operation(envelope.hash()) + ), + &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); + } + + match iter.peek() { + Some(&x) if thread_nodes[*x].indentation() > indentation => { + indentations.push(false); + } + Some(&x) if thread_nodes[*x].indentation() < indentation => { + for _ in 0..(indentation - thread_nodes[*x].indentation()) { + indentations.pop(); + } + } + _ => {} + } + idx += 1; + } + } + + fn highlight_line_self(&mut self, idx: usize, context: &Context) { + let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] + .as_ref() + .unwrap(); + let envelope: &Envelope = mailbox.thread_to_mail(idx); + + let fg_color = if !envelope.is_seen() { + Color::Byte(0) + } else { + Color::Default + }; + let bg_color = if !envelope.is_seen() { + Color::Byte(251) + } else if idx % 2 == 0 { + Color::Byte(236) + } else { + Color::Default + }; + change_colors( + &mut self.content, + ((0, idx), (MAX_COLS - 1, idx)), + fg_color, + bg_color, + ); + } + + fn highlight_line(&self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) { + let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] + .as_ref() + .unwrap(); + let envelope: &Envelope = mailbox.thread_to_mail(idx); + + let fg_color = if !envelope.is_seen() { + Color::Byte(0) + } else { + Color::Default + }; + let bg_color = if self.cursor_pos.2 == idx { + Color::Byte(246) + } else if !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); + } + + /// 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 make_thread_entry( + envelope: &Envelope, + idx: usize, + indent: usize, + node_idx: usize, + threads: &Threads, + indentations: &[bool], + idx_width: usize, + //op: Box, + ) -> String { + let has_sibling = threads.has_sibling(node_idx); + let thread_node = &threads[node_idx]; + let has_parent = thread_node.has_parent(); + let show_subject = thread_node.show_subject(); + + let mut s = format!( + "{}{}{} ", + idx, + " ".repeat(idx_width + 2 - (idx.to_string().chars().count())), + ThreadListing::format_date(&envelope) + ); + for i in 0..indent { + if indentations.len() > i && indentations[i] { + s.push('│'); + } else { + s.push(' '); + } + if i > 0 { + s.push(' '); + } + } + if indent > 0 { + if has_sibling && has_parent { + s.push('├'); + } else if has_sibling { + s.push('┬'); + } else { + s.push('└'); + } + s.push('─'); + s.push('>'); + } + + if show_subject { + s.push_str(&format!("{:.85}", envelope.subject())); + } + /* TODO Very slow since we have to build all attachments + let attach_count = envelope.body(op).count_attachments(); + if attach_count > 1 { + s.push_str(&format!(" {}∞ ", attach_count - 1)); + } + */ + s + } + 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 ThreadListing { + 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 { + let upper_left = upper_left!(area); + let bottom_right = bottom_right!(area); + if self.length == 0 && self.dirty { + clear_area(grid, area); + context.dirty_areas.push_back(area); + } + + /* Render the mail body in a pager, basically copy what HSplit does */ + let total_rows = get_y(bottom_right) - get_y(upper_left); + let pager_ratio = context.runtime_settings.pager.pager_ratio; + let bottom_entity_rows = (pager_ratio * total_rows) / 100; + + if bottom_entity_rows > total_rows { + clear_area(grid, area); + context.dirty_areas.push_back(area); + return; + } + /* Mark message as read */ + let idx = self.cursor_pos.2; + let must_highlight = { + if self.length == 0 { + false + } else { + let account = &mut context.accounts[self.cursor_pos.0]; + let (hash, is_seen) = { + let mailbox = &mut account[self.cursor_pos.1].as_mut().unwrap(); + let envelope: &mut Envelope = mailbox.thread_to_mail_mut(idx); + (envelope.hash(), envelope.is_seen()) + }; + if !is_seen { + let op = { + let backend = &account.backend; + backend.operation(hash) + }; + let mailbox = &mut account[self.cursor_pos.1].as_mut().unwrap(); + let envelope: &mut Envelope = mailbox.thread_to_mail_mut(idx); + envelope.set_seen(op).unwrap(); + true + } else { + false + } + } + }; + if must_highlight { + self.highlight_line_self(idx, context); + } + let mid = get_y(upper_left) + total_rows - bottom_entity_rows; + self.draw_list( + grid, + ( + upper_left, + (get_x(bottom_right), get_y(upper_left) + mid - 1), + ), + context, + ); + if self.length == 0 { + self.dirty = false; + return; + } + { + /* TODO: Move the box drawing business in separate functions */ + if get_x(upper_left) > 0 && grid[(get_x(upper_left) - 1, mid)].ch() == VERT_BOUNDARY + { + grid[(get_x(upper_left) - 1, mid)].set_ch(LIGHT_VERTICAL_AND_RIGHT); + } + + for i in get_x(upper_left)..=get_x(bottom_right) { + grid[(i, mid)].set_ch(HORZ_BOUNDARY); + } + context + .dirty_areas + .push_back((set_y(upper_left, mid), set_y(bottom_right, mid))); + } + // TODO: Make headers view configurable + + if !self.dirty { + if let Some(v) = self.view.as_mut() { + v.draw(grid, (set_y(upper_left, mid + 1), bottom_right), context); + } + return; + } + { + let account = &context.accounts[self.cursor_pos.0]; + let mailbox = &account[self.cursor_pos.1].as_ref().unwrap(); + let coordinates = ( + self.cursor_pos.0, + self.cursor_pos.1, + mailbox.threaded_mail(self.cursor_pos.2), + ); + self.view = Some(MailView::new(coordinates, None, None)); + } + self.view.as_mut().unwrap().draw( + grid, + (set_y(upper_left, mid + 1), bottom_right), + 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 -= 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('m')) if !self.unfocused => { + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::Action(Tab(NewDraft)), + }); + 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; + } +} diff --git a/ui/src/execute/actions.rs b/ui/src/execute/actions.rs index e76b00f7c..bef27f3d2 100644 --- a/ui/src/execute/actions.rs +++ b/ui/src/execute/actions.rs @@ -30,7 +30,9 @@ use uuid::Uuid; #[derive(Debug, Clone)] pub enum ListingAction { - ToggleThreaded, + SetPlain, + SetThreaded, + SetCompact, } #[derive(Debug, Clone)] diff --git a/ui/src/execute/mod.rs b/ui/src/execute/mod.rs index 58f3d4592..c2d7da2f3 100644 --- a/ui/src/execute/mod.rs +++ b/ui/src/execute/mod.rs @@ -72,11 +72,22 @@ named!( named!( threaded, - map!(ws!(tag!("threaded")), |_| Listing(ToggleThreaded)) + map!(ws!(tag!("threaded")), |_| Listing(SetThreaded)) ); + +named!( + plain, + map!(ws!(tag!("plain")), |_| Listing(SetPlain)) +); + +named!( + compact, + map!(ws!(tag!("compact")), |_| Listing(SetCompact)) +); + named!( toggle, - preceded!(tag!("toggle "), alt_complete!(threaded)) + preceded!(tag!("set "), alt_complete!(threaded | plain | compact)) ); named!(pub parse_command, diff --git a/ui/src/state.rs b/ui/src/state.rs index 1098925dd..ef2f655ac 100644 --- a/ui/src/state.rs +++ b/ui/src/state.rs @@ -505,18 +505,6 @@ impl State { } return; } - UIEventType::Input(Key::Char('t')) => for i in 0..self.entities.len() { - self.entities[i].rcv_event( - &UIEvent { - id: 0, - event_type: UIEventType::Action(Action::Listing( - ListingAction::ToggleThreaded, - )), - }, - &mut self.context, - ); - }, - _ => {} } /* inform each entity */