diff --git a/docs/meli.conf.5 b/docs/meli.conf.5 index f767db60..2793e760 100644 --- a/docs/meli.conf.5 +++ b/docs/meli.conf.5 @@ -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. diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index 23709d28..3ceebbd9 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -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 { 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::>() + { + context.replies.push_back(UIEvent::Notification( + None, + err.to_string(), + None, + )); + } } self.mode = ViewMode::Send(UIConfirmationDialog::new( "send mail?", diff --git a/src/components/mail/compose/hooks.rs b/src/components/mail/compose/hooks.rs index 384bfa56..a917237d 100644 --- a/src/components/mail/compose/hooks.rs +++ b/src/components/mail/compose/hooks.rs @@ -24,7 +24,6 @@ pub use std::borrow::Cow; use super::*; -/* pub enum HookFn { /// Stateful hook. Closure(Box 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)] diff --git a/src/conf.rs b/src/conf.rs index b06f678a..986793ae 100644 --- a/src/conf.rs +++ b/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>>( + use serde::{de, Deserialize, Deserializer}; + + pub(in crate::conf) fn non_empty_opt_string<'de, D, T: std::convert::From>>( deserializer: D, ) -> std::result::Result where @@ -744,6 +745,20 @@ mod deserializers { } } + pub(in crate::conf) fn non_empty_string<'de, D, T: std::convert::From>( + deserializer: D, + ) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = ::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 where diff --git a/src/conf/composing.rs b/src/conf/composing.rs index 34398974..a00386bd 100644 --- a/src/conf/composing.rs +++ b/src/conf/composing.rs @@ -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, /// Disabled `compose-hooks`. #[serde(default, alias = "disabled-compose-hooks")] pub disabled_compose_hooks: Vec, @@ -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 for crate::components::mail::hooks::Hook { + fn from(c: ComposeHook) -> Self { + Self::new_shell_command(c.name.into(), c.command) + } +} diff --git a/src/conf/overrides.rs b/src/conf/overrides.rs index 36f87182..4e755c7a 100644 --- a/src/conf/overrides.rs +++ b/src/conf/overrides.rs @@ -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 } } } diff --git a/src/conf/pager.rs b/src/conf/pager.rs index 8bbce8d3..3701cc57 100644 --- a/src/conf/pager.rs +++ b/src/conf/pager.rs @@ -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, /// 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, @@ -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, /// 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, diff --git a/src/conf/terminal.rs b/src/conf/terminal.rs index 6b3beeb1..9ace3952 100644 --- a/src/conf/terminal.rs +++ b/src/conf/terminal.rs @@ -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, - #[serde(deserialize_with = "non_empty_string")] + #[serde(deserialize_with = "non_empty_opt_string")] pub window_title: Option, - #[serde(deserialize_with = "non_empty_string")] + #[serde(deserialize_with = "non_empty_opt_string")] pub file_picker_command: Option, /// Choose between 30-something built in sequences (integers between 0-30) /// or define your own list of strings for the progress spinner