diff --git a/melib/src/mailbox/email/attachments.rs b/melib/src/mailbox/email/attachments.rs index ec126ca2..ed0a60ab 100644 --- a/melib/src/mailbox/email/attachments.rs +++ b/melib/src/mailbox/email/attachments.rs @@ -412,9 +412,9 @@ fn decode_rfc822(_raw: &[u8]) -> Attachment { */ } -type Filter = Box) -> ()>; +type Filter<'a> = Box) -> () + 'a>; -fn decode_rec_helper(a: &Attachment, filter: &Option) -> Vec { +fn decode_rec_helper<'a>(a: &'a Attachment, filter: &mut Option>) -> Vec { let mut ret = match a.content_type { ContentType::Unsupported { .. } => Vec::new(), ContentType::Text { .. } => decode_helper(a, filter), @@ -449,11 +449,11 @@ fn decode_rec_helper(a: &Attachment, filter: &Option) -> Vec { ret } -pub fn decode_rec(a: &Attachment, filter: Option) -> Vec { - decode_rec_helper(a, &filter) +pub fn decode_rec<'a>(a: &'a Attachment, mut filter: Option>) -> Vec { + decode_rec_helper(a, &mut filter) } -fn decode_helper(a: &Attachment, filter: &Option) -> Vec { +fn decode_helper<'a>(a: &'a Attachment, filter: &mut Option>) -> Vec { let charset = match a.content_type { ContentType::Text { charset: c, .. } => c, _ => Default::default(), @@ -488,6 +488,6 @@ fn decode_helper(a: &Attachment, filter: &Option) -> Vec { ret } -pub fn decode(a: &Attachment, filter: Option) -> Vec { - decode_helper(a, &filter) +pub fn decode<'a>(a: &'a Attachment, mut filter: Option>) -> Vec { + decode_helper(a, &mut filter) } diff --git a/ui/src/components/mail/view.rs b/ui/src/components/mail/view.rs index d2549330..b73056fb 100644 --- a/ui/src/components/mail/view.rs +++ b/ui/src/components/mail/view.rs @@ -116,31 +116,52 @@ impl MailView { } /// Returns the string to be displayed in the Viewer - fn attachment_to_text(&self, body: &Attachment) -> String { + fn attachment_to_text<'closure, 's: 'closure, 'context: 's>( + &'s self, + body: &'context Attachment, + context: &'context mut Context, + ) -> String { let finder = LinkFinder::new(); let body_text = String::from_utf8_lossy(&decode_rec( - &body, - Some(Box::new(|a: &Attachment, v: &mut Vec| { + body, + Some(Box::new(move |a: &'closure Attachment, v: &mut Vec| { if a.content_type().is_text_html() { use std::io::Write; use std::process::{Command, Stdio}; + let settings = context.accounts[self.coordinates.0].runtime_settings.conf(); + if let Some(filter_invocation) = settings.html_filter() { + let parts = split_command!(filter_invocation); + let (cmd, args) = (parts[0], &parts[1..]); + let command_obj = Command::new(cmd) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn(); + if command_obj.is_err() { + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::Notification( + Some(format!( + "Failed to start html filter process: {}", + filter_invocation, + )), + String::new(), + ), + }); + return; + } - 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(&v) - .expect("Failed to write to w3m stdin"); - *v = b"Text piped through `w3m`. Press `v` to open in web browser. \n\n" - .to_vec(); - v.extend(html_filter.wait_with_output().unwrap().stdout); + let mut html_filter = command_obj.unwrap(); + html_filter + .stdin + .as_mut() + .unwrap() + .write_all(&v) + .expect("Failed to write to stdin"); + *v = format!("Text piped through `{}`. Press `v` to open in web browser. \n\n", + filter_invocation).into_bytes(); + v.extend(html_filter.wait_with_output().unwrap().stdout); + } } })), )) @@ -326,35 +347,42 @@ impl Component for MailView { }; if self.dirty { - let mailbox_idx = self.coordinates; // coordinates are mailbox idxs - let mailbox = &context.accounts[mailbox_idx.0][mailbox_idx.1] - .as_ref() - .unwrap(); - let envelope: &Envelope = &mailbox.collection[&mailbox_idx.2]; - let op = context.accounts[mailbox_idx.0] - .backend - .operation(envelope.hash(), mailbox.folder.hash()); - let body = envelope.body(op); + let body = { + let mailbox_idx = self.coordinates; // coordinates are mailbox idxs + let mailbox = &context.accounts[mailbox_idx.0][mailbox_idx.1] + .as_ref() + .unwrap(); + let envelope: &Envelope = &mailbox.collection[&mailbox_idx.2]; + let op = context.accounts[mailbox_idx.0] + .backend + .operation(envelope.hash(), mailbox.folder.hash()); + envelope.body(op) + }; match self.mode { ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => { self.pager = None; - self.subview = Some(Box::new(HtmlView::new(decode( - &body.attachments()[aidx], - None, - )))); + let attachment = &body.attachments()[aidx]; + self.subview = Some(Box::new(HtmlView::new( + decode(&attachment, None), + context, + self.coordinates.0, + ))); self.mode = ViewMode::Subview; } ViewMode::Normal if body.is_html() => { - self.subview = Some(Box::new(HtmlView::new(decode(&body, None)))); + self.subview = Some(Box::new(HtmlView::new( + decode(&body, None), + context, + self.coordinates.0, + ))); self.pager = None; self.mode = ViewMode::Subview; } ViewMode::Subview | ViewMode::ContactSelector(_) => {} _ => { let text = { - self.attachment_to_text(&body) + self.attachment_to_text(&body, context) /* - let text = self.attachment_to_text(&body); // URL indexes must be colored (ugh..) MailView::plain_text_to_buf(&text, self.mode == ViewMode::Url) */ @@ -530,8 +558,12 @@ impl Component for MailView { self.mode = ViewMode::Subview; match EnvelopeWrapper::new(u.bytes().to_vec()) { Ok(wrapper) => { - self.subview = - Some(Box::new(EnvelopeView::new(wrapper, None, None))); + self.subview = Some(Box::new(EnvelopeView::new( + wrapper, + None, + None, + self.coordinates.0, + ))); } Err(e) => { context.replies.push_back(UIEvent { diff --git a/ui/src/components/mail/view/envelope.rs b/ui/src/components/mail/view/envelope.rs index 9f028195..5c490579 100644 --- a/ui/src/components/mail/view/envelope.rs +++ b/ui/src/components/mail/view/envelope.rs @@ -53,6 +53,7 @@ pub struct EnvelopeView { mode: ViewMode, wrapper: EnvelopeWrapper, + account_pos: usize, cmd_buf: String, } @@ -68,6 +69,7 @@ impl EnvelopeView { wrapper: EnvelopeWrapper, pager: Option, subview: Option>, + account_pos: usize, ) -> Self { EnvelopeView { pager, @@ -75,13 +77,17 @@ impl EnvelopeView { dirty: true, mode: ViewMode::Normal, wrapper, - + account_pos, cmd_buf: String::with_capacity(4), } } /// Returns the string to be displayed in the Viewer - fn attachment_to_text(&self, body: &Attachment) -> String { + fn attachment_to_text<'closure, 's: 'closure, 'context: 's>( + &'s self, + body: &'context Attachment, + context: &'context mut Context, + ) -> String { let finder = LinkFinder::new(); let body_text = String::from_utf8_lossy(&decode_rec( &body, @@ -89,23 +95,40 @@ impl EnvelopeView { if a.content_type().is_text_html() { use std::io::Write; use std::process::{Command, Stdio}; + let settings = context.accounts[self.account_pos].runtime_settings.conf(); + if let Some(filter_invocation) = settings.html_filter() { + let parts = split_command!(filter_invocation); + let (cmd, args) = (parts[0], &parts[1..]); + let command_obj = Command::new(cmd) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn(); + if command_obj.is_err() { + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::Notification( + Some(format!( + "Failed to start html filter process: {}", + filter_invocation, + )), + String::new(), + ), + }); + return; + } - 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(&v) - .expect("Failed to write to w3m stdin"); - *v = b"Text piped through `w3m`. Press `v` to open in web browser. \n\n" - .to_vec(); - v.extend(html_filter.wait_with_output().unwrap().stdout); + let mut html_filter = command_obj.unwrap(); + html_filter + .stdin + .as_mut() + .unwrap() + .write_all(&v) + .expect("Failed to write to stdin"); + *v = format!("Text piped through `{}`. Press `v` to open in web browser. \n\n", + filter_invocation).into_bytes(); + v.extend(html_filter.wait_with_output().unwrap().stdout); + } } })), )) @@ -288,18 +311,24 @@ impl Component for EnvelopeView { let body = self.wrapper.body_bytes(self.wrapper.buffer()); match self.mode { ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => { - self.subview = Some(Box::new(HtmlView::new(decode( - &body.attachments()[aidx], - None, - )))); + let attachment = &body.attachments()[aidx]; + self.subview = Some(Box::new(HtmlView::new( + decode(&attachment, None), + context, + self.account_pos, + ))); } ViewMode::Normal if body.is_html() => { - self.subview = Some(Box::new(HtmlView::new(decode(&body, None)))); + self.subview = Some(Box::new(HtmlView::new( + decode(&body, None), + context, + self.account_pos, + ))); self.mode = ViewMode::Subview; } _ => { let text = { - self.attachment_to_text(&body) + self.attachment_to_text(&body, context) /* let text = self.attachment_to_text(&body); // URL indexes must be colored (ugh..) diff --git a/ui/src/components/mail/view/html.rs b/ui/src/components/mail/view/html.rs index 4eb4f2bc..b4ca9f55 100644 --- a/ui/src/components/mail/view/html.rs +++ b/ui/src/components/mail/view/html.rs @@ -30,27 +30,94 @@ pub struct HtmlView { } 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, - )); + pub fn new(bytes: Vec, context: &mut Context, account_pos: usize) -> Self { + let settings = context.accounts[account_pos].runtime_settings.conf(); + if let Some(filter_invocation) = settings.html_filter() { + let parts = split_command!(filter_invocation); + let (cmd, args) = (parts[0], &parts[1..]); + let command_obj = Command::new(cmd) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn(); + if command_obj.is_err() { + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::Notification( + Some(format!( + "Failed to start html filter process: {}", + filter_invocation + )), + String::new(), + ), + }); + let pager = Pager::from_string( + String::from_utf8_lossy(&bytes).to_string(), + None, + None, + None, + ); + HtmlView { pager, bytes } + } else { + let mut html_filter = command_obj.unwrap(); + html_filter + .stdin + .as_mut() + .unwrap() + .write_all(&bytes) + .expect("Failed to write to html filter stdin"); + let mut display_text = format!( + "Text piped through `{}`. Press `v` to open in web browser. \n\n", + filter_invocation + ); + display_text.push_str(&String::from_utf8_lossy( + &html_filter.wait_with_output().unwrap().stdout, + )); - let pager = Pager::from_string(display_text, None, None, None); - HtmlView { pager, bytes } + let pager = Pager::from_string(display_text, None, None, None); + HtmlView { pager, bytes } + } + } else { + if let Ok(mut html_filter) = Command::new("w3m") + .args(&["-I", "utf-8", "-T", "text/html"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + { + html_filter + .stdin + .as_mut() + .unwrap() + .write_all(&bytes) + .expect("Failed to write to html filter 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 pager = Pager::from_string(display_text, None, None, None); + HtmlView { pager, bytes } + } else { + context.replies.push_back(UIEvent { + id: 0, + event_type: UIEventType::Notification( + Some(format!( + "Failed to find any application to use as html filter" + )), + String::new(), + ), + }); + let pager = Pager::from_string( + String::from_utf8_lossy(&bytes).to_string(), + None, + None, + None, + ); + HtmlView { pager, bytes } + } + } } } diff --git a/ui/src/conf.rs b/ui/src/conf.rs index 564540bf..2a5c96e1 100644 --- a/ui/src/conf.rs +++ b/ui/src/conf.rs @@ -33,6 +33,7 @@ pub use self::accounts::Account; use self::config::{Config, File, FileFormat}; pub use self::shortcuts::*; +use self::default_vals::*; use self::notifications::NotificationsSettings; use melib::conf::AccountSettings; use melib::error::*; @@ -43,8 +44,11 @@ use std::collections::HashMap; use std::env; use std::path::PathBuf; -fn true_val() -> bool { - true +#[macro_export] +macro_rules! split_command { + ($cmd:expr) => {{ + $cmd.split_whitespace().collect::>() + }}; } #[derive(Debug, Clone, Default, Deserialize)] @@ -68,9 +72,17 @@ pub struct FileAccount { sent_folder: String, draft_folder: String, identity: String, + + #[serde(default = "none")] display_name: Option, #[serde(deserialize_with = "index_from_str")] index: IndexStyle, + + /// A command to pipe html output before displaying it in a pager + /// Default: None + #[serde(default = "none", deserialize_with = "non_empty_string")] + html_filter: Option, + folders: Option>, } @@ -108,6 +120,9 @@ impl FileAccount { pub fn index(&self) -> IndexStyle { self.index } + pub fn html_filter(&self) -> Option<&str> { + self.html_filter.as_ref().map(|f| f.as_str()) + } } #[derive(Debug, Clone, Default, Deserialize)] @@ -218,3 +233,41 @@ where _ => Err(de::Error::custom("invalid `index` value")), } } + +fn non_empty_string<'de, D>(deserializer: D) -> std::result::Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = ::deserialize(deserializer)?; + if s.is_empty() { + Ok(None) + } else { + Ok(Some(s)) + } +} + +/* + * Deserialize default functions + */ + +mod default_vals { + pub(in conf) fn false_val() -> bool { + true + } + + pub(in conf) fn true_val() -> bool { + true + } + + pub(in conf) fn zero_val() -> usize { + 0 + } + + pub(in conf) fn eighty_percent() -> usize { + 80 + } + + pub(in conf) fn none() -> Option { + None + } +} diff --git a/ui/src/conf/pager.rs b/ui/src/conf/pager.rs index 0575d98f..59eaa2fc 100644 --- a/ui/src/conf/pager.rs +++ b/ui/src/conf/pager.rs @@ -19,25 +19,7 @@ * along with meli. If not, see . */ -fn false_val() -> bool { - true -} - -fn true_val() -> bool { - true -} - -fn zero_val() -> usize { - 0 -} -fn eighty_percent() -> usize { - 80 -} - -fn none() -> Option { - None -} - +use super::default_vals::*; /// Settings for the pager function. #[derive(Debug, Deserialize, Clone, Default)] pub struct PagerSettings {