mail/compose: add configurable custom hooks with shell commands
Quoting the docs at meli.conf(5): ```text custom_compose_hooks [{ name = String, command = String }] (optional) Custom compose-hooks that run shell scripts. compose-hooks run before submitting an e-mail. They perform draft validation and/or transformations. If a custom hook exits with an error status or prints output to stdout and stderr, it will show up in the UI as a notification. Example: [composing] editor_cmd = '~/.local/bin/vim +/^$' embed = true custom_compose_hooks = [ { name ="spellcheck", command="aspell --mode email --dont-suggest --ignore-case list" }] ```duesee/experiment/use_imap_codec
parent
cc27639fca
commit
c9d26bb415
|
@ -613,6 +613,20 @@ When used in a reply, the field body MAY start with the string "Re: " (from
|
|||
the Latin "res", in the matter of) followed by the contents of the "Subject:"
|
||||
field body of the original message.
|
||||
.Ed
|
||||
.It Ic custom_compose_hooks Ar [{ name = String, command = String }]
|
||||
.Pq Em optional
|
||||
Custom compose-hooks that run shell scripts.
|
||||
compose-hooks run before submitting an e-mail.
|
||||
They perform draft validation and/or transformations.
|
||||
If a custom hook exits with an error status or prints output to stdout and stderr, it will show up in the UI as a notification.
|
||||
.Pp
|
||||
Example:
|
||||
.Bd -literal
|
||||
[composing]
|
||||
editor_cmd = '~/.local/bin/vim +/^$'
|
||||
embed = true
|
||||
custom_compose_hooks = [ { name ="spellcheck", command="aspell --mode email --dont-suggest --ignore-case list" }]
|
||||
.Ed
|
||||
.It Ic disabled_compose_hooks Ar [String]
|
||||
.Pq Em optional
|
||||
Disabled compose-hooks.
|
||||
|
|
|
@ -183,6 +183,16 @@ impl Composer {
|
|||
account_hash,
|
||||
..Composer::new(context)
|
||||
};
|
||||
|
||||
// Add user's custom hooks.
|
||||
for hook in account_settings!(context[account_hash].composing.custom_compose_hooks)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(Into::into)
|
||||
{
|
||||
ret.hooks.push(hook);
|
||||
}
|
||||
|
||||
ret.hooks.retain(|h| {
|
||||
!account_settings!(context[account_hash].composing.disabled_compose_hooks)
|
||||
.iter()
|
||||
|
@ -216,6 +226,14 @@ impl Composer {
|
|||
context: &Context,
|
||||
) -> Result<Self> {
|
||||
let mut ret = Composer::with_account(account_hash, context);
|
||||
// Add user's custom hooks.
|
||||
for hook in account_settings!(context[account_hash].composing.custom_compose_hooks)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(Into::into)
|
||||
{
|
||||
ret.hooks.push(hook);
|
||||
}
|
||||
ret.hooks.retain(|h| {
|
||||
!account_settings!(context[account_hash].composing.disabled_compose_hooks)
|
||||
.iter()
|
||||
|
@ -236,6 +254,14 @@ impl Composer {
|
|||
reply_to_all: bool,
|
||||
) -> Self {
|
||||
let mut ret = Composer::with_account(account_hash, context);
|
||||
// Add user's custom hooks.
|
||||
for hook in account_settings!(context[account_hash].composing.custom_compose_hooks)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(Into::into)
|
||||
{
|
||||
ret.hooks.push(hook);
|
||||
}
|
||||
ret.hooks.retain(|h| {
|
||||
!account_settings!(context[account_hash].composing.disabled_compose_hooks)
|
||||
.iter()
|
||||
|
@ -524,15 +550,6 @@ To: {}
|
|||
}
|
||||
}
|
||||
|
||||
fn run_hooks(&mut self, ctx: &mut Context) -> Result<()> {
|
||||
for h in self.hooks.iter_mut() {
|
||||
if let err @ Err(_) = h(ctx, &mut self.draft) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_form(&mut self) {
|
||||
let old_cursor = self.form.cursor();
|
||||
self.form = FormWidget::new(("Save".into(), true));
|
||||
|
@ -1438,10 +1455,31 @@ impl Component for Composer {
|
|||
&& self.mode.is_edit() =>
|
||||
{
|
||||
self.update_draft();
|
||||
if let Err(err) = self.run_hooks(context) {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::Notification(None, err.to_string(), None));
|
||||
|
||||
{
|
||||
let Self {
|
||||
ref mut hooks,
|
||||
ref mut draft,
|
||||
..
|
||||
} = self;
|
||||
|
||||
for err in hooks
|
||||
.iter_mut()
|
||||
.filter_map(|h| {
|
||||
if let Err(err) = h(context, draft) {
|
||||
Some(err)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
context.replies.push_back(UIEvent::Notification(
|
||||
None,
|
||||
err.to_string(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
}
|
||||
self.mode = ViewMode::Send(UIConfirmationDialog::new(
|
||||
"send mail?",
|
||||
|
|
|
@ -24,7 +24,6 @@ pub use std::borrow::Cow;
|
|||
|
||||
use super::*;
|
||||
|
||||
/*
|
||||
pub enum HookFn {
|
||||
/// Stateful hook.
|
||||
Closure(Box<dyn FnMut(&mut Context, &mut Draft) -> Result<()> + Send + Sync>),
|
||||
|
@ -32,22 +31,49 @@ pub enum HookFn {
|
|||
/// Static hook.
|
||||
Ptr(fn(&mut Context, &mut Draft) -> Result<()>),
|
||||
}
|
||||
*/
|
||||
|
||||
impl std::fmt::Debug for HookFn {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.debug_struct(stringify!(HookFn))
|
||||
.field(
|
||||
"kind",
|
||||
&match self {
|
||||
Self::Closure(_) => "closure",
|
||||
Self::Ptr(_) => "function ptr",
|
||||
},
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for HookFn {
|
||||
type Target = dyn FnMut(&mut Context, &mut Draft) -> Result<()> + Send + Sync;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Ptr(ref v) => v,
|
||||
Self::Closure(ref v) => v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for HookFn {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
match self {
|
||||
Self::Ptr(ref mut v) => v,
|
||||
Self::Closure(ref mut v) => v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Pre-submission hook for draft validation and/or transformations.
|
||||
pub struct Hook {
|
||||
/// Hook name for enabling/disabling it from configuration.
|
||||
///
|
||||
/// See [`ComposingSettings::disabled_compose_hooks`].
|
||||
name: Cow<'static, str>,
|
||||
hook_fn: fn(&mut Context, &mut Draft) -> Result<()>,
|
||||
//hook_fn: HookFn,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Hook {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.debug_struct(self.name.as_ref()).finish()
|
||||
}
|
||||
hook_fn: HookFn,
|
||||
}
|
||||
|
||||
impl Hook {
|
||||
|
@ -57,19 +83,72 @@ impl Hook {
|
|||
pub fn name(&self) -> &str {
|
||||
self.name.as_ref()
|
||||
}
|
||||
|
||||
pub fn new_shell_command(name: Cow<'static, str>, command: String) -> Self {
|
||||
let name_ = name.clone();
|
||||
Self {
|
||||
name,
|
||||
hook_fn: HookFn::Closure(Box::new(move |_, draft| -> Result<()> {
|
||||
use std::thread;
|
||||
|
||||
let mut child = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&command)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|err| -> Error {
|
||||
format!(
|
||||
"could not execute `{command}`. Check if its binary is in PATH or if \
|
||||
the command is valid. Original error: {err}"
|
||||
)
|
||||
.into()
|
||||
})?;
|
||||
let mut stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| Error::new("failed to get stdin"))?;
|
||||
|
||||
thread::scope(|s| {
|
||||
s.spawn(move || {
|
||||
stdin
|
||||
.write_all(draft.body.as_bytes())
|
||||
.expect("failed to write to stdin");
|
||||
});
|
||||
});
|
||||
let output = child.wait_with_output().map_err(|err| -> Error {
|
||||
format!("failed to wait on hook child {name_}: {err}").into()
|
||||
})?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if !output.status.success() || !stdout.is_empty() || !stderr.is_empty() {
|
||||
return Err(format!(
|
||||
"{name_}\n exit code: {:?}\n stdout:\n{}\n stderr:\n{}",
|
||||
output.status.code(),
|
||||
stdout,
|
||||
stderr,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Hook {
|
||||
type Target = dyn FnMut(&mut Context, &mut Draft) -> Result<()> + Send + Sync;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.hook_fn
|
||||
self.hook_fn.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Hook {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.hook_fn
|
||||
self.hook_fn.deref_mut()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,7 +177,7 @@ fn past_date_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
|||
/// Warn if [`melib::Draft`] Date is far in the past/future.
|
||||
pub const PASTDATEWARN: Hook = Hook {
|
||||
name: Cow::Borrowed("past-date-warn"),
|
||||
hook_fn: past_date_warn,
|
||||
hook_fn: HookFn::Ptr(past_date_warn),
|
||||
};
|
||||
|
||||
fn important_header_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
||||
|
@ -138,7 +217,7 @@ fn important_header_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
|||
/// Warn if important [`melib::Draft`] header is missing or invalid.
|
||||
pub const HEADERWARN: Hook = Hook {
|
||||
name: Cow::Borrowed("important-header-warn"),
|
||||
hook_fn: important_header_warn,
|
||||
hook_fn: HookFn::Ptr(important_header_warn),
|
||||
};
|
||||
|
||||
fn missing_attachment_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
||||
|
@ -162,7 +241,7 @@ fn missing_attachment_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()>
|
|||
/// Warn if Subject and/or draft body mentions attachments but they are missing.
|
||||
pub const MISSINGATTACHMENTWARN: Hook = Hook {
|
||||
name: Cow::Borrowed("missing-attachment-warn"),
|
||||
hook_fn: missing_attachment_warn,
|
||||
hook_fn: HookFn::Ptr(missing_attachment_warn),
|
||||
};
|
||||
|
||||
fn empty_draft_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
||||
|
@ -182,7 +261,7 @@ fn empty_draft_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
|||
/// Warn if draft has no subject and no body.
|
||||
pub const EMPTYDRAFTWARN: Hook = Hook {
|
||||
name: Cow::Borrowed("empty-draft-warn"),
|
||||
hook_fn: empty_draft_warn,
|
||||
hook_fn: HookFn::Ptr(empty_draft_warn),
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
21
src/conf.rs
21
src/conf.rs
|
@ -34,7 +34,7 @@ use std::{
|
|||
|
||||
use melib::{backends::TagHash, search::Query, StderrLogger};
|
||||
|
||||
use crate::{conf::deserializers::non_empty_string, terminal::Color};
|
||||
use crate::{conf::deserializers::non_empty_opt_string, terminal::Color};
|
||||
|
||||
#[rustfmt::skip]
|
||||
mod overrides;
|
||||
|
@ -729,8 +729,9 @@ mod default_vals {
|
|||
}
|
||||
|
||||
mod deserializers {
|
||||
use serde::{Deserialize, Deserializer};
|
||||
pub(in crate::conf) fn non_empty_string<'de, D, T: std::convert::From<Option<String>>>(
|
||||
use serde::{de, Deserialize, Deserializer};
|
||||
|
||||
pub(in crate::conf) fn non_empty_opt_string<'de, D, T: std::convert::From<Option<String>>>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<T, D::Error>
|
||||
where
|
||||
|
@ -744,6 +745,20 @@ mod deserializers {
|
|||
}
|
||||
}
|
||||
|
||||
pub(in crate::conf) fn non_empty_string<'de, D, T: std::convert::From<String>>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = <String>::deserialize(deserializer)?;
|
||||
if s.is_empty() {
|
||||
Err(de::Error::custom("This field value cannot be empty."))
|
||||
} else {
|
||||
Ok(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
use toml::Value;
|
||||
fn any_of<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
|
||||
where
|
||||
|
|
|
@ -24,7 +24,10 @@ use std::collections::HashMap;
|
|||
|
||||
use melib::ToggleFlag;
|
||||
|
||||
use super::default_vals::{ask, false_val, none, true_val};
|
||||
use super::{
|
||||
default_vals::{ask, false_val, none, true_val},
|
||||
deserializers::non_empty_string,
|
||||
};
|
||||
|
||||
/// Settings for writing and sending new e-mail
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
@ -100,6 +103,9 @@ pub struct ComposingSettings {
|
|||
/// The prefix to use in reply subjects. The de facto prefix is "Re:".
|
||||
#[serde(default = "res", alias = "reply-prefix")]
|
||||
pub reply_prefix: String,
|
||||
/// Custom `compose-hooks`.
|
||||
#[serde(default, alias = "custom-compose-hooks")]
|
||||
pub custom_compose_hooks: Vec<ComposeHook>,
|
||||
/// Disabled `compose-hooks`.
|
||||
#[serde(default, alias = "disabled-compose-hooks")]
|
||||
pub disabled_compose_hooks: Vec<String>,
|
||||
|
@ -121,6 +127,7 @@ impl Default for ComposingSettings {
|
|||
forward_as_attachment: ToggleFlag::Ask,
|
||||
reply_prefix_list_to_strip: None,
|
||||
reply_prefix: res(),
|
||||
custom_compose_hooks: vec![],
|
||||
disabled_compose_hooks: vec![],
|
||||
}
|
||||
}
|
||||
|
@ -177,3 +184,19 @@ pub enum SendMail {
|
|||
ServerSubmission,
|
||||
ShellCommand(String),
|
||||
}
|
||||
|
||||
/// Shell command compose hooks (See [`Hook`])
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ComposeHook {
|
||||
#[serde(deserialize_with = "non_empty_string")]
|
||||
name: String,
|
||||
#[serde(deserialize_with = "non_empty_string")]
|
||||
command: String,
|
||||
}
|
||||
|
||||
impl From<ComposeHook> for crate::components::mail::hooks::Hook {
|
||||
fn from(c: ComposeHook) -> Self {
|
||||
Self::new_shell_command(c.name.into(), c.command)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
//! This module is automatically generated by config_macros.rs.
|
||||
use super::*;
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "headers-sticky")] # [serde (default)] pub headers_sticky : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { PagerSettingsOverride { pager_context : None , pager_stop : None , headers_sticky : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None } } }
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "headers-sticky")] # [serde (default)] pub headers_sticky : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { PagerSettingsOverride { pager_context : None , pager_stop : None , headers_sticky : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ListingSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "context-lines")] # [serde (default)] pub context_lines : Option < usize > , # [doc = "Show auto-hiding scrollbar in accounts sidebar menu."] # [doc = "Default: True"] # [serde (default)] pub show_menu_scrollbar : Option < bool > , # [doc = " Datetime formatting passed verbatim to strftime(3)."] # [doc = " Default: %Y-%m-%d %T"] # [serde (alias = "datetime-fmt")] # [serde (default)] pub datetime_fmt : Option < Option < String > > , # [doc = " Show recent dates as `X {minutes,hours,days} ago`, up to 7 days."] # [doc = " Default: true"] # [serde (alias = "recent-dates")] # [serde (default)] pub recent_dates : Option < bool > , # [doc = " Show only envelopes that match this query"] # [doc = " Default: None"] # [serde (default)] pub filter : Option < Option < Query > > , # [serde (alias = "index-style")] # [serde (default)] pub index_style : Option < IndexStyle > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling_leaf : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling_leaf : Option < Option < String > > , # [doc = "Default: ' '"] # [serde (default)] pub sidebar_divider : Option < char > , # [doc = "Default: 90"] # [serde (default)] pub sidebar_ratio : Option < usize > , # [doc = " Flag to show if thread entry contains unseen mail."] # [doc = " Default: \"●\""] # [serde (default)] pub unseen_flag : Option < Option < String > > , # [doc = " Flag to show if thread has been snoozed."] # [doc = " Default: \"💤\""] # [serde (default)] pub thread_snoozed_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry has been selected."] # [doc = " Default: \"☑\u{fe0f}\""] # [serde (default)] pub selected_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry contains attachments."] # [doc = " Default: \"📎\""] # [serde (default)] pub attachment_flag : Option < Option < String > > , # [doc = " Should threads with differentiating Subjects show a list of those"] # [doc = " subjects on the entry title?"] # [doc = " Default: \"true\""] # [serde (default)] pub thread_subject_pack : Option < bool > } impl Default for ListingSettingsOverride { fn default () -> Self { ListingSettingsOverride { context_lines : None , show_menu_scrollbar : None , datetime_fmt : None , recent_dates : None , filter : None , index_style : None , sidebar_mailbox_tree_has_sibling : None , sidebar_mailbox_tree_no_sibling : None , sidebar_mailbox_tree_has_sibling_leaf : None , sidebar_mailbox_tree_no_sibling_leaf : None , sidebar_divider : None , sidebar_ratio : None , unseen_flag : None , thread_snoozed_flag : None , selected_flag : None , attachment_flag : None , thread_subject_pack : None } } }
|
||||
|
||||
|
@ -33,7 +33,7 @@ use super::*;
|
|||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ShortcutsOverride { # [serde (default)] pub general : Option < GeneralShortcuts > , # [serde (default)] pub listing : Option < ListingShortcuts > , # [serde (default)] pub composing : Option < ComposingShortcuts > , # [serde (alias = "contact-list")] # [serde (default)] pub contact_list : Option < ContactListShortcuts > , # [serde (alias = "envelope-view")] # [serde (default)] pub envelope_view : Option < EnvelopeViewShortcuts > , # [serde (alias = "thread-view")] # [serde (default)] pub thread_view : Option < ThreadViewShortcuts > , # [serde (default)] pub pager : Option < PagerShortcuts > } impl Default for ShortcutsOverride { fn default () -> Self { ShortcutsOverride { general : None , listing : None , composing : None , contact_list : None , envelope_view : None , thread_view : None , pager : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [doc = " Command to launch editor. Can have arguments. Draft filename is given as"] # [doc = " the last argument. If it's missing, the environment variable $EDITOR is"] # [doc = " looked up."] # [serde (alias = "editor-command" , alias = "editor-cmd" , alias = "editor_cmd")] # [serde (default)] pub editor_command : Option < Option < String > > , # [doc = " Embed editor (for terminal interfaces) instead of forking and waiting."] # [serde (default)] pub embed : Option < bool > , # [doc = " Set \"format=flowed\" in plain text attachments."] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = "Set User-Agent"] # [doc = "Default: empty"] # [serde (alias = "insert_user_agent")] # [serde (default)] pub insert_user_agent : Option < bool > , # [doc = " Set default header values for new drafts"] # [doc = " Default: empty"] # [serde (alias = "default-header-values")] # [serde (default)] pub default_header_values : Option < HashMap < String , String > > , # [doc = " Wrap header preample when editing a draft in an editor. This allows you"] # [doc = " to write non-plain text email without the preamble creating syntax"] # [doc = " errors. They are stripped when you return from the editor. The"] # [doc = " values should be a two element array of strings, a prefix and suffix."] # [doc = " Default: None"] # [serde (alias = "wrap-header-preample")] # [serde (default)] pub wrap_header_preamble : Option < Option < (String , String) > > , # [doc = " Store sent mail after successful submission. This setting is meant to be"] # [doc = " disabled for non-standard behaviour in gmail, which auto-saves sent"] # [doc = " mail on its own. Default: true"] # [serde (default)] pub store_sent_mail : Option < bool > , # [doc = " The attribution line appears above the quoted reply text."] # [doc = " The format specifiers for the replied address are:"] # [doc = " - `%+f` — the sender's name and email address."] # [doc = " - `%+n` — the sender's name (or email address, if no name is included)."] # [doc = " - `%+a` — the sender's email address."] # [doc = " The format string is passed to strftime(3) with the replied envelope's"] # [doc = " date. Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] # [serde (default)] pub attribution_format_string : Option < Option < String > > , # [doc = " Whether the strftime call for the attribution string uses the POSIX"] # [doc = " locale instead of the user's active locale"] # [doc = " Default: true"] # [serde (default)] pub attribution_use_posix_locale : Option < bool > , # [doc = " Forward emails as attachment? (Alternative is inline)"] # [doc = " Default: ask"] # [serde (alias = "forward-as-attachment")] # [serde (default)] pub forward_as_attachment : Option < ToggleFlag > , # [doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] # [doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \"Sv:\","] # [doc = " \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \"TR:\", \"AW:\","] # [doc = " \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"ΣΧΕΤ:\", \"Σχετ:\","] # [doc = " \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \"Továbbítás:\", \"R:\", \"I:\","] # [doc = " \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \"VB:\", \"RV:\", \"RES:\", \"Res\","] # [doc = " \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \"İLT:\", \"ATB:\", \"YML:\"]`"] # [serde (alias = "reply-prefix-list-to-strip")] # [serde (default)] pub reply_prefix_list_to_strip : Option < Option < Vec < String > > > , # [doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] # [serde (alias = "reply-prefix")] # [serde (default)] pub reply_prefix : Option < String > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { ComposingSettingsOverride { send_mail : None , editor_command : None , embed : None , format_flowed : None , insert_user_agent : None , default_header_values : None , wrap_header_preamble : None , store_sent_mail : None , attribution_format_string : None , attribution_use_posix_locale : None , forward_as_attachment : None , reply_prefix_list_to_strip : None , reply_prefix : None , disabled_compose_hooks : None } } }
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [doc = " Command to launch editor. Can have arguments. Draft filename is given as"] # [doc = " the last argument. If it's missing, the environment variable $EDITOR is"] # [doc = " looked up."] # [serde (alias = "editor-command" , alias = "editor-cmd" , alias = "editor_cmd")] # [serde (default)] pub editor_command : Option < Option < String > > , # [doc = " Embed editor (for terminal interfaces) instead of forking and waiting."] # [serde (default)] pub embed : Option < bool > , # [doc = " Set \"format=flowed\" in plain text attachments."] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = "Set User-Agent"] # [doc = "Default: empty"] # [serde (alias = "insert_user_agent")] # [serde (default)] pub insert_user_agent : Option < bool > , # [doc = " Set default header values for new drafts"] # [doc = " Default: empty"] # [serde (alias = "default-header-values")] # [serde (default)] pub default_header_values : Option < HashMap < String , String > > , # [doc = " Wrap header preample when editing a draft in an editor. This allows you"] # [doc = " to write non-plain text email without the preamble creating syntax"] # [doc = " errors. They are stripped when you return from the editor. The"] # [doc = " values should be a two element array of strings, a prefix and suffix."] # [doc = " Default: None"] # [serde (alias = "wrap-header-preample")] # [serde (default)] pub wrap_header_preamble : Option < Option < (String , String) > > , # [doc = " Store sent mail after successful submission. This setting is meant to be"] # [doc = " disabled for non-standard behaviour in gmail, which auto-saves sent"] # [doc = " mail on its own. Default: true"] # [serde (default)] pub store_sent_mail : Option < bool > , # [doc = " The attribution line appears above the quoted reply text."] # [doc = " The format specifiers for the replied address are:"] # [doc = " - `%+f` — the sender's name and email address."] # [doc = " - `%+n` — the sender's name (or email address, if no name is included)."] # [doc = " - `%+a` — the sender's email address."] # [doc = " The format string is passed to strftime(3) with the replied envelope's"] # [doc = " date. Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] # [serde (default)] pub attribution_format_string : Option < Option < String > > , # [doc = " Whether the strftime call for the attribution string uses the POSIX"] # [doc = " locale instead of the user's active locale"] # [doc = " Default: true"] # [serde (default)] pub attribution_use_posix_locale : Option < bool > , # [doc = " Forward emails as attachment? (Alternative is inline)"] # [doc = " Default: ask"] # [serde (alias = "forward-as-attachment")] # [serde (default)] pub forward_as_attachment : Option < ToggleFlag > , # [doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] # [doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \"Sv:\","] # [doc = " \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \"TR:\", \"AW:\","] # [doc = " \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"ΣΧΕΤ:\", \"Σχετ:\","] # [doc = " \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \"Továbbítás:\", \"R:\", \"I:\","] # [doc = " \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \"VB:\", \"RV:\", \"RES:\", \"Res\","] # [doc = " \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \"İLT:\", \"ATB:\", \"YML:\"]`"] # [serde (alias = "reply-prefix-list-to-strip")] # [serde (default)] pub reply_prefix_list_to_strip : Option < Option < Vec < String > > > , # [doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] # [serde (alias = "reply-prefix")] # [serde (default)] pub reply_prefix : Option < String > , # [doc = " Custom `compose-hooks`."] # [serde (alias = "custom-compose-hooks")] # [serde (default)] pub custom_compose_hooks : Option < Vec < ComposeHook > > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { ComposingSettingsOverride { send_mail : None , editor_command : None , embed : None , format_flowed : None , insert_user_agent : None , default_header_values : None , wrap_header_preamble : None , store_sent_mail : None , attribution_format_string : None , attribution_use_posix_locale : None , forward_as_attachment : None , reply_prefix_list_to_strip : None , reply_prefix : None , custom_compose_hooks : None , disabled_compose_hooks : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct TagsSettingsOverride { # [serde (deserialize_with = "tag_color_de")] # [serde (default)] pub colors : Option < HashMap < TagHash , Color > > , # [serde (deserialize_with = "tag_set_de" , alias = "ignore-tags")] # [serde (default)] pub ignore_tags : Option < HashSet < TagHash > > } impl Default for TagsSettingsOverride { fn default () -> Self { TagsSettingsOverride { colors : None , ignore_tags : None } } }
|
||||
|
||||
|
|
|
@ -51,14 +51,14 @@ pub struct PagerSettings {
|
|||
|
||||
/// A command to pipe mail output through for viewing in pager.
|
||||
/// Default: None
|
||||
#[serde(default = "none", deserialize_with = "non_empty_string")]
|
||||
#[serde(default = "none", deserialize_with = "non_empty_opt_string")]
|
||||
pub filter: Option<String>,
|
||||
|
||||
/// A command to pipe html output before displaying it in a pager
|
||||
/// Default: None
|
||||
#[serde(
|
||||
default = "none",
|
||||
deserialize_with = "non_empty_string",
|
||||
deserialize_with = "non_empty_opt_string",
|
||||
alias = "html-filter"
|
||||
)]
|
||||
pub html_filter: Option<String>,
|
||||
|
@ -94,14 +94,14 @@ pub struct PagerSettings {
|
|||
|
||||
/// A command to launch URLs with. The URL will be given as the first
|
||||
/// argument of the command. Default: None
|
||||
#[serde(default = "none", deserialize_with = "non_empty_string")]
|
||||
#[serde(default = "none", deserialize_with = "non_empty_opt_string")]
|
||||
pub url_launcher: Option<String>,
|
||||
|
||||
/// A command to open html files.
|
||||
/// Default: None
|
||||
#[serde(
|
||||
default = "none",
|
||||
deserialize_with = "non_empty_string",
|
||||
deserialize_with = "non_empty_opt_string",
|
||||
alias = "html-open"
|
||||
)]
|
||||
pub html_open: Option<String>,
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
use melib::{Error, Result, ToggleFlag};
|
||||
|
||||
use super::{deserializers::non_empty_string, DotAddressable, Themes};
|
||||
use super::{deserializers::non_empty_opt_string, DotAddressable, Themes};
|
||||
|
||||
/// Settings for terminal display
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
|
@ -40,11 +40,11 @@ pub struct TerminalSettings {
|
|||
pub use_mouse: ToggleFlag,
|
||||
/// String to show in status bar if mouse is active.
|
||||
/// Default: "🖱️ "
|
||||
#[serde(deserialize_with = "non_empty_string")]
|
||||
#[serde(deserialize_with = "non_empty_opt_string")]
|
||||
pub mouse_flag: Option<String>,
|
||||
#[serde(deserialize_with = "non_empty_string")]
|
||||
#[serde(deserialize_with = "non_empty_opt_string")]
|
||||
pub window_title: Option<String>,
|
||||
#[serde(deserialize_with = "non_empty_string")]
|
||||
#[serde(deserialize_with = "non_empty_opt_string")]
|
||||
pub file_picker_command: Option<String>,
|
||||
/// Choose between 30-something built in sequences (integers between 0-30)
|
||||
/// or define your own list of strings for the progress spinner
|
||||
|
|
Loading…
Reference in New Issue