From befe00dea632aca4aa042c4e7e0d0412f769b1ab Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Thu, 9 Aug 2018 16:50:33 +0300 Subject: [PATCH] Add html view --- melib/src/mailbox/email/attachments.rs | 16 ++- ui/src/components/mail/view/html.rs | 89 ++++++++++++ .../components/mail/{view.rs => view/mod.rs} | 131 ++++++++++-------- ui/src/state.rs | 3 - 4 files changed, 176 insertions(+), 63 deletions(-) create mode 100644 ui/src/components/mail/view/html.rs rename ui/src/components/mail/{view.rs => view/mod.rs} (82%) diff --git a/melib/src/mailbox/email/attachments.rs b/melib/src/mailbox/email/attachments.rs index eac6bb19..9952f052 100644 --- a/melib/src/mailbox/email/attachments.rs +++ b/melib/src/mailbox/email/attachments.rs @@ -268,18 +268,18 @@ impl fmt::Display for Attachment { AttachmentType::Data { .. } => { write!(f, "Data attachment of type {}", self.mime_type()) } - AttachmentType::Text { .. } => write!(f, "Text attachment"), + AttachmentType::Text { .. } => write!(f, "Text attachment of type {}", self.mime_type()), AttachmentType::Multipart { of_type: ref multipart_type, subattachments: ref sub_att_vec, } => if *multipart_type == MultipartType::Alternative { write!( f, - "Multipart/alternative attachment with {} subs", - sub_att_vec.len() + "{} attachment with {} subs", + self.mime_type(), sub_att_vec.len() ) } else { - write!(f, "Multipart attachment with {} subs", sub_att_vec.len()) + write!(f, "{} attachment with {} subs", self.mime_type(), sub_att_vec.len()) }, } } @@ -332,9 +332,10 @@ impl Attachment { match att.attachment_type { AttachmentType::Data { .. } | AttachmentType::Text { .. } => ret.push(att.clone()), AttachmentType::Multipart { - of_type: ref multipart_type, + of_type: _, subattachments: ref sub_att_vec, - } => if *multipart_type != MultipartType::Alternative { + } => { + ret.push(att.clone()); // TODO: Fix this, wrong count for a in sub_att_vec { count_recursive(a, ret); @@ -358,6 +359,9 @@ impl Attachment { pub fn content_transfer_encoding(&self) -> &ContentTransferEncoding { &self.content_transfer_encoding } + pub fn is_html(&self) -> bool { + self.content_type().0.is_text() && self.content_type().1.is_html() + } } pub fn interpret_format_flowed(_t: &str) -> String { diff --git a/ui/src/components/mail/view/html.rs b/ui/src/components/mail/view/html.rs new file mode 100644 index 00000000..d58dfc2a --- /dev/null +++ b/ui/src/components/mail/view/html.rs @@ -0,0 +1,89 @@ +/* + * 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 HtmlView { + pager: Pager, + bytes: Vec +} + +impl HtmlView { + 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); + HtmlView { + pager, + bytes + } + } +} + +impl Component for HtmlView { + 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() + } +} diff --git a/ui/src/components/mail/view.rs b/ui/src/components/mail/view/mod.rs similarity index 82% rename from ui/src/components/mail/view.rs rename to ui/src/components/mail/view/mod.rs index 01657ccc..2131642c 100644 --- a/ui/src/components/mail/view.rs +++ b/ui/src/components/mail/view/mod.rs @@ -23,6 +23,10 @@ use super::*; use linkify::{Link, LinkFinder}; use std::process::{Command, Stdio}; +mod html; + +pub use self::html::*; + use mime_apps::query_default_app; #[derive(PartialEq, Debug)] @@ -31,6 +35,7 @@ enum ViewMode { Url, Attachment(usize), Raw, + Subview, } impl ViewMode { @@ -47,7 +52,7 @@ impl ViewMode { pub struct MailView { coordinates: (usize, usize, usize), pager: Option, - subview: Option>, + subview: Option>, dirty: bool, mode: ViewMode, @@ -58,7 +63,7 @@ impl MailView { pub fn new( coordinates: (usize, usize, usize), pager: Option, - subview: Option>, + subview: Option>, ) -> Self { MailView { coordinates, @@ -72,10 +77,11 @@ impl MailView { } /// Returns the string to be displayed in the Viewer - fn attachment_to_text(&self, envelope: &Envelope) -> String { + fn attachment_to_text(&self, body: Attachment) -> String { let finder = LinkFinder::new(); - let body = envelope.body(); 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"); + s.extend( String::from_utf8_lossy(&decode(&body, Some(Box::new(|a: &Attachment| { use std::io::Write; use std::process::{Command, Stdio}; @@ -90,12 +96,13 @@ impl MailView { 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() + })))).into_owned().chars()); + s } else { String::from_utf8_lossy(&decode_rec(&body, None)).into() }; match self.mode { - ViewMode::Normal => { + ViewMode::Normal | ViewMode::Subview => { let mut t = body_text.to_string(); if body.count_attachments() > 1 { t = body.attachments().iter().enumerate().fold( @@ -108,7 +115,7 @@ impl MailView { } t } - ViewMode::Raw => String::from_utf8_lossy(&envelope.bytes()).into_owned(), + ViewMode::Raw => String::from_utf8_lossy(body.bytes()).into_owned(), ViewMode::Url => { let mut t = body_text.to_string(); for (lidx, l) in finder.links(&body.text()).enumerate() { @@ -119,7 +126,7 @@ impl MailView { } else if lidx < 1000 { 385 + (lidx - 99) * 5 } else { - panic!("BUG: Message body with more than 100 urls"); + panic!("BUG: Message body with more than 100 urls, fix this"); }; t.insert_str(l.start() + offset, &format!("[{}]", lidx)); } @@ -142,6 +149,38 @@ impl MailView { } } } + pub fn plain_text_to_buf(s: &String, highlight_urls: bool) -> CellBuffer { + let mut buf = CellBuffer::from(s); + + if highlight_urls { + let lines: Vec<&str> = s.split('\n').map(|l| l.trim_right()).collect(); + let mut shift = 0; + let mut lidx_total = 0; + let finder = LinkFinder::new(); + for r in &lines { + for l in finder.links(&r) { + let offset = if lidx_total < 10 { + 3 + } else if lidx_total < 100 { + 4 + } else if lidx_total < 1000 { + 5 + } else { + panic!("BUG: Message body with more than 100 urls"); + }; + for i in 1..=offset { + buf[(l.start() + shift - i, 0)].set_fg(Color::Byte(226)); + //buf[(l.start() + shift - 2, 0)].set_fg(Color::Byte(226)); + //buf[(l.start() + shift - 3, 0)].set_fg(Color::Byte(226)); + } + lidx_total += 1; + } + // Each Cell represents one char so next line will be: + shift += r.chars().count() + 1; + } + } + buf + } } impl Component for MailView { @@ -244,55 +283,39 @@ impl Component for MailView { }; if self.dirty { - let buf = { - let mailbox_idx = self.coordinates; // coordinates are mailbox idxs - let mailbox = &mut context.accounts[mailbox_idx.0][mailbox_idx.1] - .as_ref() - .unwrap(); - let envelope: &Envelope = &mailbox.collection[envelope_idx]; - let text = self.attachment_to_text(envelope); - - let mut buf = CellBuffer::from(&text); - if self.mode == ViewMode::Url { - // URL indexes must be colored (ugh..) - let lines: Vec<&str> = text.split('\n').map(|l| l.trim_right()).collect(); - let mut shift = 0; - let mut lidx_total = 0; - let finder = LinkFinder::new(); - for r in &lines { - for l in finder.links(&r) { - let offset = if lidx_total < 10 { - 3 - } else if lidx_total < 100 { - 4 - } else if lidx_total < 1000 { - 5 - } else { - panic!("BUG: Message body with more than 100 urls"); - }; - for i in 1..=offset { - buf[(l.start() + shift - i, 0)].set_fg(Color::Byte(226)); - //buf[(l.start() + shift - 2, 0)].set_fg(Color::Byte(226)); - //buf[(l.start() + shift - 3, 0)].set_fg(Color::Byte(226)); - } - lidx_total += 1; - } - // Each Cell represents one char so next line will be: - shift += r.chars().count() + 1; - } - } - buf + let mailbox_idx = self.coordinates; // coordinates are mailbox idxs + let mailbox = &mut context.accounts[mailbox_idx.0][mailbox_idx.1] + .as_ref() + .unwrap(); + let envelope: &Envelope = &mailbox.collection[envelope_idx]; + 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)))); + }, + 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); + // URL indexes must be colored (ugh..) + MailView::plain_text_to_buf(&text, self.mode == ViewMode::Url) + }; + let cursor_pos = if self.mode.is_attachment() { + Some(0) + } else { + self.pager.as_mut().map(|p| p.cursor_pos()) + }; + self.pager = Some(Pager::from_buf(&buf, cursor_pos)); + }, }; - let cursor_pos = if self.mode.is_attachment() { - Some(0) - } else { - self.pager.as_mut().map(|p| p.cursor_pos()) - }; - self.pager = Some(Pager::from_buf(&buf, cursor_pos)); self.dirty = false; } - - if let Some(p) = self.pager.as_mut() { + if let Some(s) = self.subview.as_mut() { + s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); + } else if let Some(p) = self.pager.as_mut() { p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); } } diff --git a/ui/src/state.rs b/ui/src/state.rs index a9fdcd24..e9f280ec 100644 --- a/ui/src/state.rs +++ b/ui/src/state.rs @@ -63,9 +63,6 @@ impl Context { pub fn input_thread(&mut self) -> &mut chan::Sender { &mut self.input_thread } - pub fn add_temp(&mut self, f: File) -> () { - self.temp_files.push(f); - } } /// A State object to manage and own components and entities of the UI. `State` is responsible for