From b2c74309076fcb5fd24cea24e8180be326a1bc48 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Thu, 16 Aug 2018 16:32:47 +0300 Subject: [PATCH] Add compact view listing, and compose tab pager concerns #3 --- melib/src/mailbox/thread.rs | 103 ++++-- src/bin.rs | 46 +-- ui/src/components/mail/compose.rs | 107 +++++- ui/src/components/mail/listing/compact.rs | 418 +++++----------------- ui/src/components/mail/listing/mod.rs | 24 +- ui/src/components/mail/view/thread.rs | 153 +++++--- ui/src/components/utilities.rs | 19 + ui/src/state.rs | 77 +++- ui/src/types/helpers.rs | 10 +- ui/src/types/position.rs | 18 + 10 files changed, 480 insertions(+), 495 deletions(-) diff --git a/melib/src/mailbox/thread.rs b/melib/src/mailbox/thread.rs index e4fb6eddb..8ac29650c 100644 --- a/melib/src/mailbox/thread.rs +++ b/melib/src/mailbox/thread.rs @@ -107,6 +107,8 @@ pub struct Container { struct ContainerTree { id: usize, children: Option>, + len: usize, + has_unseen: bool, } impl ContainerTree { @@ -114,6 +116,8 @@ impl ContainerTree { ContainerTree { id, children: None, + len: 1, + has_unseen: false, } } } @@ -173,8 +177,33 @@ impl<'a> IntoIterator for &'a Threads { } +pub struct RootIterator<'a> { + pos: usize, + tree: Ref<'a ,Vec>, +} + +impl<'a> Iterator for RootIterator<'a> { + type Item = (usize, usize, bool); + fn next(&mut self) -> Option<(usize, usize, bool)> { + if self.pos == self.tree.len() { + return None; + } + let node = &self.tree[self.pos]; + self.pos += 1; + return Some((node.id, node.len, node.has_unseen)); + } +} impl Threads { + pub fn root_len(&self) -> usize { + self.tree.borrow().len() + } + pub fn root_set(&self) -> &Vec { + &self.root_set + } + pub fn root_set_iter(&self) -> RootIterator { + RootIterator { pos: 0, tree: self.tree.borrow() } + } pub fn thread_to_mail(&self, i: usize) -> usize { let thread = self.containers[self.threaded_collection[i]]; thread.message().unwrap() @@ -226,49 +255,49 @@ impl Threads { } } }); - } } + } } fn inner_sort_by(&self, sort: (SortField, SortOrder), collection: &[Envelope]) { let tree = &mut self.tree.borrow_mut(); let containers = &self.containers; - tree.sort_by(|a, b| { match sort { - (SortField::Date, SortOrder::Desc) => { - let a = containers[a.id]; - let b = containers[b.id]; - b.date.cmp(&a.date) + tree.sort_by(|a, b| { match sort { + (SortField::Date, SortOrder::Desc) => { + let a = containers[a.id]; + let b = containers[b.id]; + b.date.cmp(&a.date) - } - (SortField::Date, SortOrder::Asc) => { - let a = containers[a.id]; - let b = containers[b.id]; - a.date.cmp(&b.date) - } - (SortField::Subject, SortOrder::Desc) => { - let a = containers[a.id].message(); - let b = containers[b.id].message(); - - if a.is_none() || b.is_none() { - return Ordering::Equal; - } - let ma = &collection[a.unwrap()]; - let mb = &collection[b.unwrap()]; - ma.subject().cmp(&mb.subject()) - } - (SortField::Subject, SortOrder::Asc) => { - let a = containers[a.id].message(); - let b = containers[b.id].message(); - - if a.is_none() || b.is_none() { - return Ordering::Equal; - } - let ma = &collection[a.unwrap()]; - let mb = &collection[b.unwrap()]; - mb.subject().cmp(&ma.subject()) - } } - }); + (SortField::Date, SortOrder::Asc) => { + let a = containers[a.id]; + let b = containers[b.id]; + a.date.cmp(&b.date) + } + (SortField::Subject, SortOrder::Desc) => { + let a = containers[a.id].message(); + let b = containers[b.id].message(); + + if a.is_none() || b.is_none() { + return Ordering::Equal; + } + let ma = &collection[a.unwrap()]; + let mb = &collection[b.unwrap()]; + ma.subject().cmp(&mb.subject()) + } + (SortField::Subject, SortOrder::Asc) => { + let a = containers[a.id].message(); + let b = containers[b.id].message(); + + if a.is_none() || b.is_none() { + return Ordering::Equal; + } + let ma = &collection[a.unwrap()]; + let mb = &collection[b.unwrap()]; + mb.subject().cmp(&ma.subject()) + } + } + }); } pub fn sort_by(&self, sort: (SortField, SortOrder), subsort: (SortField, SortOrder), collection: &[Envelope]) { if *self.sort.borrow() != sort { @@ -292,7 +321,6 @@ impl Threads { root_subject_idx: usize, collection: &[Envelope], ) { - tree.id = i; let thread = containers[i]; if let Some(msg_idx) = containers[root_subject_idx].message() { let root_subject = collection[msg_idx].subject(); @@ -301,6 +329,7 @@ impl Threads { * list.) */ if indentation > 0 && thread.has_message() { let subject = collection[thread.message().unwrap()].subject(); + tree.has_unseen = !collection[thread.message().unwrap()].is_seen(); if subject == root_subject || subject.starts_with("Re: ") && subject.as_ref().ends_with(root_subject.as_ref()) @@ -332,6 +361,7 @@ impl Threads { loop { let mut new_child_tree = ContainerTree::new(fc); build_threaded(&mut new_child_tree, containers, indentation, threaded, fc, i, collection); + tree.has_unseen |= new_child_tree.has_unseen; child_vec.push(new_child_tree); let thread_ = containers[fc]; if !thread_.has_sibling() { @@ -339,6 +369,7 @@ impl Threads { } fc = thread_.next_sibling().unwrap(); } + tree.len = child_vec.iter().map(|c| c.len).sum(); tree.children = Some(child_vec); } } diff --git a/src/bin.rs b/src/bin.rs index 52676c2d4..c41787ace 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -35,7 +35,6 @@ extern crate ui; pub use melib::*; pub use ui::*; -use std::thread; #[macro_use] extern crate chan; @@ -45,28 +44,6 @@ use chan_signal::Signal; extern crate nix; -fn make_input_thread( - sx: chan::Sender, - rx: chan::Receiver, -) -> thread::JoinHandle<()> { - let stdin = std::io::stdin(); - thread::Builder::new() - .name("input-thread".to_string()) - .spawn(move || { - get_events( - stdin, - |k| { - sx.send(ThreadEvent::Input(k)); - }, - || { - sx.send(ThreadEvent::UIEvent(UIEventType::ChangeMode(UIMode::Fork))); - }, - &rx, - ) - }) - .unwrap() -} - fn main() { /* Lock all stdio outs */ //let _stdout = stdout(); @@ -79,31 +56,22 @@ fn main() { /* Catch SIGWINCH to handle terminal resizing */ let signal = chan_signal::notify(&[Signal::WINCH]); - /* Create a channel to communicate with other threads. The main process is the sole receiver. - * */ - let (sender, receiver) = chan::sync(::std::mem::size_of::()); - - /* - * Create async channel to block the input-thread if we need to fork and stop it from reading - * stdin, see get_events() for details - * */ - let (tx, rx) = chan::async(); - /* Get input thread handle to join it if we need to */ - let mut _thread_handler = make_input_thread(sender.clone(), rx.clone()); /* Create the application State. This is the 'System' part of an ECS architecture */ - let mut state = State::new(sender.clone(), tx); + let mut state = State::new(); + + let receiver = state.receiver(); /* Register some reasonably useful interfaces */ let menu = Entity { component: Box::new(AccountMenu::new(&state.context.accounts)), }; - let listing = MailListing::new(); + let listing = CompactListing::new(); let b = Entity { component: Box::new(listing), }; let mut tabs = Box::new(Tabbed::new(vec![Box::new(VSplit::new(menu, b, 90, true))])); - tabs.add_component(Box::new(Composer {})); + tabs.add_component(Box::new(Composer::default())); let window = Entity { component: tabs }; let status_bar = Entity { @@ -137,7 +105,7 @@ fn main() { let self_pid = nix::unistd::Pid::this(); nix::sys::signal::kill(self_pid, nix::sys::signal::Signal::SIGSTOP).unwrap(); state.switch_to_alternate_screen(); - _thread_handler = make_input_thread(sender.clone(), rx.clone()); + state.restore_input(); // BUG: thread sends input event after one received key state.update_size(); state.render(); @@ -237,7 +205,7 @@ fn main() { 'reap: loop { match state.try_wait_on_child() { Some(true) => { - make_input_thread(sender.clone(), rx.clone()); + state.restore_input(); state.mode = UIMode::Normal; state.render(); } diff --git a/ui/src/components/mail/compose.rs b/ui/src/components/mail/compose.rs index 6e55a4376..296d3daf7 100644 --- a/ui/src/components/mail/compose.rs +++ b/ui/src/components/mail/compose.rs @@ -21,7 +21,26 @@ use super::*; -pub struct Composer {} +pub struct Composer { + dirty: bool, + mode: ViewMode, + pager: Pager, +} + +impl Default for Composer { + fn default() -> Self { + Composer { + dirty: true, + mode: ViewMode::Overview, + pager: Pager::from_str("asdfs\nfdsfds\ndsfdsfs\n\n\n\naaaaaaaaaaaaaa\nfdgfd", None), + } + } +} + +enum ViewMode { + //Compose, + Overview, +} impl fmt::Display for Composer { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -32,14 +51,90 @@ impl fmt::Display for Composer { impl Component for Composer { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - clear_area(grid, area); - context.dirty_areas.push_back(area); + if self.dirty { + clear_area(grid, area); + } + let upper_left = upper_left!(area); + let bottom_right = bottom_right!(area); + + let header_height = 12; + let width = width!(area); + let mid = if width > 80 { + let width = width - 80; + let mid = width / 2;; + + if self.dirty { + 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); + grid[(mid + 80, i)].set_ch(VERT_BOUNDARY); + grid[(mid + 80, i)].set_fg(Color::Default); + grid[(mid + 80, i)].set_bg(Color::Default); + } + } + mid + } else { 0 }; + + if self.dirty { + for i in get_x(upper_left)+ mid + 1..=get_x(upper_left) + mid + 79 { + grid[(i, header_height)].set_ch(HORZ_BOUNDARY); + grid[(i, header_height)].set_fg(Color::Default); + grid[(i, header_height)].set_bg(Color::Default); + } + } + + let body_area = ((mid + 1, header_height+2), (mid + 78, get_y(bottom_right))); + + if self.dirty { + context.dirty_areas.push_back(area); + self.dirty = false; + } + match self.mode { + ViewMode::Overview => { + self.pager.draw(grid, body_area, context); + + }, + } } - fn process_event(&mut self, _event: &UIEvent, _context: &mut Context) {} + fn process_event(&mut self, event: &UIEvent, context: &mut Context) { + match event.event_type { + UIEventType::Resize => { + self.dirty = true; + } + UIEventType::Input(Key::Char('\n')) => { + use std::process::{Command, Stdio}; + /* Kill input thread so that spawned command can be sole receiver of stdin */ + { + context.input_kill(); + } + let mut f = create_temp_file(&new_draft(context), None); + //let mut f = Box::new(std::fs::File::create(&dir).unwrap()); + + // TODO: check exit status + Command::new("vim") + .arg("+/^$") + .arg(&f.path()) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .output() + .expect("failed to execute process"); + self.pager.update_from_string(f.read_to_string()); + context.restore_input(); + self.dirty = true; + return; + }, + _ => {}, + } + self.pager.process_event(event, context); + } fn is_dirty(&self) -> bool { - true + self.dirty || self.pager.is_dirty() + } + fn set_dirty(&mut self) { + self.dirty = true; + self.pager.set_dirty(); } - fn set_dirty(&mut self) {} } diff --git a/ui/src/components/mail/listing/compact.rs b/ui/src/components/mail/listing/compact.rs index 13ec64faa..ec91aca80 100644 --- a/ui/src/components/mail/listing/compact.rs +++ b/ui/src/components/mail/listing/compact.rs @@ -20,18 +20,20 @@ */ use super::*; -use melib::mailbox::backends::BackendOp; + +//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 thread's content in a +/// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the `Envelope` content in a /// `ThreadView`. -pub struct CompactMailListing { +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), + subsort: (SortField, SortOrder), /// Cache current view. content: CellBuffer, /// If we must redraw on next redraw event @@ -41,27 +43,48 @@ pub struct CompactMailListing { view: Option, } -impl Default for CompactMailListing { +impl Default for CompactListing { fn default() -> Self { Self::new() } } -impl fmt::Display for CompactMailListing { +impl fmt::Display for CompactListing { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "mail") } } -impl CompactMailListing { +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(' ')); - CompactMailListing { + CompactListing { cursor_pos: (0, 1, 0), new_cursor_pos: (0, 0, 0), length: 0, - sort: (SortField::Date, SortOrder::Desc), - //subsort: (SortField::Date, SortOrder::Asc), + sort: (Default::default(), Default::default()), + subsort: (Default::default(), Default::default()), content: content, dirty: true, unfocused: false, @@ -94,187 +117,86 @@ impl CompactMailListing { .as_ref() .unwrap(); - self.length = mailbox.threads.containers().len(); - let mut content = CellBuffer::new(MAX_COLS, self.length + 1, Cell::with_char(' ')); + + 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 content, + &mut self.content, Color::Default, Color::Default, ((0, 0), (MAX_COLS - 1, 0)), true, ); - self.content = content; return; } - - // TODO: Fix the threaded hell and refactor stuff into seperate functions and/or modules. - let mut indentations: Vec = Vec::with_capacity(6); - let mut thread_idx = 0; // needed for alternate thread colors - /* Draw threaded view. */ - let mut local_collection: Vec = mailbox.threads.threaded_collection().clone(); - let threads: &Vec = &mailbox.threads.containers(); - local_collection.sort_by(|a, b| match self.sort { - (SortField::Date, SortOrder::Desc) => { - mailbox.thread(*b).date().cmp(&mailbox.thread(*a).date()) - } - (SortField::Date, SortOrder::Asc) => { - mailbox.thread(*a).date().cmp(&mailbox.thread(*b).date()) - } - (SortField::Subject, SortOrder::Desc) => { - let a = mailbox.thread(*a); - let b = mailbox.thread(*b); - let ma = &mailbox.collection[*a.message().as_ref().unwrap()]; - let mb = &mailbox.collection[*b.message().as_ref().unwrap()]; - ma.subject().cmp(&mb.subject()) - } - (SortField::Subject, SortOrder::Asc) => { - let a = mailbox.thread(*a); - let b = mailbox.thread(*b); - let ma = &mailbox.collection[*a.message().as_ref().unwrap()]; - let mb = &mailbox.collection[*b.message().as_ref().unwrap()]; - mb.subject().cmp(&ma.subject()) - } - }); - let mut iter = local_collection.iter().enumerate().peekable(); - let len = mailbox - .threads - .threaded_collection() - .len() - .to_string() - .chars() - .count(); - /* This is just a desugared for loop so that we can use .peek() */ - while let Some((idx, i)) = iter.next() { - let container = &threads[*i]; - let indentation = container.indentation(); - - if indentation == 0 { - thread_idx += 1; - } - - assert!(container.has_message()); - match iter.peek() { - Some(&(_, x)) if threads[*x].indentation() == indentation => { - indentations.pop(); - indentations.push(true); - } - _ => { - indentations.pop(); - indentations.push(false); - } - } - if container.has_sibling() { - indentations.pop(); - indentations.push(true); - } - let envelope: &Envelope = &mailbox.collection[container.message().unwrap()]; - let fg_color = if !envelope.is_seen() { + 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 !envelope.is_seen() { + let bg_color = if has_unseen { Color::Byte(251) - } else if thread_idx % 2 == 0 { + } else if idx % 2 == 0 { Color::Byte(236) } else { Color::Default }; let (x, _) = write_string_to_grid( - &CompactMailListing::make_thread_entry( - envelope, - idx, - indentation, - container, - &indentations, - len, - context.accounts[self.cursor_pos.0].backend.operation(envelope.hash()), - ), - &mut content, + &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 { - content[(x, idx)].set_ch(' '); - content[(x, idx)].set_bg(bg_color); + self.content[(x, idx)].set_ch(' '); + self.content[(x, idx)].set_bg(bg_color); } - match iter.peek() { - Some(&(_, x)) if threads[*x].indentation() > indentation => { - indentations.push(false); - } - Some(&(_, x)) if threads[*x].indentation() < indentation => { - for _ in 0..(indentation - threads[*x].indentation()) { - indentations.pop(); - } - } - _ => {} - } + } - self.content = content; - } - - fn highlight_line_self(&mut self, idx: usize, context: &Context) { - let threaded = context.accounts[self.cursor_pos.0] - .runtime_settings - .threaded; - let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] - .as_ref() - .unwrap(); - let envelope: &Envelope = if threaded { - let i = mailbox.threaded_mail(idx); - &mailbox.collection[i] - } else { - &mailbox.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 - .threaded; let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] .as_ref() .unwrap(); - let envelope: &Envelope = if threaded { - let i = mailbox.threaded_mail(idx); - &mailbox.collection[i] + 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 { - &mailbox.collection[idx] + threads.containers()[ + container.first_child().unwrap() + ].message().unwrap() }; - - let fg_color = if !envelope.is_seen() { + 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 !envelope.is_seen() { + } else if !root_envelope.is_seen() { Color::Byte(251) } else if idx % 2 == 0 { Color::Byte(236) @@ -343,56 +265,6 @@ impl CompactMailListing { context.dirty_areas.push_back(area); } - fn make_thread_entry( - envelope: &Envelope, - idx: usize, - indent: usize, - container: &Container, - indentations: &[bool], - idx_width: usize, - op: Box, - ) -> String { - let has_sibling = container.has_sibling(); - let has_parent = container.has_parent(); - let show_subject = container.show_subject(); - - let mut s = format!( - "{}{}{} ", - idx, - " ".repeat(idx_width + 2 - (idx.to_string().chars().count())), - CompactMailListing::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())); - } - 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(); @@ -407,7 +279,7 @@ impl CompactMailListing { } } -impl Component for CompactMailListing { +impl Component for CompactListing { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { if !self.unfocused { if !self.is_dirty() { @@ -434,88 +306,14 @@ impl Component for CompactMailListing { 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 - .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 = if threaded { - let i = mailbox.threaded_mail(idx); - &mut mailbox.collection[i] - } else { - &mut mailbox.collection[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 = if threaded { - let i = mailbox.threaded_mail(idx); - &mut mailbox.collection[i] - } else { - &mut mailbox.collection[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('─'); - } - 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; } - self.view = Some(ThreadView::new(Vec::new())); + self.view = Some(ThreadView::new(self.cursor_pos)); self.view.as_mut().unwrap().draw( grid, (set_y(upper_left, mid + 1), bottom_right), @@ -542,53 +340,6 @@ impl Component for CompactMailListing { self.unfocused = true; self.dirty = true; } - UIEventType::Input(Key::Char('m')) if !self.unfocused => { - use std::process::{Command, Stdio}; - /* Kill input thread so that spawned command can be sole receiver of stdin */ - { - /* I tried thread::park() here but for some reason it never blocked and always - * returned. Spinlocks are also useless because you have to keep the mutex - * guard alive til the child process exits, which requires some effort. - * - * The only problem with this approach is tht the user has to send some input - * in order for the input-thread to wake up and realise it should kill itself. - * - * I tried writing to stdin/tty manually but for some reason rustty didn't - * acknowledge it. - */ - - /* - * tx sends to input-thread and it kills itself. - */ - let tx = context.input_thread(); - tx.send(true); - } - let mut f = create_temp_file(&new_draft(context), None); - //let mut f = Box::new(std::fs::File::create(&dir).unwrap()); - - // TODO: check exit status - let mut output = Command::new("vim") - .arg("+/^$") - .arg(&f.path()) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .spawn() - .expect("failed to execute process"); - - /* - * Main loop will wait on children and when they reap them the loop spawns a new - * input-thread - */ - context.replies.push_back(UIEvent { - id: 0, - event_type: UIEventType::Fork(ForkType::NewDraft(f, output)), - }); - context.replies.push_back(UIEvent { - id: 0, - event_type: UIEventType::ChangeMode(UIMode::Fork), - }); - return; - } UIEventType::Input(Key::Char('i')) if self.unfocused => { self.unfocused = false; self.dirty = true; @@ -649,9 +400,10 @@ impl Component for CompactMailListing { self.view = None; } UIEventType::MailboxUpdate((ref idxa, ref idxf)) => { - if *idxa == self.new_cursor_pos.1 && *idxf == self.new_cursor_pos.0 { - self.refresh_mailbox(context); + if *idxa == self.new_cursor_pos.0 && *idxf == self.new_cursor_pos.1 { self.dirty = true; + self.refresh_mailbox(context); + return; } } UIEventType::ChangeMode(UIMode::Normal) => { @@ -677,24 +429,28 @@ impl Component for CompactMailListing { self.refresh_mailbox(context); return; } - Action::Sort(field, order) => { - self.sort = (field.clone(), order.clone()); + Action::SubSort(field, order) => { + eprintln!("SubSort {:?} , {:?}", field, order); + self.subsort = (*field, *order); self.dirty = true; self.refresh_mailbox(context); return; } - _ => {} + Action::Sort(field, order) => { + eprintln!("Sort {:?} , {:?}", field, order); + self.sort = (*field, *order); + self.dirty = true; + self.refresh_mailbox(context); + return; + } + // _ => {} }, _ => {} } - if let Some(ref mut v) = self.view { - v.process_event(event, context); - } } fn is_dirty(&self) -> bool { - self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false) + true } fn set_dirty(&mut self) { - self.dirty = true; } } diff --git a/ui/src/components/mail/listing/mod.rs b/ui/src/components/mail/listing/mod.rs index d82fe1a8d..5ad511e02 100644 --- a/ui/src/components/mail/listing/mod.rs +++ b/ui/src/components/mail/listing/mod.rs @@ -119,17 +119,16 @@ impl MailListing { } else { mailbox.len() }; - let mut content = CellBuffer::new(MAX_COLS, self.length + 1, Cell::with_char(' ')); + 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 content, + &mut self.content, Color::Default, Color::Default, ((0, 0), (MAX_COLS - 1, 0)), true, ); - self.content = content; return; } @@ -200,15 +199,15 @@ impl MailListing { len, // context.accounts[self.cursor_pos.0].backend.operation(envelope.hash()) ), - &mut content, + &mut self.content, fg_color, bg_color, ((0, idx), (MAX_COLS - 1, idx)), false, ); for x in x..MAX_COLS { - content[(x, idx)].set_ch(' '); - content[(x, idx)].set_bg(bg_color); + self.content[(x, idx)].set_ch(' '); + self.content[(x, idx)].set_bg(bg_color); } match iter.peek() { @@ -230,7 +229,7 @@ impl MailListing { 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 content, ((0, y), (MAX_COLS - 1, self.length))); + clear_area(&mut self.content, ((0, y), (MAX_COLS - 1, self.length))); break; } /* Write an entire line for each envelope entry. */ @@ -274,7 +273,7 @@ impl MailListing { }; let (x, y) = write_string_to_grid( &MailListing::make_entry_string(envelope, idx), - &mut content, + &mut self.content, fg_color, bg_color, ((0, y), (MAX_COLS - 1, y)), @@ -282,15 +281,13 @@ impl MailListing { ); for x in x..MAX_COLS { - content[(x, y)].set_ch(' '); - content[(x, y)].set_bg(bg_color); + self.content[(x, y)].set_ch(' '); + self.content[(x, y)].set_bg(bg_color); } idx += 1; } } - - self.content = content; } fn highlight_line_self(&mut self, idx: usize, context: &Context) { @@ -636,8 +633,7 @@ impl Component for MailListing { /* * tx sends to input-thread and it kills itself. */ - let tx = context.input_thread(); - tx.send(true); + context.input_kill(); } let mut f = create_temp_file(&new_draft(context), None); //let mut f = Box::new(std::fs::File::create(&dir).unwrap()); diff --git a/ui/src/components/mail/view/thread.rs b/ui/src/components/mail/view/thread.rs index 065e22c6f..4c0c96a4d 100644 --- a/ui/src/components/mail/view/thread.rs +++ b/ui/src/components/mail/view/thread.rs @@ -20,37 +20,20 @@ */ use super::*; -use std::io::Write; -use std::process::{Command, Stdio}; pub struct ThreadView { - pager: Pager, - bytes: Vec, + dirty: bool, + coordinates: (usize, usize, usize), + } impl ThreadView { - pub fn new(bytes: Vec) -> Self { - let mut html_filter = Command::new("w3m") - .args(&["-I", "utf-8", "-T", "text/html"]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("Failed to start html filter process"); - html_filter - .stdin - .as_mut() - .unwrap() - .write_all(&bytes) - .expect("Failed to write to w3m stdin"); - let mut display_text = - String::from("Text piped through `w3m`. Press `v` to open in web browser. \n\n"); - display_text.push_str(&String::from_utf8_lossy( - &html_filter.wait_with_output().unwrap().stdout, - )); - - let buf = MailView::plain_text_to_buf(&display_text, true); - let pager = Pager::from_buf(&buf, None); - ThreadView { pager, bytes } + pub fn new(coordinates: (usize, usize, usize), + ) -> Self { + ThreadView { + dirty: true, + coordinates, + } } } @@ -63,39 +46,95 @@ impl fmt::Display for ThreadView { impl Component for ThreadView { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - self.pager.draw(grid, area, context); - } - fn process_event(&mut self, event: &UIEvent, context: &mut Context) { - match event.event_type { - UIEventType::Input(Key::Char('v')) => { - // TODO: Optional filter that removes outgoing resource requests (images and - // scripts) - let binary = query_default_app("text/html"); - if let Ok(binary) = binary { - let mut p = create_temp_file(&self.bytes, None); - Command::new(&binary) - .arg(p.path()) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .unwrap_or_else(|_| panic!("Failed to start {}", binary.display())); - context.temp_files.push(p); - } else { - context.replies.push_back(UIEvent { - id: 0, - event_type: UIEventType::StatusNotification(format!( - "Couldn't find a default application for html files." - )), - }); - } - return; - } - _ => {} + let upper_left = upper_left!(area); + let bottom_right = bottom_right!(area); + let mailbox = &mut context.accounts[self.coordinates.0][self.coordinates.1].as_ref().unwrap(); + let threads = &mailbox.threads; + let container = &threads.containers()[threads.root_set()[self.coordinates.2]]; + let i = if let Some(i) = container.message() { + i + } else { + threads.containers()[ + container.first_child().unwrap() + ].message().unwrap() + }; + let envelope: &Envelope = &mailbox.collection[i]; + let (x, y) = write_string_to_grid( + &format!("Date: {}", envelope.date_as_str()), + grid, + Color::Byte(33), + Color::Default, + area, + true, + ); + for x in x..=get_x(bottom_right) { + grid[(x, y)].set_ch(' '); + grid[(x, y)].set_bg(Color::Default); + grid[(x, y)].set_fg(Color::Default); } - self.pager.process_event(event, context); + let (x, y) = write_string_to_grid( + &format!("From: {}", envelope.from_to_string()), + grid, + Color::Byte(33), + Color::Default, + (set_y(upper_left, y + 1), bottom_right), + true, + ); + for x in x..=get_x(bottom_right) { + grid[(x, y)].set_ch(' '); + grid[(x, y)].set_bg(Color::Default); + grid[(x, y)].set_fg(Color::Default); + } + let (x, y) = write_string_to_grid( + &format!("To: {}", envelope.to_to_string()), + grid, + Color::Byte(33), + Color::Default, + (set_y(upper_left, y + 1), bottom_right), + true, + ); + for x in x..=get_x(bottom_right) { + grid[(x, y)].set_ch(' '); + grid[(x, y)].set_bg(Color::Default); + grid[(x, y)].set_fg(Color::Default); + } + let (x, y) = write_string_to_grid( + &format!("Subject: {}", envelope.subject()), + grid, + Color::Byte(33), + Color::Default, + (set_y(upper_left, y + 1), bottom_right), + true, + ); + for x in x..=get_x(bottom_right) { + grid[(x, y)].set_ch(' '); + grid[(x, y)].set_bg(Color::Default); + grid[(x, y)].set_fg(Color::Default); + } + let (x, y) = write_string_to_grid( + &format!("Message-ID: <{}>", envelope.message_id_raw()), + grid, + Color::Byte(33), + Color::Default, + (set_y(upper_left, y + 1), bottom_right), + true, + ); + for x in x..=get_x(bottom_right) { + grid[(x, y)].set_ch(' '); + grid[(x, y)].set_bg(Color::Default); + grid[(x, y)].set_fg(Color::Default); + } + clear_area(grid, (set_y(upper_left, y + 1), set_y(bottom_right, y + 2))); + context + .dirty_areas + .push_back((upper_left, set_y(bottom_right, y + 1))); + } + fn process_event(&mut self, _event: &UIEvent, _context: &mut Context) { } fn is_dirty(&self) -> bool { - self.pager.is_dirty() + self.dirty + } + fn set_dirty(&mut self) { + self.dirty = true; } - fn set_dirty(&mut self) {} } diff --git a/ui/src/components/utilities.rs b/ui/src/components/utilities.rs index f8db0f455..41363b22b 100644 --- a/ui/src/components/utilities.rs +++ b/ui/src/components/utilities.rs @@ -193,6 +193,12 @@ pub struct Pager { content: CellBuffer, } +impl Default for Pager { + fn default() -> Self { + Pager::from_str("", None) + } +} + impl fmt::Display for Pager { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // TODO display info @@ -201,6 +207,19 @@ impl fmt::Display for Pager { } impl Pager { + pub fn update_from_string(&mut self, text: String) -> () { + let lines: Vec<&str> = text.trim().split('\n').collect(); + let height = lines.len() + 1; + let width = lines.iter().map(|l| l.len()).max().unwrap_or(0); + let mut content = CellBuffer::new(width, height, Cell::with_char(' ')); + //interpret_format_flowed(&text); + Pager::print_string(&mut content, &text); + self.content = content; + self.height = height; + self.width = width; + self.dirty = true; + self.cursor_pos = 0; + } pub fn from_string(mut text: String, context: &mut Context, cursor_pos: Option) -> Self { let pager_filter: Option<&String> = context.settings.pager.filter.as_ref(); //let format_flowed: bool = context.settings.pager.format_flowed; diff --git a/ui/src/state.rs b/ui/src/state.rs index c6f85c645..109f9a3f9 100644 --- a/ui/src/state.rs +++ b/ui/src/state.rs @@ -29,7 +29,7 @@ */ use super::*; -use chan::Sender; +use chan::{Receiver, Sender}; use fnv::FnvHashMap; use std::io::Write; use std::thread; @@ -38,6 +38,36 @@ use termion::raw::IntoRawMode; use termion::screen::AlternateScreen; use termion::{clear, cursor, style}; +struct InputHandler { + rx: Receiver, + tx: Sender, +} + +impl InputHandler { + fn restore(&self, tx: Sender) { + let stdin = std::io::stdin(); + let rx = self.rx.clone(); + thread::Builder::new() + .name("input-thread".to_string()) + .spawn(move || { + get_events( + stdin, + |k| { + tx.send(ThreadEvent::Input(k)); + }, + || { + tx.send(ThreadEvent::UIEvent(UIEventType::ChangeMode(UIMode::Fork))); + }, + &rx, + ) + }) + .unwrap(); + } + fn kill(&self) { + self.tx.send(false); + } +} + /// A context container for loaded settings, accounts, UI changes, etc. pub struct Context { pub accounts: Vec, @@ -50,8 +80,10 @@ pub struct Context { /// Events queue that components send back to the state pub replies: VecDeque, + sender: Sender, + receiver: Receiver, + input: InputHandler, - input_thread: chan::Sender, pub temp_files: Vec, } @@ -59,8 +91,11 @@ impl Context { pub fn replies(&mut self) -> Vec { self.replies.drain(0..).collect() } - pub fn input_thread(&mut self) -> &mut chan::Sender { - &mut self.input_thread + pub fn input_kill(&self) { + self.input.kill(); + } + pub fn restore_input(&self) { + self.input.restore(self.sender.clone()); } } @@ -74,7 +109,6 @@ pub struct State { stdout: Option>>, child: Option, pub mode: UIMode, - sender: Sender, entities: Vec, pub context: Context, @@ -99,7 +133,16 @@ impl Drop for State { } impl State { - pub fn new(sender: Sender, input_thread: chan::Sender) -> Self { + pub fn new() -> Self { + /* Create a channel to communicate with other threads. The main process is the sole receiver. + * */ + let (sender, receiver) = chan::sync(::std::mem::size_of::()); + + /* + * Create async channel to block the input-thread if we need to fork and stop it from reading + * stdin, see get_events() for details + * */ + let input_thread = chan::async(); let _stdout = std::io::stdout(); _stdout.lock(); let backends = Backends::new(); @@ -149,7 +192,6 @@ impl State { stdout: Some(stdout), child: None, mode: UIMode::Normal, - sender, entities: Vec::with_capacity(1), context: Context { @@ -162,7 +204,12 @@ impl State { replies: VecDeque::with_capacity(5), temp_files: Vec::new(), - input_thread, + sender, + receiver, + input: InputHandler { + rx: input_thread.1, + tx: input_thread.0, + }, }, startup_thread: Some(startup_tx.clone()), threads: FnvHashMap::with_capacity_and_hasher(1, Default::default()), @@ -181,11 +228,12 @@ impl State { for (y, folder) in account.backend.folders().iter().enumerate() { s.context.mailbox_hashes.insert(folder.hash(), (x, y)); } - let sender = s.sender.clone(); + let sender = s.context.sender.clone(); account.watch(RefreshEventConsumer::new(Box::new(move |r| { sender.send(ThreadEvent::from(r)); }))); } + s.restore_input(); s } /* @@ -198,7 +246,7 @@ impl State { self.context.accounts[idxa].reload(idxm); let (startup_tx, startup_rx) = chan::async(); let startup_thread = { - let sender = self.sender.clone(); + let sender = self.context.sender.clone(); let startup_rx = startup_rx.clone(); thread::Builder::new() @@ -257,7 +305,7 @@ impl State { ).unwrap(); self.flush(); self.stdout = None; - self.context.input_thread.send(false); + self.context.input.kill(); } pub fn switch_to_alternate_screen(&mut self) { let s = std::io::stdout(); @@ -274,6 +322,13 @@ impl State { } } impl State { + pub fn receiver(&self) -> Receiver { + self.context.receiver.clone() + } + pub fn restore_input(&mut self) { + self.context.restore_input(); + } + /// On `SIGWNICH` the `State` redraws itself according to the new terminal size. pub fn update_size(&mut self) { let termsize = termion::terminal_size().ok(); diff --git a/ui/src/types/helpers.rs b/ui/src/types/helpers.rs index 074530b65..6b437e2ad 100644 --- a/ui/src/types/helpers.rs +++ b/ui/src/types/helpers.rs @@ -20,8 +20,9 @@ */ use std; +use std::fs; use std::fs::OpenOptions; -use std::io::Write; +use std::io::{Write, Read}; use std::path::PathBuf; use uuid::Uuid; @@ -50,6 +51,13 @@ impl File { pub fn path(&self) -> &PathBuf { &self.path } + pub fn read_to_string(&self) -> String { + + let mut buf = Vec::new(); + let mut f = fs::File::open(&self.path).expect(&format!("Can't open {}", &self.path.display())); + f.read_to_end(&mut buf).expect(&format!("Can't read {}", &self.path.display())); + String::from_utf8(buf).unwrap() + } } /// Returned `File` will be deleted when dropped, so make sure to add it on `context.temp_files` diff --git a/ui/src/types/position.rs b/ui/src/types/position.rs index 90a5cfee1..88c91f406 100644 --- a/ui/src/types/position.rs +++ b/ui/src/types/position.rs @@ -75,6 +75,24 @@ macro_rules! height { }; } +/// Get an area's width +/// +/// Example: +/// ``` +/// # #[macro_use] extern crate ui; fn main() { +/// use ui::*; +/// +/// let new_area = ((0, 0), (1, 1)); +/// assert_eq!(width!(new_area), 1); +/// # } +/// ``` +#[macro_export] +macro_rules! width{ + ($a:expr) => { + (get_x(bottom_right!($a))).saturating_sub(get_x(upper_left!($a))) + }; +} + /// Get the upper left Position of an area /// /// Example: