From 406af1848f01c2e5fe4ab3748ce934199190b827 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Fri, 9 Oct 2020 21:21:15 +0300 Subject: [PATCH] compose: add `add-attachment-file-picker` command --- CHANGELOG.md | 2 + docs/meli.1 | 10 ++++ docs/meli.conf.5 | 8 +++ src/command.rs | 19 +++++-- src/command/actions.rs | 1 + src/components/mail/compose.rs | 96 ++++++++++++++++++++++++++++++---- src/conf/terminal.rs | 4 ++ 7 files changed, 128 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 441a39e19..8142effdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add import command to import email from files into accounts +- Add add-attachment-file-picker command and `file_picker_command` setting to + use external commands to choose files when composing new mail ## [alpha-0.6.2] - 2020-09-24 diff --git a/docs/meli.1 b/docs/meli.1 index 4eefb6956..d07890223 100644 --- a/docs/meli.1 +++ b/docs/meli.1 @@ -451,6 +451,16 @@ as an attachment in composer, pipe .Ar CMD Ar ARGS output into an attachment +.It Cm add-attachment-file-picker +Launch command defined in the configuration value +.Ic file_picker_command +in +.Xr meli.conf 5 TERMINAL +.It Cm add-attachment-file-picker < Ar CMD Ar ARGS +Launch command +.Ar CMD Ar ARGS Ns +\&. +The command should print file paths in stderr, separated by NULL bytes. .It Cm remove-attachment Ar INDEX remove attachment with given index .It Cm toggle sign diff --git a/docs/meli.conf.5 b/docs/meli.conf.5 index a183251ef..174bec26e 100644 --- a/docs/meli.conf.5 +++ b/docs/meli.conf.5 @@ -920,6 +920,14 @@ If false, no ANSI colors are used. Set window title in xterm compatible terminals An empty string means no window title is set. .\" default value .Pq Em "meli" +.It Ic file_picker_command Ar String +.Pq Em optional +Set command that prints file paths in stderr, separated by NULL bytes. +Used with +.Ic add-attachment-file-picker +when composing new mail. +.\" default value +.Pq Em None .It Ic themes Ar hash table String[String[Attribute]] Define UI themes. See diff --git a/src/command.rs b/src/command.rs index 370ff8332..6c6f56b4e 100644 --- a/src/command.rs +++ b/src/command.rs @@ -57,7 +57,7 @@ macro_rules! to_stream { }; ($($tokens:expr),*) => { TokenStream { - tokens: &[$($token),*], + tokens: &[$($tokens),*], } }; } @@ -495,9 +495,10 @@ define_commands!([ } ) }, - { tags: ["add-attachment "], + { tags: ["add-attachment ", "add-attachment-file-picker "], desc: "add-attachment PATH", - tokens: &[One(Literal("add-attachment")), One(Filepath)], + tokens: &[One( +Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_stream!(One(Literal("add-attachment-file-picker")))]))], parser:( fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Action> { alt(( @@ -515,6 +516,18 @@ define_commands!([ let (input, path) = quoted_argument(input)?; let (input, _) = eof(input)?; Ok((input, Compose(AddAttachment(path.to_string())))) + }, |input: &'a [u8]| -> IResult<&'a [u8], Action> { + let (input, _) = tag("add-attachment-file-picker")(input.trim())?; + let (input, _) = eof(input)?; + Ok((input, Compose(AddAttachmentFilePicker(None)))) + }, |input: &'a [u8]| -> IResult<&'a [u8], Action> { + let (input, _) = tag("add-attachment-file-picker")(input.trim())?; + let (input, _) = is_a(" ")(input)?; + let (input, _) = tag("<")(input.trim())?; + let (input, _) = is_a(" ")(input)?; + let (input, shell) = map_res(not_line_ending, std::str::from_utf8)(input)?; + let (input, _) = eof(input)?; + Ok((input, Compose(AddAttachmentFilePicker(Some(shell.to_string()))))) } ))(input) } diff --git a/src/command/actions.rs b/src/command/actions.rs index d41445999..0dd97e686 100644 --- a/src/command/actions.rs +++ b/src/command/actions.rs @@ -79,6 +79,7 @@ pub enum ViewAction { #[derive(Debug)] pub enum ComposeAction { AddAttachment(String), + AddAttachmentFilePicker(Option), AddAttachmentPipe(String), RemoveAttachment(usize), SaveDraft, diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index 8fb0e4a55..98d844d3a 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -31,6 +31,7 @@ use indexmap::IndexSet; use nix::sys::wait::WaitStatus; use std::convert::TryInto; use std::future::Future; +use std::process::{Command, Stdio}; use std::str::FromStr; use std::sync::{Arc, Mutex}; @@ -1262,7 +1263,6 @@ impl Component for Composer { self.mode = ViewMode::Embed; return true; } - use std::process::{Command, Stdio}; /* Kill input thread so that spawned command can be sole receiver of stdin */ { context.input_kill(); @@ -1334,17 +1334,22 @@ impl Component for Composer { return false; } let f = create_temp_file(&[], None, None, true); - match std::process::Command::new("sh") + match Command::new("sh") .args(&["-c", command]) - .stdin(std::process::Stdio::null()) - .stdout(std::process::Stdio::from(f.file())) + .stdin(Stdio::null()) + .stdout(Stdio::from(f.file())) .spawn() + .and_then(|child| Ok(child.wait_with_output()?.stderr)) { - Ok(child) => { - let _ = child - .wait_with_output() - .expect("failed to launch command") - .stdout; + Ok(stderr) => { + if !stderr.is_empty() { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(format!( + "Command stderr output: `{}`.", + String::from_utf8_lossy(&stderr) + )), + )); + } let attachment = match melib::email::compose::attachment_from_file(f.path()) { Ok(a) => a, @@ -1361,6 +1366,7 @@ impl Component for Composer { } }; self.draft.attachments_mut().push(attachment); + self.has_changes = true; self.dirty = true; return true; } @@ -1388,6 +1394,78 @@ impl Component for Composer { } }; self.draft.attachments_mut().push(attachment); + self.has_changes = true; + self.dirty = true; + return true; + } + Action::Compose(ComposeAction::AddAttachmentFilePicker(ref command)) => { + let command = if let Some(ref cmd) = command + .as_ref() + .or_else(|| context.settings.terminal.file_picker_command.as_ref()) + { + cmd.as_str() + } else { + context.replies.push_back(UIEvent::Notification( + None, + "You haven't defined any command to launch.".into(), + Some(NotificationType::Error(melib::error::ErrorKind::None)), + )); + return true; + }; + /* Kill input thread so that spawned command can be sole receiver of stdin */ + { + context.input_kill(); + } + + log( + format!("Executing: sh -c \"{}\"", command.replace("\"", "\\\"")), + DEBUG, + ); + match Command::new("sh") + .args(&["-c", command]) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::piped()) + .spawn() + .and_then(|child| Ok(child.wait_with_output()?.stderr)) + { + Ok(stderr) => { + debug!(&String::from_utf8_lossy(&stderr)); + for path in stderr.split(|c| [b'\0', b'\t', b'\n'].contains(c)) { + match melib::email::compose::attachment_from_file( + &String::from_utf8_lossy(&path).as_ref(), + ) { + Ok(a) => { + self.draft.attachments_mut().push(a); + self.has_changes = true; + } + Err(err) => { + context.replies.push_back(UIEvent::Notification( + Some(format!( + "could not add attachment: {}", + String::from_utf8_lossy(&path) + )), + err.to_string(), + Some(NotificationType::Error( + melib::error::ErrorKind::None, + )), + )); + } + }; + } + } + Err(err) => { + let command = command.to_string(); + context.replies.push_back(UIEvent::Notification( + Some(format!("Failed to execute {}: {}", command, err)), + err.to_string(), + Some(NotificationType::Error(melib::error::ErrorKind::External)), + )); + context.restore_input(); + return true; + } + } + context.replies.push_back(UIEvent::Fork(ForkType::Finished)); self.dirty = true; return true; } diff --git a/src/conf/terminal.rs b/src/conf/terminal.rs index f872e105a..2b6c94e5a 100644 --- a/src/conf/terminal.rs +++ b/src/conf/terminal.rs @@ -37,6 +37,8 @@ pub struct TerminalSettings { pub use_color: ToggleFlag, #[serde(deserialize_with = "non_empty_string")] pub window_title: Option, + #[serde(deserialize_with = "non_empty_string")] + pub file_picker_command: Option, } impl Default for TerminalSettings { @@ -47,6 +49,7 @@ impl Default for TerminalSettings { ascii_drawing: false, use_color: ToggleFlag::InternalVal(true), window_title: Some("meli".to_string()), + file_picker_command: None, } } } @@ -74,6 +77,7 @@ impl DotAddressable for TerminalSettings { "ascii_drawing" => self.ascii_drawing.lookup(field, tail), "use_color" => self.use_color.lookup(field, tail), "window_title" => self.window_title.lookup(field, tail), + "file_picker_command" => self.file_picker_command.lookup(field, tail), other => Err(MeliError::new(format!( "{} has no field named {}", parent_field, other