Add html view
parent
a3a98f894f
commit
befe00dea6
|
@ -268,18 +268,18 @@ impl fmt::Display for Attachment {
|
||||||
AttachmentType::Data { .. } => {
|
AttachmentType::Data { .. } => {
|
||||||
write!(f, "Data attachment of type {}", self.mime_type())
|
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 {
|
AttachmentType::Multipart {
|
||||||
of_type: ref multipart_type,
|
of_type: ref multipart_type,
|
||||||
subattachments: ref sub_att_vec,
|
subattachments: ref sub_att_vec,
|
||||||
} => if *multipart_type == MultipartType::Alternative {
|
} => if *multipart_type == MultipartType::Alternative {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"Multipart/alternative attachment with {} subs",
|
"{} attachment with {} subs",
|
||||||
sub_att_vec.len()
|
self.mime_type(), sub_att_vec.len()
|
||||||
)
|
)
|
||||||
} else {
|
} 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 {
|
match att.attachment_type {
|
||||||
AttachmentType::Data { .. } | AttachmentType::Text { .. } => ret.push(att.clone()),
|
AttachmentType::Data { .. } | AttachmentType::Text { .. } => ret.push(att.clone()),
|
||||||
AttachmentType::Multipart {
|
AttachmentType::Multipart {
|
||||||
of_type: ref multipart_type,
|
of_type: _,
|
||||||
subattachments: ref sub_att_vec,
|
subattachments: ref sub_att_vec,
|
||||||
} => if *multipart_type != MultipartType::Alternative {
|
} => {
|
||||||
|
ret.push(att.clone());
|
||||||
// TODO: Fix this, wrong count
|
// TODO: Fix this, wrong count
|
||||||
for a in sub_att_vec {
|
for a in sub_att_vec {
|
||||||
count_recursive(a, ret);
|
count_recursive(a, ret);
|
||||||
|
@ -358,6 +359,9 @@ impl Attachment {
|
||||||
pub fn content_transfer_encoding(&self) -> &ContentTransferEncoding {
|
pub fn content_transfer_encoding(&self) -> &ContentTransferEncoding {
|
||||||
&self.content_transfer_encoding
|
&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 {
|
pub fn interpret_format_flowed(_t: &str) -> String {
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
pub struct HtmlView {
|
||||||
|
pager: Pager,
|
||||||
|
bytes: Vec<u8>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HtmlView {
|
||||||
|
pub fn new(bytes: Vec<u8>) -> 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,10 @@ use super::*;
|
||||||
use linkify::{Link, LinkFinder};
|
use linkify::{Link, LinkFinder};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
mod html;
|
||||||
|
|
||||||
|
pub use self::html::*;
|
||||||
|
|
||||||
use mime_apps::query_default_app;
|
use mime_apps::query_default_app;
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
|
@ -31,6 +35,7 @@ enum ViewMode {
|
||||||
Url,
|
Url,
|
||||||
Attachment(usize),
|
Attachment(usize),
|
||||||
Raw,
|
Raw,
|
||||||
|
Subview,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViewMode {
|
impl ViewMode {
|
||||||
|
@ -47,7 +52,7 @@ impl ViewMode {
|
||||||
pub struct MailView {
|
pub struct MailView {
|
||||||
coordinates: (usize, usize, usize),
|
coordinates: (usize, usize, usize),
|
||||||
pager: Option<Pager>,
|
pager: Option<Pager>,
|
||||||
subview: Option<Box<MailView>>,
|
subview: Option<Box<Component>>,
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
mode: ViewMode,
|
mode: ViewMode,
|
||||||
|
|
||||||
|
@ -58,7 +63,7 @@ impl MailView {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
coordinates: (usize, usize, usize),
|
coordinates: (usize, usize, usize),
|
||||||
pager: Option<Pager>,
|
pager: Option<Pager>,
|
||||||
subview: Option<Box<MailView>>,
|
subview: Option<Box<Component>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
MailView {
|
MailView {
|
||||||
coordinates,
|
coordinates,
|
||||||
|
@ -72,10 +77,11 @@ impl MailView {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the string to be displayed in the Viewer
|
/// 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 finder = LinkFinder::new();
|
||||||
let body = envelope.body();
|
|
||||||
let body_text = if body.content_type().0.is_text() && body.content_type().1.is_html() {
|
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| {
|
String::from_utf8_lossy(&decode(&body, Some(Box::new(|a: &Attachment| {
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::process::{Command, Stdio};
|
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.stdin.as_mut().unwrap().write_all(&raw).expect("Failed to write to w3m stdin");
|
||||||
html_filter.wait_with_output().unwrap().stdout
|
html_filter.wait_with_output().unwrap().stdout
|
||||||
})))).into_owned()
|
})))).into_owned().chars());
|
||||||
|
s
|
||||||
} else {
|
} else {
|
||||||
String::from_utf8_lossy(&decode_rec(&body, None)).into()
|
String::from_utf8_lossy(&decode_rec(&body, None)).into()
|
||||||
};
|
};
|
||||||
match self.mode {
|
match self.mode {
|
||||||
ViewMode::Normal => {
|
ViewMode::Normal | ViewMode::Subview => {
|
||||||
let mut t = body_text.to_string();
|
let mut t = body_text.to_string();
|
||||||
if body.count_attachments() > 1 {
|
if body.count_attachments() > 1 {
|
||||||
t = body.attachments().iter().enumerate().fold(
|
t = body.attachments().iter().enumerate().fold(
|
||||||
|
@ -108,7 +115,7 @@ impl MailView {
|
||||||
}
|
}
|
||||||
t
|
t
|
||||||
}
|
}
|
||||||
ViewMode::Raw => String::from_utf8_lossy(&envelope.bytes()).into_owned(),
|
ViewMode::Raw => String::from_utf8_lossy(body.bytes()).into_owned(),
|
||||||
ViewMode::Url => {
|
ViewMode::Url => {
|
||||||
let mut t = body_text.to_string();
|
let mut t = body_text.to_string();
|
||||||
for (lidx, l) in finder.links(&body.text()).enumerate() {
|
for (lidx, l) in finder.links(&body.text()).enumerate() {
|
||||||
|
@ -119,7 +126,7 @@ impl MailView {
|
||||||
} else if lidx < 1000 {
|
} else if lidx < 1000 {
|
||||||
385 + (lidx - 99) * 5
|
385 + (lidx - 99) * 5
|
||||||
} else {
|
} 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));
|
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 {
|
impl Component for MailView {
|
||||||
|
@ -244,55 +283,39 @@ impl Component for MailView {
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.dirty {
|
if self.dirty {
|
||||||
let buf = {
|
let mailbox_idx = self.coordinates; // coordinates are mailbox idxs
|
||||||
let mailbox_idx = self.coordinates; // coordinates are mailbox idxs
|
let mailbox = &mut context.accounts[mailbox_idx.0][mailbox_idx.1]
|
||||||
let mailbox = &mut context.accounts[mailbox_idx.0][mailbox_idx.1]
|
.as_ref()
|
||||||
.as_ref()
|
.unwrap();
|
||||||
.unwrap();
|
let envelope: &Envelope = &mailbox.collection[envelope_idx];
|
||||||
let envelope: &Envelope = &mailbox.collection[envelope_idx];
|
let body = envelope.body();
|
||||||
let text = self.attachment_to_text(envelope);
|
match self.mode {
|
||||||
|
ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => {
|
||||||
let mut buf = CellBuffer::from(&text);
|
self.subview = Some(Box::new(HtmlView::new(decode(&body.attachments()[aidx], None))));
|
||||||
if self.mode == ViewMode::Url {
|
},
|
||||||
// URL indexes must be colored (ugh..)
|
ViewMode::Normal if body.is_html() => {
|
||||||
let lines: Vec<&str> = text.split('\n').map(|l| l.trim_right()).collect();
|
self.subview = Some(Box::new(HtmlView::new(decode(&body, None))));
|
||||||
let mut shift = 0;
|
self.mode = ViewMode::Subview;
|
||||||
let mut lidx_total = 0;
|
},
|
||||||
let finder = LinkFinder::new();
|
_ => {
|
||||||
for r in &lines {
|
let buf = {
|
||||||
for l in finder.links(&r) {
|
let text = self.attachment_to_text(body);
|
||||||
let offset = if lidx_total < 10 {
|
// URL indexes must be colored (ugh..)
|
||||||
3
|
MailView::plain_text_to_buf(&text, self.mode == ViewMode::Url)
|
||||||
} else if lidx_total < 100 {
|
};
|
||||||
4
|
let cursor_pos = if self.mode.is_attachment() {
|
||||||
} else if lidx_total < 1000 {
|
Some(0)
|
||||||
5
|
} else {
|
||||||
} else {
|
self.pager.as_mut().map(|p| p.cursor_pos())
|
||||||
panic!("BUG: Message body with more than 100 urls");
|
};
|
||||||
};
|
self.pager = Some(Pager::from_buf(&buf, cursor_pos));
|
||||||
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 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;
|
self.dirty = false;
|
||||||
}
|
}
|
||||||
|
if let Some(s) = self.subview.as_mut() {
|
||||||
if let Some(p) = self.pager.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);
|
p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -63,9 +63,6 @@ impl Context {
|
||||||
pub fn input_thread(&mut self) -> &mut chan::Sender<bool> {
|
pub fn input_thread(&mut self) -> &mut chan::Sender<bool> {
|
||||||
&mut self.input_thread
|
&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
|
/// A State object to manage and own components and entities of the UI. `State` is responsible for
|
||||||
|
|
Loading…
Reference in New Issue