view: rework attachment rendering logic with filters

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
view-filters-redo
Manos Pitsidianakis 3 months ago
parent 8dd87c1ac5
commit e8a3205ba9
No known key found for this signature in database
GPG Key ID: 7729C7707F7E09D0

@ -1061,6 +1061,8 @@ impl Component for EnvelopeView {
context,
) {
self.filters.push(filter);
} else if let Ok(filter) = ViewFilter::new_attachment(&body, context) {
self.filters.push(filter);
}
} else if let Ok(filter) = ViewFilter::new_attachment(&body, context) {
self.filters.push(filter);
@ -1070,35 +1072,46 @@ impl Component for EnvelopeView {
)
.to_string();
}
if !self.initialised {
if dbg!(!self.initialised) {
self.initialised = true;
let mut text = if let Some(ViewFilter {
filter_invocation,
body_text,
notice,
..
}) = self.filters.last()
{
let mut text = if self.filters.len() == 1 {
if filter_invocation.is_empty() {
String::new()
} else {
format!("Text piped through `{filter_invocation}`\n\n")
}
} else {
notice
.as_ref()
.map(|s| s.to_string())
.or_else(|| {
if filter_invocation.is_empty() {
None
} else {
Some(format!("Text piped through `{filter_invocation}`\n\n"))
let mut text = if !self.filters.is_empty() {
let mut text = String::new();
if let Some(last) = self.filters.last() {
let mut stack = vec![last];
while let Some(ViewFilter {
filter_invocation,
body_text,
notice,
..
}) = stack.pop()
{
text.push_str(
&notice
.as_ref()
.map(|s| s.to_string())
.or_else(|| {
if filter_invocation.is_empty() {
None
} else {
Some(format!("Text filtered by `{filter_invocation}`\n\n"))
}
})
.unwrap_or_default(),
);
match body_text {
ViewFilterContent::Filtered { inner } => text.push_str(
&self.options.convert(&mut self.links, &self.body, inner),
),
ViewFilterContent::Error { inner } => text.push_str(&inner.to_string()),
ViewFilterContent::Running { .. } => {
text.push_str("Filter job running in background.")
}
})
.unwrap_or_default()
};
text.push_str(&self.options.convert(&mut self.links, &self.body, body_text));
ViewFilterContent::InlineAttachments { parts } => {
stack.extend(parts.iter());
}
}
}
}
text
} else {
self.options
@ -1156,6 +1169,134 @@ impl Component for EnvelopeView {
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
if matches!(event, UIEvent::StatusEvent(StatusEvent::JobFinished(_))) {
match *event {
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
if self.active_jobs.contains(job_id) =>
{
let mut caught = false;
for d in self.display.iter_mut() {
match d {
AttachmentDisplay::SignedPending {
ref mut inner,
handle,
display,
job_id: our_job_id,
} if *our_job_id == *job_id => {
caught = true;
match handle.chan.try_recv() {
Err(_) => { /* Job was canceled */ }
Ok(None) => { /* something happened,
* perhaps a worker thread
* panicked */
}
Ok(Some(Ok(()))) => {
*d = AttachmentDisplay::SignedVerified {
inner: std::mem::replace(
inner,
Box::new(AttachmentBuilder::new(&[]).build()),
),
display: std::mem::take(display),
description: String::new(),
};
}
Ok(Some(Err(error))) => {
*d = AttachmentDisplay::SignedFailed {
inner: std::mem::replace(
inner,
Box::new(AttachmentBuilder::new(&[]).build()),
),
display: std::mem::take(display),
error,
};
}
}
}
AttachmentDisplay::EncryptedPending {
ref mut inner,
handle,
} if handle.job_id == *job_id => {
caught = true;
match handle.chan.try_recv() {
Err(_) => { /* Job was canceled */ }
Ok(None) => { /* something happened,
* perhaps a worker thread
* panicked */
}
Ok(Some(Ok((metadata, decrypted_bytes)))) => {
let plaintext = Box::new(
AttachmentBuilder::new(&decrypted_bytes).build(),
);
let mut plaintext_display = vec![];
Self::attachment_to_display_helper(
&plaintext,
&self.main_loop_handler,
&mut self.active_jobs,
&mut plaintext_display,
&self.view_settings,
(&self.force_charset).into(),
);
*d = AttachmentDisplay::EncryptedSuccess {
inner: std::mem::replace(
inner,
Box::new(AttachmentBuilder::new(&[]).build()),
),
plaintext,
plaintext_display,
description: format!("{:?}", metadata),
};
}
Ok(Some(Err(error))) => {
*d = AttachmentDisplay::EncryptedFailed {
inner: std::mem::replace(
inner,
Box::new(AttachmentBuilder::new(&[]).build()),
),
error,
};
}
}
}
_ => {}
}
}
if caught {
self.links.clear();
self.regenerate_body_text();
self.initialised = false;
self.set_dirty(true);
}
self.active_jobs.remove(job_id);
}
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
if dbg!(self.filters.iter_mut().any(|filter| {
dbg!(&filter);
dbg!(if let Some(cb) = filter.event_handler {
if cb(
filter,
&mut UIEvent::StatusEvent(StatusEvent::JobFinished(*job_id)),
context,
) {
return true;
}
});
false
})) =>
{
log::trace!(
"after calling job event handles, filters are: {:?}",
&self.filters
);
self.links.clear();
self.regenerate_body_text();
self.initialised = false;
self.set_dirty(true);
}
_ => {}
}
log::trace!("envelope.rs got job finished: {:?}", event);
}
match (&mut self.force_charset, &event) {
(ForceCharset::Dialog(selector), UIEvent::FinishedUIDialog(id, results))
if *id == selector.id() =>
@ -1669,103 +1810,6 @@ impl Component for EnvelopeView {
self.dirty = true;
return true;
}
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
if self.active_jobs.contains(job_id) =>
{
let mut caught = false;
for d in self.display.iter_mut() {
match d {
AttachmentDisplay::SignedPending {
ref mut inner,
handle,
display,
job_id: our_job_id,
} if *our_job_id == *job_id => {
caught = true;
match handle.chan.try_recv() {
Err(_) => { /* Job was canceled */ }
Ok(None) => { /* something happened,
* perhaps a worker thread
* panicked */
}
Ok(Some(Ok(()))) => {
*d = AttachmentDisplay::SignedVerified {
inner: std::mem::replace(
inner,
Box::new(AttachmentBuilder::new(&[]).build()),
),
display: std::mem::take(display),
description: String::new(),
};
}
Ok(Some(Err(error))) => {
*d = AttachmentDisplay::SignedFailed {
inner: std::mem::replace(
inner,
Box::new(AttachmentBuilder::new(&[]).build()),
),
display: std::mem::take(display),
error,
};
}
}
}
AttachmentDisplay::EncryptedPending {
ref mut inner,
handle,
} if handle.job_id == *job_id => {
caught = true;
match handle.chan.try_recv() {
Err(_) => { /* Job was canceled */ }
Ok(None) => { /* something happened,
* perhaps a worker thread
* panicked */
}
Ok(Some(Ok((metadata, decrypted_bytes)))) => {
let plaintext =
Box::new(AttachmentBuilder::new(&decrypted_bytes).build());
let mut plaintext_display = vec![];
Self::attachment_to_display_helper(
&plaintext,
&self.main_loop_handler,
&mut self.active_jobs,
&mut plaintext_display,
&self.view_settings,
(&self.force_charset).into(),
);
*d = AttachmentDisplay::EncryptedSuccess {
inner: std::mem::replace(
inner,
Box::new(AttachmentBuilder::new(&[]).build()),
),
plaintext,
plaintext_display,
description: format!("{:?}", metadata),
};
}
Ok(Some(Err(error))) => {
*d = AttachmentDisplay::EncryptedFailed {
inner: std::mem::replace(
inner,
Box::new(AttachmentBuilder::new(&[]).build()),
),
error,
};
}
}
}
_ => {}
}
}
if caught {
self.links.clear();
self.regenerate_body_text();
self.initialised = false;
self.set_dirty(true);
}
self.active_jobs.remove(job_id);
}
_ => {}
}
false

@ -31,25 +31,77 @@ type ProcessEventFn = fn(&mut ViewFilter, &mut UIEvent, &mut Context) -> bool;
use melib::{
attachment_types::{ContentType, MultipartType, Text},
error::*,
log,
parser::BytesExt,
text::Truncate,
utils::xdg::query_default_app,
Attachment, Result,
Attachment, AttachmentBuilder, Result,
};
use crate::{
components::*,
desktop_exec_to_command,
jobs::{JobId, JoinHandle},
terminal::{Area, CellBuffer},
Context, ErrorKind, File, StatusEvent, UIEvent,
try_recv_timeout, Context, ErrorKind, File, StatusEvent, UIEvent,
};
#[derive(Clone)]
use smallvec::SmallVec;
type FilterResult = std::result::Result<(Attachment, Vec<u8>), (Error, Vec<u8>)>;
type OnSuccessNoticeCb = Arc<dyn (Fn() -> Cow<'static, str>) + Send + Sync>;
pub enum ViewFilterContent {
Running {
job_id: JobId,
on_success_notice_cb: OnSuccessNoticeCb,
job_handle: JoinHandle<FilterResult>,
},
Error {
inner: Error,
},
Filtered {
inner: String,
},
InlineAttachments {
parts: Vec<ViewFilter>,
},
}
impl std::fmt::Debug for ViewFilterContent {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
use ViewFilterContent::*;
match self {
Running {
ref job_id,
on_success_notice_cb: _,
job_handle: _,
} => fmt
.debug_struct(stringify!(ViewFilterContent::Running))
.field("job_id", &job_id)
.finish(),
Error { ref inner } => fmt
.debug_struct(stringify!(ViewFilterContent::Error))
.field("error", inner)
.finish(),
Filtered { ref inner } => fmt
.debug_struct(stringify!(ViewFilterContent::Filtered))
.field("body_text", &inner.trim_at_boundary(18))
.field("body_text_len", &inner.len())
.finish(),
InlineAttachments { ref parts } => fmt
.debug_struct(stringify!(ViewFilterContent::InlineAttachments))
.field("parts", &parts.len())
.finish(),
}
}
}
pub struct ViewFilter {
pub filter_invocation: String,
pub content_type: ContentType,
pub notice: Option<Cow<'static, str>>,
pub body_text: String,
pub body_text: ViewFilterContent,
pub unfiltered: Vec<u8>,
pub event_handler: Option<ProcessEventFn>,
pub id: ComponentId,
@ -61,8 +113,7 @@ impl std::fmt::Debug for ViewFilter {
.field("filter_invocation", &self.filter_invocation)
.field("content_type", &self.content_type)
.field("notice", &self.notice)
.field("body_text", &self.body_text.trim_at_boundary(18))
.field("body_text_len", &self.body_text.len())
.field("body_text", &self.body_text)
.field("event_handler", &self.event_handler.is_some())
.field("id", &self.id)
.finish()
@ -171,53 +222,99 @@ impl ViewFilter {
_ => {}
}
}
let settings = &context.settings;
let (filter_invocation, cmd, args): (
Cow<'static, str>,
&'static str,
SmallVec<[Cow<'static, str>; 8]>,
) = if let Some(filter_invocation) = settings.pager.html_filter.as_ref() {
(
filter_invocation.to_string().into(),
"sh",
smallvec::smallvec!["-c".into(), filter_invocation.to_string().into()],
)
} else {
(
"w3m -I utf-8 -T text/html".into(),
"w3m",
smallvec::smallvec!["-I".into(), "utf-8".into(), "-T".into(), "text/html".into()],
)
};
let bytes: Vec<u8> = att.decode(Default::default());
let settings = &context.settings;
if let Some(filter_invocation) = settings.pager.html_filter.as_ref() {
match run("sh", &["-c", filter_invocation], &bytes) {
let filter_invocation2 = filter_invocation.to_string();
let bytes2 = bytes.clone();
let job = async move {
let filter_invocation = filter_invocation2;
let bytes = bytes2;
let borrowed_args = args
.iter()
.map(|a| a.as_ref())
.collect::<SmallVec<[&str; 8]>>();
match run(cmd, &borrowed_args, &bytes) {
Err(err) => {
return Err(Error::new(format!(
"Failed to start html filter process `{}`",
filter_invocation,
))
.set_source(Some(Arc::new(err)))
.set_kind(ErrorKind::External));
return Err((
Error::new(format!(
"Failed to start html filter process `{}`",
filter_invocation,
))
.set_source(Some(Arc::new(err)))
.set_kind(ErrorKind::External),
bytes,
));
}
Ok(body_text) => {
let notice =
Some(format!("Text piped through `{}`.\n\n", filter_invocation).into());
return Ok(Self {
filter_invocation: filter_invocation.clone(),
content_type: att.content_type.clone(),
notice,
body_text,
unfiltered: bytes,
event_handler: Some(Self::html_process_event),
id: ComponentId::default(),
});
let mut att = AttachmentBuilder::default();
att.set_raw(body_text.into_bytes()).set_body_to_raw();
return Ok((att.build(), bytes));
}
}
};
let filter_invocation2 = filter_invocation.to_string();
let open_html_shortcut = settings.shortcuts.envelope_view.open_html.clone();
let on_success_notice_cb = move || {
format!(
"Text piped through `{}` Press `{}` to open in web browser.\n\n",
filter_invocation2, open_html_shortcut
)
.into()
};
let mut job_handle = context
.main_loop_handler
.job_executor
.spawn_blocking(filter_invocation.to_string().into(), job);
let mut retval = Self {
filter_invocation: filter_invocation.to_string(),
content_type: att.content_type.clone(),
notice: None,
unfiltered: bytes,
body_text: ViewFilterContent::Filtered {
inner: String::new(),
},
event_handler: Some(Self::job_process_event),
id: ComponentId::default(),
};
if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) {
retval.event_handler = Some(Self::html_process_event);
Self::process_job_result(
&mut retval,
Ok(Some(job_result)),
Arc::new(on_success_notice_cb),
context,
);
return Ok(retval);
}
if let Ok(body_text) = run("w3m", &["-I", "utf-8", "-T", "text/html"], &bytes) {
return Ok(Self {
filter_invocation: "w3m -I utf-8 -T text/html".into(),
content_type: att.content_type.clone(),
notice: Some("Text piped through `w3m -I utf-8 -T text/html`.\n\n".into()),
body_text,
unfiltered: bytes,
event_handler: Some(Self::html_process_event),
id: ComponentId::default(),
});
}
Err(
Error::new("Failed to find any application to use as html filter")
.set_kind(ErrorKind::Configuration),
)
return Ok(Self {
body_text: ViewFilterContent::Running {
job_id: job_handle.job_id,
on_success_notice_cb: Arc::new(on_success_notice_cb),
job_handle,
},
..retval
});
}
pub fn new_attachment(att: &Attachment, context: &mut Context) -> Result<Self> {
pub fn new_attachment(att: &Attachment, context: &Context) -> Result<Self> {
if matches!(
att.content_type,
ContentType::Other { .. } | ContentType::OctetStream { .. }
@ -247,15 +344,21 @@ impl ViewFilter {
..
} = att.content_type
{
if let Some(v @ Ok(_)) = parts.iter().find_map(|p| {
if let v @ Ok(_) = Self::new_attachment(p, context) {
Some(v)
} else {
None
}
}) {
return v;
}
let notice = Some(format!("multipart/related with {} parts.\n\n", parts.len()).into());
return Ok(Self {
filter_invocation: String::new(),
content_type: att.content_type.clone(),
notice,
body_text: ViewFilterContent::InlineAttachments {
parts: parts
.into_iter()
.filter_map(|p| ViewFilter::new_attachment(p, context).ok())
.collect::<Vec<ViewFilter>>(),
},
unfiltered: att.decode(Default::default()),
event_handler: None,
id: ComponentId::default(),
});
}
if att.is_html() {
return Self::new_html(att, context);
@ -271,13 +374,87 @@ impl ViewFilter {
filter_invocation: String::new(),
content_type: att.content_type.clone(),
notice: None,
body_text: String::new(),
body_text: ViewFilterContent::Filtered {
inner: String::new(),
},
unfiltered: vec![],
event_handler: None,
id: ComponentId::default(),
});
}
if let ContentType::Multipart {
} else if let ContentType::Multipart {
kind: MultipartType::Encrypted,
ref parts,
..
} = att.content_type
{
#[cfg(not(feature = "gpgme"))]
{
return Ok(Self {
filter_invocation: String::new(),
content_type: att.content_type.clone(),
notice: None,
body_text: ViewFilterContent::Error {
inner: Error::new(
"Cannot decrypt: meli must be compiled with libgpgme support.",
),
},
unfiltered: vec![],
event_handler: None,
id: ComponentId::default(),
});
}
#[cfg(feature = "gpgme")]
{
for a in parts {
if a.content_type == "application/octet-stream" {
let content = a.raw();
let bytes = content.trim().to_vec();
let decrypt_fut = async {
let (_metadata, bytes) = crate::mail::pgp::decrypt(
melib::email::pgp::convert_attachment_to_rfc_spec(&bytes),
)
.await
.map_err(|err| (err, bytes))?;
Ok((AttachmentBuilder::new(&bytes).build(), bytes))
};
let mut job_handle = context
.main_loop_handler
.job_executor
.spawn_specialized("gpg::decrypt".into(), decrypt_fut);
let on_success_notice_cb = || "Decrypted content.\n\n".into();
let mut retval = Self {
filter_invocation: "gpg::decrypt".into(),
content_type: att.content_type.clone(),
notice: None,
body_text: ViewFilterContent::Filtered {
inner: String::new(),
},
unfiltered: a.raw().to_vec(),
event_handler: Some(Self::job_process_event),
id: ComponentId::default(),
};
if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) {
retval.event_handler = None;
Self::process_job_result(
&mut retval,
Ok(Some(job_result)),
Arc::new(on_success_notice_cb),
context,
);
return Ok(retval);
}
return Ok(Self {
body_text: ViewFilterContent::Running {
job_id: job_handle.job_id,
on_success_notice_cb: Arc::new(on_success_notice_cb),
job_handle,
},
..retval
});
}
}
}
} else if let ContentType::Multipart {
kind: MultipartType::Mixed,
ref parts,
..
@ -294,12 +471,72 @@ impl ViewFilter {
return Ok(res);
}
}
let notice = Some("Viewing attachment.\n\n".into());
#[cfg(feature = "gpgme")]
if let ContentType::Text {
kind: Text::Plain, ..
} = att.content_type
{
let content = att.text();
if content
.trim_start()
.starts_with("-----BEGIN PGP MESSAGE-----")
&& content.trim_end().ends_with("-----END PGP MESSAGE-----")
{
let bytes = content.trim().to_string().into_bytes();
let decrypt_fut = async {
let (_metadata, bytes) = crate::mail::pgp::decrypt(
melib::email::pgp::convert_attachment_to_rfc_spec(&bytes),
)
.await
.map_err(|err| (err, bytes))?;
Ok((AttachmentBuilder::new(&bytes).build(), bytes))
};
let mut job_handle = context
.main_loop_handler
.job_executor
.spawn_specialized("gpg::decrypt".into(), decrypt_fut);
let on_success_notice_cb = || "Decrypted content.\n\n".into();
let mut retval = Self {
filter_invocation: "gpg::decrypt".into(),
content_type: att.content_type.clone(),
notice: None,
body_text: ViewFilterContent::Filtered {
inner: String::new(),
},
unfiltered: content.into_bytes(),
event_handler: Some(Self::job_process_event),
id: ComponentId::default(),
};
if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) {
retval.event_handler = None;
Self::process_job_result(
&mut retval,
Ok(Some(job_result)),
Arc::new(on_success_notice_cb),
context,
);
return Ok(retval);
}
return Ok(Self {
body_text: ViewFilterContent::Running {
job_id: job_handle.job_id,
on_success_notice_cb: Arc::new(on_success_notice_cb),
job_handle,
},
..retval
});
}
}
let notice = if att.content_type.is_text_plain() {
None
} else {
Some("Viewing attachment.\n\n".into())
};
Ok(Self {
filter_invocation: String::new(),
content_type: att.content_type.clone(),
notice,
body_text: att.text(),
body_text: ViewFilterContent::Filtered { inner: att.text() },
unfiltered: att.decode(Default::default()),
event_handler: None,
id: ComponentId::default(),
@ -372,6 +609,97 @@ impl ViewFilter {
}
false
}
pub fn contains_job_id(&self, match_job_id: JobId) -> bool {
if let ViewFilterContent::Running { ref job_id, .. } = self.body_text {
return *job_id == match_job_id;
}
if let ViewFilterContent::InlineAttachments { ref parts, .. } = self.body_text {
return parts.iter().any(|p| p.contains_job_id(match_job_id));
}
false
}
fn job_process_event(_self: &mut Self, event: &mut UIEvent, context: &mut Context) -> bool {
log::trace!(
"job_process_event: _self = {:?}, event = {:?}",
_self,
event
);
if matches!(event, UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)) if _self.contains_job_id(*job_id))
{
if let ViewFilterContent::Running {
job_id: _,
mut job_handle,
on_success_notice_cb,
} = std::mem::replace(
&mut _self.body_text,
ViewFilterContent::Filtered {
inner: String::new(),
},
) {
log::trace!("job_process_event: inside if let ");
let job_result = job_handle.chan.try_recv();
Self::process_job_result(_self, job_result, on_success_notice_cb, context);
}
return true;
}
false
}
fn process_job_result(
_self: &mut Self,
result: std::result::Result<Option<FilterResult>, ::futures::channel::oneshot::Canceled>,
on_success_notice_cb: OnSuccessNoticeCb,
context: &Context,
) {
match result {
Err(err) => {
_self.event_handler = None;
/* Job was cancelled */
_self.body_text = ViewFilterContent::Error {
inner: Error::new("Job was cancelled.").set_source(Some(Arc::new(err))),
};
_self.notice = Some(format!("{} cancelled", _self.filter_invocation).into());
}
Ok(None) => {
_self.event_handler = None;
// something happened, perhaps a worker thread panicked
_self.body_text = ViewFilterContent::Error {
inner: Error::new(
"Unknown error. Maybe some process panicked in the background?",
),
};
_self.notice = Some(format!("{} failed", _self.filter_invocation).into());
}
Ok(Some(Ok((att, bytes)))) => {
_self.event_handler = None;
log::trace!("job_process_event: OK ");
match ViewFilter::new_attachment(&att, context) {
Ok(mut new_self) => {
if _self.content_type.is_text_html() {
new_self.event_handler = Some(Self::html_process_event);
}
new_self.unfiltered = bytes;
new_self.notice = Some(on_success_notice_cb());
*_self = new_self;
}
Err(err) => {
_self.body_text = ViewFilterContent::Error { inner: err };
_self.notice = Some(
format!("decoding result of {} failed", _self.filter_invocation).into(),
);
}
}
}
Ok(Some(Err((error, bytes)))) => {
_self.event_handler = None;
_self.body_text = ViewFilterContent::Error { inner: error };
_self.unfiltered = bytes;
_self.notice = Some(format!("{} failed", _self.filter_invocation).into());
}
}
}
}
impl Component for ViewFilter {

Loading…
Cancel
Save