From b98a04f35bffa29392c66cc8094f0c1572ebdac4 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 11 Aug 2018 18:00:21 +0300 Subject: [PATCH] Make backend folders completely agnostic (remove maildir logic from conf) --- benches/maildir.rs | 10 +- melib/src/conf/mod.rs | 111 +-- melib/src/error.rs | 2 +- melib/src/mailbox/accounts.rs | 27 +- melib/src/mailbox/backends/imap.rs | 9 +- melib/src/mailbox/backends/maildir.rs | 189 +++-- melib/src/mailbox/backends/mbox.rs | 7 +- melib/src/mailbox/backends/mod.rs | 59 +- melib/src/mailbox/email/attachment_types.rs | 8 +- melib/src/mailbox/email/attachments.rs | 64 +- melib/src/mailbox/email/mod.rs | 27 + melib/src/mailbox/email/parser.rs | 47 +- melib/src/mailbox/mod.rs | 21 +- src/bin.rs | 11 +- src/python/mod.rs | 9 +- ui/src/components/mail/compose.rs | 45 ++ ui/src/components/mail/listing/compact.rs | 679 ++++++++++++++++++ .../mail/{listing.rs => listing/mod.rs} | 16 +- ui/src/components/mail/mod.rs | 28 +- ui/src/components/mail/view/html.rs | 8 + ui/src/components/mail/view/mod.rs | 97 ++- ui/src/components/mail/view/thread.rs | 101 +++ ui/src/components/mod.rs | 15 +- ui/src/components/notifications.rs | 8 + ui/src/components/utilities.rs | 154 +++- ui/src/compose/mod.rs | 1 + ui/src/state.rs | 15 +- ui/src/types/helpers.rs | 9 +- 28 files changed, 1472 insertions(+), 305 deletions(-) create mode 100644 ui/src/components/mail/compose.rs create mode 100644 ui/src/components/mail/listing/compact.rs rename ui/src/components/mail/{listing.rs => listing/mod.rs} (98%) create mode 100644 ui/src/components/mail/view/thread.rs diff --git a/benches/maildir.rs b/benches/maildir.rs index dae3c506f..c83ecd723 100644 --- a/benches/maildir.rs +++ b/benches/maildir.rs @@ -9,35 +9,35 @@ use self::test::Bencher; #[bench] fn bench_threads_1(b: &mut Bencher) { b.iter(|| { - let folder = Folder::new(String::from(""), vec![]); + let folder = Folder::new(String::from(""), String::from(""), vec![]); MaildirType::new("").multicore(1, &folder) }); } #[bench] fn bench_threads_2(b: &mut Bencher) { b.iter(|| { - let folder = Folder::new(String::from(""), vec![]); + let folder = Folder::new(String::from(""), String::from(""), vec![]); MaildirType::new("").multicore(2, &folder) }); } #[bench] fn bench_threads_3(b: &mut Bencher) { b.iter(|| { - let folder = Folder::new(String::from(""), vec![]); + let folder = Folder::new(String::from(""), String::from(""), vec![]); MaildirType::new("").multicore(3, &folder) }); } #[bench] fn bench_threads_4(b: &mut Bencher) { b.iter(|| { - let folder = Folder::new(String::from(""), vec![]); + let folder = Folder::new(String::from(""), String::from(""), vec![]); MaildirType::new("").multicore(4, &folder) }); } #[bench] fn bench_threads_6(b: &mut Bencher) { b.iter(|| { - let folder = Folder::new(String::from(""), vec![]); + let folder = Folder::new(String::from(""), String::from(""), vec![]); MaildirType::new("").multicore(6, &folder) }); } diff --git a/melib/src/conf/mod.rs b/melib/src/conf/mod.rs index 2ddc0f788..0a9b04551 100644 --- a/melib/src/conf/mod.rs +++ b/melib/src/conf/mod.rs @@ -26,53 +26,22 @@ pub mod pager; use pager::PagerSettings; -use std::collections::hash_map::DefaultHasher; use std::collections::HashMap; -use std::fs; -use std::hash::Hasher; -use std::path::{Path, PathBuf}; - -#[derive(Debug, Default, Clone)] -pub struct Folder { - hash: u64, - name: String, - path: String, - children: Vec, -} - -impl Folder { - pub fn new(path: String, file_name: String, children: Vec) -> Self { - let mut h = DefaultHasher::new(); - h.write(&path.as_bytes()); - Folder { - hash: h.finish(), - name: file_name, - path: path, - children: children, - } - } - pub fn hash(&self) -> u64 { - self.hash - } - pub fn path(&self) -> &str { - &self.path - } - pub fn name(&self) -> &str { - &self.name - } - pub fn children(&self) -> &Vec { - &self.children - } -} #[derive(Debug, Deserialize)] -struct FileAccount { - folders: String, +pub struct FileAccount { + root_folder: String, format: String, sent_folder: String, threaded: bool, } +impl FileAccount { + pub fn folder(&self) -> &str { + &self.root_folder + } +} + #[derive(Debug, Deserialize)] struct FileSettings { accounts: HashMap, @@ -82,7 +51,7 @@ struct FileSettings { #[derive(Debug, Clone)] pub struct AccountSettings { name: String, - pub folders: Vec, + root_folder: String, format: String, pub sent_folder: String, pub threaded: bool, @@ -95,6 +64,9 @@ impl AccountSettings { pub fn name(&self) -> &str { &self.name } + pub fn root_folder(&self) -> &str { + &self.root_folder + } } #[derive(Debug, Clone, Default)] @@ -126,53 +98,20 @@ impl Settings { let mut s: HashMap = HashMap::new(); for (id, x) in fs.accounts { - let mut folders = Vec::new(); - fn recurse_folders>(folders: &mut Vec, p: P) -> Vec { - let mut children = Vec::new(); - for mut f in fs::read_dir(p).unwrap() { - for f in f.iter_mut() { - { - let path = f.path(); - if path.ends_with("cur") - || path.ends_with("new") - || path.ends_with("tmp") - { - continue; - } - if path.is_dir() { - let path_children = recurse_folders(folders, &path); - folders.push(Folder::new( - path.to_str().unwrap().to_string(), - path.file_name().unwrap().to_str().unwrap().to_string(), - path_children, - )); - children.push(folders.len() - 1); - } - } - } - } - children + let format = x.format.to_lowercase(); + let sent_folder = x.sent_folder; + let threaded = x.threaded; + let root_folder = x.root_folder; + + let acc = AccountSettings { + name: id.clone(), + root_folder, + format, + sent_folder, + threaded, }; - let path = PathBuf::from(&x.folders); - let path_children = recurse_folders(&mut folders, &path); - if path.is_dir() { - folders.push(Folder::new( - path.to_str().unwrap().to_string(), - path.file_name().unwrap().to_str().unwrap().to_string(), - path_children, - )); - } - //folders.sort_by(|a, b| b.name.cmp(&a.name)); - s.insert( - id.clone(), - AccountSettings { - name: id.clone(), - folders: folders, - format: x.format.to_lowercase(), - sent_folder: x.sent_folder.clone(), - threaded: x.threaded, - }, - ); + + s.insert(id, acc); } Settings { diff --git a/melib/src/error.rs b/melib/src/error.rs index 02a219850..1bdd9021d 100644 --- a/melib/src/error.rs +++ b/melib/src/error.rs @@ -23,11 +23,11 @@ * An error object for `melib` */ +use std::borrow::Cow; use std::error::Error; use std::fmt; use std::io; use std::result; -use std::borrow::Cow; use nom; diff --git a/melib/src/mailbox/accounts.rs b/melib/src/mailbox/accounts.rs index 837b5627d..d9d6cceaa 100644 --- a/melib/src/mailbox/accounts.rs +++ b/melib/src/mailbox/accounts.rs @@ -24,7 +24,7 @@ */ use async::*; -use conf::{AccountSettings, Folder}; +use conf::AccountSettings; use mailbox::backends::{Backends, RefreshEventConsumer}; use mailbox::*; use std::ops::{Index, IndexMut}; @@ -47,15 +47,15 @@ pub struct Account { } impl Account { - pub fn new(name: String, settings: AccountSettings, backends: &Backends) -> Self { - let sent_folder = settings - .folders + pub fn new(name: String, settings: AccountSettings, map: &Backends) -> Self { + let backend = map.get(settings.format())(&settings); + let ref_folders: Vec = backend.folders(); + let mut folders: Vec>> = Vec::with_capacity(ref_folders.len()); + let mut workers: Vec = Vec::new(); + let sent_folder = ref_folders .iter() - .position(|x| *x.path() == settings.sent_folder); - let mut folders = Vec::with_capacity(settings.folders.len()); - let mut workers = Vec::new(); - let backend = backends.get(settings.format()); - for f in &settings.folders { + .position(|x: &Folder| x.name() == settings.sent_folder); + for f in ref_folders { folders.push(None); let mut handle = backend.get(&f); workers.push(Some(handle)); @@ -73,14 +73,14 @@ impl Account { } } pub fn watch(&self, r: RefreshEventConsumer) -> () { - self.backend.watch(r, &self.settings.folders[..]); + self.backend.watch(r).unwrap(); } /* This doesn't represent the number of correctly parsed mailboxes though */ pub fn len(&self) -> usize { self.folders.len() } pub fn list_folders(&self) -> Vec { - self.settings.folders.clone() + self.backend.folders() } pub fn name(&self) -> &str { &self.name @@ -89,7 +89,8 @@ impl Account { &mut self.workers } fn load_mailbox(&mut self, index: usize, envelopes: Result>) -> () { - let folder = &self.settings.folders[index]; + let folders = self.backend.folders(); + let folder = &folders[index]; if self.sent_folder.is_some() { let id = self.sent_folder.unwrap(); if id == index { @@ -105,7 +106,7 @@ impl Account { ) } }; - let sent_path = &self.settings.folders[id]; + let sent_path = &folders[id]; if sent[0].is_none() { sent[0] = Some(Mailbox::new(sent_path, &None, envelopes.clone())); } diff --git a/melib/src/mailbox/backends/imap.rs b/melib/src/mailbox/backends/imap.rs index 6ff26cb7a..6f5ab1abf 100644 --- a/melib/src/mailbox/backends/imap.rs +++ b/melib/src/mailbox/backends/imap.rs @@ -19,10 +19,10 @@ * along with meli. If not, see . */ +/* use async::*; -use conf::Folder; use error::Result; -use mailbox::backends::{MailBackend, RefreshEventConsumer}; +use mailbox::backends::{MailBackend, RefreshEventConsumer, Folder}; use mailbox::email::Envelope; /// `BackendOp` implementor for Imap @@ -35,7 +35,6 @@ impl ImapOp { } } -/* impl BackendOp for ImapOp { fn description(&self) -> String { @@ -53,7 +52,7 @@ impl BackendOp for ImapOp { fn fetch_flags(&self) -> Flag { unimplemented!(); } -}*/ +} /// Imap backend #[derive(Debug)] @@ -72,4 +71,4 @@ impl ImapType { pub fn new(_path: &str) -> Self { ImapType {} } -} +}*/ diff --git a/melib/src/mailbox/backends/maildir.rs b/melib/src/mailbox/backends/maildir.rs index 7a88053d9..ba7a0dc0d 100644 --- a/melib/src/mailbox/backends/maildir.rs +++ b/melib/src/mailbox/backends/maildir.rs @@ -20,10 +20,11 @@ */ use async::*; -use conf::Folder; +use conf::AccountSettings; use error::{MeliError, Result}; use mailbox::backends::{ - BackendOp, BackendOpGenerator, MailBackend, RefreshEvent, RefreshEventConsumer, + BackendFolder, BackendOp, BackendOpGenerator, Folder, MailBackend, RefreshEvent, + RefreshEventConsumer, }; use mailbox::email::parser; use mailbox::email::{Envelope, Flag}; @@ -43,7 +44,7 @@ use memmap::{Mmap, Protection}; use std::collections::hash_map::DefaultHasher; use std::fs; use std::hash::Hasher; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// `BackendOp` implementor for Maildir #[derive(Debug, Default)] @@ -152,38 +153,42 @@ impl BackendOp for MaildirOp { /// Maildir backend https://cr.yp.to/proto/maildir.html #[derive(Debug)] pub struct MaildirType { + folders: Vec, path: String, - idx: (usize, usize), } impl MailBackend for MaildirType { + fn folders(&self) -> Vec { + self.folders.iter().map(|f| f.clone()).collect() + } fn get(&self, folder: &Folder) -> Async>> { self.multicore(4, folder) } - fn watch(&self, sender: RefreshEventConsumer, folders: &[Folder]) -> () { - let folders = folders.to_vec(); - + fn watch(&self, sender: RefreshEventConsumer) -> Result<()> { + let (tx, rx) = channel(); + let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); + for f in &self.folders { + if f.is_valid().is_err() { + continue; + } + eprintln!("watching {:?}", f); + let mut p = PathBuf::from(&f.path); + p.push("cur"); + watcher.watch(&p, RecursiveMode::NonRecursive).unwrap(); + p.pop(); + p.push("new"); + watcher.watch(&p, RecursiveMode::NonRecursive).unwrap(); + } thread::Builder::new() .name("folder watch".to_string()) .spawn(move || { - let (tx, rx) = channel(); - let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); - for f in folders { - if MaildirType::is_valid(&f).is_err() { - continue; - } - eprintln!("watching {}", f.path()); - let mut p = PathBuf::from(&f.path()); - p.push("cur"); - watcher.watch(&p, RecursiveMode::NonRecursive).unwrap(); - p.pop(); - p.push("new"); - watcher.watch(&p, RecursiveMode::NonRecursive).unwrap(); - } + // Move `watcher` in the closure's scope so that it doesn't get dropped. + let _watcher = watcher; loop { match rx.recv() { Ok(event) => match event { - DebouncedEvent::Create(mut pathbuf) | DebouncedEvent::Remove(mut pathbuf) => { + DebouncedEvent::Create(mut pathbuf) + | DebouncedEvent::Remove(mut pathbuf) => { let path = if pathbuf.is_dir() { if pathbuf.ends_with("cur") | pathbuf.ends_with("new") { pathbuf.pop(); @@ -207,45 +212,76 @@ impl MailBackend for MaildirType { Err(e) => eprintln!("watch error: {:?}", e), } } - }) - .unwrap(); + })?; + Ok(()) } } impl MaildirType { - pub fn new(path: &str, idx: (usize, usize)) -> Self { - MaildirType { - path: path.to_string(), - idx: idx, - } - } - fn is_valid(f: &Folder) -> Result<()> { - let path = f.path(); - let mut p = PathBuf::from(path); - for d in &["cur", "new", "tmp"] { - p.push(d); - if !p.is_dir() { - return Err(MeliError::new(format!( - "{} is not a valid maildir folder", - path - ))); + pub fn new(f: &AccountSettings) -> Self { + let mut folders: Vec = Vec::new(); + fn recurse_folders>(folders: &mut Vec, p: P) -> Vec { + let mut children = Vec::new(); + for mut f in fs::read_dir(p).unwrap() { + for f in f.iter_mut() { + { + let path = f.path(); + if path.ends_with("cur") || path.ends_with("new") || path.ends_with("tmp") { + continue; + } + if path.is_dir() { + let path_children = recurse_folders(folders, &path); + if let Ok(f) = MaildirFolder::new( + path.to_str().unwrap().to_string(), + path.file_name().unwrap().to_str().unwrap().to_string(), + path_children, + ) { + folders.push(f); + children.push(folders.len() - 1); + } + } + } + } + } + children + }; + let path = PathBuf::from(f.root_folder()); + let path_children = recurse_folders(&mut folders, &path); + if path.is_dir() { + if let Ok(f) = MaildirFolder::new( + path.to_str().unwrap().to_string(), + path.file_name().unwrap().to_str().unwrap().to_string(), + path_children, + ) { + folders.push(f); } - p.pop(); } - Ok(()) + MaildirType { + folders, + path: f.root_folder().to_string(), + } } + fn owned_folder_idx(&self, folder: &Folder) -> usize { + for (idx, f) in self.folders.iter().enumerate() { + if f.hash() == folder.hash() { + return idx; + } + } + unreachable!() + } + pub fn multicore(&self, cores: usize, folder: &Folder) -> Async>> { let mut w = AsyncBuilder::new(); let handle = { let tx = w.tx(); // TODO: Avoid clone - let folder = folder.clone(); + let folder: &MaildirFolder = &self.folders[self.owned_folder_idx(folder)]; + let path = folder.path().to_string(); + let name = format!("parsing {:?}", folder.name()); thread::Builder::new() - .name(format!("parsing {:?}", folder)) + .name(name) .spawn(move || { - MaildirType::is_valid(&folder)?; - let path = folder.path(); let mut path = PathBuf::from(path); path.push("cur"); let iter = path.read_dir()?; @@ -310,3 +346,64 @@ impl MaildirType { w.build(handle) } } + +#[derive(Debug, Default)] +pub struct MaildirFolder { + hash: u64, + name: String, + path: String, + children: Vec, +} + +impl MaildirFolder { + pub fn new(path: String, file_name: String, children: Vec) -> Result { + let mut h = DefaultHasher::new(); + h.write(&path.as_bytes()); + + let ret = MaildirFolder { + hash: h.finish(), + name: file_name, + path: path, + children: children, + }; + ret.is_valid()?; + Ok(ret) + } + pub fn path(&self) -> &str { + &self.path + } + fn is_valid(&self) -> Result<()> { + let path = self.path(); + let mut p = PathBuf::from(path); + for d in &["cur", "new", "tmp"] { + p.push(d); + if !p.is_dir() { + return Err(MeliError::new(format!( + "{} is not a valid maildir folder", + path + ))); + } + p.pop(); + } + Ok(()) + } +} +impl BackendFolder for MaildirFolder { + fn hash(&self) -> u64 { + self.hash + } + fn name(&self) -> &str { + &self.name + } + fn children(&self) -> &Vec { + &self.children + } + fn clone(&self) -> Folder { + Box::new(MaildirFolder { + hash: self.hash, + name: self.name.clone(), + path: self.path.clone(), + children: self.children.clone(), + }) + } +} diff --git a/melib/src/mailbox/backends/mbox.rs b/melib/src/mailbox/backends/mbox.rs index 0701666a0..db38a8b12 100644 --- a/melib/src/mailbox/backends/mbox.rs +++ b/melib/src/mailbox/backends/mbox.rs @@ -23,10 +23,10 @@ * https://wiki2.dovecot.org/MailboxFormat/mbox */ +/* use async::*; -use conf::Folder; use error::Result; -use mailbox::backends::{MailBackend, RefreshEventConsumer}; +use mailbox::backends::{MailBackend, RefreshEventConsumer, Folder}; use mailbox::email::Envelope; /// `BackendOp` implementor for Mbox @@ -39,7 +39,6 @@ impl MboxOp { } } -/* impl BackendOp for MboxOp { fn description(&self) -> String { unimplemented!(); @@ -60,7 +59,6 @@ impl BackendOp for MboxOp { unimplemented!() } } -*/ /// Mbox backend #[derive(Debug)] @@ -80,3 +78,4 @@ impl MboxType { MboxType {} } } +*/ diff --git a/melib/src/mailbox/backends/mod.rs b/melib/src/mailbox/backends/mod.rs index 2e998d980..f2e7fcc9c 100644 --- a/melib/src/mailbox/backends/mod.rs +++ b/melib/src/mailbox/backends/mod.rs @@ -23,22 +23,25 @@ pub mod maildir; pub mod mbox; use async::*; -use conf::Folder; +use conf::AccountSettings; use error::Result; -use mailbox::backends::imap::ImapType; +//use mailbox::backends::imap::ImapType; +//use mailbox::backends::mbox::MboxType; use mailbox::backends::maildir::MaildirType; -use mailbox::backends::mbox::MboxType; use mailbox::email::{Envelope, Flag}; use std::fmt; +use std::fmt::Debug; extern crate fnv; use self::fnv::FnvHashMap; use std; +pub type BackendCreator = Box Box>; + /// A hashmap containing all available mail backends. /// An abstraction over any available backends. pub struct Backends { - map: FnvHashMap Box>>, + map: FnvHashMap BackendCreator>>, } impl Backends { @@ -48,20 +51,20 @@ impl Backends { }; b.register( "maildir".to_string(), - Box::new(|| Box::new(MaildirType::new("", (0, 0)))), + Box::new(|| Box::new(|f| Box::new(MaildirType::new(f)))), ); - b.register("mbox".to_string(), Box::new(|| Box::new(MboxType::new("")))); - b.register("imap".to_string(), Box::new(|| Box::new(ImapType::new("")))); + //b.register("mbox".to_string(), Box::new(|| Box::new(MboxType::new("")))); + //b.register("imap".to_string(), Box::new(|| Box::new(ImapType::new("")))); b } - pub fn get(&self, key: &str) -> Box { + pub fn get(&self, key: &str) -> BackendCreator { if !self.map.contains_key(key) { panic!("{} is not a valid mail backend", key); } self.map[key]() } - pub fn register(&mut self, key: String, backend: Box Box>) -> () { + pub fn register(&mut self, key: String, backend: Box BackendCreator>) -> () { if self.map.contains_key(&key) { panic!("{} is an already registered backend", key); } @@ -90,8 +93,8 @@ impl RefreshEventConsumer { } pub trait MailBackend: ::std::fmt::Debug { fn get(&self, folder: &Folder) -> Async>>; - fn watch(&self, sender: RefreshEventConsumer, folders: &[Folder]) -> (); - //fn new(folders: &Vec) -> Box; + fn watch(&self, sender: RefreshEventConsumer) -> Result<()>; + fn folders(&self) -> Vec; //login function } @@ -168,3 +171,37 @@ impl fmt::Debug for BackendOpGenerator { write!(f, "BackendOpGenerator: {}", op.description()) } } + +pub trait BackendFolder: Debug { + fn hash(&self) -> u64; + fn name(&self) -> &str; + fn clone(&self) -> Folder; + fn children(&self) -> &Vec; +} + +#[derive(Debug)] +struct DummyFolder { + v: Vec, +} + +impl BackendFolder for DummyFolder { + fn hash(&self) -> u64 { + 0 + } + fn name(&self) -> &str { + "" + } + fn clone(&self) -> Folder { + folder_default() + } + fn children(&self) -> &Vec { + &self.v + } +} +pub fn folder_default() -> Folder { + Box::new(DummyFolder { + v: Vec::with_capacity(0), + }) +} + +pub type Folder = Box; diff --git a/melib/src/mailbox/email/attachment_types.rs b/melib/src/mailbox/email/attachment_types.rs index 36d7596db..0e73550b9 100644 --- a/melib/src/mailbox/email/attachment_types.rs +++ b/melib/src/mailbox/email/attachment_types.rs @@ -20,7 +20,7 @@ impl Default for Charset { } } -impl<'a> From<&'a[u8]> for Charset { +impl<'a> From<&'a [u8]> for Charset { fn from(b: &'a [u8]) -> Self { // TODO: Case insensitivity match b { @@ -69,9 +69,10 @@ pub enum ContentType { impl Default for ContentType { fn default() -> Self { - ContentType::Text{ charset: Charset::UTF8 } + ContentType::Text { + charset: Charset::UTF8, + } } - } impl Display for ContentType { @@ -128,4 +129,3 @@ pub enum ContentTransferEncoding { QuotedPrintable, Other { tag: Vec }, } - diff --git a/melib/src/mailbox/email/attachments.rs b/melib/src/mailbox/email/attachments.rs index 9952f0520..8f0cfea39 100644 --- a/melib/src/mailbox/email/attachments.rs +++ b/melib/src/mailbox/email/attachments.rs @@ -18,10 +18,10 @@ * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ -use std::fmt; -use std::str; use data_encoding::BASE64_MIME; use mailbox::email::parser; +use std::fmt; +use std::str; pub use mailbox::email::attachment_types::*; @@ -44,7 +44,14 @@ impl fmt::Debug for AttachmentType { match self { AttachmentType::Data { .. } => write!(f, "AttachmentType::Data {{ .. }}"), AttachmentType::Text { .. } => write!(f, "AttachmentType::Text {{ .. }}"), - AttachmentType::Multipart { of_type, subattachments } => write!(f, "AttachmentType::Multipart {{ of_type: {:?},\nsubattachments: {:?} }}", of_type, subattachments), + AttachmentType::Multipart { + of_type, + subattachments, + } => write!( + f, + "AttachmentType::Multipart {{ of_type: {:?},\nsubattachments: {:?} }}", + of_type, subattachments + ), } } } @@ -96,7 +103,7 @@ impl fmt::Display for AttachmentType { impl AttachmentBuilder { pub fn new(content: &[u8]) -> Self { AttachmentBuilder { - content_type: (Default::default() , ContentSubType::Plain), + content_type: (Default::default(), ContentSubType::Plain), content_transfer_encoding: ContentTransferEncoding::_7Bit, raw: content.to_vec(), } @@ -124,7 +131,9 @@ impl AttachmentBuilder { self.content_type.0 = Default::default(); for (n, v) in params { if n.eq_ignore_ascii_case(b"charset") { - self.content_type.0 = ContentType::Text { charset: Charset::from(v) }; + self.content_type.0 = ContentType::Text { + charset: Charset::from(v), + }; break; } } @@ -168,7 +177,7 @@ impl AttachmentBuilder { fn decode(&self) -> Vec { // TODO merge this and standalone decode() function let charset = match self.content_type.0 { - ContentType::Text{ charset: c } => c, + ContentType::Text { charset: c } => c, _ => Default::default(), }; @@ -180,13 +189,17 @@ impl AttachmentBuilder { ContentTransferEncoding::QuotedPrintable => parser::quoted_printable_bytes(&self.raw) .to_full_result() .unwrap(), - ContentTransferEncoding::_7Bit - | ContentTransferEncoding::_8Bit - | ContentTransferEncoding::Other { .. } => self.raw.to_vec(), + ContentTransferEncoding::_7Bit + | ContentTransferEncoding::_8Bit + | ContentTransferEncoding::Other { .. } => self.raw.to_vec(), }; let decoded_result = parser::decode_charset(&bytes, charset); - decoded_result.as_ref().map(|v| v.as_bytes()).unwrap_or_else(|_| &self.raw).to_vec() + decoded_result + .as_ref() + .map(|v| v.as_bytes()) + .unwrap_or_else(|_| &self.raw) + .to_vec() } pub fn build(self) -> Attachment { let attachment_type = match self.content_type.0 { @@ -197,7 +210,9 @@ impl AttachmentBuilder { let multipart_type = match self.content_type.1 { ContentSubType::Other { ref tag } => match &tag[..] { b"mixed" | b"Mixed" | b"MIXED" => MultipartType::Mixed, - b"alternative" | b"Alternative" | b"ALTERNATIVE" => MultipartType::Alternative, + b"alternative" | b"Alternative" | b"ALTERNATIVE" => { + MultipartType::Alternative + } b"digest" | b"Digest" | b"DIGEST" => MultipartType::Digest, _ => MultipartType::Unsupported { tag: tag.clone() }, }, @@ -261,14 +276,15 @@ impl AttachmentBuilder { } } - impl fmt::Display for Attachment { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.attachment_type { AttachmentType::Data { .. } => { write!(f, "Data attachment of type {}", self.mime_type()) } - AttachmentType::Text { .. } => write!(f, "Text attachment of type {}", self.mime_type()), + AttachmentType::Text { .. } => { + write!(f, "Text attachment of type {}", self.mime_type()) + } AttachmentType::Multipart { of_type: ref multipart_type, subattachments: ref sub_att_vec, @@ -276,10 +292,16 @@ impl fmt::Display for Attachment { write!( f, "{} attachment with {} subs", - self.mime_type(), sub_att_vec.len() + self.mime_type(), + sub_att_vec.len() ) } else { - write!(f, "{} attachment with {} subs", self.mime_type(), sub_att_vec.len()) + write!( + f, + "{} attachment with {} subs", + self.mime_type(), + sub_att_vec.len() + ) }, } } @@ -340,7 +362,7 @@ impl Attachment { for a in sub_att_vec { count_recursive(a, ret); } - }, + } } } @@ -374,7 +396,7 @@ fn decode_rec_helper(a: &Attachment, filter: &Option Vec< return filter(a); } match a.attachment_type { - AttachmentType::Data { .. } => { Vec::new()}, + AttachmentType::Data { .. } => Vec::new(), AttachmentType::Text { .. } => decode_helper(a, filter), AttachmentType::Multipart { of_type: ref multipart_type, @@ -405,7 +427,7 @@ fn decode_helper(a: &Attachment, filter: &Option Vec> } let charset = match a.content_type.0 { - ContentType::Text{ charset: c } => c, + ContentType::Text { charset: c } => c, _ => Default::default(), }; @@ -424,7 +446,11 @@ fn decode_helper(a: &Attachment, filter: &Option Vec> if a.content_type().0.is_text() { let decoded_result = parser::decode_charset(&bytes, charset); - decoded_result.as_ref().map(|v| v.as_bytes()).unwrap_or_else(|_| a.bytes()).to_vec() + decoded_result + .as_ref() + .map(|v| v.as_bytes()) + .unwrap_or_else(|_| a.bytes()) + .to_vec() } else { bytes.to_vec() } diff --git a/melib/src/mailbox/email/mod.rs b/melib/src/mailbox/email/mod.rs index 8f3039a70..14814a485 100644 --- a/melib/src/mailbox/email/mod.rs +++ b/melib/src/mailbox/email/mod.rs @@ -203,6 +203,33 @@ bitflags! { } } +#[derive(Debug, Clone, Default)] +pub struct EnvelopeBuilder { + from: Option>, + to: Vec
, + body: Option, + in_reply_to: Option, + flags: Flag, +} + +impl EnvelopeBuilder { + pub fn new() -> Self { + Default::default() + } + + pub fn build(self) -> Envelope { + unimplemented!(); + + /* + * 1. Check for date. Default is now + * 2. + Envelope { + + + */ + } +} + /// `Envelope` represents all the data of an email we need to know. /// /// Attachments (the email's body) is parsed on demand with `body`. diff --git a/melib/src/mailbox/email/parser.rs b/melib/src/mailbox/email/parser.rs index dcad4940e..cd0e9c128 100644 --- a/melib/src/mailbox/email/parser.rs +++ b/melib/src/mailbox/email/parser.rs @@ -166,7 +166,7 @@ named!(pub attachment<(std::vec::Vec<(&[u8], &[u8])>, &[u8])>, /* TODO: make a map of encodings and decoding functions so that they can be reused and easily * extended */ -use encoding::all::{ISO_8859_1, ISO_8859_2, ISO_8859_7, WINDOWS_1253, WINDOWS_1252, GBK}; +use encoding::all::{ISO_8859_1, ISO_8859_2, ISO_8859_7, WINDOWS_1252, WINDOWS_1253, GBK}; fn encoded_word(input: &[u8]) -> IResult<&[u8], Vec> { if input.len() < 5 { @@ -266,33 +266,15 @@ fn encoded_word(input: &[u8]) -> IResult<&[u8], Vec> { pub fn decode_charset(s: &[u8], charset: Charset) -> Result { match charset { - Charset::UTF8 | Charset::Ascii => { - Ok(String::from_utf8(s.to_vec()).unwrap()) - } - Charset::ISO8859_7 => { - Ok(ISO_8859_7.decode(s, DecoderTrap::Strict)?) - } - Charset::ISO8859_1 => { - Ok(ISO_8859_1.decode(s, DecoderTrap::Strict)?) - } - Charset::ISO8859_2 => { - Ok(ISO_8859_2.decode(s, DecoderTrap::Strict)?) - } - Charset::GBK => { - Ok(GBK.decode(s, DecoderTrap::Strict)?) - } - Charset::Windows1252 => { - Ok(WINDOWS_1252.decode(s, DecoderTrap::Strict)?) - }, - Charset::Windows1253 => { - Ok(WINDOWS_1253.decode(s, DecoderTrap::Strict)?) - }, - Charset::GB2312 => { - unimplemented!() - }, - Charset::UTF16 => { - unimplemented!() - }, + Charset::UTF8 | Charset::Ascii => Ok(String::from_utf8(s.to_vec()).unwrap()), + Charset::ISO8859_7 => Ok(ISO_8859_7.decode(s, DecoderTrap::Strict)?), + Charset::ISO8859_1 => Ok(ISO_8859_1.decode(s, DecoderTrap::Strict)?), + Charset::ISO8859_2 => Ok(ISO_8859_2.decode(s, DecoderTrap::Strict)?), + Charset::GBK => Ok(GBK.decode(s, DecoderTrap::Strict)?), + Charset::Windows1252 => Ok(WINDOWS_1252.decode(s, DecoderTrap::Strict)?), + Charset::Windows1253 => Ok(WINDOWS_1253.decode(s, DecoderTrap::Strict)?), + Charset::GB2312 => unimplemented!(), + Charset::UTF16 => unimplemented!(), } } @@ -322,12 +304,10 @@ named!( pub quoted_printable_bytes>, many0!(alt_complete!( preceded!(quoted_printable_soft_break, quoted_printable_byte) | - preceded!(quoted_printable_soft_break, le_u8) - | quoted_printable_byte | le_u8 + preceded!(quoted_printable_soft_break, le_u8) | quoted_printable_byte | le_u8 )) ); - named!( encoded_word_list>, ws!(do_parse!( @@ -397,7 +377,10 @@ fn display_addr(input: &[u8]) -> IResult<&[u8], Address> { IResult::Done(rest, raw) => { display_name.length = raw.find(b"<").unwrap().saturating_sub(1); address_spec.offset = display_name.length + 2; - address_spec.length = raw.len().saturating_sub(display_name.length).saturating_sub(3); + address_spec.length = raw + .len() + .saturating_sub(display_name.length) + .saturating_sub(3); IResult::Done( rest, Address::Mailbox(MailboxAddress { diff --git a/melib/src/mailbox/mod.rs b/melib/src/mailbox/mod.rs index 9e0a8a6a0..22be77f04 100644 --- a/melib/src/mailbox/mod.rs +++ b/melib/src/mailbox/mod.rs @@ -30,18 +30,16 @@ pub use self::email::*; /* Mail backends. Currently only maildir is supported */ pub mod backends; use error::Result; -use mailbox::backends::MailBackend; +use mailbox::backends::{folder_default, Folder, MailBackend}; pub mod accounts; pub use mailbox::accounts::Account; pub mod thread; pub use mailbox::thread::{build_threads, Container}; -use conf::Folder; - use std::option::Option; /// `Mailbox` represents a folder of mail. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Mailbox { pub folder: Folder, pub collection: Vec, @@ -49,10 +47,21 @@ pub struct Mailbox { pub threads: Vec, } +impl Clone for Mailbox { + fn clone(&self) -> Self { + Mailbox { + folder: self.folder.clone(), + collection: self.collection.clone(), + threaded_collection: self.threaded_collection.clone(), + threads: self.threads.clone(), + } + } +} + impl Mailbox { pub fn new_dummy() -> Self { Mailbox { - folder: Folder::default(), + folder: folder_default(), collection: Vec::with_capacity(0), threaded_collection: Vec::with_capacity(0), threads: Vec::with_capacity(0), @@ -67,7 +76,7 @@ impl Mailbox { collection.sort_by(|a, b| a.date().cmp(&b.date())); let (threads, threaded_collection) = build_threads(&mut collection, sent_folder); Ok(Mailbox { - folder: folder.clone(), + folder: (*folder).clone(), collection: collection, threads: threads, threaded_collection: threaded_collection, diff --git a/src/bin.rs b/src/bin.rs index ccbca5062..df422ebd4 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -59,9 +59,7 @@ fn make_input_thread( sx.send(ThreadEvent::Input(k)); }, || { - sx.send(ThreadEvent::UIEvent(UIEventType::ChangeMode( - UIMode::Fork, - ))); + sx.send(ThreadEvent::UIEvent(UIEventType::ChangeMode(UIMode::Fork))); }, &rx, ) @@ -104,9 +102,10 @@ fn main() { let b = Entity { component: Box::new(listing), }; - let window = Entity { - component: Box::new(VSplit::new(menu, b, 90, true)), - }; + let mut tabs = Box::new(Tabbed::new(vec![Box::new(VSplit::new(menu, b, 90, true))])); + tabs.add_component(Box::new(Composer {})); + let window = Entity { component: tabs }; + let status_bar = Entity { component: Box::new(StatusBar::new(window)), }; diff --git a/src/python/mod.rs b/src/python/mod.rs index f4ededd37..e3e9c9a0b 100644 --- a/src/python/mod.rs +++ b/src/python/mod.rs @@ -1,22 +1,21 @@ #![cfg(feature = "python")] use pyo3::prelude::*; - #[pymodinit(pythonmeli)] fn pythonmeli(py: Python, m: &PyModule) -> PyResult<()> { // pyo3 aware function. All of our python interface could be declared in a separate module. // Note that the `#[pyfn()]` annotation automatically converts the arguments from // Python objects to Rust values; and the Rust return value back into a Python object. #[pyfn(m, "sum_as_string")] - fn sum_as_string_py(_py: Python, a:i64, b:i64) -> PyResult { - let out = sum_as_string(a, b); - Ok(out) + fn sum_as_string_py(_py: Python, a: i64, b: i64) -> PyResult { + let out = sum_as_string(a, b); + Ok(out) } Ok(()) } // logic implemented as a normal rust function -fn sum_as_string(a:i64, b:i64) -> String { +fn sum_as_string(a: i64, b: i64) -> String { format!("{}", a + b).to_string() } diff --git a/ui/src/components/mail/compose.rs b/ui/src/components/mail/compose.rs new file mode 100644 index 000000000..0a01eea84 --- /dev/null +++ b/ui/src/components/mail/compose.rs @@ -0,0 +1,45 @@ +/* + * meli - ui crate + * + * Copyright 2017-2018 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use super::*; + +pub struct Composer {} + +impl fmt::Display for Composer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display subject/info + write!(f, "compose") + } +} + +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); + } + + fn process_event(&mut self, event: &UIEvent, context: &mut Context) {} + + fn is_dirty(&self) -> bool { + true + } + fn set_dirty(&mut self) {} +} diff --git a/ui/src/components/mail/listing/compact.rs b/ui/src/components/mail/listing/compact.rs new file mode 100644 index 000000000..71159b3da --- /dev/null +++ b/ui/src/components/mail/listing/compact.rs @@ -0,0 +1,679 @@ +/* + * meli - ui crate. + * + * Copyright 2017-2018 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use super::*; +const MAX_COLS: usize = 500; + +/// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the thread's content in a +/// `ThreadView`. +pub struct CompactMailListing { + /// (x, y, z): x is accounts, y is folders, z is index inside a folder. + cursor_pos: (usize, usize, usize), + new_cursor_pos: (usize, usize, usize), + length: usize, + sort: (SortField, SortOrder), + //subsort: (SortField, SortOrder), + /// Cache current view. + content: CellBuffer, + /// If we must redraw on next redraw event + dirty: bool, + /// If `self.view` exists or not. + unfocused: bool, + view: Option, +} + +impl Default for CompactMailListing { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Display for CompactMailListing { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "mail") + } +} + +impl CompactMailListing { + pub fn new() -> Self { + let content = CellBuffer::new(0, 0, Cell::with_char(' ')); + CompactMailListing { + cursor_pos: (0, 1, 0), + new_cursor_pos: (0, 0, 0), + length: 0, + sort: (SortField::Date, SortOrder::Desc), + //subsort: (SortField::Date, SortOrder::Asc), + content: content, + dirty: true, + unfocused: false, + view: None, + } + } + /// Fill the `self.content` `CellBuffer` with the contents of the account folder the user has + /// chosen. + fn refresh_mailbox(&mut self, context: &mut Context) { + self.dirty = true; + self.cursor_pos.2 = 0; + self.new_cursor_pos.2 = 0; + self.cursor_pos.1 = self.new_cursor_pos.1; + self.cursor_pos.0 = self.new_cursor_pos.0; + + // Inform State that we changed the current folder view. + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::RefreshMailbox((self.cursor_pos.0, self.cursor_pos.1)), + }); + // Get mailbox as a reference. + // + loop { + // TODO: Show progress visually + if let Ok(()) = context.accounts[self.cursor_pos.0].status(self.cursor_pos.1) { + break; + } + } + let mailbox = &mut context.accounts[self.cursor_pos.0][self.cursor_pos.1] + .as_ref() + .unwrap(); + + self.length = mailbox.threads.len(); + let mut 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, + 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.threaded_collection.clone(); + let threads: &Vec = &mailbox.threads; + 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 + .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() { + Color::Byte(0) + } else { + Color::Default + }; + let bg_color = if !envelope.is_seen() { + Color::Byte(251) + } else if thread_idx % 2 == 0 { + Color::Byte(236) + } else { + Color::Default + }; + let (x, _) = write_string_to_grid( + &CompactMailListing::make_thread_entry( + envelope, + idx, + indentation, + container, + &indentations, + len, + ), + &mut 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); + } + + 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] + } else { + &mailbox.collection[idx] + }; + + let fg_color = if !envelope.is_seen() { + Color::Byte(0) + } else { + Color::Default + }; + let bg_color = if self.cursor_pos.2 == idx { + Color::Byte(246) + } else if !envelope.is_seen() { + Color::Byte(251) + } else if idx % 2 == 0 { + Color::Byte(236) + } else { + Color::Default + }; + change_colors(grid, area, fg_color, bg_color); + } + + /// Draw the list of `Envelope`s. + fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + if self.cursor_pos.1 != self.new_cursor_pos.1 { + self.refresh_mailbox(context); + } + let upper_left = upper_left!(area); + let bottom_right = bottom_right!(area); + if self.length == 0 { + clear_area(grid, area); + copy_area(grid, &self.content, area, ((0, 0), (MAX_COLS - 1, 0))); + context.dirty_areas.push_back(area); + return; + } + let rows = get_y(bottom_right) - get_y(upper_left) + 1; + let prev_page_no = (self.cursor_pos.2).wrapping_div(rows); + let page_no = (self.new_cursor_pos.2).wrapping_div(rows); + + let top_idx = page_no * rows; + + /* If cursor position has changed, remove the highlight from the previous position and + * apply it in the new one. */ + if self.cursor_pos.2 != self.new_cursor_pos.2 && prev_page_no == page_no { + let old_cursor_pos = self.cursor_pos; + self.cursor_pos = self.new_cursor_pos; + for idx in &[old_cursor_pos.2, self.new_cursor_pos.2] { + if *idx >= self.length { + continue; //bounds check + } + let new_area = ( + set_y(upper_left, get_y(upper_left) + (*idx % rows)), + set_y(bottom_right, get_y(upper_left) + (*idx % rows)), + ); + self.highlight_line(grid, new_area, *idx, context); + context.dirty_areas.push_back(new_area); + } + return; + } else if self.cursor_pos != self.new_cursor_pos { + self.cursor_pos = self.new_cursor_pos; + } + + /* Page_no has changed, so draw new page */ + copy_area( + grid, + &self.content, + area, + ((0, top_idx), (MAX_COLS - 1, self.length)), + ); + self.highlight_line( + grid, + ( + set_y(upper_left, get_y(upper_left) + (self.cursor_pos.2 % rows)), + set_y(bottom_right, get_y(upper_left) + (self.cursor_pos.2 % rows)), + ), + self.cursor_pos.2, + context, + ); + context.dirty_areas.push_back(area); + } + + fn make_thread_entry( + envelope: &Envelope, + idx: usize, + indent: usize, + container: &Container, + indentations: &[bool], + idx_width: usize, + ) -> 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().count_attachments(); + if attach_count > 1 { + s.push_str(&format!(" {}∞ ", attach_count - 1)); + } + s + } + fn format_date(envelope: &Envelope) -> String { + let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(envelope.date()); + let now: std::time::Duration = std::time::SystemTime::now().duration_since(d).unwrap(); + match now.as_secs() { + n if n < 10 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(8)), + n if n < 24 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(7)), + n if n < 4 * 24 * 60 * 60 => { + format!("{} days ago{}", n / (24 * 60 * 60), " ".repeat(9)) + } + _ => envelope.datetime().format("%Y-%m-%d %H:%M:%S").to_string(), + } + } +} + +impl Component for CompactMailListing { + fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + if !self.unfocused { + if !self.is_dirty() { + return; + } + self.dirty = false; + /* Draw the entire list */ + self.draw_list(grid, area, context); + } else { + let upper_left = upper_left!(area); + let bottom_right = bottom_right!(area); + if self.length == 0 && self.dirty { + clear_area(grid, area); + context.dirty_areas.push_back(area); + } + + /* Render the mail body in a pager, basically copy what HSplit does */ + let total_rows = get_y(bottom_right) - get_y(upper_left); + let pager_ratio = context.runtime_settings.pager.pager_ratio; + let bottom_entity_rows = (pager_ratio * total_rows) / 100; + + if bottom_entity_rows > total_rows { + clear_area(grid, area); + context.dirty_areas.push_back(area); + return; + } + /* Mark message as read */ + let idx = self.cursor_pos.2; + let must_highlight = { + if self.length == 0 { + false + } else { + let threaded = context.accounts[self.cursor_pos.0] + .runtime_settings + .threaded; + let mailbox = &mut context.accounts[self.cursor_pos.0][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] + }; + if !envelope.is_seen() { + envelope.set_seen().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.as_mut().unwrap().draw( + grid, + (set_y(upper_left, mid + 1), bottom_right), + context, + ); + self.dirty = false; + } + } + fn process_event(&mut self, event: &UIEvent, context: &mut Context) { + match event.event_type { + UIEventType::Input(Key::Up) => { + if self.cursor_pos.2 > 0 { + self.new_cursor_pos.2 -= 1; + self.dirty = true; + } + } + UIEventType::Input(Key::Down) => { + if self.length > 0 && self.new_cursor_pos.2 < self.length - 1 { + self.new_cursor_pos.2 += 1; + self.dirty = true; + } + } + UIEventType::Input(Key::Char('\n')) if !self.unfocused => { + 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; + self.view = None; + } + UIEventType::Input(Key::Char(k @ 'J')) | UIEventType::Input(Key::Char(k @ 'K')) => { + let folder_length = context.accounts[self.cursor_pos.0].len(); + let accounts_length = context.accounts.len(); + match k { + 'J' if folder_length > 0 => { + if self.new_cursor_pos.1 < folder_length - 1 { + self.new_cursor_pos.1 = self.cursor_pos.1 + 1; + self.dirty = true; + self.refresh_mailbox(context); + } else if accounts_length > 0 && self.new_cursor_pos.0 < accounts_length - 1 + { + self.new_cursor_pos.0 = self.cursor_pos.0 + 1; + self.new_cursor_pos.1 = 0; + self.dirty = true; + self.refresh_mailbox(context); + } + } + 'K' => { + if self.cursor_pos.1 > 0 { + self.new_cursor_pos.1 = self.cursor_pos.1 - 1; + self.dirty = true; + self.refresh_mailbox(context); + } else if self.cursor_pos.0 > 0 { + self.new_cursor_pos.0 = self.cursor_pos.0 - 1; + self.new_cursor_pos.1 = 0; + self.dirty = true; + self.refresh_mailbox(context); + } + } + _ => {} + } + } + UIEventType::Input(Key::Char(k @ 'h')) | UIEventType::Input(Key::Char(k @ 'l')) => { + let accounts_length = context.accounts.len(); + match k { + 'h' if accounts_length > 0 && self.new_cursor_pos.0 < accounts_length - 1 => { + self.new_cursor_pos.0 = self.cursor_pos.0 + 1; + self.new_cursor_pos.1 = 0; + self.dirty = true; + self.refresh_mailbox(context); + } + 'l' if self.cursor_pos.0 > 0 => { + self.new_cursor_pos.0 = self.cursor_pos.0 - 1; + self.new_cursor_pos.1 = 0; + self.dirty = true; + self.refresh_mailbox(context); + } + _ => {} + } + } + UIEventType::RefreshMailbox(_) => { + self.dirty = true; + 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); + self.dirty = true; + } + } + UIEventType::ChangeMode(UIMode::Normal) => { + self.dirty = true; + } + UIEventType::Resize => { + self.dirty = true; + } + UIEventType::Action(ref action) => match action { + Action::MailListing(MailListingAction::ToggleThreaded) => { + context.accounts[self.cursor_pos.0] + .runtime_settings + .threaded = !context.accounts[self.cursor_pos.0] + .runtime_settings + .threaded; + self.refresh_mailbox(context); + self.dirty = true; + return; + } + Action::ViewMailbox(idx) => { + self.new_cursor_pos.1 = *idx; + self.dirty = true; + self.refresh_mailbox(context); + return; + } + Action::Sort(field, order) => { + self.sort = (field.clone(), order.clone()); + 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) + } + fn set_dirty(&mut self) { + self.dirty = true; + } +} diff --git a/ui/src/components/mail/listing.rs b/ui/src/components/mail/listing/mod.rs similarity index 98% rename from ui/src/components/mail/listing.rs rename to ui/src/components/mail/listing/mod.rs index f6484b473..d16228af0 100644 --- a/ui/src/components/mail/listing.rs +++ b/ui/src/components/mail/listing/mod.rs @@ -21,6 +21,9 @@ use super::*; +mod compact; +pub use self::compact::*; + const MAX_COLS: usize = 500; /// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the `Envelope` content in a @@ -31,7 +34,7 @@ pub struct MailListing { 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 @@ -47,6 +50,12 @@ impl Default for MailListing { } } +impl fmt::Display for MailListing { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "mail") + } +} + impl MailListing { /// Helper function to format entry strings for MailListing */ /* TODO: Make this configurable */ @@ -66,7 +75,7 @@ impl MailListing { new_cursor_pos: (0, 0, 0), length: 0, sort: (SortField::Date, SortOrder::Desc), - subsort: (SortField::Date, SortOrder::Asc), + //subsort: (SortField::Date, SortOrder::Asc), content: content, dirty: true, unfocused: false, @@ -728,4 +737,7 @@ impl Component for MailListing { fn is_dirty(&self) -> bool { self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false) } + fn set_dirty(&mut self) { + self.dirty = true; + } } diff --git a/ui/src/components/mail/mod.rs b/ui/src/components/mail/mod.rs index e04e44c49..96cfc8b82 100644 --- a/ui/src/components/mail/mod.rs +++ b/ui/src/components/mail/mod.rs @@ -22,11 +22,14 @@ /*! Entities that handle Mail specific functions. */ use super::*; +use melib::backends::Folder; pub mod listing; -pub mod view; pub use listing::*; +pub mod view; pub use view::*; +mod compose; +pub use self::compose::*; #[derive(Debug)] struct AccountMenuEntry { @@ -43,6 +46,13 @@ pub struct AccountMenu { cursor: Option<(usize, usize)>, } +impl fmt::Display for AccountMenu { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display subject/info + write!(f, "menu") + } +} + impl AccountMenu { pub fn new(accounts: &[Account]) -> Self { let accounts = accounts @@ -53,8 +63,10 @@ impl AccountMenu { index: i, entries: { let mut entries = Vec::with_capacity(a.len()); - for (idx, acc) in a.list_folders().iter().enumerate() { - entries.push((idx, acc.clone())); + let mut idx = 0; + for acc in a.list_folders() { + entries.push((idx, acc)); + idx += 1; } entries }, @@ -187,12 +199,7 @@ impl AccountMenu { ); if highlight && idx > 1 && self.cursor.unwrap().1 == idx - 2 { - change_colors( - grid, - ((x, y), (get_x(bottom_right), y)), - color_fg, - color_bg, - ); + change_colors(grid, ((x, y), (get_x(bottom_right), y)), color_fg, color_bg); } else { change_colors(grid, ((x, y), set_y(bottom_right, y)), color_fg, color_bg); } @@ -240,4 +247,7 @@ impl Component for AccountMenu { fn is_dirty(&self) -> bool { self.dirty } + fn set_dirty(&mut self) { + self.dirty = true; + } } diff --git a/ui/src/components/mail/view/html.rs b/ui/src/components/mail/view/html.rs index d8812c4dc..9ccbbf7a0 100644 --- a/ui/src/components/mail/view/html.rs +++ b/ui/src/components/mail/view/html.rs @@ -54,6 +54,13 @@ impl HtmlView { } } +impl fmt::Display for HtmlView { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display subject/info + write!(f, "view") + } +} + impl Component for HtmlView { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { self.pager.draw(grid, area, context); @@ -90,4 +97,5 @@ impl Component for HtmlView { fn is_dirty(&self) -> bool { self.pager.is_dirty() } + fn set_dirty(&mut self) {} } diff --git a/ui/src/components/mail/view/mod.rs b/ui/src/components/mail/view/mod.rs index 17a290c5a..25c7733e6 100644 --- a/ui/src/components/mail/view/mod.rs +++ b/ui/src/components/mail/view/mod.rs @@ -24,8 +24,9 @@ use linkify::{Link, LinkFinder}; use std::process::{Command, Stdio}; mod html; - pub use self::html::*; +mod thread; +pub use self::thread::*; use mime_apps::query_default_app; @@ -59,6 +60,13 @@ pub struct MailView { cmd_buf: String, } +impl fmt::Display for MailView { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display subject/info + write!(f, "view mail") + } +} + impl MailView { pub fn new( coordinates: (usize, usize, usize), @@ -80,23 +88,34 @@ impl MailView { fn attachment_to_text(&self, body: Attachment) -> String { let finder = LinkFinder::new(); let body_text = if body.content_type().0.is_text() && body.content_type().1.is_html() { - let mut s = String::from("Text piped through `w3m`. Press `v` to open in web browser. \n\n"); + let mut s = + String::from("Text piped through `w3m`. Press `v` to open in web browser. \n\n"); s.extend( - String::from_utf8_lossy(&decode(&body, Some(Box::new(|a: &Attachment| { - use std::io::Write; - use std::process::{Command, Stdio}; + String::from_utf8_lossy(&decode( + &body, + Some(Box::new(|a: &Attachment| { + use std::io::Write; + use std::process::{Command, Stdio}; - let raw = decode(a, None); - 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"); + let raw = decode(a, None); + 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(&raw).expect("Failed to write to w3m stdin"); - html_filter.wait_with_output().unwrap().stdout - })))).into_owned().chars()); + html_filter + .stdin + .as_mut() + .unwrap() + .write_all(&raw) + .expect("Failed to write to w3m stdin"); + html_filter.wait_with_output().unwrap().stdout + })), + )).into_owned() + .chars(), + ); s } else { String::from_utf8_lossy(&decode_rec(&body, None)).into() @@ -105,13 +124,14 @@ impl MailView { ViewMode::Normal | ViewMode::Subview => { let mut t = body_text.to_string(); if body.count_attachments() > 1 { - t = body.attachments().iter().enumerate().fold( - t, - |mut s, (idx, a)| { + t = body + .attachments() + .iter() + .enumerate() + .fold(t, |mut s, (idx, a)| { s.push_str(&format!("[{}] {}\n\n", idx, a)); s - }, - ); + }); } t } @@ -131,13 +151,14 @@ impl MailView { t.insert_str(l.start() + offset, &format!("[{}]", lidx)); } if body.count_attachments() > 1 { - t = body.attachments().iter().enumerate().fold( - t, - |mut s, (idx, a)| { + t = body + .attachments() + .iter() + .enumerate() + .fold(t, |mut s, (idx, a)| { s.push_str(&format!("[{}] {}\n\n", idx, a)); s - }, - ); + }); } t } @@ -190,9 +211,7 @@ impl Component for MailView { let (envelope_idx, y): (usize, usize) = { let accounts = &mut context.accounts; - let threaded = accounts[self.coordinates.0] - .runtime_settings - .threaded; + let threaded = accounts[self.coordinates.0].runtime_settings.threaded; let mailbox = &mut accounts[self.coordinates.0][self.coordinates.1] .as_ref() .unwrap(); @@ -291,12 +310,15 @@ impl Component for MailView { let body = envelope.body(); match self.mode { ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => { - self.subview = Some(Box::new(HtmlView::new(decode(&body.attachments()[aidx], None)))); - }, + self.subview = Some(Box::new(HtmlView::new(decode( + &body.attachments()[aidx], + None, + )))); + } ViewMode::Normal if body.is_html() => { self.subview = Some(Box::new(HtmlView::new(decode(&body, None)))); self.mode = ViewMode::Subview; - }, + } _ => { let buf = { let text = self.attachment_to_text(body); @@ -309,7 +331,7 @@ impl Component for MailView { self.pager.as_mut().map(|p| p.cursor_pos()) }; self.pager = Some(Pager::from_buf(&buf, cursor_pos)); - }, + } }; self.dirty = false; } @@ -351,9 +373,7 @@ impl Component for MailView { { let accounts = &mut context.accounts; - let threaded = accounts[self.coordinates.0] - .runtime_settings - .threaded; + let threaded = accounts[self.coordinates.0].runtime_settings.threaded; let mailbox = &mut accounts[self.coordinates.0][self.coordinates.1] .as_ref() .unwrap(); @@ -424,9 +444,7 @@ impl Component for MailView { self.cmd_buf.clear(); let url = { let accounts = &mut context.accounts; - let threaded = accounts[self.coordinates.0] - .runtime_settings - .threaded; + let threaded = accounts[self.coordinates.0].runtime_settings.threaded; let mailbox = &mut accounts[self.coordinates.0][self.coordinates.1] .as_ref() .unwrap(); @@ -482,4 +500,7 @@ impl Component for MailView { || self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false) || self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false) } + fn set_dirty(&mut self) { + self.dirty = true; + } } diff --git a/ui/src/components/mail/view/thread.rs b/ui/src/components/mail/view/thread.rs new file mode 100644 index 000000000..065e22c6f --- /dev/null +++ b/ui/src/components/mail/view/thread.rs @@ -0,0 +1,101 @@ +/* + * meli - ui crate. + * + * Copyright 2017-2018 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use super::*; +use std::io::Write; +use std::process::{Command, Stdio}; + +pub struct ThreadView { + pager: Pager, + bytes: Vec, +} + +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 } + } +} + +impl fmt::Display for ThreadView { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display subject/info + write!(f, "view thread") + } +} + +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; + } + _ => {} + } + self.pager.process_event(event, context); + } + fn is_dirty(&self) -> bool { + self.pager.is_dirty() + } + fn set_dirty(&mut self) {} +} diff --git a/ui/src/components/mod.rs b/ui/src/components/mod.rs index 490c6f1c8..e8c8d89ec 100644 --- a/ui/src/components/mod.rs +++ b/ui/src/components/mod.rs @@ -35,6 +35,10 @@ pub mod notifications; pub mod utilities; pub use self::utilities::*; +use std::fmt; +use std::fmt::Display; +use std::ops::Deref; + use super::{Key, UIEvent, UIEventType}; /// The upper and lower boundary char. const HORZ_BOUNDARY: char = '─'; @@ -65,6 +69,14 @@ pub struct Entity { pub component: Box, // more than one? } +impl Deref for Entity { + type Target = Box; + + fn deref(&self) -> &Box { + &self.component + } +} + impl Entity { /// Pass events to child component. pub fn rcv_event(&mut self, event: &UIEvent, context: &mut Context) { @@ -75,12 +87,13 @@ impl Entity { /// Types implementing this Trait can draw on the terminal and receive events. /// If a type wants to skip drawing if it has not changed anything, it can hold some flag in its /// fields (eg self.dirty = false) and act upon that in their `draw` implementation. -pub trait Component { +pub trait Component: Display { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context); fn process_event(&mut self, event: &UIEvent, context: &mut Context); fn is_dirty(&self) -> bool { true } + fn set_dirty(&mut self); } // TODO: word break. diff --git a/ui/src/components/notifications.rs b/ui/src/components/notifications.rs index 1478420b9..d659b8f01 100644 --- a/ui/src/components/notifications.rs +++ b/ui/src/components/notifications.rs @@ -29,6 +29,13 @@ use super::*; /// Passes notifications to the OS using the XDG specifications. pub struct XDGNotifications {} +impl fmt::Display for XDGNotifications { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display subject/info + write!(f, "") + } +} + impl Component for XDGNotifications { fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, _context: &mut Context) {} fn process_event(&mut self, event: &UIEvent, _context: &mut Context) { @@ -41,4 +48,5 @@ impl Component for XDGNotifications { .unwrap(); } } + fn set_dirty(&mut self) {} } diff --git a/ui/src/components/utilities.rs b/ui/src/components/utilities.rs index 9ced99f6d..6d0033046 100644 --- a/ui/src/components/utilities.rs +++ b/ui/src/components/utilities.rs @@ -31,6 +31,13 @@ pub struct HSplit { ratio: usize, // bottom/whole height * 100 } +impl fmt::Display for HSplit { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display subject/info + self.top.fmt(f) + } +} + impl HSplit { pub fn new(top: Entity, bottom: Entity, ratio: usize, show_divider: bool) -> Self { HSplit { @@ -79,6 +86,10 @@ impl Component for HSplit { fn is_dirty(&self) -> bool { self.top.component.is_dirty() || self.bottom.component.is_dirty() } + fn set_dirty(&mut self) { + self.top.component.set_dirty(); + self.bottom.component.set_dirty(); + } } /// A vertically split in half container. @@ -90,6 +101,13 @@ pub struct VSplit { ratio: usize, // right/(container width) * 100 } +impl fmt::Display for VSplit { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display focused entity + self.right.fmt(f) + } +} + impl VSplit { pub fn new(left: Entity, right: Entity, ratio: usize, show_divider: bool) -> Self { VSplit { @@ -155,6 +173,10 @@ impl Component for VSplit { fn is_dirty(&self) -> bool { self.left.component.is_dirty() || self.right.component.is_dirty() } + fn set_dirty(&mut self) { + self.left.component.set_dirty(); + self.right.component.set_dirty(); + } } /// A pager for text. @@ -168,6 +190,13 @@ pub struct Pager { content: CellBuffer, } +impl fmt::Display for Pager { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display info + write!(f, "pager") + } +} + impl Pager { 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(); @@ -325,6 +354,9 @@ impl Component for Pager { fn is_dirty(&self) -> bool { self.dirty } + fn set_dirty(&mut self) { + self.dirty = true; + } } /// Status bar. @@ -338,6 +370,13 @@ pub struct StatusBar { dirty: bool, } +impl fmt::Display for StatusBar { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display info + write!(f, "status bar") + } +} + impl StatusBar { pub fn new(container: Entity) -> Self { StatusBar { @@ -497,6 +536,9 @@ impl Component for StatusBar { fn is_dirty(&self) -> bool { self.dirty || self.container.component.is_dirty() } + fn set_dirty(&mut self) { + self.dirty = true; + } } // A box with a text content. @@ -510,11 +552,17 @@ impl TextBox { } } +impl fmt::Display for TextBox { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display info + write!(f, "text box") + } +} + impl Component for TextBox { fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, _context: &mut Context) {} - fn process_event(&mut self, _event: &UIEvent, _context: &mut Context) { - return; - } + fn process_event(&mut self, _event: &UIEvent, _context: &mut Context) {} + fn set_dirty(&mut self) {} } pub struct Progress { @@ -552,6 +600,13 @@ impl Progress { } } +impl fmt::Display for Progress { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display info + write!(f, "progress bar") + } +} + impl Component for Progress { fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, _context: &mut Context) { unimplemented!() @@ -559,4 +614,97 @@ impl Component for Progress { fn process_event(&mut self, _event: &UIEvent, _context: &mut Context) { return; } + fn set_dirty(&mut self) {} +} + +pub struct Tabbed { + children: Vec>, + cursor_pos: usize, +} + +impl Tabbed { + pub fn new(children: Vec>) -> Self { + Tabbed { + children, + cursor_pos: 0, + } + } + fn draw_tabs(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + let mut x = get_x(upper_left!(area)); + let mut y: usize = get_y(upper_left!(area)); + for (idx, c) in self.children.iter().enumerate() { + let (fg, bg) = if idx == self.cursor_pos { + (Color::Default, Color::Default) + } else { + (Color::Byte(15), Color::Byte(8)) + }; + let (x_, _y_) = write_string_to_grid( + &format!(" {} ", c), + grid, + fg, + bg, + (set_x(upper_left!(area), x), bottom_right!(area)), + false, + ); + x = x_ + 1; + if y != _y_ { + break; + } + y = _y_; + } + let (cols, _) = grid.size(); + let cslice: &mut [Cell] = grid; + for c in cslice[(y * cols) + x..(y * cols) + cols].iter_mut() { + c.set_bg(Color::Byte(7)); + } + context.dirty_areas.push_back(area); + } + pub fn add_component(&mut self, new: Box) { + self.children.push(new); + } +} + +impl fmt::Display for Tabbed { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO display info + write!(f, "tabs") + } +} + +impl Component for Tabbed { + fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + if self.children.len() > 1 { + self.draw_tabs( + grid, + ( + upper_left!(area), + set_x(upper_left!(area), get_x(bottom_right!(area))), + ), + context, + ); + let y = get_y(upper_left!(area)); + self.children[self.cursor_pos].draw( + grid, + (set_y(upper_left!(area), y + 1), bottom_right!(area)), + context, + ); + } else { + self.children[self.cursor_pos].draw(grid, area, context); + } + } + fn process_event(&mut self, event: &UIEvent, context: &mut Context) { + match &event.event_type { + UIEventType::Input(Key::Char('T')) => { + self.cursor_pos = (self.cursor_pos + 1) % self.children.len(); + self.children[self.cursor_pos].set_dirty(); + return; + } + _ => {} + } + 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) {} } diff --git a/ui/src/compose/mod.rs b/ui/src/compose/mod.rs index e69de29bb..8b1378917 100644 --- a/ui/src/compose/mod.rs +++ b/ui/src/compose/mod.rs @@ -0,0 +1 @@ + diff --git a/ui/src/state.rs b/ui/src/state.rs index d497a4b98..8119b5962 100644 --- a/ui/src/state.rs +++ b/ui/src/state.rs @@ -50,7 +50,7 @@ pub struct Context { /// Events queue that components send back to the state pub replies: VecDeque, - _backends: Backends, + backends: Backends, input_thread: chan::Sender, pub temp_files: Vec, @@ -103,8 +103,8 @@ impl State { pub fn new(sender: Sender, input_thread: chan::Sender) -> Self { let _stdout = std::io::stdout(); _stdout.lock(); - let settings = Settings::new(); let backends = Backends::new(); + let settings = Settings::new(); let stdout = AlternateScreen::from(_stdout.into_raw_mode().unwrap()); let termsize = termion::terminal_size().ok(); @@ -155,7 +155,7 @@ impl State { accounts, mailbox_hashes: FnvHashMap::with_capacity_and_hasher(1, Default::default()), - _backends: backends, + backends, settings: settings.clone(), runtime_settings: settings, dirty_areas: VecDeque::with_capacity(5), @@ -178,7 +178,7 @@ impl State { ).unwrap(); s.flush(); for (x, account) in s.context.accounts.iter_mut().enumerate() { - for (y, folder) in account.settings.folders.iter().enumerate() { + for (y, folder) in account.backend.folders().iter().enumerate() { s.context.mailbox_hashes.insert(folder.hash(), (x, y)); } let sender = s.sender.clone(); @@ -463,8 +463,7 @@ impl State { _ => { return None; } - } - { + } { if let Some(ForkType::NewDraft(f, _)) = std::mem::replace(&mut self.child, None) { self.rcv_event(UIEvent { id: 0, @@ -476,7 +475,9 @@ impl State { Some(false) } fn flush(&mut self) { - if let Some(s) = self.stdout.as_mut() { s.flush().unwrap(); } + if let Some(s) = self.stdout.as_mut() { + s.flush().unwrap(); + } } fn stdout(&mut self) -> &mut termion::screen::AlternateScreen> { self.stdout.as_mut().unwrap() diff --git a/ui/src/types/helpers.rs b/ui/src/types/helpers.rs index 37afb0c3b..074530b65 100644 --- a/ui/src/types/helpers.rs +++ b/ui/src/types/helpers.rs @@ -20,9 +20,9 @@ */ use std; +use std::fs::OpenOptions; use std::io::Write; use std::path::PathBuf; -use std::fs::OpenOptions; use uuid::Uuid; @@ -39,7 +39,12 @@ impl Drop for File { impl File { pub fn file(&mut self) -> std::fs::File { - OpenOptions::new().read(true).write(true).create(true).open(&self.path).unwrap() + OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&self.path) + .unwrap() } pub fn path(&self) -> &PathBuf {