diff --git a/melib/src/mailbox/email/compose/mod.rs b/melib/src/mailbox/email/compose/mod.rs index b4c4e71a1..ae5370014 100644 --- a/melib/src/mailbox/email/compose/mod.rs +++ b/melib/src/mailbox/email/compose/mod.rs @@ -22,12 +22,14 @@ pub struct Draft { impl Default for Draft { fn default() -> Self { let mut headers = FnvHashMap::with_capacity_and_hasher(8, Default::default()); - headers.insert("From".into(), "x".into()); - headers.insert("To".into(), "x".into()); + headers.insert("From".into(), "".into()); + headers.insert("To".into(), "".into()); + headers.insert("Cc".into(), "".into()); + headers.insert("Bcc".into(), "".into()); let now: DateTime = Local::now(); headers.insert("Date".into(), now.to_rfc2822()); - headers.insert("Subject".into(), "x".into()); + headers.insert("Subject".into(), "".into()); headers.insert("Message-ID".into(), random::gen_message_id()); headers.insert("User-Agent".into(), "meli".into()); Draft { @@ -58,7 +60,7 @@ impl Draft { pub fn to_string(&self) -> Result { let mut ret = String::new(); - let headers = &["Date", "From", "To", "Subject", "Message-ID"]; + let headers = &["Date", "From", "To", "Cc", "Bcc", "Subject", "Message-ID"]; for k in headers { ret.extend(format!("{}: {}\n", k, &self.headers[*k]).chars()); } @@ -131,6 +133,8 @@ fn ignore_header(header: &[u8]) -> bool { b"Reply-to" => false, b"Cc" => false, b"Bcc" => false, + b"In-Reply-To" => false, + b"References" => false, h if h.starts_with(b"X-") => false, _ => true, } diff --git a/melib/src/mailbox/email/mod.rs b/melib/src/mailbox/email/mod.rs index a73a01d55..7f282521e 100644 --- a/melib/src/mailbox/email/mod.rs +++ b/melib/src/mailbox/email/mod.rs @@ -41,6 +41,7 @@ use std::collections::hash_map::DefaultHasher; use std::fmt; use std::hash::Hasher; use std::option::Option; +use std::str; use std::string::String; use chrono; @@ -181,6 +182,16 @@ fn test_strbuilder() { ); } +impl fmt::Display for MessageID { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.val().is_ascii() { + write!(f, "{}", unsafe { str::from_utf8_unchecked(self.val()) }) + } else { + write!(f, "{}", String::from_utf8_lossy(self.val())) + } + } +} + impl PartialEq for MessageID { fn eq(&self, other: &MessageID) -> bool { self.raw() == other.raw() @@ -261,6 +272,8 @@ pub struct Envelope { date: String, from: Vec
, to: Vec
, + cc: Vec
, + bcc: Vec
, body: Option, subject: Option>, message_id: Option, @@ -281,6 +294,8 @@ impl Envelope { date: String::new(), from: Vec::new(), to: Vec::new(), + cc: Vec::new(), + bcc: Vec::new(), body: None, subject: None, message_id: None, @@ -342,6 +357,22 @@ impl Envelope { Vec::new() }; self.set_to(value); + } else if name.eq_ignore_ascii_case(b"cc") { + let parse_result = parser::rfc2822address_list(value); + let value = if parse_result.is_done() { + parse_result.to_full_result().unwrap() + } else { + Vec::new() + }; + self.set_cc(value); + } else if name.eq_ignore_ascii_case(b"bcc") { + let parse_result = parser::rfc2822address_list(value); + let value = if parse_result.is_done() { + parse_result.to_full_result().unwrap() + } else { + Vec::new() + }; + self.set_bcc(value); } else if name.eq_ignore_ascii_case(b"from") { let parse_result = parser::rfc2822address_list(value); let value = if parse_result.is_done() { @@ -424,7 +455,14 @@ impl Envelope { pub fn from(&self) -> &Vec
{ &self.from } - + pub fn field_bcc_to_string(&self) -> String { + let _strings: Vec = self.bcc.iter().map(|a| format!("{}", a)).collect(); + _strings.join(", ") + } + pub fn field_cc_to_string(&self) -> String { + let _strings: Vec = self.cc.iter().map(|a| format!("{}", a)).collect(); + _strings.join(", ") + } pub fn field_from_to_string(&self) -> String { let _strings: Vec = self.from.iter().map(|a| format!("{}", a)).collect(); _strings.join(", ") @@ -530,6 +568,12 @@ impl Envelope { fn set_date(&mut self, new_val: &[u8]) -> () { self.date = String::from_utf8_lossy(new_val).into_owned(); } + fn set_bcc(&mut self, new_val: Vec
) -> () { + self.from = new_val; + } + fn set_cc(&mut self, new_val: Vec
) -> () { + self.from = new_val; + } fn set_from(&mut self, new_val: Vec
) -> () { self.from = new_val; } diff --git a/melib/src/mailbox/thread.rs b/melib/src/mailbox/thread.rs index 5fd8064a4..17c8ac0c9 100644 --- a/melib/src/mailbox/thread.rs +++ b/melib/src/mailbox/thread.rs @@ -103,6 +103,21 @@ pub struct Container { show_subject: bool, } +impl Default for Container { + fn default() -> Container { + Container { + id: 0, + message: None, + parent: None, + first_child: None, + next_sibling: None, + date: UnixTimestamp::default(), + indentation: 0, + show_subject: true, + } + } +} + #[derive(Clone, Debug)] struct ContainerTree { id: usize, @@ -513,12 +528,8 @@ fn build_collection( threads.push(Container { message: Some(i), id: x_index, - parent: None, - first_child: None, - next_sibling: None, date: x.date(), - indentation: 0, - show_subject: true, + ..Default::default() }); x.set_thread(x_index); id_table.insert(m_id, x_index); @@ -565,12 +576,9 @@ fn build_collection( threads.push(Container { message: None, id: idx, - parent: None, first_child: Some(curr_ref), - next_sibling: None, date: x.date(), - indentation: 0, - show_subject: true, + ..Default::default() }); if threads[curr_ref].parent.is_none() { threads[curr_ref].parent = Some(idx); @@ -661,11 +669,8 @@ pub fn build_threads( message: Some(idx), id: tidx, parent: Some(p), - first_child: None, - next_sibling: None, date: x.date(), - indentation: 0, - show_subject: true, + ..Default::default() }); id_table.insert(Cow::from(m_id.into_owned()), tidx); x.set_thread(tidx); diff --git a/src/bin.rs b/src/bin.rs index 47f666580..1a3eee563 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -61,25 +61,18 @@ fn main() { let receiver = state.receiver(); /* Register some reasonably useful interfaces */ - let menu = Entity { - component: Box::new(AccountMenu::new(&state.context.accounts)), - }; + let menu = Entity::from(Box::new(AccountMenu::new(&state.context.accounts))); let listing = CompactListing::new(); - let b = Entity { - component: Box::new(listing), - }; + let b = Entity::from(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::default())); - let window = Entity { component: tabs }; + let window = Entity::from(tabs); - let status_bar = Entity { - component: Box::new(StatusBar::new(window)), - }; + let status_bar = Entity::from(Box::new(StatusBar::new(window))); state.register_entity(status_bar); - let xdg_notifications = Entity { - component: Box::new(ui::components::notifications::XDGNotifications {}), - }; + let xdg_notifications = + Entity::from(Box::new(ui::components::notifications::XDGNotifications {})); state.register_entity(xdg_notifications); /* Keep track of the input mode. See ui::UIMode for details */ @@ -203,8 +196,7 @@ fn main() { match state.try_wait_on_child() { Some(true) => { state.restore_input(); - state.mode = UIMode::Normal; - state.render(); + state.switch_to_alternate_screen(); } Some(false) => { use std::{thread, time}; @@ -214,6 +206,8 @@ fn main() { continue 'reap; } None => { + state.mode = UIMode::Normal; + state.render(); break 'reap; } } diff --git a/ui/src/components/mail/compose.rs b/ui/src/components/mail/compose.rs index 3a95cff7d..5f339f93c 100644 --- a/ui/src/components/mail/compose.rs +++ b/ui/src/components/mail/compose.rs @@ -25,49 +25,148 @@ use melib::Draft; #[derive(Debug)] pub struct Composer { - mode: ViewMode, - pager: Pager, - - draft: Draft, + reply_context: Option<((usize, usize), Box)>, // (folder_index, container_index) account_cursor: usize, + pager: Pager, + draft: Draft, + + mode: ViewMode, dirty: bool, + initialized: bool, } impl Default for Composer { fn default() -> Self { Composer { - dirty: true, - mode: ViewMode::Overview, + reply_context: None, + account_cursor: 0, + pager: Pager::default(), draft: Draft::default(), - account_cursor: 0, + + mode: ViewMode::Overview, + dirty: true, + initialized: false, } } } #[derive(Debug)] enum ViewMode { - //Compose, + Discard(Uuid), + Pager, Overview, } +impl ViewMode { + fn is_discard(&self) -> bool { + if let ViewMode::Discard(_) = self { + true + } else { + false + } + } + + fn is_overview(&self) -> bool { + if let ViewMode::Overview = self { + true + } else { + false + } + } + + fn is_pager(&self) -> bool { + if let ViewMode::Pager = self { + true + } else { + false + } + } +} + impl fmt::Display for Composer { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // TODO display subject/info - write!(f, "compose") + if self.reply_context.is_some() { + write!(f, "reply: {:8}", self.draft.headers()["Subject"]) + } else { + write!(f, "compose") + } } } impl Composer { - fn draw_header_table(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + /* + * coordinates: (account index, mailbox index, root set container index) + * msg: index of message we reply to in containers + * context: current context + */ + pub fn with_context(coordinates: (usize, usize, usize), msg: usize, context: &Context) -> Self { + let mailbox = &context.accounts[coordinates.0][coordinates.1] + .as_ref() + .unwrap(); + let threads = &mailbox.threads; + let containers = &threads.containers(); + + let mut ret = Composer::default(); + + let p = containers[msg]; + ret.draft.headers_mut().insert( + "Subject".into(), + if p.show_subject() { + format!( + "Re: {}", + mailbox.collection[p.message().unwrap()].subject().clone() + ) + } else { + mailbox.collection[p.message().unwrap()].subject().into() + }, + ); + let parent_message = &mailbox.collection[p.message().unwrap()]; + ret.draft.headers_mut().insert( + "References".into(), + format!( + "{} {}", + parent_message + .references() + .iter() + .fold(String::new(), |mut acc, x| { + if !acc.is_empty() { + acc.push(' '); + } + acc.push_str(&x.to_string()); + acc + }), + parent_message.message_id() + ), + ); + ret.draft + .headers_mut() + .insert("In-Reply-To".into(), parent_message.message_id().into()); + ret.draft + .headers_mut() + .insert("To".into(), parent_message.field_from_to_string()); + ret.draft + .headers_mut() + .insert("Cc".into(), parent_message.field_cc_to_string()); + + ret.account_cursor = coordinates.0; + ret.reply_context = Some(( + (coordinates.1, coordinates.2), + Box::new(ThreadView::new(coordinates, Some(msg), context)), + )); + ret + } + + fn draw_header_table(&mut self, grid: &mut CellBuffer, area: Area) { let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); let headers = self.draft.headers(); { let (mut x, mut y) = upper_left; - for k in &["Date", "From", "To", "Subject"] { + for k in &["Date", "From", "To", "Cc", "Bcc", "Subject"] { let update = { let (x, y) = write_string_to_grid( k, @@ -127,22 +226,37 @@ impl Composer { impl Component for Composer { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if self.dirty { - self.draft.headers_mut().insert( - "From".into(), - get_display_name(context, self.account_cursor), - ); + if !self.initialized { clear_area(grid, area); + self.initialized = true; } + let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); let upper_left = set_y(upper_left, get_y(upper_left) + 1); let header_height = 5; - let width = width!(area); + let width = if width!(area) > 80 && self.reply_context.is_some() { + width!(area) / 2 + } else { + width!(area) + }; + let mid = if width > 80 { let width = width - 80; - let mid = width / 2; + let mid = if self.reply_context.is_some() { + width!(area) / 2 + width / 2 + } else { + width / 2 + }; + + if self.reply_context.is_some() { + for i in get_y(upper_left)..=get_y(bottom_right) { + set_and_join_box(grid, (mid, i), VERT_BOUNDARY); + grid[(mid, i)].set_fg(Color::Default); + grid[(mid, i)].set_bg(Color::Default); + } + } if self.dirty { for i in get_y(upper_left)..=get_y(bottom_right) { @@ -159,6 +273,12 @@ impl Component for Composer { 0 }; + if width > 80 && self.reply_context.is_some() { + let area = (upper_left, set_x(bottom_right, mid - 1)); + let view = &mut self.reply_context.as_mut().unwrap().1; + view.draw(grid, area, context); + } + if self.dirty { for i in get_x(upper_left) + mid + 1..=get_x(upper_left) + mid + 79 { //set_and_join_box(grid, (i, header_height), HORZ_BOUNDARY); @@ -174,26 +294,62 @@ impl Component for Composer { ); if self.dirty { - context.dirty_areas.push_back(area); + self.draft.headers_mut().insert( + "From".into(), + get_display_name(context, self.account_cursor), + ); self.dirty = false; } - match self.mode { - ViewMode::Overview => { - self.draw_header_table(grid, header_area, context); - self.pager.draw(grid, body_area, context); + + /* Regardless of view mode, do the following */ + clear_area(grid, header_area); + clear_area(grid, body_area); + self.draw_header_table(grid, header_area); + self.pager.draw(grid, body_area, context); + + if let ViewMode::Discard(_) = self.mode { + let mid_x = width!(area) / 2; + let mid_y = height!(area) / 2; + + for i in mid_x - 40..=mid_x + 40 { + set_and_join_box(grid, (i, mid_y - 11), HORZ_BOUNDARY); + + set_and_join_box(grid, (i, mid_y + 11), HORZ_BOUNDARY); + } + + for i in mid_y - 11..=mid_y + 11 { + set_and_join_box(grid, (mid_x - 40, i), VERT_BOUNDARY); + + set_and_join_box(grid, (mid_x + 40, i), VERT_BOUNDARY); } } + + context.dirty_areas.push_back(area); } fn process_event(&mut self, event: &UIEvent, context: &mut Context) -> bool { - if self.pager.process_event(event, context) { - return true; + match (&mut self.mode, &mut self.reply_context) { + (ViewMode::Pager, _) => { + /* Cannot mutably borrow in pattern guard, pah! */ + if self.pager.process_event(event, context) { + return true; + } + } + (ViewMode::Overview, Some((_, ref mut view))) => { + if view.process_event(event, context) { + self.dirty = true; + return true; + } + } + _ => {} } match event.event_type { UIEventType::Resize => { self.dirty = true; + self.initialized = false; } + /* Switch e-mail From: field to the `left` configured account. */ UIEventType::Input(Key::Left) => { self.account_cursor = self.account_cursor.saturating_sub(1); self.draft.headers_mut().insert( @@ -203,6 +359,7 @@ impl Component for Composer { self.dirty = true; return true; } + /* Switch e-mail From: field to the `right` configured account. */ UIEventType::Input(Key::Right) => { if self.account_cursor + 1 < context.accounts.len() { self.account_cursor += 1; @@ -214,7 +371,38 @@ impl Component for Composer { } return true; } - UIEventType::Input(Key::Char('\n')) => { + UIEventType::Input(Key::Char(k)) if self.mode.is_discard() => { + match (k, &self.mode) { + ('y', ViewMode::Discard(u)) => { + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::Action(Tab(Kill(u.clone()))), + }); + return true; + } + ('n', _) => {} + _ => { + return false; + } + } + self.mode = ViewMode::Overview; + self.set_dirty(); + return true; + } + /* Switch to Overview mode if we're on Pager mode */ + UIEventType::Input(Key::Char('o')) if self.mode.is_pager() => { + self.mode = ViewMode::Overview; + self.set_dirty(); + return true; + } + /* Switch to Pager mode if we're on Overview mode */ + UIEventType::Input(Key::Char('p')) if self.mode.is_overview() => { + self.mode = ViewMode::Pager; + self.set_dirty(); + return true; + } + /* Edit draft in $EDITOR */ + UIEventType::Input(Key::Char('e')) => { use std::process::{Command, Stdio}; /* Kill input thread so that spawned command can be sole receiver of stdin */ { @@ -235,10 +423,15 @@ impl Component for Composer { let result = f.read_to_string(); self.draft = Draft::from_str(result.as_str()).unwrap(); self.pager.update_from_str(self.draft.body()); + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::Fork(ForkType::Finished), + }); context.restore_input(); self.dirty = true; return true; } + // TODO: Replace EditDraft with compose tabs UIEventType::Input(Key::Char('m')) => { let mut f = create_temp_file(self.draft.to_string().unwrap().as_str().as_bytes(), None); @@ -256,11 +449,24 @@ impl Component for Composer { fn is_dirty(&self) -> bool { self.dirty || self.pager.is_dirty() + || self + .reply_context + .as_ref() + .map(|(_, p)| p.is_dirty()) + .unwrap_or(false) } fn set_dirty(&mut self) { self.dirty = true; + self.initialized = false; self.pager.set_dirty(); + if let Some((_, ref mut view)) = self.reply_context { + view.set_dirty(); + } + } + + fn kill(&mut self, uuid: Uuid) { + self.mode = ViewMode::Discard(uuid); } } diff --git a/ui/src/components/mail/listing/compact.rs b/ui/src/components/mail/listing/compact.rs index 4810d8b6d..fad027272 100644 --- a/ui/src/components/mail/listing/compact.rs +++ b/ui/src/components/mail/listing/compact.rs @@ -303,6 +303,7 @@ impl Component for CompactListing { if self.length == 0 && self.dirty { clear_area(grid, area); context.dirty_areas.push_back(area); + return; } /* Render the mail body in a pager */ @@ -312,7 +313,7 @@ impl Component for CompactListing { } return; } - self.view = Some(ThreadView::new(self.cursor_pos, context)); + self.view = Some(ThreadView::new(self.cursor_pos, None, context)); self.view.as_mut().unwrap().draw(grid, area, context); self.dirty = false; } @@ -418,15 +419,6 @@ impl Component for CompactListing { self.dirty = true; } UIEventType::Action(ref action) => match action { - Action::PlainListing(PlainListingAction::ToggleThreaded) => { - context.accounts[self.cursor_pos.0] - .runtime_settings - .conf_mut() - .toggle_threaded(); - self.refresh_mailbox(context); - self.dirty = true; - return true; - } Action::ViewMailbox(idx) => { self.new_cursor_pos.1 = *idx; self.dirty = true; @@ -446,7 +438,8 @@ impl Component for CompactListing { self.dirty = true; self.refresh_mailbox(context); return true; - } // _ => {} + } + _ => {} }, _ => {} } diff --git a/ui/src/components/mail/listing/mod.rs b/ui/src/components/mail/listing/mod.rs index 78f8f232e..a96ff7bff 100644 --- a/ui/src/components/mail/listing/mod.rs +++ b/ui/src/components/mail/listing/mod.rs @@ -760,7 +760,7 @@ impl Component for PlainListing { self.dirty = true; } UIEventType::Action(ref action) => match action { - Action::PlainListing(PlainListingAction::ToggleThreaded) => { + Action::Listing(ListingAction::ToggleThreaded) => { context.accounts[self.cursor_pos.0] .runtime_settings .conf_mut() @@ -788,7 +788,8 @@ impl Component for PlainListing { self.dirty = true; self.refresh_mailbox(context); return true; - } // _ => {} + } + _ => {} }, _ => {} } diff --git a/ui/src/components/mail/view/html.rs b/ui/src/components/mail/view/html.rs index 8fbe90fde..d802a2bf7 100644 --- a/ui/src/components/mail/view/html.rs +++ b/ui/src/components/mail/view/html.rs @@ -70,6 +70,7 @@ impl Component for HtmlView { if self.pager.process_event(event, context) { return true; } + if let UIEventType::Input(Key::Char('v')) = event.event_type { // TODO: Optional filter that removes outgoing resource requests (images and // scripts) @@ -98,5 +99,7 @@ impl Component for HtmlView { fn is_dirty(&self) -> bool { self.pager.is_dirty() } - fn set_dirty(&mut self) {} + fn set_dirty(&mut self) { + self.pager.set_dirty(); + } } diff --git a/ui/src/components/mail/view/mod.rs b/ui/src/components/mail/view/mod.rs index 06e4bc2f4..6d28e0a38 100644 --- a/ui/src/components/mail/view/mod.rs +++ b/ui/src/components/mail/view/mod.rs @@ -309,15 +309,19 @@ impl Component for MailView { let body = envelope.body(op); match self.mode { ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => { + self.pager = None; self.subview = Some(Box::new(HtmlView::new(decode( &body.attachments()[aidx], None, )))); + self.mode = ViewMode::Subview; } ViewMode::Normal if body.is_html() => { self.subview = Some(Box::new(HtmlView::new(decode(&body, None)))); + self.pager = None; self.mode = ViewMode::Subview; } + ViewMode::Subview => {} _ => { let buf = { let text = self.attachment_to_text(&body); @@ -330,27 +334,43 @@ impl Component for MailView { self.pager.as_mut().map(|p| p.cursor_pos()) }; self.pager = Some(Pager::from_buf(buf.split_newlines(), cursor_pos)); + self.subview = None; } }; self.dirty = false; } - if let Some(s) = self.subview.as_mut() { - s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); - } else if let Some(p) = self.pager.as_mut() { - p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); + match self.mode { + ViewMode::Subview => { + if let Some(s) = self.subview.as_mut() { + s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); + } + } + _ => { + if let Some(p) = self.pager.as_mut() { + p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); + } + } } } fn process_event(&mut self, event: &UIEvent, context: &mut Context) -> bool { - if let Some(ref mut sub) = self.subview { - if sub.process_event(event, context) { - return true; + match self.mode { + ViewMode::Subview => { + if let Some(s) = self.subview.as_mut() { + if s.process_event(event, context) { + return true; + } + } } - } else if let Some(ref mut p) = self.pager { - if p.process_event(event, context) { - return true; + _ => { + if let Some(p) = self.pager.as_mut() { + if p.process_event(event, context) { + return true; + } + } } } + match event.event_type { UIEventType::Input(Key::Esc) | UIEventType::Input(Key::Alt('')) => { self.cmd_buf.clear(); @@ -542,5 +562,18 @@ impl Component for MailView { } fn set_dirty(&mut self) { self.dirty = true; + match self.mode { + ViewMode::Normal => { + if let Some(p) = self.pager.as_mut() { + p.set_dirty(); + } + } + ViewMode::Subview => { + if let Some(s) = self.subview.as_mut() { + s.set_dirty(); + } + } + _ => {} + } } } diff --git a/ui/src/components/mail/view/thread.rs b/ui/src/components/mail/view/thread.rs index 3f1607b0d..f28cbd873 100644 --- a/ui/src/components/mail/view/thread.rs +++ b/ui/src/components/mail/view/thread.rs @@ -46,7 +46,17 @@ pub struct ThreadView { } impl ThreadView { - pub fn new(coordinates: (usize, usize, usize), context: &Context) -> Self { + /* + * coordinates: (account index, mailbox index, root set container index) + * expanded_idx: optional position of expanded entry when we render the threadview. Default + * expanded message is the last one. + * context: current context + */ + pub fn new( + coordinates: (usize, usize, usize), + expanded_idx: Option, + context: &Context, + ) -> Self { /* stack to push thread messages in order in order to pop and print them later */ let mut stack: Vec<(usize, usize)> = Vec::with_capacity(32); let mailbox = &context.accounts[coordinates.0][coordinates.1] @@ -78,6 +88,13 @@ impl ThreadView { let entry = view.make_entry(context, (ind, idx, line)); view.entries.push(entry); line += 1; + match expanded_idx { + Some(expanded_idx) if expanded_idx == idx => { + view.new_expanded_pos = view.entries.len().saturating_sub(1); + view.expanded_pos = view.new_expanded_pos + 1; + } + _ => {} + } let container = &threads.containers()[idx]; if let Some(i) = container.next_sibling() { stack.push((ind, i)); @@ -87,8 +104,10 @@ impl ThreadView { stack.push((ind + 1, i)); } } - view.new_expanded_pos = view.entries.len().saturating_sub(1); - view.expanded_pos = view.new_expanded_pos + 1; + if expanded_idx.is_none() { + view.new_expanded_pos = view.entries.len().saturating_sub(1); + view.expanded_pos = view.new_expanded_pos + 1; + } let height = 2 * view.entries.len() + 1; let mut width = 0; @@ -545,6 +564,16 @@ impl Component for ThreadView { return true; } match event.event_type { + UIEventType::Input(Key::Char('R')) => { + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::Reply( + self.coordinates, + self.entries[self.expanded_pos].index.1, + ), + }); + return true; + } UIEventType::Input(Key::Up) => { if self.cursor_pos > 0 { self.new_cursor_pos = self.new_cursor_pos.saturating_sub(1); @@ -574,7 +603,7 @@ impl Component for ThreadView { return true; } UIEventType::Resize => { - self.dirty = true; + self.set_dirty(); } _ => {} } @@ -584,6 +613,7 @@ impl Component for ThreadView { self.dirty || self.mailview.is_dirty() } fn set_dirty(&mut self) { + self.initiated = false; self.dirty = true; self.mailview.set_dirty(); } diff --git a/ui/src/components/mod.rs b/ui/src/components/mod.rs index 06ba74406..b91f33da3 100644 --- a/ui/src/components/mod.rs +++ b/ui/src/components/mod.rs @@ -37,7 +37,9 @@ pub use self::utilities::*; use std::fmt; use std::fmt::{Debug, Display}; -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; + +use uuid::Uuid; use super::{Key, StatusEvent, UIEvent, UIEventType}; /// The upper and lower boundary char. @@ -62,20 +64,46 @@ const LIGHT_DOWN_AND_HORIZONTAL: char = '┬'; const LIGHT_UP_AND_HORIZONTAL: char = '┴'; -/// `Entity` is a container for Components. Totally useless now so if it is not useful in the -/// future (ie hold some information, id or state) it should be removed. +/// `Entity` is a container for Components. #[derive(Debug)] pub struct Entity { - //context: VecDeque, + id: Uuid, pub component: Box, // more than one? } +impl From> for Entity { + fn from(kind: Box) -> Entity { + Entity { + id: Uuid::new_v4(), + component: kind, + } + } +} + +impl From> for Entity +where + C: Component, +{ + fn from(kind: Box) -> Entity { + Entity { + id: Uuid::new_v4(), + component: kind, + } + } +} + impl Display for Entity { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Display::fmt(&self.component, f) } } +impl DerefMut for Entity { + fn deref_mut(&mut self) -> &mut Box { + &mut self.component + } +} + impl Deref for Entity { type Target = Box; @@ -85,6 +113,9 @@ impl Deref for Entity { } impl Entity { + pub fn uuid(&self) -> &Uuid { + &self.id + } /// Pass events to child component. pub fn rcv_event(&mut self, event: &UIEvent, context: &mut Context) -> bool { self.component.process_event(&event, context) @@ -101,6 +132,7 @@ pub trait Component: Display + Debug { true } fn set_dirty(&mut self); + fn kill(&mut self, _uuid: Uuid) {} } fn new_draft(_context: &mut Context) -> Vec { @@ -113,6 +145,7 @@ fn new_draft(_context: &mut Context) -> Vec { v.into_bytes() } +/* pub(crate) fn is_box_char(ch: char) -> bool { match ch { HORZ_BOUNDARY | VERT_BOUNDARY => true, @@ -120,7 +153,6 @@ pub(crate) fn is_box_char(ch: char) -> bool { } } -/* * pub(crate) fn is_box_char(ch: char) -> bool { * match ch { * '└' | '─' | '┘' | '┴' | '┌' | '│' | '├' | '┐' | '┬' | '┤' | '┼' | '╷' | '╵' | '╴' | '╶' => true, diff --git a/ui/src/components/utilities.rs b/ui/src/components/utilities.rs index b757834f3..2eb3ceea2 100644 --- a/ui/src/components/utilities.rs +++ b/ui/src/components/utilities.rs @@ -712,14 +712,17 @@ impl Component for Progress { #[derive(Debug)] pub struct Tabbed { - children: Vec>, + children: Vec, cursor_pos: usize, } impl Tabbed { pub fn new(children: Vec>) -> Self { Tabbed { - children, + children: children + .into_iter() + .map(|x: Box| Entity::from(x)) + .collect(), cursor_pos: 0, } } @@ -748,13 +751,15 @@ impl Tabbed { } let (cols, _) = grid.size(); let cslice: &mut [Cell] = grid; - for c in cslice[(y * cols) + x..(y * cols) + cols].iter_mut() { + for c in cslice[(y * cols) + x - 1..(y * cols) + cols].iter_mut() { c.set_bg(Color::Byte(7)); + c.set_ch(' '); } + context.dirty_areas.push_back(area); } pub fn add_component(&mut self, new: Box) { - self.children.push(new); + self.children.push(Entity::from(new)); } } @@ -787,15 +792,44 @@ impl Component for Tabbed { } } fn process_event(&mut self, event: &UIEvent, context: &mut Context) -> bool { - if let UIEventType::Input(Key::Char('T')) = event.event_type { - self.cursor_pos = (self.cursor_pos + 1) % self.children.len(); - self.children[self.cursor_pos].set_dirty(); - return true; + match event.event_type { + UIEventType::Input(Key::Char('T')) => { + self.cursor_pos = (self.cursor_pos + 1) % self.children.len(); + self.set_dirty(); + return true; + } + UIEventType::Reply(coordinates, msg) => { + self.add_component(Box::new(Composer::with_context(coordinates, msg, context))); + self.cursor_pos = self.children.len() - 1; + self.children[self.cursor_pos].set_dirty(); + return true; + } + UIEventType::Action(Tab(Close)) => { + let uuid = self.children[self.cursor_pos].uuid().clone(); + self.children[self.cursor_pos].kill(uuid); + return true; + } + UIEventType::Action(Tab(Kill(ref uuid))) => { + if let Some(c_idx) = self.children.iter().position(|x| x.uuid() == uuid) { + self.children.remove(c_idx); + self.cursor_pos = self.cursor_pos.saturating_sub(1); + self.set_dirty(); + return true; + } else { + eprintln!( + "DEBUG: Child entity with uuid {:?} not found.\nList: {:?}", + uuid, self.children + ); + } + } + _ => {} } self.children[self.cursor_pos].process_event(event, context) } fn is_dirty(&self) -> bool { self.children[self.cursor_pos].is_dirty() } - fn set_dirty(&mut self) {} + fn set_dirty(&mut self) { + self.children[self.cursor_pos].set_dirty(); + } } diff --git a/ui/src/execute/actions.rs b/ui/src/execute/actions.rs index 113425462..e2e587e43 100644 --- a/ui/src/execute/actions.rs +++ b/ui/src/execute/actions.rs @@ -25,15 +25,25 @@ pub use melib::mailbox::{SortField, SortOrder}; +extern crate uuid; +use uuid::Uuid; + #[derive(Debug, Clone)] -pub enum PlainListingAction { +pub enum ListingAction { ToggleThreaded, } +#[derive(Debug, Clone)] +pub enum TabAction { + Close, + Kill(Uuid), +} + #[derive(Debug, Clone)] pub enum Action { - PlainListing(PlainListingAction), + Listing(ListingAction), ViewMailbox(usize), Sort(SortField, SortOrder), SubSort(SortField, SortOrder), + Tab(TabAction), } diff --git a/ui/src/execute/mod.rs b/ui/src/execute/mod.rs index 0c2d4aca9..58f3d4592 100644 --- a/ui/src/execute/mod.rs +++ b/ui/src/execute/mod.rs @@ -21,10 +21,13 @@ /*! A parser module for user commands passed through the Ex mode. */ +pub use melib::mailbox::{SortField, SortOrder}; use nom::{digit, not_line_ending}; use std; pub mod actions; -pub use actions::*; +pub use actions::Action::{self, *}; +pub use actions::ListingAction::{self, *}; +pub use actions::TabAction::{self, *}; named!( usize_c, @@ -50,6 +53,7 @@ named!( ) ); +named!(close, map!(ws!(tag!("close")), |_| Tab(Close))); named!( goto, preceded!(tag!("b "), map!(call!(usize_c), Action::ViewMailbox)) @@ -57,22 +61,18 @@ named!( named!( subsort, - do_parse!(tag!("subsort ") >> p: pair!(sortfield, sortorder) >> (Action::SubSort(p.0, p.1))) + do_parse!(tag!("subsort ") >> p: pair!(sortfield, sortorder) >> (SubSort(p.0, p.1))) ); named!( sort, do_parse!( - tag!("sort ") - >> p: separated_pair!(sortfield, tag!(" "), sortorder) - >> (Action::Sort(p.0, p.1)) + tag!("sort ") >> p: separated_pair!(sortfield, tag!(" "), sortorder) >> (Sort(p.0, p.1)) ) ); named!( threaded, - map!(ws!(tag!("threaded")), |_| { - Action::PlainListing(PlainListingAction::ToggleThreaded) - }) + map!(ws!(tag!("threaded")), |_| Listing(ToggleThreaded)) ); named!( toggle, @@ -80,5 +80,5 @@ named!( ); named!(pub parse_command, - alt_complete!( goto | toggle | sort | subsort) + alt_complete!( goto | toggle | sort | subsort | close) ); diff --git a/ui/src/state.rs b/ui/src/state.rs index d657ac553..d7afbab70 100644 --- a/ui/src/state.rs +++ b/ui/src/state.rs @@ -39,6 +39,8 @@ use termion::raw::IntoRawMode; use termion::screen::AlternateScreen; use termion::{clear, cursor, style}; +type StateStdout = termion::screen::AlternateScreen>; + struct InputHandler { rx: Receiver, tx: Sender, @@ -133,12 +135,12 @@ impl Context { /// A State object to manage and own components and entities of the UI. `State` is responsible for /// managing the terminal and interfacing with `melib` -pub struct State { +pub struct State { cols: usize, rows: usize, grid: CellBuffer, - stdout: Option>>, + stdout: Option, child: Option, pub mode: UIMode, entities: Vec, @@ -149,7 +151,7 @@ pub struct State { threads: FnvHashMap, thread::JoinHandle<()>)>, } -impl Drop for State { +impl Drop for State { fn drop(&mut self) { // When done, restore the defaults to avoid messing with the terminal. write!( @@ -164,13 +166,13 @@ impl Drop for State { } } -impl Default for State { +impl Default for State { fn default() -> Self { Self::new() } } -impl State { +impl State { pub fn new() -> Self { /* Create a channel to communicate with other threads. The main process is the sole receiver. * */ @@ -370,8 +372,7 @@ impl State { ).unwrap(); self.flush(); } -} -impl State { + pub fn receiver(&self) -> Receiver { self.context.receiver.clone() } @@ -509,7 +510,15 @@ impl State { UIEventType::Fork(child) => { self.mode = UIMode::Fork; self.child = Some(child); - self.flush(); + if let Some(ForkType::Finished) = self.child { + /* + * Fork has finished in the past. + * We're back in the AlternateScreen, but the cursor is reset to Shown, so fix + * it. + */ + write!(self.stdout(), "{}", cursor::Hide,).unwrap(); + self.flush(); + } return; } UIEventType::EditDraft(mut file) => { @@ -538,8 +547,8 @@ impl State { self.entities[i].rcv_event( &UIEvent { id: 0, - event_type: UIEventType::Action(Action::PlainListing( - PlainListingAction::ToggleThreaded, + event_type: UIEventType::Action(Action::Listing( + ListingAction::ToggleThreaded, )), }, &mut self.context, @@ -578,7 +587,7 @@ impl State { } pub fn try_wait_on_child(&mut self) -> Option { - if match self.child { + let should_return_flag = match self.child { Some(ForkType::NewDraft(_, ref mut c)) => { let mut w = c.try_wait(); match w { @@ -599,10 +608,16 @@ impl State { } } } + Some(ForkType::Finished) => { + /* Fork has already finished */ + std::mem::replace(&mut self.child, None); + return None; + } _ => { return None; } - } { + }; + if should_return_flag { if let Some(ForkType::NewDraft(f, _)) = std::mem::replace(&mut self.child, None) { self.rcv_event(UIEvent { id: 0, @@ -618,7 +633,7 @@ impl State { s.flush().unwrap(); } } - fn stdout(&mut self) -> &mut termion::screen::AlternateScreen> { + fn stdout(&mut self) -> &mut StateStdout { self.stdout.as_mut().unwrap() } } diff --git a/ui/src/types/mod.rs b/ui/src/types/mod.rs index bd7a67e65..3fd70b65e 100644 --- a/ui/src/types/mod.rs +++ b/ui/src/types/mod.rs @@ -40,6 +40,7 @@ use melib::RefreshEvent; use std; use std::fmt; use std::thread; +use uuid::Uuid; #[derive(Debug)] pub enum StatusEvent { @@ -71,6 +72,7 @@ impl From for ThreadEvent { #[derive(Debug)] pub enum ForkType { + Finished, // Already finished fork, we only want to restore input/output Generic(std::process::Child), NewDraft(File, std::process::Child), } @@ -91,7 +93,10 @@ pub enum UIEventType { EditDraft(File), Action(Action), StatusEvent(StatusEvent), - MailboxUpdate((usize, usize)), + MailboxUpdate((usize, usize)), // (account_idx, mailbox_idx) + + Reply((usize, usize, usize), usize), // thread coordinates (account, mailbox, root_set idx) and message idx + EntityKill(Uuid), StartupCheck, }