compose: add `add-attachment-file-picker` command

jmap-eventsource
Manos Pitsidianakis 2020-10-09 21:21:15 +03:00
parent a4b78532b7
commit 406af1848f
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
7 changed files with 128 additions and 12 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -79,6 +79,7 @@ pub enum ViewAction {
#[derive(Debug)]
pub enum ComposeAction {
AddAttachment(String),
AddAttachmentFilePicker(Option<String>),
AddAttachmentPipe(String),
RemoveAttachment(usize),
SaveDraft,

View File

@ -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;
}

View File

@ -37,6 +37,8 @@ pub struct TerminalSettings {
pub use_color: ToggleFlag,
#[serde(deserialize_with = "non_empty_string")]
pub window_title: Option<String>,
#[serde(deserialize_with = "non_empty_string")]
pub file_picker_command: Option<String>,
}
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