From 351c9d9a290ca15431b9dfa21479a981dbba4d00 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sun, 23 Jul 2017 14:01:17 +0300 Subject: [PATCH] initial commit Signed-off-by: Manos Pitsidianakis --- .gitignore | 10 +++ Cargo.toml | 12 +++ src/main.rs | 40 +++++++++ src/ui/index.rs | 211 ++++++++++++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 26 ++++++ src/ui/pager.rs | 227 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 526 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/main.rs create mode 100644 src/ui/index.rs create mode 100644 src/ui/mod.rs create mode 100644 src/ui/pager.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..08ec5e589 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +target/ +**/*.rs.bk +Cargo.lock +target/ +**/*.rs.bk +target/ +**/*.rs.bk +.gdb_history +*.log + diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..9dee76dcc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "nutt" +version = "0.1.0" +authors = ["Manos Pitsidianakis "] + +[dependencies] +mailparse = "0.5.1" +maildir = "0.1.1" +[dependencies.ncurses] +features = ["wide"] +optional = false +version = "5.86.0" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 000000000..3a23353d2 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,40 @@ +extern crate ncurses; +extern crate maildir; +extern crate mailparse; + +mod ui; + +use ui::index::*; + +fn main() { + let locale_conf = ncurses::LcCategory::all; + ncurses::setlocale(locale_conf, "en_US.UTF-8"); + ui::initialize(); + let maildir = maildir::Maildir::from("./Inbox"); + let iter = maildir.list_cur(); + let mut index = Index::new(iter); + ncurses::refresh(); + + index.draw(); + + let mut ch; + loop { + ch = ncurses::get_wch(); + match ch { + Some(ncurses::WchResult::KeyCode(k @ ncurses::KEY_UP)) | + Some(ncurses::WchResult::KeyCode(k @ ncurses::KEY_DOWN)) => { + index.scroll(k); + continue; + } + Some(ncurses::WchResult::Char(10)) => { + index.show_pager(); + index.draw(); + continue; + } + Some(ncurses::WchResult::KeyCode(ncurses::KEY_F1)) => { + break; + } + _ => {} + } + } +} diff --git a/src/ui/index.rs b/src/ui/index.rs new file mode 100644 index 000000000..e1ea6551a --- /dev/null +++ b/src/ui/index.rs @@ -0,0 +1,211 @@ +extern crate ncurses; +extern crate maildir; +extern crate mailparse; +use mailparse::*; + +pub struct Index { + mailbox: Vec<(usize, maildir::MailEntry)>, + length: usize, + + win: ncurses::WINDOW, + pad: ncurses::WINDOW, + screen_width: i32, + screen_height: i32, + + cursor_idx: usize, +} +impl Index { + pub fn new(iter: maildir::MailEntries) -> Index { + let mut collection: Vec<(usize, maildir::MailEntry)> = Vec::new(); + for (i, x) in iter.enumerate() { + collection.push((i, x.unwrap())); + } + let length = collection.len(); + let mut screen_height = 0; + let mut screen_width = 0; + /* Get the screen bounds. */ + ncurses::getmaxyx(ncurses::stdscr(), &mut screen_height, &mut screen_width); + // let win = ncurses::newwin( ncurses::LINES(), ncurses::COLS()-30, 0, 30); + let win = ncurses::newwin(0, 0, 0, 0); + ncurses::getmaxyx(win, &mut screen_height, &mut screen_width); + ncurses::wbkgd( + win, + ' ' as ncurses::chtype | + ncurses::COLOR_PAIR(super::COLOR_PAIR_DEFAULT) as ncurses::chtype, + ); + //eprintln!("length is {}\n", length); + let pad = ncurses::newpad(length as i32, screen_width); + Index { + mailbox: collection, + length: length, + win: win, + pad: pad, + screen_width: 0, + screen_height: 0, + cursor_idx: 0, + } + } + + pub fn draw(&mut self) { + let mut x = 0; + let mut y = 0; + ncurses::getbegyx(self.win, &mut y, &mut x); + + ncurses::wclear(self.pad); + + for &mut (i, ref mut x) in self.mailbox.as_mut_slice() { + if i == self.cursor_idx { + ncurses::wattron(self.pad, ncurses::COLOR_PAIR(super::COLOR_PAIR_CURSOR)); + } + Index::draw_entry(self.pad, x, i); + if i == self.cursor_idx { + ncurses::wattroff(self.pad, ncurses::COLOR_PAIR(super::COLOR_PAIR_CURSOR)); + } + } + ncurses::getmaxyx(self.win, &mut self.screen_height, &mut self.screen_width); + let pminrow = + (self.cursor_idx as i32).wrapping_div(self.screen_height) * self.screen_height; + ncurses::prefresh( + self.pad, + pminrow, + 0, + y, + x, + self.screen_height - 1, + self.screen_width - 1, + ); + } + + pub fn scroll(&mut self, motion: i32) { + ncurses::getmaxyx(self.win, &mut self.screen_height, &mut self.screen_width); + if self.screen_height == 0 { + return; + } + let mut x = 0; + let mut y = 0; + ncurses::getbegyx(self.win, &mut y, &mut x); + let prev_idx = self.cursor_idx; + match motion { + ncurses::KEY_UP => { + if self.cursor_idx > 0 { + self.cursor_idx -= 1; + } else { + return; + } + } + ncurses::KEY_DOWN => { + if self.cursor_idx < self.length - 1 { + self.cursor_idx += 1; + } else { + return; + } + } + _ => { + return; + } + } + + /* Draw newly highlighted entry */ + ncurses::wmove(self.pad, self.cursor_idx as i32, 0); + /* Borrow x from self.mailbox in separate scopes or else borrow checker complains */ + { + let (_, ref mut x) = self.mailbox.as_mut_slice()[self.cursor_idx]; + ncurses::wattron(self.pad, ncurses::COLOR_PAIR(super::COLOR_PAIR_CURSOR)); + Index::draw_entry(self.pad, x, self.cursor_idx); + ncurses::wattroff(self.pad, ncurses::COLOR_PAIR(super::COLOR_PAIR_CURSOR)); + } + /* Draw previous highlighted entry normally */ + ncurses::wmove(self.pad, prev_idx as i32, 0); + { + let (_, ref mut x) = self.mailbox.as_mut_slice()[prev_idx]; + Index::draw_entry(self.pad, x, prev_idx); + } + + /* Calculate the pad row of the first entry to be displayed in the window */ + let pminrow = + (self.cursor_idx as i32).wrapping_div(self.screen_height) * self.screen_height; + let pminrow_prev = (prev_idx as i32).wrapping_div(self.screen_height) * self.screen_height; + /* + * Refresh window if new page has less rows than window rows, ie + * window rows = r + * pad rows (total emails) = n + * pminrow = i + * + * ┌- i + * │ i+1 + * │ i+2 + * r ┥ ... + * │ n + * │ .. ┐ + * │ i-2 ├ 'dead' entries (must be empty) + * └ i-1 ┘ + */ + if pminrow != pminrow_prev && pminrow + self.screen_height > self.length as i32 { + /* touch Index window (tell ncurses to redraw the entire window in next refresh */ + ncurses::touchwin(self.win); + ncurses::wrefresh(self.win); + } + ncurses::prefresh( + self.pad, + pminrow, + 0, + y, + x, + self.screen_height - 1, + self.screen_width - 1, + ); + } + fn draw_entry(win: ncurses::WINDOW, entry: &mut maildir::MailEntry, i: usize) { + ncurses::waddstr(win, &format!("{}\t", i)); + let d = entry.headers().unwrap().get_first_value("Date").unwrap(); + match d { + Some(t) => { + ncurses::waddstr(win, &t.to_string()); + } + _ => {} + } + ncurses::waddch(win, ' ' as u64); + let c = entry.headers().unwrap().get_first_value("Subject").unwrap(); + match c { + Some(t) => { + ncurses::waddstr(win, &t.to_string()); + } + _ => {} + } + ncurses::wprintw(win, "\n"); + } + pub fn show_pager(&mut self) { + ncurses::getmaxyx(self.win, &mut self.screen_height, &mut self.screen_width); + let (_, ref mut mail) = self.mailbox[self.cursor_idx]; + let mut pager = super::pager::Pager::new(self.win, mail); + 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); + } + _ => {} + } + // ncurses::wprintw(pager, "test\n"); + //ncurses::wrefresh(pager); + ch = ncurses::getch(); + } + drop(pager); + ncurses::wrefresh(self.win); + } +} +impl Drop for Index { + fn drop(&mut self) { + /* Final prompt before closing. */ + //ncurses::mv(self.screen_height - 1, 0); + //prompt(); + ncurses::delwin(self.win); + ncurses::delwin(self.pad); + ncurses::endwin(); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 000000000..0617cd335 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,26 @@ +extern crate ncurses; +pub mod index; +pub mod pager; +/* Color pairs; foreground && background. */ +pub static COLOR_PAIR_DEFAULT: i16 = 1; +pub static COLOR_PAIR_CURSOR: i16 = 2; +pub static COLOR_PAIR_HEADERS: i16 = 3; +pub fn initialize() { + /* start ncurses */ + ncurses::initscr(); + ncurses::keypad(ncurses::stdscr(), true); + ncurses::noecho(); + ncurses::curs_set(ncurses::CURSOR_VISIBILITY::CURSOR_INVISIBLE); + /* Start colors. */ + + ncurses::start_color(); + + ncurses::init_pair(COLOR_PAIR_DEFAULT, 15, 0); + ncurses::init_pair(COLOR_PAIR_CURSOR, 251, 235); + ncurses::init_pair(COLOR_PAIR_HEADERS, 33, 0); + + /* Set the window's background color. */ + ncurses::bkgd( + ' ' as ncurses::chtype | ncurses::COLOR_PAIR(COLOR_PAIR_DEFAULT) as ncurses::chtype, + ); +} diff --git a/src/ui/pager.rs b/src/ui/pager.rs new file mode 100644 index 000000000..de9134d0d --- /dev/null +++ b/src/ui/pager.rs @@ -0,0 +1,227 @@ +extern crate ncurses; +extern crate maildir; +extern crate mailparse; +use mailparse::*; +pub struct Pager { + win: ncurses::WINDOW, + pad: ncurses::WINDOW, + rows: i32, + header_height: i32, + + curr_y: i32, +} + +impl Pager { + pub fn new(parent: ncurses::WINDOW, entry: &mut maildir::MailEntry) -> Pager { + + let mut screen_height = 0; + let mut screen_width = 0; + ncurses::getmaxyx(parent, &mut screen_height, &mut screen_width); + let mut x = 0; + let mut y = 0; + ncurses::getbegyx(parent, &mut y, &mut x); + let win = ncurses::subwin( + parent, + screen_height - (screen_height / 3), + screen_width, + y + (screen_height / 3), + x, + ); + ncurses::wclear(win); + ncurses::touchwin(win); + for _ in 1..screen_width + 1 { + ncurses::waddstr(win, "─"); + } + //ncurses::wprintw(win, "\n"); + let (mail_pad, rows, header_height) = Pager::print_entry(win, entry); + ncurses::wrefresh(win); + Pager { + pad: mail_pad, + win: win, + rows: rows, + header_height: header_height, + curr_y: 0, + } + } + pub fn scroll(&mut self, motion: i32) { + let mut h = 0; + let mut w = 0; + ncurses::getmaxyx(self.win, &mut h, &mut w); + let mut x = 0; + let mut y = 0; + ncurses::getparyx(self.win, &mut y, &mut x); + let mut p_x = 0; + let mut p_y = 0; + ncurses::getbegyx(self.win, &mut p_y, &mut p_x); + let pager_size: i32 = h - self.header_height; + if pager_size == 0 { + return; + } + match motion { + ncurses::KEY_UP => { + if self.curr_y > 0 { + self.curr_y -= 1; + } + } + ncurses::KEY_DOWN => { + if self.curr_y < self.rows && self.rows - self.curr_y > pager_size { + self.curr_y += 1; + } + } + ncurses::KEY_NPAGE => { + if self.curr_y + h < self.rows && self.rows - self.curr_y - h > pager_size { + self.curr_y += pager_size; + } else { + self.curr_y = if self.rows > h { + self.rows - pager_size + } else { + 0 + }; + } + } + ncurses::KEY_PPAGE => { + if self.curr_y >= pager_size { + self.curr_y -= pager_size; + } else { + self.curr_y = 0 + } + } + _ => {} + } + /* + * ┌ ┏━━━━━━━━━┓ ┐ + * │ ┃ ┃ │ + * y ┃ ┃ │ + * │ ┃ ┃ │ + * ├ x━━━━━━━━━┫ ┐ │ index + * │ ┃ ┃ │ │ + * h ┃ ┃ │ pager │ + * └ ┗━━━━━━━━━w ┘ ┘ + */ + ncurses::touchwin(self.win); + ncurses::prefresh( + self.pad, + self.curr_y, + 0, + y + self.header_height, + p_x + x, + y + h - 1, + w - 1, + ); + } + fn print_entry_headers(win: ncurses::WINDOW, mail: &mut maildir::MailEntry) -> i32 { + let mut i = 0; + ncurses::wattron(win, ncurses::COLOR_PAIR(super::COLOR_PAIR_HEADERS)); + ncurses::waddstr(win, "Date: "); + ncurses::waddstr( + win, + &mail.headers() + .unwrap() + .get_first_value("Date") + .unwrap() + .unwrap(), + ); + ncurses::waddstr(win, "\n"); + i += 1; + ncurses::waddstr(win, "From: "); + ncurses::waddstr( + win, + &mail.headers() + .unwrap() + .get_first_value("From") + .unwrap() + .unwrap(), + ); + ncurses::waddstr(win, "\n"); + i += 1; + ncurses::waddstr(win, "To: "); + ncurses::waddstr( + win, + &mail.headers() + .unwrap() + .get_first_value("To") + .unwrap() + .unwrap(), + ); + ncurses::waddstr(win, "\n"); + i += 1; + ncurses::waddstr(win, "Subject: "); + ncurses::waddstr( + win, + &mail.headers() + .unwrap() + .get_first_value("Subject") + .unwrap() + .unwrap(), + ); + ncurses::waddstr(win, "\n"); + i += 1; + ncurses::wattroff(win, ncurses::COLOR_PAIR(super::COLOR_PAIR_HEADERS)); + /* return number of header lines so we don't overwrite it */ + i + } + fn print_entry_content( + win: ncurses::WINDOW, + mail: &mut maildir::MailEntry, + height: i32, + ) -> (ncurses::WINDOW, i32, i32) { + let mut h = 0; + let mut w = 0; + /* height and width of self.win, the pager window */ + ncurses::getmaxyx(win, &mut h, &mut w); + let mut x = 0; + let mut y = 0; + /* y,x coordinates of upper left corner of win */ + ncurses::getparyx(win, &mut y, &mut x); + match &mail.parsed() { + &Ok(ref v) => { + match &v.get_body() { + &Ok(ref b) => { + let lines: Vec<&str> = b.split('\n').collect(); + let lines_length = lines.len(); + let pad = ncurses::newpad(lines_length as i32, 1024); + ncurses::wclear(pad); + for l in lines { + ncurses::waddstr(pad, &l.replace("%", "%%")); + ncurses::waddstr(pad, "\n"); + } + /* + * ┌ ┏━━━━━━━━━┓ ┐ + * │ ┃ ┃ │ + * y ┃ ┃ │ + * │ ┃ ┃ │ + * ├ x━━━━━━━━━┫ ┐ │ index + * │ ┃ ┃ │ │ + * h ┃ ┃ │ pager │ + * └ ┗━━━━━━━━━w ┘ ┘ + */ + ncurses::pnoutrefresh(pad, 0, 0, y + height, x, y + height - 1, w - 1); + return (pad, lines_length as i32, height); + } + _ => { + return (ncurses::newpad(0, 0), 0, height); + } + } + } + _ => { + return (ncurses::newpad(0, 0), 0, height); + } + } + } + fn print_entry( + win: ncurses::WINDOW, + mail: &mut maildir::MailEntry, + ) -> (ncurses::WINDOW, i32, i32) { + let header_height = Pager::print_entry_headers(win, mail); + Pager::print_entry_content(win, mail, header_height + 2) + } +} + + +impl Drop for Pager { + fn drop(&mut self) { + //ncurses::touchwin(self.win); + ncurses::delwin(self.win); + ncurses::delwin(self.pad); + } +}