diff --git a/Cargo.toml b/Cargo.toml index e70c542f..9d724139 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ crossbeam = "^0.3.0" fnv = "1.0.3" encoding = "0.2.33" bitflags = "1.0" +notify = "4.0.1" [dependencies.ncurses] features = ["wide"] diff --git a/README b/README index 40ffc70f..2fad2e87 100644 --- a/README +++ b/README @@ -1,10 +1,10 @@ __ __/ \__ / \__/ \__ . -\__/ \__/ \ , _ , _ ___ | ' -/ \__ \__/ |' `|' `. .' ` | | -\__/ \__/ \ | | | |----' | | - \__/ \__/ / ' / `.___, /\__ / +\__/ \__/ \ , _ , _ ___ │ ' +/ \__ \__/ │' `│ `┒ .' ` │ │ +\__/ \__/ \ │ │ │ |────' │ │ + \__/ \__/ │ / `.___, /\__ / \__/ terminal mail user agent diff --git a/src/bin.rs b/src/bin.rs index d4f78b36..4565e317 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -21,22 +21,47 @@ mod ui; use ui::index::*; +use ui::ThreadEvent; extern crate melib; use melib::*; -use mailbox::*; -use conf::*; extern crate ncurses; +use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; +use std::thread; + fn main() { let locale_conf = ncurses::LcCategory::all; ncurses::setlocale(locale_conf, "en_US.UTF-8"); let set = Settings::new(); let ui = ui::TUI::initialize(); + + let (sender, receiver): (SyncSender, Receiver) = + sync_channel(::std::mem::size_of::()); + { + let sender = sender.clone(); + let mut ch = None; + thread::Builder::new() + .name("input-thread".to_string()) + .spawn(move || loop { + ch = ncurses::get_wch(); + if let Some(k) = ch { + sender.send(ThreadEvent::Input(k)).unwrap(); + } + }) + .unwrap(); + } + let mut j = 0; let folder_length = set.accounts["norn"].folders.len(); let mut account = Account::new("norn".to_string(), set.accounts["norn"].clone()); + { + let sender = sender.clone(); + account.watch(RefreshEventConsumer::new(Box::new(move |r| { + sender.send(ThreadEvent::from(r)).unwrap(); + }))); + } 'main: loop { ncurses::touchwin(ncurses::stdscr()); ncurses::mv(0, 0); @@ -50,37 +75,48 @@ fn main() { index.draw(); - let mut ch; 'inner: loop { - ch = ncurses::get_wch(); - match ch { - Some(ncurses::WchResult::KeyCode(k @ ncurses::KEY_UP)) | - Some(ncurses::WchResult::KeyCode(k @ ncurses::KEY_DOWN)) => { - index.handle_input(k); - continue; - } - Some(ncurses::WchResult::Char(k @ 10)) => { - index.handle_input(k as i32); - continue; - } - Some(ncurses::WchResult::KeyCode(ncurses::KEY_F1)) | - Some(ncurses::WchResult::Char(113)) => { - break 'main; - } - Some(ncurses::WchResult::Char(74)) => if j < folder_length - 1 { - j += 1; - break 'inner; + match receiver.recv().unwrap() { + ThreadEvent::Input(k) => match k { + ncurses::WchResult::KeyCode(k @ ncurses::KEY_UP) + | ncurses::WchResult::KeyCode(k @ ncurses::KEY_DOWN) => { + index.handle_input(k); + continue; + } + ncurses::WchResult::Char(k @ 10) => { + index.handle_input(k as i32); + continue; + } + ncurses::WchResult::KeyCode(k @ ncurses::KEY_F1) => { + if !index.handle_input(k) { + break 'main; + } + } + ncurses::WchResult::Char(113) => { + break 'main; + } + ncurses::WchResult::Char(74) => { + if j < folder_length - 1 { + j += 1; + break 'inner; + } + } + ncurses::WchResult::Char(75) => { + if j > 0 { + j -= 1; + break 'inner; + } + } + ncurses::WchResult::KeyCode(ncurses::KEY_RESIZE) => { + eprintln!("key_resize"); + index.redraw(); + continue; + } + _ => {} }, - Some(ncurses::WchResult::Char(75)) => if j > 0 { - j -= 1; - break 'inner; - }, - Some(ncurses::WchResult::KeyCode(ncurses::KEY_RESIZE)) => { - eprintln!("key_resize"); - index.redraw(); - continue; + ThreadEvent::RefreshMailbox { name: n } => { + eprintln!("Refresh mailbox {}", n); } - _ => {} } } } diff --git a/src/lib.rs b/src/lib.rs index 1bd073b6..d80ec37b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,3 +35,8 @@ extern crate encoding; #[macro_use] extern crate bitflags; + +pub use mailbox::*; +pub use conf::*; + +pub use mailbox::backends::RefreshEventConsumer; diff --git a/src/mailbox/accounts.rs b/src/mailbox/accounts.rs index 828cabf9..1f2972fc 100644 --- a/src/mailbox/accounts.rs +++ b/src/mailbox/accounts.rs @@ -20,6 +20,7 @@ */ use mailbox::*; +use mailbox::backends::RefreshEventConsumer; use conf::AccountSettings; use std::ops::{Index, IndexMut}; #[derive(Debug)] @@ -30,9 +31,11 @@ pub struct Account { sent_folder: Option, settings: AccountSettings, + pub backend: Box, } +use mailbox::backends::maildir::MaildirType; impl Account { pub fn new(name: String, settings: AccountSettings) -> Self { eprintln!("new acc"); @@ -44,6 +47,7 @@ impl Account { for _ in 0..settings.folders.len() { folders.push(None); } + let backend = Box::new(MaildirType::new("")); Account { name: name, folders: folders, @@ -51,8 +55,12 @@ impl Account { sent_folder: sent_folder, settings: settings, + backend: backend, } } + pub fn watch(&self, r: RefreshEventConsumer) -> () { + self.backend.watch(r, &self.settings.folders); + } } impl Index for Account { diff --git a/src/mailbox/backends/maildir.rs b/src/mailbox/backends/maildir.rs index 2bd7c29d..e14ee584 100644 --- a/src/mailbox/backends/maildir.rs +++ b/src/mailbox/backends/maildir.rs @@ -21,18 +21,23 @@ use mailbox::email::{Envelope, Flag}; use error::{MeliError, Result}; -use mailbox::backends::{BackendOp, BackendOpGenerator, MailBackend}; +use mailbox::backends::{BackendOp, BackendOpGenerator, MailBackend, RefreshEvent, RefreshEventConsumer}; use mailbox::email::parser; +extern crate notify; + +use self::notify::{Watcher, RecursiveMode, watcher}; +use std::time::Duration; + +use std::sync::mpsc::channel; +//use std::sync::mpsc::sync_channel; +//use std::sync::mpsc::SyncSender; +//use std::time::Duration; +use std::thread; extern crate crossbeam; use std::path::PathBuf; use memmap::{Mmap, Protection}; - -pub struct MaildirType { - path: String, -} - #[derive(Debug, Default)] pub struct MaildirOp { path: String, @@ -64,9 +69,10 @@ impl BackendOp for MaildirOp { fn as_bytes(&mut self) -> Result<&[u8]> { if self.slice.is_none() { self.slice = Some( - Mmap::open_path(self.path.to_string(), Protection::Read).unwrap(), + Mmap::open_path(self.path.to_string(), Protection::Read)?, ); } + /* Unwrap is safe since we use ? above. */ Ok(unsafe { self.slice.as_ref().unwrap().as_slice() }) } fn fetch_headers(&mut self) -> Result<&[u8]> { @@ -107,6 +113,12 @@ impl BackendOp for MaildirOp { } +#[derive(Debug)] +pub struct MaildirType { + path: String, +} + + impl MailBackend for MaildirType { fn get(&self) -> Result> { self.get_multicore(4) @@ -132,6 +144,34 @@ impl MailBackend for MaildirType { Ok(r) */ } + fn watch(&self, sender: RefreshEventConsumer, folders: &[String]) -> () { + let folders = folders.to_vec(); + + 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; + } + let mut p = PathBuf::from(&f); + p.push("cur"); + watcher.watch(&p, RecursiveMode::NonRecursive).unwrap(); + p.pop(); + p.push("new"); + watcher.watch(&p, RecursiveMode::NonRecursive).unwrap(); + eprintln!("watching {:?}", f); + } + loop { + match rx.recv() { + Ok(event) => { + sender.send(RefreshEvent { folder: format!("{:?}", event) }); + } + Err(e) => eprintln!("watch error: {:?}", e), + } + } + }).unwrap(); + } } impl MaildirType { @@ -198,9 +238,10 @@ panic!("didn't parse"); }, let mut local_r: Vec = Vec::with_capacity(chunk.len()); for e in chunk { let e_copy = e.to_string(); - if let Some(e) = Envelope::from(Box::new(BackendOpGenerator::new( + if let Some(mut e) = Envelope::from(Box::new(BackendOpGenerator::new( Box::new(move || Box::new(MaildirOp::new(e_copy.clone()))), ))) { + e.populate_headers(); local_r.push(e); } } diff --git a/src/mailbox/backends/mod.rs b/src/mailbox/backends/mod.rs index 7705af04..d83a8676 100644 --- a/src/mailbox/backends/mod.rs +++ b/src/mailbox/backends/mod.rs @@ -25,10 +25,26 @@ use error::Result; use std::fmt; -pub trait MailBackend { - fn get(&self) -> Result>; +pub struct RefreshEvent { + pub folder: String, } +pub struct RefreshEventConsumer(Box ()>); +unsafe impl Send for RefreshEventConsumer {} +unsafe impl Sync for RefreshEventConsumer {} +impl RefreshEventConsumer { + pub fn new(b: Box ()>) -> Self { + RefreshEventConsumer(b) + } + pub fn send(&self, r: RefreshEvent) -> () { + self.0(r); + } +} +pub trait MailBackend: ::std::fmt::Debug { + fn get(&self) -> Result>; + fn watch(&self, sender:RefreshEventConsumer, folders: &[String]) -> (); + //fn new(folders: &Vec) -> Box; +} /// A `BackendOp` manages common operations for the various mail backends. They only live for the /// duration of the operation. They are generated by `BackendOpGenerator` on demand. diff --git a/src/mailbox/email/attachments.rs b/src/mailbox/email/attachments.rs index c44cc623..a7298911 100644 --- a/src/mailbox/email/attachments.rs +++ b/src/mailbox/email/attachments.rs @@ -156,6 +156,7 @@ impl AttachmentBuilder { self } fn decode(&self) -> String { + // TODO: Use charset for decoding match self.content_transfer_encoding { ContentTransferEncoding::Base64 => { match ::base64::decode(&::std::str::from_utf8(&self.raw) @@ -178,8 +179,8 @@ impl AttachmentBuilder { } } ContentTransferEncoding::QuotedPrintable => parser::quoted_printable_text(&self.raw) - .to_full_result() - .unwrap(), + .to_full_result() + .unwrap(), ContentTransferEncoding::_7Bit | ContentTransferEncoding::_8Bit | ContentTransferEncoding::Other { .. } => { diff --git a/src/mailbox/mod.rs b/src/mailbox/mod.rs index c380fcb8..f22849e8 100644 --- a/src/mailbox/mod.rs +++ b/src/mailbox/mod.rs @@ -41,31 +41,25 @@ pub struct Mailbox { pub collection: Vec, pub threaded_collection: Vec, threads: Vec, - length: usize, } impl Mailbox { pub fn new(path: &str, sent_folder: &Option>) -> Result { let mut collection: Vec = maildir::MaildirType::new(path).get()?; - for e in &mut collection { - e.populate_headers(); - } collection.sort_by(|a, b| a.get_date().cmp(&b.get_date())); let (threads, threaded_collection) = build_threads(&mut collection, sent_folder); - let length = collection.len(); Ok(Mailbox { path: path.to_string(), collection: collection, threads: threads, - length: length, threaded_collection: threaded_collection, }) } pub fn get_length(&self) -> usize { - self.length + self.collection.len() } pub fn get_threaded_mail(&self, i: usize) -> usize { let thread = self.threads[self.threaded_collection[i]]; diff --git a/src/ui/index.rs b/src/ui/index.rs index e1b24f0b..e3ab0e6c 100644 --- a/src/ui/index.rs +++ b/src/ui/index.rs @@ -21,6 +21,8 @@ use mailbox::email::Envelope; use mailbox::*; use error::MeliError; + +use super::pager::Pager; use std::error::Error; extern crate ncurses; @@ -28,7 +30,7 @@ extern crate ncurses; pub trait Window { fn draw(&mut self) -> (); fn redraw(&mut self) -> (); - fn handle_input(&mut self, input: i32) -> (); + fn handle_input(&mut self, input: i32) -> bool; } pub struct ErrorWindow { @@ -46,7 +48,7 @@ impl Window for ErrorWindow { ncurses::waddstr(self.win, &self.description); ncurses::wrefresh(self.win); } - fn handle_input(&mut self, _: i32) -> () {} + fn handle_input(&mut self, _: i32) -> bool { false } } impl ErrorWindow { pub fn new(err: MeliError) -> Self { @@ -78,12 +80,14 @@ pub struct Index { threaded: bool, cursor_idx: usize, + + pager: Option, } impl Window for Index { fn draw(&mut self) { - if self.mailbox.get_length() == 0 { + if self.get_length() == 0 { return; } let mut x = 0; @@ -199,7 +203,7 @@ impl Window for Index { fn redraw(&mut self) -> () { ncurses::wnoutrefresh(self.win); ncurses::doupdate(); - if self.mailbox.get_length() == 0 { + if self.get_length() == 0 { return; } /* Draw newly highlighted entry */ @@ -224,13 +228,31 @@ impl Window for Index { ); ncurses::wrefresh(self.win); } - fn handle_input(&mut self, motion: i32) { - if self.mailbox.get_length() == 0 { - return; + fn handle_input(&mut self, motion: i32) -> bool { + if self.get_length() == 0 { + return false; } ncurses::getmaxyx(self.win, &mut self.screen_height, &mut self.screen_width); if self.screen_height == 0 { - return; + return false; + } + if self.pager.is_some() { + match motion { + m @ ncurses::KEY_UP | + m @ ncurses::KEY_DOWN | + m @ ncurses::KEY_NPAGE | + m @ ncurses::KEY_PPAGE => { + self.pager.as_mut().unwrap().scroll(m); + return true; + }, + ncurses::KEY_F1 => { + self.pager = None; + self.redraw(); + return true; + }, + _ => {} + } + return false; } let mut x = 0; let mut y = 0; @@ -240,19 +262,19 @@ impl Window for Index { ncurses::KEY_UP => if self.cursor_idx > 0 { self.cursor_idx -= 1; } else { - return; + return false; }, - ncurses::KEY_DOWN => if self.cursor_idx < self.mailbox.get_length() - 1 { + ncurses::KEY_DOWN => if self.cursor_idx < self.get_length() - 1 { self.cursor_idx += 1; } else { - return; + return false; }, 10 => { self.show_pager(); - self.redraw(); + return true; } _ => { - return; + return false; } } @@ -318,10 +340,10 @@ impl Window for Index { * └ i-1 ┘ */ if pminrow != pminrow_prev && - pminrow + self.screen_height > self.mailbox.get_length() as i32 + pminrow + self.screen_height > self.get_length() as i32 { /* touch dead entries in index (tell ncurses to redraw the empty lines next refresh) */ - let live_entries = self.mailbox.get_length() as i32 - pminrow; + let live_entries = self.get_length() as i32 - pminrow; ncurses::wredrawln(self.win, live_entries, self.screen_height); ncurses::wrefresh(self.win); } @@ -334,6 +356,7 @@ impl Window for Index { self.screen_height - 1, self.screen_width - 1, ); + return true; } } impl Index { @@ -365,15 +388,6 @@ impl Index { ncurses::printw(&format!("Mailbox {} is empty.\n", mailbox.path)); ncurses::refresh(); } - let mut color = true; - let mut thread_color = Vec::with_capacity(mailbox_length); - for i in &mailbox.threaded_collection { - let container = mailbox.get_thread(*i); - if !container.has_parent() { - color = !color; - } - thread_color.push(color); - } Index { mailbox: mailbox, win: win, @@ -382,9 +396,16 @@ impl Index { screen_height: 0, threaded: true, cursor_idx: 0, + pager: None, + } + } + fn get_length(&self) -> usize { + if self.threaded { + self.mailbox.threaded_collection.len() + } else { + self.mailbox.get_length() } } - /* draw_entry() doesn't take &mut self because borrow checker complains if it's called from * another method. */ fn draw_entry( @@ -465,7 +486,7 @@ impl Index { ncurses::wattroff(win, attr); } fn show_pager(&mut self) { - if self.mailbox.get_length() == 0 { + if self.get_length() == 0 { return; } ncurses::getmaxyx(self.win, &mut self.screen_height, &mut self.screen_width); @@ -475,22 +496,11 @@ impl Index { } else { &mut self.mailbox.collection[self.cursor_idx] }; - let mut pager = super::pager::Pager::new(self.win, x); + let mut pager = Pager::new(self.win, x); + /* TODO: Fix this: */ pager.scroll(ncurses::KEY_DOWN); pager.scroll(ncurses::KEY_UP); - let mut ch = ncurses::getch(); - while ch != ncurses::KEY_F(1) { - match ch { - m @ ncurses::KEY_UP | - m @ ncurses::KEY_DOWN | - m @ ncurses::KEY_NPAGE | - m @ ncurses::KEY_PPAGE => { - pager.scroll(m); - } - _ => {} - } - ch = ncurses::getch(); - } + self.pager = Some(pager); } } impl Drop for Index { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7aaad4ad..9ec2e79e 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -23,6 +23,8 @@ pub mod index; pub mod pager; extern crate ncurses; +extern crate melib; +use melib::mailbox::backends::RefreshEvent; /* Color pairs; foreground && background. */ pub static COLOR_PAIR_DEFAULT: i16 = 1; @@ -64,3 +66,15 @@ impl Drop for TUI { ncurses::endwin(); } } + +pub enum ThreadEvent { + Input(ncurses::WchResult), + RefreshMailbox{ name: String }, + //Decode { _ }, // For gpg2 signature check +} + +impl From for ThreadEvent { + fn from(event: RefreshEvent) -> Self { + ThreadEvent::RefreshMailbox { name: event.folder } + } +}