diff --git a/melib/src/addressbook.rs b/melib/src/addressbook.rs index 71695c43..20803e54 100644 --- a/melib/src/addressbook.rs +++ b/melib/src/addressbook.rs @@ -22,6 +22,7 @@ use chrono::{DateTime, Local}; use uuid::Uuid; use fnv::FnvHashMap; +use std::ops::Deref; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct AddressBook { @@ -71,6 +72,14 @@ impl AddressBook { } } +impl Deref for AddressBook { + type Target = FnvHashMap; + + fn deref(&self) -> &FnvHashMap { + &self.cards + } +} + impl Card { pub fn new() -> Card { @@ -94,6 +103,10 @@ impl Card { } } + pub fn uuid(&self) -> &Uuid { + &self.uuid + } + pub fn title(&self) -> &str { self.title.as_str() } @@ -121,6 +134,9 @@ impl Card { pub fn key(&self) -> &str { self.key.as_str() } + pub fn last_edited(&self) -> String { + self.last_edited.to_rfc2822() + } pub fn set_title(&mut self, new: &str) { self.title = new.to_string();() diff --git a/src/bin.rs b/src/bin.rs index 49585720..c6fe391a 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -64,9 +64,9 @@ fn main() { /* Register some reasonably useful interfaces */ let menu = Entity::from(Box::new(AccountMenu::new(&state.context.accounts))); - let listing = listing::Listing::default(); + let listing = listing::Listing::from(IndexStyle::Compact); let b = Entity::from(Box::new(listing)); - let tabs = Box::new(Tabbed::new(vec![Box::new(VSplit::new(menu, b, 90, true)), Box::new(AccountsPanel::new(&state.context))])); + let tabs = Box::new(Tabbed::new(vec![Box::new(VSplit::new(menu, b, 90, true)), Box::new(AccountsPanel::new(&state.context)), Box::new(ContactManager::default())])); let window = Entity::from(tabs); let status_bar = Entity::from(Box::new(StatusBar::new(window))); diff --git a/ui/src/components/contacts.rs b/ui/src/components/contacts.rs index ba01acd2..1d91e878 100644 --- a/ui/src/components/contacts.rs +++ b/ui/src/components/contacts.rs @@ -21,8 +21,31 @@ use super::*; +macro_rules! write_field { + ($title:expr, $value:expr, $target_grid:expr, $fg_color:expr, $bg_color:expr, $width:expr, $y:expr) => {{ + let (x, y) = write_string_to_grid( + $title, + &mut $target_grid, + $fg_color, + $bg_color, + ((1, $y + 2), ($width - 1, $y + 2)), + false, + ); + write_string_to_grid( + &$value, + &mut $target_grid, + Color::Default, + Color::Default, + ((x, y), ($width - 1, y)), + false, + ); + y + }} +} + #[derive(Debug)] pub struct ContactManager { + pub card: Card, content: CellBuffer, dirty: bool, initialized: bool, @@ -31,7 +54,8 @@ pub struct ContactManager { impl Default for ContactManager { fn default() -> Self { ContactManager { - content: CellBuffer::default(), + card: Card::new(), + content: CellBuffer::new(200, 100, Cell::with_char(' ')), dirty: true, initialized: false, } @@ -45,14 +69,66 @@ impl fmt::Display for ContactManager { } impl ContactManager { + fn initialize(&mut self) { + let (width, height) = self.content.size(); + + let (x, y) = write_string_to_grid( + "Contact Name ", + &mut self.content, + Color::Byte(33), + Color::Default, + ((0, 0), (width, 0)), + false, + ); + let (x, y) = write_string_to_grid( + "Last edited: ", + &mut self.content, + Color::Byte(250), + Color::Default, + ((x, 0), (width, 0)), + false, + ); + let (x, y) = write_string_to_grid( + &self.card.last_edited(), + &mut self.content, + Color::Byte(250), + Color::Default, + ((x, 0), (width, 0)), + false, + ); + for x in 0..width { + set_and_join_box(&mut self.content, (x, 2), HORZ_BOUNDARY); + set_and_join_box(&mut self.content, (x, 4), HORZ_BOUNDARY); + set_and_join_box(&mut self.content, (x, 6), HORZ_BOUNDARY); + set_and_join_box(&mut self.content, (x, 8), HORZ_BOUNDARY); + set_and_join_box(&mut self.content, (x, 10), HORZ_BOUNDARY); + set_and_join_box(&mut self.content, (x, 12), HORZ_BOUNDARY); + set_and_join_box(&mut self.content, (x, 14), HORZ_BOUNDARY); + set_and_join_box(&mut self.content, (x, 16), HORZ_BOUNDARY); + } + for y in 0..height { + set_and_join_box(&mut self.content, (width - 1, y), VERT_BOUNDARY); + } + let mut y = write_field!("First Name: ", self.card.firstname(), self.content, Color::Byte(250), Color::Default, width, 1); + y = write_field!("Last Name: ", self.card.lastname(), self.content, Color::Byte(250), Color::Default, width, y); + y = write_field!("Additional Name: ", self.card.additionalname(), self.content, Color::Byte(250), Color::Default, width, y); + y = write_field!("Name Prefix: ", self.card.name_prefix(), self.content, Color::Byte(250), Color::Default, width, y); + y = write_field!("Name Suffix: ", self.card.name_suffix(), self.content, Color::Byte(250), Color::Default, width, y); + y = write_field!("E-mail: ", self.card.email(), self.content, Color::Byte(250), Color::Default, width, y); + y = write_field!("url: ", self.card.url(), self.content, Color::Byte(250), Color::Default, width, y); + y = write_field!("key: ", self.card.key(), self.content, Color::Byte(250), Color::Default, width, y); + } } impl Component for ContactManager { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { if !self.initialized { - clear_area(grid, area); + self.initialize(); self.initialized = true; } + clear_area(grid, area); + let (width, height) = self.content.size(); + copy_area(grid, &self.content, area, ((0, 0), (width - 1, height -1))); context.dirty_areas.push_back(area); } diff --git a/ui/src/components/mail/compose.rs b/ui/src/components/mail/compose.rs index e68d7fc0..37a2f3cd 100644 --- a/ui/src/components/mail/compose.rs +++ b/ui/src/components/mail/compose.rs @@ -20,15 +20,29 @@ */ use super::*; +use std::dbg; use melib::Draft; use std::str::FromStr; + +#[derive(Debug, PartialEq)] +enum Cursor { + From, + To, + Cc, + Bcc, + Body, + Attachments, +} + #[derive(Debug)] pub struct Composer { reply_context: Option<((usize, usize), Box)>, // (folder_index, thread_node_index) account_cursor: usize, + cursor: Cursor, + pager: Pager, draft: Draft, @@ -43,6 +57,8 @@ impl Default for Composer { reply_context: None, account_cursor: 0, + cursor: Cursor::To, + pager: Pager::default(), draft: Draft::default(), @@ -57,6 +73,7 @@ impl Default for Composer { enum ViewMode { Discard(Uuid), Pager, + Selector(Selector), Overview, } @@ -84,6 +101,14 @@ impl ViewMode { false } } + + fn is_selector(&self) -> bool { + if let ViewMode::Selector(_) = self { + true + } else { + false + } + } } impl fmt::Display for Composer { @@ -145,13 +170,21 @@ impl Composer { let headers = self.draft.headers(); { let (mut x, mut y) = upper_left; - for k in &["Date", "From", "To", "Cc", "Bcc", "Subject"] { + for &k in &["Date", "From", "To", "Cc", "Bcc", "Subject"] { + let bg_color = match self.cursor { + Cursor::Cc if k == "Cc" => Color::Byte(240), + Cursor::Bcc if k == "Bcc" => Color::Byte(240), + Cursor::To if k == "To" => Color::Byte(240), + _ => Color::Default, + }; + + let update = { let (x, y) = write_string_to_grid( k, grid, Color::Default, - Color::Default, + bg_color, ((x, y), set_y(bottom_right, y)), true, ); @@ -159,11 +192,11 @@ impl Composer { ": ", grid, Color::Default, - Color::Default, + bg_color, ((x, y), set_y(bottom_right, y)), true, ); - let (x, y) = if k == &"From" { + let (x, y) = if k == "From" { write_string_to_grid( "◀ ", grid, @@ -176,14 +209,14 @@ impl Composer { (x, y) }; let (x, y) = write_string_to_grid( - &headers[*k], + &headers[k], grid, Color::Default, - Color::Default, + bg_color, ((x, y), set_y(bottom_right, y)), true, ); - if k == &"From" { + if k == "From" { write_string_to_grid( " ▶", grid, @@ -284,64 +317,72 @@ impl Component for Composer { clear_area(grid, header_area); clear_area(grid, body_area); self.draw_header_table(grid, header_area); - self.pager.draw(grid, body_area, context); - /* Let user choose whether to quit with/without saving or cancel */ - if let ViewMode::Discard(_) = self.mode { - let mid_x = width!(area) / 2; - let mid_y = height!(area) / 2; - for x in mid_x - 40..=mid_x + 40 { - for y in mid_y - 11..=mid_y + 11 { - grid[(x, y)] = Cell::default(); + match self.mode { + ViewMode::Overview | ViewMode::Pager => { + self.pager.draw(grid, body_area, context); + }, + ViewMode::Selector(ref mut s) => { + s.draw(grid, body_area, context); + }, + ViewMode::Discard(_) => { + /* Let user choose whether to quit with/without saving or cancel */ + let mid_x = width!(area) / 2; + let mid_y = height!(area) / 2; + for x in mid_x - 40..=mid_x + 40 { + for y in mid_y - 11..=mid_y + 11 { + grid[(x, y)] = Cell::default(); + } } - } - for i in mid_x - 40..=mid_x + 40 { - set_and_join_box(grid, (i, mid_y - 11), HORZ_BOUNDARY); + 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); - } + 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); + 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); - } + set_and_join_box(grid, (mid_x + 40, i), VERT_BOUNDARY); + } - let area = ((mid_x - 20, mid_y - 7), (mid_x + 39, mid_y + 10)); + let area = ((mid_x - 20, mid_y - 7), (mid_x + 39, mid_y + 10)); - let (_, y) = write_string_to_grid( - &format!("Draft \"{:10}\"", self.draft.headers()["Subject"]), - grid, - Color::Default, - Color::Default, - area, - true, - ); - let (_, y) = write_string_to_grid( - "[x] quit without saving", - grid, - Color::Byte(124), - Color::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, - (set_y(upper_left!(area), y + 1), bottom_right!(area)), - true, - ); - write_string_to_grid( - "[n] cancel", - grid, - Color::Byte(124), - Color::Default, - (set_y(upper_left!(area), y + 1), bottom_right!(area)), - true, - ); + let (_, y) = write_string_to_grid( + &format!("Draft \"{:10}\"", self.draft.headers()["Subject"]), + grid, + Color::Default, + Color::Default, + area, + true, + ); + let (_, y) = write_string_to_grid( + "[x] quit without saving", + grid, + Color::Byte(124), + Color::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, + (set_y(upper_left!(area), y + 1), bottom_right!(area)), + true, + ); + write_string_to_grid( + "[n] cancel", + grid, + Color::Byte(124), + Color::Default, + (set_y(upper_left!(area), y + 1), bottom_right!(area)), + true, + ); + + }, } context.dirty_areas.push_back(area); @@ -360,16 +401,81 @@ impl Component for Composer { self.dirty = true; return true; } - } + }, + (ViewMode::Selector(ref mut s), _) => { + if s.process_event(event, context) { + self.dirty = true; + return true; + } + }, _ => {} } match event.event_type { + UIEventType::Input(Key::Up) if self.mode.is_overview() => { + match self.cursor { + Cursor::From => {}, + Cursor::To => { + self.cursor = Cursor::From; + self.dirty = true; + }, + Cursor::Cc => { + self.cursor = Cursor::To; + self.dirty = true; + }, + Cursor::Bcc => { + self.cursor = Cursor::Cc; + self.dirty = true; + }, + Cursor::Body => { + self.cursor = Cursor::Bcc; + self.dirty = true; + }, + _ => {}, + } + return true; + }, + UIEventType::Input(Key::Down) if self.mode.is_overview() => { + match self.cursor { + Cursor::From => { + self.cursor = Cursor::To; + self.dirty = true; + }, + Cursor::To => { + self.cursor = Cursor::Cc; + self.dirty = true; + }, + Cursor::Cc => { + self.cursor = Cursor::Bcc; + self.dirty = true; + }, + Cursor::Bcc => { + self.cursor = Cursor::Body; + self.dirty = true; + }, + Cursor::Body => {}, + _ => {}, + } + return true; + }, + UIEventType::Input(Key::Esc) if self.mode.is_selector() => { + self.mode = ViewMode::Overview; + return true; + }, + UIEventType::Input(Key::Char('\n')) if self.mode.is_selector() => { + let mut old_mode = std::mem::replace(&mut self.mode, ViewMode::Overview); + if let ViewMode::Selector(s) = old_mode { + eprintln!("collected {:?}", s.collect()); + } else { + unreachable!() + } + return true; + }, UIEventType::Resize => { self.set_dirty(); - } + }, /* Switch e-mail From: field to the `left` configured account. */ - UIEventType::Input(Key::Left) => { + UIEventType::Input(Key::Left) if self.cursor == Cursor::From => { self.account_cursor = self.account_cursor.saturating_sub(1); self.draft.headers_mut().insert( "From".into(), @@ -379,7 +485,7 @@ impl Component for Composer { return true; } /* Switch e-mail From: field to the `right` configured account. */ - UIEventType::Input(Key::Right) => { + UIEventType::Input(Key::Right) if self.cursor == Cursor::From => { if self.account_cursor + 1 < context.accounts.len() { self.account_cursor += 1; self.draft.headers_mut().insert( @@ -432,33 +538,48 @@ impl Component for Composer { 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 */ - { - context.input_kill(); - } - let mut f = - create_temp_file(self.draft.to_string().unwrap().as_str().as_bytes(), None); - //let mut f = Box::new(std::fs::File::create(&dir).unwrap()); + match self.cursor { + Cursor::Body => { + /* Edit draft in $EDITOR */ + 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(self.draft.to_string().unwrap().as_str().as_bytes(), 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"); - 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(); + // TODO: check exit status + Command::new("vim") + .arg("+/^$") + .arg(&f.path()) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .output() + .expect("failed to execute process"); + 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(); + }, + Cursor::To | Cursor::Cc | Cursor::Bcc => { + let account = &context.accounts[self.account_cursor]; + let mut entries = account.address_book.values().map(|v| (v.uuid().as_bytes().to_vec(), v.email().to_string())).collect(); + self.mode = ViewMode::Selector(Selector::new(entries, true)); + }, + Cursor::Attachments => { + unimplemented!() + }, + Cursor::From => { + return true; + } + } self.dirty = true; return true; } diff --git a/ui/src/components/mail/listing.rs b/ui/src/components/mail/listing.rs index 4bd6898d..326d7841 100644 --- a/ui/src/components/mail/listing.rs +++ b/ui/src/components/mail/listing.rs @@ -116,3 +116,14 @@ impl Component for Listing { } } } + +impl From for Listing { + fn from(index_style: IndexStyle) -> Self { + match index_style { + IndexStyle::Plain => Listing::Plain(Default::default()), + IndexStyle::Threaded => Listing::Threaded(Default::default()), + IndexStyle::Compact => Listing::Compact(Default::default()), + + } + } +} diff --git a/ui/src/components/mail/listing/thread.rs b/ui/src/components/mail/listing/thread.rs index 3b856afd..9dcc457f 100644 --- a/ui/src/components/mail/listing/thread.rs +++ b/ui/src/components/mail/listing/thread.rs @@ -20,6 +20,7 @@ */ use super::*; +use std::dbg; const MAX_COLS: usize = 500; @@ -434,6 +435,7 @@ impl Component for ThreadListing { /* Draw the entire list */ self.draw_list(grid, area, context); } else { + self.cursor_pos = self.new_cursor_pos; let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); if self.length == 0 && self.dirty { @@ -469,8 +471,9 @@ impl Component for ThreadListing { let account = &mut context.accounts[self.cursor_pos.0]; let (hash, is_seen) = { let mailbox = &mut account[self.cursor_pos.1].as_mut().unwrap(); + eprintln!("key is {}", self.locations[dbg!(self.cursor_pos).2]); let envelope: &Envelope = - &mailbox.collection[&self.locations[self.new_cursor_pos.2]]; + &mailbox.collection[&self.locations[self.cursor_pos.2]]; (envelope.hash(), envelope.is_seen()) }; if !is_seen { @@ -484,7 +487,7 @@ impl Component for ThreadListing { }; let mailbox = &mut account[self.cursor_pos.1].as_mut().unwrap(); let envelope: &mut Envelope = - mailbox.collection.get_mut(&self.locations[self.new_cursor_pos.2]).unwrap(); + mailbox.collection.get_mut(&self.locations[self.cursor_pos.2]).unwrap(); envelope.set_seen(op).unwrap(); true } else { diff --git a/ui/src/components/mail/view.rs b/ui/src/components/mail/view.rs index b588bac4..a3a7c55c 100644 --- a/ui/src/components/mail/view.rs +++ b/ui/src/components/mail/view.rs @@ -395,6 +395,13 @@ impl Component for MailView { match self.mode { ViewMode::ContactSelector(_) => { if let ViewMode::ContactSelector(s) = std::mem::replace(&mut self.mode, ViewMode::Normal) { + for c in s.collect() { + let mut new_card: Card = Card::new(); + let email = String::from_utf8(c).unwrap(); + new_card.set_email(&email); + new_card.set_firstname(""); + context.accounts[self.coordinates.0].address_book.add_card(new_card); + } //eprintln!("{:?}", s.collect()); } return true; diff --git a/ui/src/components/utilities.rs b/ui/src/components/utilities.rs index 4139fae0..da0426df 100644 --- a/ui/src/components/utilities.rs +++ b/ui/src/components/utilities.rs @@ -899,7 +899,7 @@ impl Component for Selector { self.cursor -= 1; return true; }, - UIEventType::Input(Key::Down) if self.cursor < height - 1=> { + UIEventType::Input(Key::Down) if self.cursor < height.saturating_sub(1) => { self.cursor += 1; return true; }, diff --git a/ui/src/conf.rs b/ui/src/conf.rs index b10dd2ae..7e6a1c26 100644 --- a/ui/src/conf.rs +++ b/ui/src/conf.rs @@ -28,11 +28,13 @@ pub mod pager; pub mod accounts; pub use self::accounts::Account; +use self::config::{Config, File, FileFormat}; use melib::conf::AccountSettings; use melib::error::*; use pager::PagerSettings; +use self::serde::{de, Deserialize, Deserializer}; use std::collections::HashMap; use std::env; use std::path::PathBuf; @@ -63,7 +65,8 @@ pub struct FileAccount { draft_folder: String, identity: String, display_name: Option, - threaded: bool, + #[serde(deserialize_with = "index_from_str")] + index: IndexStyle, folders: Option>, } @@ -98,11 +101,8 @@ impl FileAccount { pub fn folder(&self) -> &str { &self.root_folder } - pub fn threaded(&self) -> bool { - self.threaded - } - pub fn toggle_threaded(&mut self) { - self.threaded = !self.threaded; + pub fn index(&self) -> IndexStyle { + self.index } } @@ -136,7 +136,6 @@ pub struct Settings { pub pager: PagerSettings, } -use self::config::{Config, File, FileFormat}; impl FileSettings { pub fn new() -> Result { let config_path = match env::var("MELI_CONFIG") { @@ -183,3 +182,29 @@ impl Settings { } } } + + +#[derive(Copy, Debug, Clone, Deserialize)] +pub enum IndexStyle { + Plain, + Threaded, + Compact, +} + +impl Default for IndexStyle { + fn default() -> Self { + IndexStyle::Compact + } +} + +fn index_from_str<'de, D>(deserializer: D) -> std::result::Result + where D: Deserializer<'de> +{ + let s = ::deserialize(deserializer)?; + match s.as_str() { + "Plain" | "plain" => Ok(IndexStyle::Plain), + "Threaded" | "threaded" => Ok(IndexStyle::Threaded), + "Compact" | "compact" => Ok(IndexStyle::Compact), + _ => Err(de::Error::custom("invalid `index` value")), + } +} diff --git a/ui/src/types/cells.rs b/ui/src/types/cells.rs index 7c196ef7..0669ada9 100644 --- a/ui/src/types/cells.rs +++ b/ui/src/types/cells.rs @@ -170,6 +170,10 @@ impl CellBuffer { } content } + + pub fn is_empty(&self) -> bool { + self.buf.is_empty() + } } impl HasSize for CellBuffer { @@ -566,6 +570,11 @@ pub fn copy_area_with_break( ); return upper_left!(dest); } + + if grid_src.is_empty() || grid_dest.is_empty() { + return upper_left!(dest); + } + let mut ret = bottom_right!(dest); let mut src_x = get_x(upper_left!(src)); let mut src_y = get_y(upper_left!(src)); @@ -609,6 +618,10 @@ pub fn copy_area(grid_dest: &mut CellBuffer, grid_src: &CellBuffer, dest: Area, return upper_left!(dest); } + if grid_src.is_empty() || grid_dest.is_empty() { + return upper_left!(dest); + } + let mut ret = bottom_right!(dest); let mut src_x = get_x(upper_left!(src)); let mut src_y = get_y(upper_left!(src));