forked from meli/meli
1
Fork 0

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
Manos Pitsidianakis 2023-05-19 10:34:32 +03:00
parent cc27639fca
commit c9d26bb415
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
8 changed files with 212 additions and 43 deletions

View File

@ -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:" the Latin "res", in the matter of) followed by the contents of the "Subject:"
field body of the original message. field body of the original message.
.Ed .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] .It Ic disabled_compose_hooks Ar [String]
.Pq Em optional .Pq Em optional
Disabled compose-hooks. Disabled compose-hooks.

View File

@ -183,6 +183,16 @@ impl Composer {
account_hash, account_hash,
..Composer::new(context) ..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| { ret.hooks.retain(|h| {
!account_settings!(context[account_hash].composing.disabled_compose_hooks) !account_settings!(context[account_hash].composing.disabled_compose_hooks)
.iter() .iter()
@ -216,6 +226,14 @@ impl Composer {
context: &Context, context: &Context,
) -> Result<Self> { ) -> Result<Self> {
let mut ret = Composer::with_account(account_hash, context); 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| { ret.hooks.retain(|h| {
!account_settings!(context[account_hash].composing.disabled_compose_hooks) !account_settings!(context[account_hash].composing.disabled_compose_hooks)
.iter() .iter()
@ -236,6 +254,14 @@ impl Composer {
reply_to_all: bool, reply_to_all: bool,
) -> Self { ) -> Self {
let mut ret = Composer::with_account(account_hash, context); 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| { ret.hooks.retain(|h| {
!account_settings!(context[account_hash].composing.disabled_compose_hooks) !account_settings!(context[account_hash].composing.disabled_compose_hooks)
.iter() .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) { fn update_form(&mut self) {
let old_cursor = self.form.cursor(); let old_cursor = self.form.cursor();
self.form = FormWidget::new(("Save".into(), true)); self.form = FormWidget::new(("Save".into(), true));
@ -1438,10 +1455,31 @@ impl Component for Composer {
&& self.mode.is_edit() => && self.mode.is_edit() =>
{ {
self.update_draft(); self.update_draft();
if let Err(err) = self.run_hooks(context) {
context {
.replies let Self {
.push_back(UIEvent::Notification(None, err.to_string(), None)); 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( self.mode = ViewMode::Send(UIConfirmationDialog::new(
"send mail?", "send mail?",

View File

@ -24,7 +24,6 @@ pub use std::borrow::Cow;
use super::*; use super::*;
/*
pub enum HookFn { pub enum HookFn {
/// Stateful hook. /// Stateful hook.
Closure(Box<dyn FnMut(&mut Context, &mut Draft) -> Result<()> + Send + Sync>), Closure(Box<dyn FnMut(&mut Context, &mut Draft) -> Result<()> + Send + Sync>),
@ -32,22 +31,49 @@ pub enum HookFn {
/// Static hook. /// Static hook.
Ptr(fn(&mut Context, &mut Draft) -> Result<()>), 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. /// Pre-submission hook for draft validation and/or transformations.
pub struct Hook { pub struct Hook {
/// Hook name for enabling/disabling it from configuration. /// Hook name for enabling/disabling it from configuration.
/// ///
/// See [`ComposingSettings::disabled_compose_hooks`]. /// See [`ComposingSettings::disabled_compose_hooks`].
name: Cow<'static, str>, name: Cow<'static, str>,
hook_fn: fn(&mut Context, &mut Draft) -> Result<()>, hook_fn: HookFn,
//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()
}
} }
impl Hook { impl Hook {
@ -57,19 +83,72 @@ impl Hook {
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
self.name.as_ref() 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 { impl std::ops::Deref for Hook {
type Target = dyn FnMut(&mut Context, &mut Draft) -> Result<()> + Send + Sync; type Target = dyn FnMut(&mut Context, &mut Draft) -> Result<()> + Send + Sync;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.hook_fn self.hook_fn.deref()
} }
} }
impl std::ops::DerefMut for Hook { impl std::ops::DerefMut for Hook {
fn deref_mut(&mut self) -> &mut Self::Target { 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. /// Warn if [`melib::Draft`] Date is far in the past/future.
pub const PASTDATEWARN: Hook = Hook { pub const PASTDATEWARN: Hook = Hook {
name: Cow::Borrowed("past-date-warn"), 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<()> { 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. /// Warn if important [`melib::Draft`] header is missing or invalid.
pub const HEADERWARN: Hook = Hook { pub const HEADERWARN: Hook = Hook {
name: Cow::Borrowed("important-header-warn"), 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<()> { 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. /// Warn if Subject and/or draft body mentions attachments but they are missing.
pub const MISSINGATTACHMENTWARN: Hook = Hook { pub const MISSINGATTACHMENTWARN: Hook = Hook {
name: Cow::Borrowed("missing-attachment-warn"), 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<()> { 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. /// Warn if draft has no subject and no body.
pub const EMPTYDRAFTWARN: Hook = Hook { pub const EMPTYDRAFTWARN: Hook = Hook {
name: Cow::Borrowed("empty-draft-warn"), name: Cow::Borrowed("empty-draft-warn"),
hook_fn: empty_draft_warn, hook_fn: HookFn::Ptr(empty_draft_warn),
}; };
#[cfg(test)] #[cfg(test)]

View File

@ -34,7 +34,7 @@ use std::{
use melib::{backends::TagHash, search::Query, StderrLogger}; 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] #[rustfmt::skip]
mod overrides; mod overrides;
@ -729,8 +729,9 @@ mod default_vals {
} }
mod deserializers { mod deserializers {
use serde::{Deserialize, Deserializer}; use serde::{de, Deserialize, Deserializer};
pub(in crate::conf) fn non_empty_string<'de, D, T: std::convert::From<Option<String>>>(
pub(in crate::conf) fn non_empty_opt_string<'de, D, T: std::convert::From<Option<String>>>(
deserializer: D, deserializer: D,
) -> std::result::Result<T, D::Error> ) -> std::result::Result<T, D::Error>
where 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; use toml::Value;
fn any_of<'de, D>(deserializer: D) -> std::result::Result<String, D::Error> fn any_of<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
where where

View File

@ -24,7 +24,10 @@ use std::collections::HashMap;
use melib::ToggleFlag; 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 /// Settings for writing and sending new e-mail
#[derive(Debug, Serialize, Deserialize, Clone)] #[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:". /// The prefix to use in reply subjects. The de facto prefix is "Re:".
#[serde(default = "res", alias = "reply-prefix")] #[serde(default = "res", alias = "reply-prefix")]
pub reply_prefix: String, pub reply_prefix: String,
/// Custom `compose-hooks`.
#[serde(default, alias = "custom-compose-hooks")]
pub custom_compose_hooks: Vec<ComposeHook>,
/// Disabled `compose-hooks`. /// Disabled `compose-hooks`.
#[serde(default, alias = "disabled-compose-hooks")] #[serde(default, alias = "disabled-compose-hooks")]
pub disabled_compose_hooks: Vec<String>, pub disabled_compose_hooks: Vec<String>,
@ -121,6 +127,7 @@ impl Default for ComposingSettings {
forward_as_attachment: ToggleFlag::Ask, forward_as_attachment: ToggleFlag::Ask,
reply_prefix_list_to_strip: None, reply_prefix_list_to_strip: None,
reply_prefix: res(), reply_prefix: res(),
custom_compose_hooks: vec![],
disabled_compose_hooks: vec![], disabled_compose_hooks: vec![],
} }
} }
@ -177,3 +184,19 @@ pub enum SendMail {
ServerSubmission, ServerSubmission,
ShellCommand(String), 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)
}
}

View File

@ -25,7 +25,7 @@
//! This module is automatically generated by config_macros.rs. //! This module is automatically generated by config_macros.rs.
use super::*; 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 } } } # [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 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 } } } # [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 } } }

View File

@ -51,14 +51,14 @@ pub struct PagerSettings {
/// A command to pipe mail output through for viewing in pager. /// A command to pipe mail output through for viewing in pager.
/// Default: None /// Default: None
#[serde(default = "none", deserialize_with = "non_empty_string")] #[serde(default = "none", deserialize_with = "non_empty_opt_string")]
pub filter: Option<String>, pub filter: Option<String>,
/// A command to pipe html output before displaying it in a pager /// A command to pipe html output before displaying it in a pager
/// Default: None /// Default: None
#[serde( #[serde(
default = "none", default = "none",
deserialize_with = "non_empty_string", deserialize_with = "non_empty_opt_string",
alias = "html-filter" alias = "html-filter"
)] )]
pub html_filter: Option<String>, 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 /// A command to launch URLs with. The URL will be given as the first
/// argument of the command. Default: None /// 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>, pub url_launcher: Option<String>,
/// A command to open html files. /// A command to open html files.
/// Default: None /// Default: None
#[serde( #[serde(
default = "none", default = "none",
deserialize_with = "non_empty_string", deserialize_with = "non_empty_opt_string",
alias = "html-open" alias = "html-open"
)] )]
pub html_open: Option<String>, pub html_open: Option<String>,

View File

@ -23,7 +23,7 @@
use melib::{Error, Result, ToggleFlag}; 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 /// Settings for terminal display
#[derive(Debug, Deserialize, Clone, Serialize)] #[derive(Debug, Deserialize, Clone, Serialize)]
@ -40,11 +40,11 @@ pub struct TerminalSettings {
pub use_mouse: ToggleFlag, pub use_mouse: ToggleFlag,
/// String to show in status bar if mouse is active. /// String to show in status bar if mouse is active.
/// Default: "🖱️ " /// Default: "🖱️ "
#[serde(deserialize_with = "non_empty_string")] #[serde(deserialize_with = "non_empty_opt_string")]
pub mouse_flag: Option<String>, pub mouse_flag: Option<String>,
#[serde(deserialize_with = "non_empty_string")] #[serde(deserialize_with = "non_empty_opt_string")]
pub window_title: Option<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>, pub file_picker_command: Option<String>,
/// Choose between 30-something built in sequences (integers between 0-30) /// Choose between 30-something built in sequences (integers between 0-30)
/// or define your own list of strings for the progress spinner /// or define your own list of strings for the progress spinner