compose: add `add-attachment-file-picker` command
parent
a4b78532b7
commit
406af1848f
|
@ -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
|
||||||
|
|
||||||
|
|
10
docs/meli.1
10
docs/meli.1
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue