Add threaded view, notifications, pager filter

embed
Manos Pitsidianakis 2018-07-17 17:16:16 +03:00
parent 51813510b1
commit e95cc4c1e9
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
13 changed files with 353 additions and 90 deletions

View File

@ -28,6 +28,7 @@ notify = "4.0.1"
termion = "1.5.1"
chan = "0.1.21"
chan-signal = "0.3.1"
notify-rust = "^3"
[profile.release]
#lto = true

View File

@ -22,6 +22,7 @@ extern crate melib;
#[macro_use]
extern crate nom;
extern crate termion;
extern crate notify_rust;
pub mod ui;
use ui::*;
@ -69,10 +70,14 @@ fn main() {
let menu = Entity {component: Box::new(AccountMenu::new(&state.context.accounts)) };
let listing = MailListing::new();
let b = Entity { component: Box::new(listing) };
let window = Entity { component: Box::new(VSplit::new(menu,b,90)) };
let window = Entity { component: Box::new(VSplit::new(menu, b, 80)) };
let status_bar = Entity { component: Box::new(StatusBar::new(window)) };
state.register_entity(status_bar);
let xdg_notifications = Entity { component: Box::new(ui::components::notifications::XDGNotifications {}) };
state.register_entity(xdg_notifications);
/* Keep track of the input mode. See ui::UIMode for details */
let mut mode: UIMode = UIMode::Normal;
'main: loop {
@ -94,6 +99,7 @@ fn main() {
UIMode::Normal => {
match k {
Key::Char('q') | Key::Char('Q') => {
drop(state);
break 'main;
},
Key::Char(';') => {
@ -124,6 +130,8 @@ fn main() {
}
},
ThreadEvent::RefreshMailbox { name : n } => {
state.rcv_event(UIEvent { id: 0, event_type: UIEventType::Notification(n.clone())});
state.redraw();
/* Don't handle this yet. */
eprintln!("Refresh mailbox {}", n);
},

View File

@ -87,7 +87,7 @@ pub struct AccountSettings {
pub folders: Vec<Folder>,
format: String,
pub sent_folder: String,
threaded: bool,
pub threaded: bool,
}
impl AccountSettings {
@ -157,8 +157,7 @@ impl Settings {
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));
}
//recurse_folders(&mut folders, &x.folders);
eprintln!("folders is {:?}", folders);
//folders.sort_by(|a, b| b.name.cmp(&a.name));
s.insert(
id.clone(),
AccountSettings {
@ -170,7 +169,6 @@ impl Settings {
},
);
}
eprintln!("pager settings are {:?}", fs.pager);
Settings { accounts: s, pager: fs.pager }
}

View File

@ -13,6 +13,10 @@ fn eighty_percent () -> usize {
80
}
fn none() -> Option<String> {
None
}
/// Settings for the pager function.
#[derive(Debug, Deserialize)]
pub struct PagerSettings {
@ -35,4 +39,9 @@ pub struct PagerSettings {
/// Default: 80
#[serde(default = "eighty_percent")]
pub pager_ratio: usize,
/// A command to pipe mail output through for viewing in pager.
/// Default: None
#[serde(default = "none")]
pub filter: Option<String>,
}

View File

@ -31,7 +31,7 @@ pub struct Account {
sent_folder: Option<usize>,
settings: AccountSettings,
pub settings: AccountSettings,
pub backend: Box<MailBackend>,
}

View File

@ -31,7 +31,7 @@ pub struct ImapOp {
}
impl ImapOp {
pub fn new(path: String) -> Self {
pub fn new(_path: String) -> Self {
ImapOp {
}
}
@ -63,16 +63,16 @@ pub struct ImapType {
impl MailBackend for ImapType {
fn get(&self, folder: &Folder) -> Result<Vec<Envelope>> {
fn get(&self, _folder: &Folder) -> Result<Vec<Envelope>> {
unimplemented!();
}
fn watch(&self, sender: RefreshEventConsumer, folders: &[Folder]) -> () {
fn watch(&self, _sender: RefreshEventConsumer, _folders: &[Folder]) -> () {
unimplemented!();
}
}
impl ImapType {
pub fn new(path: &str) -> Self {
pub fn new(_path: &str) -> Self {
ImapType {
}
}

View File

@ -33,7 +33,7 @@ pub struct MboxOp {
}
impl MboxOp {
pub fn new(path: String) -> Self {
pub fn new(_path: String) -> Self {
MboxOp {
}
}
@ -65,16 +65,16 @@ pub struct MboxType {
impl MailBackend for MboxType {
fn get(&self, folder: &Folder) -> Result<Vec<Envelope>> {
fn get(&self, _folder: &Folder) -> Result<Vec<Envelope>> {
unimplemented!();
}
fn watch(&self, sender: RefreshEventConsumer, folders: &[Folder]) -> () {
fn watch(&self, _sender: RefreshEventConsumer, _folders: &[Folder]) -> () {
unimplemented!();
}
}
impl MboxType {
pub fn new(path: &str) -> Self {
pub fn new(_path: &str) -> Self {
MboxType {
}
}

View File

@ -32,7 +32,7 @@ impl MailListing {
pub fn new() -> Self {
let mut content = CellBuffer::new(0, 0, Cell::with_char(' '));
let content = CellBuffer::new(0, 0, Cell::with_char(' '));
MailListing {
cursor_pos: (0, 1, 0),
new_cursor_pos: (0, 0, 0),
@ -50,11 +50,13 @@ impl MailListing {
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;
let threaded = context.accounts[self.cursor_pos.0].settings.threaded;
// Get mailbox as a reference.
let mailbox = &mut context.accounts[self.cursor_pos.0][self.cursor_pos.1].as_ref().unwrap().as_ref().unwrap();
// Inform State that we changed the current folder view.
context.replies.push_back(UIEvent { id: 0, event_type: UIEventType::RefreshMailbox(mailbox.clone()) });
context.replies.push_back(UIEvent { id: 0, event_type: UIEventType::RefreshMailbox((self.cursor_pos.0, self.cursor_pos.1)) });
self.length = mailbox.len();
let mut content = CellBuffer::new(MAX_COLS, self.length+1, Cell::with_char(' '));
@ -69,43 +71,118 @@ impl MailListing {
return;
}
// Populate `CellBuffer` with every entry.
// TODO: Lazy load?
let mut idx = 0;
for y in 0..=self.length {
if idx >= self.length {
/* No more entries left, so fill the rest of the area with empty space */
clear_area(&mut content,
((0, y), (MAX_COLS-1, self.length)));
break;
// TODO: Fix the threaded hell and refactor stuff into seperate functions and/or modules.
if threaded {
let mut indentations: Vec<bool> = Vec::with_capacity(6);
/* Draw threaded view. */
let mut iter = mailbox
.threaded_collection
.iter()
.enumerate()
.peekable();
/* This is just a desugared for loop so that we can use .peek() */
while let Some((idx, i)) = iter.next() {
let container = mailbox.get_thread(*i);
let indentation = container.get_indentation();
assert_eq!(container.has_message(), true);
match iter.peek() {
Some(&(_, x))
if mailbox.get_thread(*x).get_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.get_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 idx % 2 == 0 {
Color::Byte(236)
} else {
Color::Default
}
let x = write_string_to_grid(&MailListing::make_thread_entry(envelope, idx, indentation, container, idx == self.cursor_pos.2, &indentations),
&mut content,
fg_color,
bg_color,
((0, idx) , (MAX_COLS-1, idx)));
for x in x..MAX_COLS {
content[(x,idx)].set_ch(' ');
content[(x,idx)].set_bg(bg_color);
}
match iter.peek() {
Some(&(_, x))
if mailbox.get_thread(*x).get_indentation() > indentation =>
{
indentations.push(false);
}
Some(&(_, x))
if mailbox.get_thread(*x).get_indentation() < indentation =>
{
for _ in 0..(indentation - mailbox.get_thread(*x).get_indentation()) {
indentations.pop();
}
}
_ => {}
}
}
/* Write an entire line for each envelope entry. */
let envelope: &Envelope = &mailbox.collection[idx];
} else {
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
};
let x = write_string_to_grid(&MailListing::make_entry_string(envelope, idx),
&mut content,
fg_color,
bg_color,
((0, y) , (MAX_COLS-1, y)));
// Populate `CellBuffer` with every entry.
// TODO: Lazy load?
let mut idx = 0;
for y in 0..=self.length {
if idx >= self.length {
/* No more entries left, so fill the rest of the area with empty space */
clear_area(&mut content,
((0, y), (MAX_COLS-1, self.length)));
break;
}
/* Write an entire line for each envelope entry. */
let envelope: &Envelope = &mailbox.collection[idx];
for x in x..MAX_COLS {
content[(x,y)].set_ch(' ');
content[(x,y)].set_bg(bg_color);
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
};
let x = write_string_to_grid(&MailListing::make_entry_string(envelope, idx),
&mut content,
fg_color,
bg_color,
((0, y) , (MAX_COLS-1, y)));
for x in x..MAX_COLS {
content[(x,y)].set_ch(' ');
content[(x,y)].set_bg(bg_color);
}
idx+=1;
}
idx+=1;
}
self.content = content;
@ -180,13 +257,69 @@ impl MailListing {
/// Create a pager for the `Envelope` currently under the cursor.
fn draw_mail_view(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
{
let threaded = context.accounts[self.cursor_pos.0].settings.threaded;
let mailbox = &mut context.accounts[self.cursor_pos.0][self.cursor_pos.1].as_ref().unwrap().as_ref().unwrap();
let envelope: &Envelope = &mailbox.collection[self.cursor_pos.2];
let envelope: &Envelope = if threaded {
let i = mailbox.get_threaded_mail(self.cursor_pos.2);
&mailbox.collection[i]
} else {
&mailbox.collection[self.cursor_pos.2]
};
self.pager = Some(Pager::new(envelope));
let pager_filter = context.settings.pager.filter.clone();
self.pager = Some(Pager::new(&envelope, pager_filter));
}
self.pager.as_mut().map(|p| p.draw(grid, area, context));
}
fn make_thread_entry(envelope: &Envelope, idx: usize, indent: usize,
container: &Container, highlight: bool, indentations: &Vec<bool>) -> String {
let has_sibling = container.has_sibling();
let has_parent = container.has_parent();
let show_subject = container.get_show_subject();
let fg_color = if !envelope.is_seen() {
Color::Byte(0)
} else {
Color::Default
};
let bg_color = if highlight {
if !envelope.is_seen() {
Color::Byte(252)
} else if idx % 2 == 0 {
Color::Byte(236)
} else {
Color::Default
}
} else {
Color::Byte(246)
};
let mut s = format!("{} {} ", idx, &envelope.get_datetime().format("%Y-%m-%d %H:%M:%S").to_string()); // {} {:.85}",idx,),e.get_subject())
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.get_subject()));
}
s
}
}
impl Component for MailListing {
@ -255,8 +388,14 @@ impl Component for MailListing {
/* Draw header */
{
let threaded = context.accounts[self.cursor_pos.0].settings.threaded;
let mailbox = &mut context.accounts[self.cursor_pos.0][self.cursor_pos.1].as_ref().unwrap().as_ref().unwrap();
let envelope: &Envelope = &mailbox.collection[self.cursor_pos.2];
let envelope: &Envelope = if threaded {
let i = mailbox.get_threaded_mail(self.cursor_pos.2);
&mailbox.collection[i]
} else {
&mailbox.collection[self.cursor_pos.2]
};
let x = write_string_to_grid(&format!("Date: {}", envelope.get_date_as_str()),
grid,
@ -369,11 +508,13 @@ impl Component for MailListing {
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);
},
@ -438,16 +579,16 @@ impl AccountMenu {
cursor: None,
}
}
fn highlight_folder(&mut self, m: &Mailbox) {
self.dirty = true;
self.cursor = None;
}
fn print_account(&self, grid: &mut CellBuffer, area: Area, a: &AccountMenuEntry) -> usize {
if !is_valid_area!(area) {
eprintln!("BUG: invalid area in print_account");
}
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
let highlight = self.cursor.map(|(x,_)| x == a.index).unwrap_or(false);
let mut parents: Vec<Option<usize>> = vec!(None; a.entries.len());
for (idx, e) in a.entries.iter().enumerate() {
@ -461,10 +602,11 @@ impl AccountMenu {
roots.push(idx);
}
}
eprintln!("roots is {:?}", roots);
let mut inc = 0;
let mut depth = String::from("");
let mut s = String::from(format!("\n\n {}\n", a.name));
let mut s = String::from(format!("{}\n", a.name));
fn pop(depth: &mut String) {
depth.pop();
depth.pop();
@ -489,8 +631,8 @@ impl AccountMenu {
}
for r in roots {
print(r, &parents, &mut depth, &a.entries, &mut s, &mut inc);
}
eprintln!("s = {}", s);
let lines: Vec<&str> = s.lines().collect();
let lines_len = lines.len();
@ -504,14 +646,40 @@ impl AccountMenu {
} else {
format!("{}", lines[idx])
};
write_string_to_grid(&s,
let color_fg = if highlight {
if idx > 1 && self.cursor.unwrap().1 == idx - 2 {
Color::Byte(233)
} else {
Color::Byte(15)
}
} else {
Color::Default
};
let color_bg = if highlight {
if idx > 1 && self.cursor.unwrap().1 == idx - 2 {
Color::Byte(15)
} else {
Color::Byte(233)
}
} else {
Color::Default
};
let x = write_string_to_grid(&s,
grid,
Color::Byte(30),
Color::Default,
color_fg,
color_bg,
(set_y(upper_left, y), bottom_right));
if highlight && idx > 1 && self.cursor.unwrap().1 == idx - 2 {
change_colors(grid, ((x, y),(get_x(bottom_right)+1, y)), color_fg , color_bg);
} else {
change_colors(grid, ((x, y),set_y(bottom_right, y)), color_fg , color_bg);
}
idx += 1;
}
idx
idx - 1
}
}
@ -525,19 +693,22 @@ impl Component for AccountMenu {
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
self.dirty = false;
let mut y = get_y(upper_left);
let mut y = get_y(upper_left) + 1;
for a in &self.accounts {
eprintln!("\n\naccount: {:?}\n\n", a);
y += self.print_account(grid,
(set_y(upper_left, y), bottom_right),
&a);
}
eprintln!("\n\naccountentries: {:?}\n\n", self.accounts);
context.dirty_areas.push_back(area);
}
fn process_event(&mut self, event: &UIEvent, _context: &mut Context) {
match event.event_type {
UIEventType::RefreshMailbox(ref m) => {
self.highlight_folder(m);
UIEventType::RefreshMailbox(c) => {
self.cursor = Some(c);
self.dirty = true;
},
UIEventType::ChangeMode(UIMode::Normal) => {
self.dirty = true;

View File

@ -21,6 +21,7 @@
pub mod utilities;
pub mod mail;
pub mod notifications;
use super::*;
pub use utilities::*;

View File

@ -0,0 +1,24 @@
use notify_rust::Notification as notify_Notification;
use ui::*;
use ui::components::*;
pub struct XDGNotifications {}
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) {
match event.event_type {
UIEventType::Notification(ref t) => {
notify_Notification::new()
.summary("Refresh Event")
.body(t)
.icon("dialog-information")
.show().unwrap();
},
_ => {}
}
}
}

View File

@ -126,21 +126,31 @@ pub struct Pager {
}
impl Pager {
pub fn new(mail: &Envelope) -> Self {
let text = mail.get_body().get_text();
pub fn new(mail: &Envelope, pager_filter: Option<String>) -> Self {
let mut text = mail.get_body().get_text();
if let Some(bin) = pager_filter {
use std::io::Write;
use std::process::{Command, Stdio};
eprintln!("{}", bin);
let mut filter_child = Command::new(bin)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start pager filter process");
{
let mut stdin =
filter_child.stdin.as_mut().expect("failed to open stdin");
stdin.write_all(text.as_bytes()).expect("Failed to write to stdin");
}
text = String::from_utf8_lossy(&filter_child.wait_with_output().expect("Failed to wait on filter").stdout).to_string();
}
let lines: Vec<&str> = text.trim().split('\n').collect();
let height = lines.len();
let width = lines.iter().map(|l| l.len()).max().unwrap_or(0);
let mut content = CellBuffer::new(width, height, Cell::with_char(' '));
if width > 0 {
for (i, l) in lines.iter().enumerate() {
write_string_to_grid(l,
&mut content,
Color::Default,
Color::Default,
((0, i), (width -1, i)));
}
}
Pager::print_string(&mut content, &text);
Pager {
cursor_pos: 0,
height: height,
@ -149,6 +159,19 @@ impl Pager {
content: content,
}
}
pub fn print_string(content: &mut CellBuffer, s: &str) {
let lines: Vec<&str> = s.trim().split('\n').collect();
let width = lines.iter().map(|l| l.len()).max().unwrap_or(0);
if width > 0 {
for (i, l) in lines.iter().enumerate() {
write_string_to_grid(l,
content,
Color::Default,
Color::Default,
((0, i), (width -1, i)));
}
}
}
}
impl Component for Pager {
@ -163,6 +186,7 @@ impl Component for Pager {
if self.height == 0 || self.height == self.cursor_pos || self.width == 0 {
return;
}
clear_area(grid,
(upper_left, bottom_right));
context.dirty_areas.push_back((upper_left, bottom_right));
@ -228,18 +252,20 @@ impl StatusBar {
clear_area(grid, area);
write_string_to_grid(&self.status,
grid,
Color::Byte(36),
Color::Default,
Color::Byte(123),
Color::Byte(26),
area);
change_colors(grid, area, Color::Byte(123), Color::Byte(26));
context.dirty_areas.push_back(area);
}
fn draw_execute_bar(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
clear_area(grid, area);
write_string_to_grid(&self.ex_buffer,
grid,
Color::Byte(124),
Color::Default,
Color::Byte(219),
Color::Byte(88),
area);
change_colors(grid, area, Color::Byte(219), Color::Byte(88));
context.dirty_areas.push_back(area);
}
}
@ -279,7 +305,8 @@ impl Component for StatusBar {
fn process_event(&mut self, event: &UIEvent, context: &mut Context) {
self.container.rcv_event(event, context);
match event.event_type {
UIEventType::RefreshMailbox(ref m) => {
UIEventType::RefreshMailbox((idx_a, idx_f)) => {
let m = &context.accounts[idx_a][idx_f].as_ref().unwrap().as_ref().unwrap();
self.status = format!("{} |Mailbox: {}, Messages: {}, New: {}", self.mode, m.folder.get_name(), m.collection.len(), m.collection.iter().filter(|e| !e.is_seen()).count());
self.dirty = true;

View File

@ -0,0 +1,11 @@
use std;
use nom::digit;
named!(usize_c<usize>,
map_res!(map_res!(ws!(digit), std::str::from_utf8), std::str::FromStr::from_str));
named!(pub goto<usize>,
preceded!(tag!("b "),
call!(usize_c))
);

View File

@ -93,12 +93,13 @@ impl From<RefreshEvent> for ThreadEvent {
pub enum UIEventType {
Input(Key),
ExInput(Key),
RefreshMailbox(Mailbox),
RefreshMailbox((usize,usize)),
//Quit?
Resize,
ChangeMailbox(usize),
ChangeMode(UIMode),
Command(String),
Notification(String),
}
@ -123,6 +124,13 @@ impl fmt::Display for UIMode {
}
}
pub struct Notification {
title: String,
content: String,
timestamp: std::time::Instant,
}
pub struct Context {
pub accounts: Vec<Account>,
settings: Settings,
@ -140,6 +148,7 @@ impl Context {
}
}
pub struct State<W: Write> {
cols: usize,
rows: usize,
@ -169,6 +178,8 @@ impl<W: Write> State<W> {
let termrows = termsize.map(|(_,h)| h);
let cols = termcols.unwrap_or(0) as usize;
let rows = termrows.unwrap_or(0) as usize;
let mut accounts: Vec<Account> = settings.accounts.iter().map(|(n, a_s)| { Account::new(n.to_string(), a_s.clone(), &backends) }).collect();
accounts.sort_by(|a,b| a.get_name().cmp(&b.get_name()) );
let mut s = State {
cols: cols,
rows: rows,
@ -178,7 +189,7 @@ impl<W: Write> State<W> {
entities: Vec::with_capacity(1),
context: Context {
accounts: settings.accounts.iter().map(|(n, a_s)| { Account::new(n.to_string(), a_s.clone(), &backends) }).collect(),
accounts: accounts,
backends: backends,
settings: settings,
dirty_areas: VecDeque::with_capacity(5),
@ -197,7 +208,6 @@ impl<W: Write> State<W> {
s
}
pub fn update_size(&mut self) {
/* update dimensions. TODO: Only do that in size change events. ie SIGWINCH */
let termsize = termion::terminal_size().ok();
let termcols = termsize.map(|(w,_)| w);
let termrows = termsize.map(|(_,h)| h);
@ -304,17 +314,20 @@ impl<W: Write> State<W> {
self.entities[i].rcv_event(&event, &mut self.context);
}
}
/// Tries to load a mailbox's content
pub fn refresh_mailbox(&mut self, account_idx: usize, folder_idx: usize) {
let mailbox = match &mut self.context.accounts[account_idx][folder_idx] {
Some(Ok(v)) => { Some(v.clone()) },
Some(Err(e)) => { eprintln!("error {:?}", e); None },
None => { eprintln!("None"); None },
let flag = match &mut self.context.accounts[account_idx][folder_idx] {
Some(Ok(_)) => {
true
},
Some(Err(e)) => { eprintln!("error {:?}", e); false },
None => { eprintln!("None"); false },
};
if let Some(m) = mailbox {
self.rcv_event(UIEvent { id: 0, event_type: UIEventType::RefreshMailbox(m) });
}
if flag {
self.rcv_event(UIEvent { id: 0, event_type: UIEventType::RefreshMailbox((account_idx, folder_idx)) });
}
}
}