diff --git a/ui/src/components/mail/compose.rs b/ui/src/components/mail/compose.rs index ca3cbd9c..ef5c9e31 100644 --- a/ui/src/components/mail/compose.rs +++ b/ui/src/components/mail/compose.rs @@ -73,21 +73,13 @@ impl Default for Composer { #[derive(Debug)] enum ViewMode { - Discard(Uuid), + Discard(Uuid, Selector), Edit, //Selector(Selector), ThreadView, } impl ViewMode { - fn is_discard(&self) -> bool { - if let ViewMode::Discard(_) = self { - true - } else { - false - } - } - fn is_edit(&self) -> bool { if let ViewMode::Edit = self { true @@ -452,62 +444,11 @@ impl Component for Composer { self.pager.set_dirty(); self.pager.draw(grid, body_area, context); } - ViewMode::Discard(_) => { + ViewMode::Discard(_, ref mut s) => { + self.pager.set_dirty(); + self.pager.draw(grid, body_area, context); /* Let user choose whether to quit with/without saving or cancel */ - let mid_x = { std::cmp::max(width!(area) / 2, width / 2) - width / 2 }; - let mid_y = { std::cmp::max(height!(area) / 2, 11) - 11 }; - - let upper_left = upper_left!(body_area); - let bottom_right = bottom_right!(body_area); - let area = ( - pos_inc(upper_left, (mid_x, mid_y)), - pos_dec(bottom_right, (mid_x, mid_y)), - ); - create_box(grid, area); - let area = ( - pos_inc(upper_left, (mid_x + 2, mid_y + 2)), - pos_dec( - bottom_right, - (mid_x.saturating_sub(2), mid_y.saturating_sub(2)), - ), - ); - - let (_, y) = write_string_to_grid( - &format!("Draft \"{:10}\"", self.draft.headers()["Subject"]), - grid, - Color::Default, - Color::Default, - Attr::Default, - area, - true, - ); - let (_, y) = write_string_to_grid( - "[x] quit without saving", - grid, - Color::Byte(124), - Color::Default, - Attr::Default, - (set_y(upper_left!(area), y + 2), bottom_right!(area)), - true, - ); - let (_, y) = write_string_to_grid( - "[y] save draft and quit", - grid, - Color::Byte(124), - Color::Default, - Attr::Default, - (set_y(upper_left!(area), y + 1), bottom_right!(area)), - true, - ); - write_string_to_grid( - "[n] cancel", - grid, - Color::Byte(124), - Color::Default, - Attr::Default, - (set_y(upper_left!(area), y + 1), bottom_right!(area)), - true, - ); + s.draw(grid, center_area(area, s.content.size()), context); } } @@ -535,6 +476,95 @@ impl Component for Composer { return true; } } + (ViewMode::Discard(_, ref mut selector), _, _) => { + if selector.process_event(event, context) { + if selector.is_done() { + let (u, s) = match std::mem::replace(&mut self.mode, ViewMode::ThreadView) { + ViewMode::Discard(u, s) => (u, s), + _ => unreachable!(), + }; + let key = s.collect()[0] as char; + match key { + 'x' => { + context.replies.push_back(UIEvent::Action(Tab(Kill(u)))); + return true; + } + 'n' => {} + 'y' => { + let mut failure = true; + let draft = std::mem::replace(&mut self.draft, Draft::default()); + + let draft = draft.finalise().unwrap(); + for folder in &[ + &context.accounts[self.account_cursor] + .special_use_folder(SpecialUseMailbox::Drafts), + &context.accounts[self.account_cursor] + .special_use_folder(SpecialUseMailbox::Inbox), + &context.accounts[self.account_cursor] + .special_use_folder(SpecialUseMailbox::Normal), + ] { + if folder.is_none() { + continue; + } + let folder = folder.unwrap(); + if let Err(e) = context.accounts[self.account_cursor].save( + draft.as_bytes(), + folder, + Some(Flag::SEEN | Flag::DRAFT), + ) { + debug!("{:?} could not save draft msg", e); + log( + format!( + "Could not save draft in '{}' folder: {}.", + folder, + e.to_string() + ), + ERROR, + ); + context.replies.push_back(UIEvent::Notification( + Some(format!( + "Could not save draft in '{}' folder.", + folder + )), + e.into(), + Some(NotificationType::ERROR), + )); + } else { + failure = false; + break; + } + } + + if failure { + let file = + create_temp_file(draft.as_bytes(), None, None, false); + debug!("message saved in {}", file.path.display()); + log( + format!( + "Message was stored in {} so that you can restore it manually.", + file.path.display() + ), + INFO, + ); + context.replies.push_back(UIEvent::Notification( + Some("Could not save in any folder".into()), + format!( + "Message was stored in {} so that you can restore it manually.", + file.path.display() + ), + Some(NotificationType::INFO), + )); + } + context.replies.push_back(UIEvent::Action(Tab(Kill(u)))); + return true; + } + _ => {} + } + self.set_dirty(); + } + return true; + } + } _ => {} } if self.form.process_event(event, context) { @@ -574,85 +604,6 @@ impl Component for Composer { UIEvent::Input(Key::Down) => { self.cursor = Cursor::Body; } - UIEvent::Input(Key::Char(key)) if self.mode.is_discard() => { - match (key, &self.mode) { - ('x', ViewMode::Discard(u)) => { - context.replies.push_back(UIEvent::Action(Tab(Kill(*u)))); - return true; - } - ('n', _) => {} - ('y', ViewMode::Discard(u)) => { - let mut failure = true; - let draft = std::mem::replace(&mut self.draft, Draft::default()); - - let draft = draft.finalise().unwrap(); - for folder in &[ - &context.accounts[self.account_cursor] - .special_use_folder(SpecialUseMailbox::Drafts), - &context.accounts[self.account_cursor] - .special_use_folder(SpecialUseMailbox::Inbox), - &context.accounts[self.account_cursor] - .special_use_folder(SpecialUseMailbox::Normal), - ] { - if folder.is_none() { - continue; - } - let folder = folder.unwrap(); - if let Err(e) = context.accounts[self.account_cursor].save( - draft.as_bytes(), - folder, - Some(Flag::SEEN | Flag::DRAFT), - ) { - debug!("{:?} could not save draft msg", e); - log( - format!( - "Could not save draft in '{}' folder: {}.", - folder, - e.to_string() - ), - ERROR, - ); - context.replies.push_back(UIEvent::Notification( - Some(format!("Could not save draft in '{}' folder.", folder)), - e.into(), - Some(NotificationType::ERROR), - )); - } else { - failure = false; - break; - } - } - - if failure { - let file = create_temp_file(draft.as_bytes(), None, None, false); - debug!("message saved in {}", file.path.display()); - log( - format!( - "Message was stored in {} so that you can restore it manually.", - file.path.display() - ), - INFO, - ); - context.replies.push_back(UIEvent::Notification( - Some("Could not save in any folder".into()), - format!( - "Message was stored in {} so that you can restore it manually.", - file.path.display() - ), - Some(NotificationType::INFO), - )); - } - context.replies.push_back(UIEvent::Action(Tab(Kill(*u)))); - return true; - } - _ => { - return false; - } - } - self.mode = ViewMode::ThreadView; - self.set_dirty(); - return true; - } /* Switch to thread view mode if we're on Edit mode */ UIEvent::Input(Key::Char('v')) if self.mode.is_edit() => { self.mode = ViewMode::ThreadView; @@ -818,7 +769,18 @@ impl Component for Composer { } fn kill(&mut self, uuid: Uuid, _context: &mut Context) { - self.mode = ViewMode::Discard(uuid); + self.mode = ViewMode::Discard( + uuid, + Selector::new( + "this draft has unsaved changes", + vec![ + ('x', "quit without saving".to_string()), + ('y', "save draft and quit".to_string()), + ('n', "cancel".to_string()), + ], + true, + ), + ); } fn get_shortcuts(&self, context: &Context) -> ShortcutMaps { @@ -855,7 +817,18 @@ impl Component for Composer { fn can_quit_cleanly(&mut self) -> bool { /* Play it safe and ask user for confirmation */ - self.mode = ViewMode::Discard(self.id); + self.mode = ViewMode::Discard( + self.id, + Selector::new( + "this draft has unsaved changes", + vec![ + ('x', "quit without saving".to_string()), + ('y', "save draft and quit".to_string()), + ('n', "cancel".to_string()), + ], + true, + ), + ); self.set_dirty(); false } diff --git a/ui/src/components/mail/view.rs b/ui/src/components/mail/view.rs index a10d6cae..768ba25d 100644 --- a/ui/src/components/mail/view.rs +++ b/ui/src/components/mail/view.rs @@ -44,7 +44,7 @@ enum ViewMode { Attachment(usize), Raw, Subview, - ContactSelector(Selector), + ContactSelector(Selector), } impl Default for ViewMode { @@ -60,6 +60,12 @@ impl ViewMode { _ => false, } } + fn is_contact_selector(&self) -> bool { + match self { + ViewMode::ContactSelector(_) => true, + _ => false, + } + } } /// Contains an Envelope view, with sticky headers, a pager for the body, and subviews for more @@ -630,16 +636,15 @@ impl Component for MailView { s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); } } - ViewMode::ContactSelector(ref mut s) => { - clear_area(grid, (set_y(upper_left, y + 1), bottom_right)); - 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); } } } + if let ViewMode::ContactSelector(ref mut s) = self.mode { + s.draw(grid, center_area(area, s.content.size()), context); + } self.dirty = false; } @@ -654,6 +659,19 @@ impl Component for MailView { } ViewMode::ContactSelector(ref mut s) => { if s.process_event(event, context) { + if s.is_done() { + if let ViewMode::ContactSelector(s) = + std::mem::replace(&mut self.mode, ViewMode::Normal) + { + let account = &mut context.accounts[self.coordinates.0]; + { + for card in s.collect() { + account.address_book.add_card(card); + } + } + } + self.set_dirty(); + } return true; } } @@ -667,55 +685,22 @@ impl Component for MailView { } match *event { - UIEvent::Input(Key::Char('c')) => { - if let ViewMode::ContactSelector(_) = self.mode { - if let ViewMode::ContactSelector(s) = - std::mem::replace(&mut self.mode, ViewMode::Normal) - { - let account = &mut context.accounts[self.coordinates.0]; - let mut results = Vec::new(); - { - let envelope: &Envelope = &account.get_env(&self.coordinates.2); - for c in s.collect() { - let c = usize::from_ne_bytes({ - [c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]] - }); - for (idx, env) in envelope - .from() - .iter() - .chain(envelope.to().iter()) - .enumerate() - { - if idx != c { - continue; - } - - let mut new_card: Card = Card::new(); - new_card.set_email(env.get_email()); - new_card.set_name(env.get_display_name()); - results.push(new_card); - } - } - } - for c in results { - account.address_book.add_card(c); - } - } - return true; - } + UIEvent::Input(Key::Char('c')) if !self.mode.is_contact_selector() => { let account = &mut context.accounts[self.coordinates.0]; let envelope: &Envelope = &account.get_env(&self.coordinates.2); let mut entries = Vec::new(); - for (idx, env) in envelope - .from() - .iter() - .chain(envelope.to().iter()) - .enumerate() - { - entries.push((idx.to_ne_bytes().to_vec(), format!("{}", env))); + for addr in envelope.from().iter().chain(envelope.to().iter()) { + let mut new_card: Card = Card::new(); + new_card.set_email(addr.get_email()); + new_card.set_name(addr.get_display_name()); + entries.push((new_card, format!("{}", addr))); } - self.mode = ViewMode::ContactSelector(Selector::new(entries, true)); + self.mode = ViewMode::ContactSelector(Selector::new( + "select contacts to add", + entries, + false, + )); self.dirty = true; return true; } diff --git a/ui/src/components/utilities.rs b/ui/src/components/utilities.rs index 02711fbe..7204d1e3 100644 --- a/ui/src/components/utilities.rs +++ b/ui/src/components/utilities.rs @@ -1568,29 +1568,44 @@ impl Component for Tabbed { } } -type EntryIdentifier = Vec; -/// Shows selection to user +#[derive(Debug, Copy, PartialEq, Clone)] +enum SelectorCursor { + /// Cursor is at an entry + Entry(usize), + /// Cursor is located on the Ok button + Ok, + /// Cursor is located on the Cancel button + Cancel, +} + +/// Shows a little window with options for user to select. +/// +/// Instantiate with Selector::new(). Set single_only to true if user should only choose one of the +/// options. After passing input events to this component, check Selector::is_done to see if the +/// user has finalised their choices. Collect the choices by consuming the Selector with +/// Selector::collect() #[derive(Debug, PartialEq, Clone)] -pub struct Selector { - single_only: bool, +pub struct Selector { /// allow only one selection - entries: Vec<(EntryIdentifier, bool)>, - selected_entry_count: u32, - content: CellBuffer, + single_only: bool, + entries: Vec<(T, bool)>, + pub content: CellBuffer, - cursor: usize, + cursor: SelectorCursor, + /// If true, user has finished their selection + done: bool, dirty: bool, id: ComponentId, } -impl fmt::Display for Selector { +impl fmt::Display for Selector { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Display::fmt("Selector", f) } } -impl Component for Selector { +impl Component for Selector { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { let (width, height) = self.content.size(); copy_area_with_break(grid, &self.content, area, ((0, 0), (width, height))); @@ -1598,17 +1613,24 @@ impl Component for Selector { } fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool { let (width, height) = self.content.size(); - match *event { - UIEvent::Input(Key::Char('\t')) => { - self.entries[self.cursor].1 = !self.entries[self.cursor].1; - if self.entries[self.cursor].1 { + match (event, self.cursor) { + (UIEvent::Input(Key::Char('\n')), _) if self.single_only => { + /* User can only select one entry, so Enter key finalises the selection */ + self.done = true; + return true; + } + (UIEvent::Input(Key::Char('\n')), SelectorCursor::Entry(c)) if !self.single_only => { + /* User can select multiple entries, so Enter key toggles the entry under the + * cursor */ + self.entries[c].1 = !self.entries[c].1; + if self.entries[c].1 { write_string_to_grid( "x", &mut self.content, Color::Default, Color::Default, Attr::Default, - ((1, self.cursor), (width, self.cursor)), + ((3, c + 2), (width - 2, c + 2)), false, ); } else { @@ -1618,23 +1640,170 @@ impl Component for Selector { Color::Default, Color::Default, Attr::Default, - ((1, self.cursor), (width, self.cursor)), + ((3, c + 2), (width - 2, c + 2)), false, ); } self.dirty = true; return true; } - UIEvent::Input(Key::Up) if self.cursor > 0 => { - self.cursor -= 1; + (UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => { + self.done = true; + return true; + } + (UIEvent::Input(Key::Char('\n')), SelectorCursor::Cancel) if !self.single_only => { + for e in self.entries.iter_mut() { + e.1 = false; + } + self.done = true; + return true; + } + (UIEvent::Input(Key::Up), SelectorCursor::Entry(c)) if c > 0 => { + if self.single_only { + // Redraw selection + change_colors( + &mut self.content, + ((2, c + 2), (width - 2, c + 2)), + Color::Default, + Color::Default, + ); + change_colors( + &mut self.content, + ((2, c + 1), (width - 2, c + 1)), + Color::Default, + Color::Byte(8), + ); + self.entries[c].1 = false; + self.entries[c - 1].1 = true; + } else { + // Redraw cursor + change_colors( + &mut self.content, + ((2, c + 2), (4, c + 2)), + Color::Default, + Color::Default, + ); + change_colors( + &mut self.content, + ((2, c + 1), (4, c + 1)), + Color::Default, + Color::Byte(8), + ); + } + self.cursor = SelectorCursor::Entry(c - 1); self.dirty = true; return true; } - UIEvent::Input(Key::Down) if self.cursor < height.saturating_sub(1) => { - self.cursor += 1; + (UIEvent::Input(Key::Up), SelectorCursor::Ok) + | (UIEvent::Input(Key::Up), SelectorCursor::Cancel) => { + change_colors( + &mut self.content, + ((width / 2, height - 2), (width - 1, height - 2)), + Color::Default, + Color::Default, + ); + let c = self.entries.len().saturating_sub(1); + self.cursor = SelectorCursor::Entry(c); + change_colors( + &mut self.content, + ((2, c + 2), (4, c + 2)), + Color::Default, + Color::Byte(8), + ); self.dirty = true; return true; } + (UIEvent::Input(Key::Down), SelectorCursor::Entry(c)) + if c < self.entries.len().saturating_sub(1) => + { + if self.single_only { + // Redraw selection + change_colors( + &mut self.content, + ((2, c + 2), (width - 2, c + 2)), + Color::Default, + Color::Default, + ); + change_colors( + &mut self.content, + ((2, c + 3), (width - 2, c + 3)), + Color::Default, + Color::Byte(8), + ); + self.entries[c].1 = false; + self.entries[c + 1].1 = true; + } else { + // Redraw cursor + change_colors( + &mut self.content, + ((2, c + 2), (4, c + 2)), + Color::Default, + Color::Default, + ); + change_colors( + &mut self.content, + ((2, c + 3), (4, c + 3)), + Color::Default, + Color::Byte(8), + ); + } + self.cursor = SelectorCursor::Entry(c + 1); + self.dirty = true; + return true; + } + (UIEvent::Input(Key::Down), SelectorCursor::Entry(c)) if !self.single_only => { + self.cursor = SelectorCursor::Ok; + change_colors( + &mut self.content, + ((2, c + 2), (4, c + 2)), + Color::Default, + Color::Default, + ); + change_colors( + &mut self.content, + ((width / 2, height - 2), (width / 2 + 1, height - 2)), + Color::Default, + Color::Byte(8), + ); + self.dirty = true; + return true; + } + (UIEvent::Input(Key::Down), _) | (UIEvent::Input(Key::Up), _) => return true, + (UIEvent::Input(Key::Right), SelectorCursor::Ok) => { + self.cursor = SelectorCursor::Cancel; + change_colors( + &mut self.content, + ((width / 2, height - 2), (width / 2 + 1, height - 2)), + Color::Default, + Color::Default, + ); + change_colors( + &mut self.content, + ((width / 2 + 6, height - 2), (width / 2 + 11, height - 2)), + Color::Default, + Color::Byte(8), + ); + self.dirty = true; + return true; + } + (UIEvent::Input(Key::Left), SelectorCursor::Cancel) => { + self.cursor = SelectorCursor::Ok; + change_colors( + &mut self.content, + ((width / 2, height - 2), (width / 2 + 1, height - 2)), + Color::Default, + Color::Byte(8), + ); + change_colors( + &mut self.content, + ((width / 2 + 6, height - 2), (width / 2 + 11, height - 2)), + Color::Default, + Color::Default, + ); + self.dirty = true; + return true; + } + (UIEvent::Input(Key::Left), _) | (UIEvent::Input(Key::Right), _) => return true, _ => {} } @@ -1655,44 +1824,179 @@ impl Component for Selector { } } -impl Selector { - pub fn new(mut entries: Vec<(EntryIdentifier, String)>, single_only: bool) -> Selector { - let width = entries - .iter() - .max_by_key(|e| e.1.len()) - .map(|v| v.1.len()) - .unwrap_or(0) - + 4; - let height = entries.len(); +impl Selector { + pub fn new(title: &str, entries: Vec<(T, String)>, single_only: bool) -> Selector { + let width = std::cmp::max( + "OK Cancel".len(), + std::cmp::max( + entries + .iter() + .max_by_key(|e| e.1.len()) + .map(|v| v.1.len()) + .unwrap_or(0), + title.len(), + ), + ) + 7; + let height = entries.len() + + 4 + + if single_only { + 0 + } else { + /* Extra room for buttons Okay/Cancel */ + 3 + }; let mut content = CellBuffer::new(width, height, Cell::with_char(' ')); - let identifiers = entries - .iter_mut() - .map(|(id, _)| (std::mem::replace(&mut *id, Vec::new()), false)) - .collect(); - for (i, e) in entries.into_iter().enumerate() { + write_string_to_grid( + "┏━", + &mut content, + Color::Byte(8), + Color::Default, + Attr::Default, + ((0, 0), (width - 1, 0)), + false, + ); + let (x, _) = write_string_to_grid( + title, + &mut content, + Color::Default, + Color::Default, + Attr::Default, + ((2, 0), (width - 1, 0)), + false, + ); + for i in 1..(width - title.len() - 1) { write_string_to_grid( - &format!("[ ] {}", e.1), + "━", &mut content, + Color::Byte(8), Color::Default, + Attr::Default, + ((x + i, 0), (width - 1, 0)), + false, + ); + } + write_string_to_grid( + "┓", + &mut content, + Color::Byte(8), + Color::Default, + Attr::Default, + ((width - 1, 0), (width - 1, 0)), + false, + ); + write_string_to_grid( + "┗", + &mut content, + Color::Byte(8), + Color::Default, + Attr::Default, + ((0, height - 1), (width - 1, height - 1)), + false, + ); + write_string_to_grid( + &"━".repeat(width - 2), + &mut content, + Color::Byte(8), + Color::Default, + Attr::Default, + ((1, height - 1), (width - 2, height - 1)), + false, + ); + write_string_to_grid( + "┛", + &mut content, + Color::Byte(8), + Color::Default, + Attr::Default, + ((width - 1, height - 1), (width - 1, height - 1)), + false, + ); + for i in 1..height - 1 { + write_string_to_grid( + "┃", + &mut content, + Color::Byte(8), Color::Default, Attr::Default, ((0, i), (width - 1, i)), false, ); + write_string_to_grid( + "┃", + &mut content, + Color::Byte(8), + Color::Default, + Attr::Default, + ((width - 1, i), (width - 1, i)), + false, + ); + } + if single_only { + for (i, e) in entries.iter().enumerate() { + write_string_to_grid( + &e.1, + &mut content, + Color::Default, + if i == 0 { + Color::Byte(8) + } else { + Color::Default + }, + Attr::Default, + ((2, i + 2), (width - 1, i + 2)), + false, + ); + } + } else { + for (i, e) in entries.iter().enumerate() { + write_string_to_grid( + &format!("[ ] {}", e.1), + &mut content, + Color::Default, + Color::Default, + Attr::Default, + ((2, i + 2), (width - 1, i + 2)), + false, + ); + if i == 0 { + content[(2, i + 2)].set_bg(Color::Byte(8)); + content[(3, i + 2)].set_bg(Color::Byte(8)); + content[(4, i + 2)].set_bg(Color::Byte(8)); + } + } + write_string_to_grid( + "OK Cancel", + &mut content, + Color::Default, + Color::Default, + Attr::Bold, + ((width / 2, height - 2), (width - 1, height - 2)), + false, + ); + } + let mut identifiers: Vec<(T, bool)> = + entries.into_iter().map(|(id, _)| (id, false)).collect(); + if single_only { + /* set default option */ + identifiers[0].1 = true; } Selector { single_only, entries: identifiers, - selected_entry_count: 0, content, - cursor: 0, + cursor: SelectorCursor::Entry(0), + done: false, dirty: true, id: ComponentId::new_v4(), } } - pub fn collect(self) -> Vec { + pub fn is_done(&self) -> bool { + self.done + } + + pub fn collect(self) -> Vec { self.entries .into_iter() .filter(|v| v.1) diff --git a/ui/src/terminal/cells.rs b/ui/src/terminal/cells.rs index 0540365a..0db2f079 100644 --- a/ui/src/terminal/cells.rs +++ b/ui/src/terminal/cells.rs @@ -799,3 +799,21 @@ pub fn clear_area(grid: &mut CellBuffer, area: Area) { } } } + +pub fn center_area(area: Area, (width, height): (usize, usize)) -> Area { + let mid_x = { std::cmp::max(width!(area) / 2, width / 2) - width / 2 }; + let mid_y = { std::cmp::max(height!(area) / 2, height / 2) - height / 2 }; + + let (upper_x, upper_y) = upper_left!(area); + let (max_x, max_y) = bottom_right!(area); + ( + ( + std::cmp::min(max_x, upper_x + mid_x), + std::cmp::min(max_y, upper_y + mid_y), + ), + ( + std::cmp::min(max_x, upper_x + mid_x + width), + std::cmp::min(max_y, upper_y + mid_y + height), + ), + ) +}