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 ### Added
- Add import command to import email from files into accounts - 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 ## [alpha-0.6.2] - 2020-09-24

View File

@ -451,6 +451,16 @@ as an attachment
in composer, pipe in composer, pipe
.Ar CMD Ar ARGS .Ar CMD Ar ARGS
output into an attachment 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 .It Cm remove-attachment Ar INDEX
remove attachment with given index remove attachment with given index
.It Cm toggle sign .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. Set window title in xterm compatible terminals An empty string means no window title is set.
.\" default value .\" default value
.Pq Em "meli" .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]] .It Ic themes Ar hash table String[String[Attribute]]
Define UI themes. Define UI themes.
See See

View File

@ -57,7 +57,7 @@ macro_rules! to_stream {
}; };
($($tokens:expr),*) => { ($($tokens:expr),*) => {
TokenStream { 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", 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:( parser:(
fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Action> { fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Action> {
alt(( alt((
@ -515,6 +516,18 @@ define_commands!([
let (input, path) = quoted_argument(input)?; let (input, path) = quoted_argument(input)?;
let (input, _) = eof(input)?; let (input, _) = eof(input)?;
Ok((input, Compose(AddAttachment(path.to_string())))) 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) ))(input)
} }

View File

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

View File

@ -31,6 +31,7 @@ use indexmap::IndexSet;
use nix::sys::wait::WaitStatus; use nix::sys::wait::WaitStatus;
use std::convert::TryInto; use std::convert::TryInto;
use std::future::Future; use std::future::Future;
use std::process::{Command, Stdio};
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -1262,7 +1263,6 @@ impl Component for Composer {
self.mode = ViewMode::Embed; self.mode = ViewMode::Embed;
return true; return true;
} }
use std::process::{Command, Stdio};
/* Kill input thread so that spawned command can be sole receiver of stdin */ /* Kill input thread so that spawned command can be sole receiver of stdin */
{ {
context.input_kill(); context.input_kill();
@ -1334,17 +1334,22 @@ impl Component for Composer {
return false; return false;
} }
let f = create_temp_file(&[], None, None, true); let f = create_temp_file(&[], None, None, true);
match std::process::Command::new("sh") match Command::new("sh")
.args(&["-c", command]) .args(&["-c", command])
.stdin(std::process::Stdio::null()) .stdin(Stdio::null())
.stdout(std::process::Stdio::from(f.file())) .stdout(Stdio::from(f.file()))
.spawn() .spawn()
.and_then(|child| Ok(child.wait_with_output()?.stderr))
{ {
Ok(child) => { Ok(stderr) => {
let _ = child if !stderr.is_empty() {
.wait_with_output() context.replies.push_back(UIEvent::StatusEvent(
.expect("failed to launch command") StatusEvent::DisplayMessage(format!(
.stdout; "Command stderr output: `{}`.",
String::from_utf8_lossy(&stderr)
)),
));
}
let attachment = let attachment =
match melib::email::compose::attachment_from_file(f.path()) { match melib::email::compose::attachment_from_file(f.path()) {
Ok(a) => a, Ok(a) => a,
@ -1361,6 +1366,7 @@ impl Component for Composer {
} }
}; };
self.draft.attachments_mut().push(attachment); self.draft.attachments_mut().push(attachment);
self.has_changes = true;
self.dirty = true; self.dirty = true;
return true; return true;
} }
@ -1388,6 +1394,78 @@ impl Component for Composer {
} }
}; };
self.draft.attachments_mut().push(attachment); 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; self.dirty = true;
return true; return true;
} }

View File

@ -37,6 +37,8 @@ pub struct TerminalSettings {
pub use_color: ToggleFlag, pub use_color: ToggleFlag,
#[serde(deserialize_with = "non_empty_string")] #[serde(deserialize_with = "non_empty_string")]
pub window_title: Option<String>, pub window_title: Option<String>,
#[serde(deserialize_with = "non_empty_string")]
pub file_picker_command: Option<String>,
} }
impl Default for TerminalSettings { impl Default for TerminalSettings {
@ -47,6 +49,7 @@ impl Default for TerminalSettings {
ascii_drawing: false, ascii_drawing: false,
use_color: ToggleFlag::InternalVal(true), use_color: ToggleFlag::InternalVal(true),
window_title: Some("meli".to_string()), 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), "ascii_drawing" => self.ascii_drawing.lookup(field, tail),
"use_color" => self.use_color.lookup(field, tail), "use_color" => self.use_color.lookup(field, tail),
"window_title" => self.window_title.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!( other => Err(MeliError::new(format!(
"{} has no field named {}", "{} has no field named {}",
parent_field, other parent_field, other