]> granicus.if.org Git - neomutt/commitdiff
Add index-format-hook and expando
authorKevin McCarthy <kevin@8t8.us>
Mon, 29 Oct 2018 20:45:02 +0000 (13:45 -0700)
committerRichard Russon <rich@flatcap.org>
Wed, 24 Apr 2019 11:20:39 +0000 (12:20 +0100)
index-format-hook is used to allow dynamic insertion/evaluation of
format strings into $index_format.

It can be used, for example, to implement date formatting based on the
age of the message.

Add a new %@name@ expando to $index_format, which evaluates the
matching index-format-hooks with "name".

Co-authored-by: Richard Russon <rich@flatcap.org>
curs_lib.c
curs_lib.h
doc/manual.xml.head
hdrline.c
hook.c
hook.h
init.c
init.h

index 08607dd0309f70fa7a5e1d1adf4d4d8efdab4ecd..e043e8606604032eda1fac596ba1a5425f61012b 100644 (file)
@@ -1038,7 +1038,7 @@ void mutt_simple_format(char *buf, size_t buflen, int min_width, int max_width,
 }
 
 /**
- * format_s_x - Format a string like snprintf()
+ * mutt_format_s_x - Format a string like snprintf()
  * @param[out] buf      Buffer in which to save string
  * @param[in]  buflen   Buffer length
  * @param[in]  prec     Field precision, e.g. "-3.4"
@@ -1051,7 +1051,7 @@ void mutt_simple_format(char *buf, size_t buflen, int min_width, int max_width,
  * except that the numbers in the conversion specification refer to
  * the number of character cells when printed.
  */
-static void format_s_x(char *buf, size_t buflen, const char *prec, const char *s, bool arboreal)
+void mutt_format_s_x(char *buf, size_t buflen, const char *prec, const char *s, bool arboreal)
 {
   enum FormatJustify justify = JUSTIFY_RIGHT;
   char *p = NULL;
@@ -1090,7 +1090,7 @@ static void format_s_x(char *buf, size_t buflen, const char *prec, const char *s
  */
 void mutt_format_s(char *buf, size_t buflen, const char *prec, const char *s)
 {
-  format_s_x(buf, buflen, prec, s, false);
+  mutt_format_s_x(buf, buflen, prec, s, false);
 }
 
 /**
@@ -1102,7 +1102,7 @@ void mutt_format_s(char *buf, size_t buflen, const char *prec, const char *s)
  */
 void mutt_format_s_tree(char *buf, size_t buflen, const char *prec, const char *s)
 {
-  format_s_x(buf, buflen, prec, s, true);
+  mutt_format_s_x(buf, buflen, prec, s, true);
 }
 
 /**
index a311f5858255a3ca45fc9c7faaf99015e9d09656..67f443f9abe7ab6c174a730e0bde2c16270b17f2 100644 (file)
@@ -59,6 +59,7 @@ void         mutt_flush_macro_to_endcond(void);
 void         mutt_flush_unget_to_endcond(void);
 void         mutt_format_s(char *buf, size_t buflen, const char *prec, const char *s);
 void         mutt_format_s_tree(char *buf, size_t buflen, const char *prec, const char *s);
+void         mutt_format_s_x(char *buf, size_t buflen, const char *prec, const char *s, bool arboreal);
 void         mutt_getch_timeout(int delay);
 struct Event mutt_getch(void);
 int          mutt_get_field_full(const char *field, char *buf, size_t buflen, CompletionFlags complete, bool multiple, char ***files, int *numfiles);
index 9fd22c9e8249aaf7bc94a6c9cf7e1c0f9cc99467..27e6e5e1a4d8391c924f052a7ba66752641ae3c2 100644 (file)
@@ -5813,6 +5813,62 @@ message-hook '~f freshmeat-news' 'set pager="less \"+/^  subject: .*\""'
       </para>
     </sect1>
 
+    <sect1 id="index-format-hook">
+      <title>Dynamically Changing $index_format using Patterns</title>
+      <para>Usage:</para>
+      <cmdsynopsis>
+        <command>index-format-hook</command>
+        <arg choice="plain">
+          <replaceable class="parameter">name</replaceable>
+        </arg>
+        <arg choice="plain">
+          <replaceable class="parameter">[!]pattern</replaceable>
+        </arg>
+        <arg choice="plain">
+          <replaceable class="parameter">format-string</replaceable>
+        </arg>
+      </cmdsynopsis>
+      <para>
+        This command is used to inject format strings dynamically into <link
+        linkend="index-format">$index_format</link>based on pattern matching
+        against the current message.
+      </para>
+      <para>
+        The <link linkend="index-format">$index_format</link>expando
+        <emphasis>%@name@</emphasis>specifies a placeholder for the injection.
+        Index-format-hooks with the same <emphasis>name</emphasis>are matched using
+        <link linkend="patterns"> <emphasis>pattern</emphasis> </link>against the
+        current message. Matching is done in the order specified in the .muttrc,
+        with the first match being used. The hook's
+        <emphasis>format-string</emphasis>is then substituted and evaluated.
+      </para>
+      <para>
+        Because the first match is used, best practice is to put a catch-all
+        <emphasis>~A</emphasis>pattern as the last hook. Here is an example showing
+        how to implement dynamic date formatting:
+      </para>
+<screen>
+set index_format="%4C %-6@date@ %-15.15F %Z (%4c) %s"
+
+index-format-hook  date  "~d&lt;1d"    "%[%H:%M]"
+index-format-hook  date  "~d&lt;1m"    "%[%a %d]"
+index-format-hook  date  "~d&lt;1y"    "%[%b %d]"
+index-format-hook  date  "~A"       "%[%m/%y]"
+</screen>
+      <para>
+        Another example, showing a way to prepend to
+        the subject. Note that without a catch-all ~A
+        pattern, no match results in the expando being
+        replaced with an empty string.
+      </para>
+<screen>
+set index_format="%4C %@subj_flags@%s"
+
+index-format-hook  subj_flags  "~f boss@example.com"    "** BOSS ** "
+index-format-hook  subj_flags  "~f spouse@example.com"  ":-) "
+</screen>
+    </sect1>
+
     <sect1 id="push">
       <title>Adding Key Sequences to the Keyboard Buffer</title>
       <para>
@@ -8398,6 +8454,11 @@ set index_format="%4C %Z %{%b %d} %-15.15L (%?l?%4l&amp;%4c?)%*  %s"
             <link linkend="iconv-hook"><command>iconv-hook</command></link>
           </para>
         </listitem>
+        <listitem>
+          <para>
+            <link linkend="index-format-hook"><command>index-format-hook</command></link>
+          </para>
+        </listitem>
         <listitem>
           <para>
             <link linkend="crypt-hook"><command>crypt-hook</command></link>
@@ -8505,7 +8566,8 @@ send-hook ~C'^b@b\.b$' my_hdr from: c@c.c
           Hooks that act upon messages (<command>message-hook</command>,
           <command>reply-hook</command>, <command>send-hook</command>,
           <command>send2-hook</command>, <command>save-hook</command>,
-          <command>fcc-hook</command>) are evaluated in a slightly different
+          <command>fcc-hook</command>, <command>index-format-hook</command>)
+          are evaluated in a slightly different
           manner. For the other types of hooks, a
           <link linkend="regex">regular expression</link> is sufficient. But in
           dealing with messages a finer grain of control is needed for matching
@@ -18179,6 +18241,22 @@ neomutt mailto:some@one.org?subject=test&amp;cc=other@one.org
             </group>
           </cmdsynopsis>
         </listitem>
+        <listitem>
+          <cmdsynopsis>
+            <command>
+              <link linkend="index-format-hook">index-format-hook</link>
+            </command>
+            <arg choice="plain">
+              <replaceable class="parameter">name</replaceable>
+            </arg>
+            <arg choice="plain">
+              <replaceable class="parameter">[!]pattern</replaceable>
+            </arg>
+            <arg choice="plain">
+              <replaceable class="parameter">format-string</replaceable>
+            </arg>
+          </cmdsynopsis>
+        </listitem>
         <listitem>
           <cmdsynopsis>
             <command>
index 1fed6b78c01b457964d2ae958d24849424f3c48d..6ba090e68a984dc32651aa7513fac9c591103048 100644 (file)
--- a/hdrline.c
+++ b/hdrline.c
@@ -46,6 +46,7 @@
 #include "curs_lib.h"
 #include "format_flags.h"
 #include "globals.h"
+#include "hook.h"
 #include "mailbox.h"
 #include "mutt_curses.h"
 #include "mutt_menu.h"
@@ -1435,6 +1436,29 @@ static const char *index_format_str(char *buf, size_t buflen, size_t col, int co
       add_index_color(buf + colorlen, buflen - colorlen, flags, MT_COLOR_INDEX);
       break;
 
+    case '@':
+    {
+      const char *end = src;
+      static unsigned char recurse = 0;
+
+      while ((*end != '\0') && (*end != '@'))
+        end++;
+      if ((*end == '@') && (recurse < 20))
+      {
+        recurse++;
+        mutt_str_substr_cpy(tmp, src, end, sizeof(tmp));
+        mutt_expando_format(tmp, sizeof(tmp), col, cols,
+                            NONULL(mutt_idxfmt_hook(tmp, m, e)),
+                            index_format_str, data, flags);
+        mutt_format_s_x(buf, buflen, prec, tmp, true);
+        recurse--;
+
+        src = end + 1;
+        break;
+      }
+    }
+      /* fallthrough */
+
     default:
       snprintf(buf, buflen, "%%%s%c", prec, op);
       break;
diff --git a/hook.c b/hook.c
index dc9b2f469f344d82c515f6127adfb370918e25ca..ffd4a22979a14947e866f36f825846f12d7748aa 100644 (file)
--- a/hook.c
+++ b/hook.c
@@ -70,8 +70,11 @@ struct Hook
   struct PatternHead *pattern; ///< Used for fcc,save,send-hook
   TAILQ_ENTRY(Hook) entries;
 };
-static TAILQ_HEAD(, Hook) Hooks = TAILQ_HEAD_INITIALIZER(Hooks);
+TAILQ_HEAD(HookList, Hook);
 
+static struct HookList Hooks = TAILQ_HEAD_INITIALIZER(Hooks);
+
+static struct Hash *IdxFmtHooks = NULL;
 static HookFlags current_hook_type = MUTT_HOOK_NO_FLAGS;
 
 /**
@@ -322,6 +325,136 @@ void mutt_delete_hooks(HookFlags type)
   }
 }
 
+/**
+ * delete_idxfmt_hooklist - Delete a index-format-hook from the Hash Table
+ * @param type Type of Hash Element
+ * @param obj  Pointer to Hashed object
+ * @param data Private data
+ */
+static void delete_idxfmt_hooklist(int type, void *obj, intptr_t data)
+{
+  struct HookList *hl = obj;
+  struct Hook *h = NULL;
+  struct Hook *tmp = NULL;
+
+  TAILQ_FOREACH_SAFE(h, hl, entries, tmp)
+  {
+    TAILQ_REMOVE(hl, h, entries);
+    delete_hook(h);
+  }
+
+  FREE(&hl);
+}
+
+/**
+ * delete_idxfmt_hooks - Delete all the index-format-hooks
+ */
+static void delete_idxfmt_hooks(void)
+{
+  mutt_hash_free(&IdxFmtHooks);
+}
+
+/**
+ * mutt_parse_idxfmt_hook - Parse the 'index-format-hook' command - Implements ::command_t
+ */
+int mutt_parse_idxfmt_hook(struct Buffer *buf, struct Buffer *s,
+                           unsigned long data, struct Buffer *err)
+{
+  enum CommandResult rc = MUTT_CMD_ERROR;
+  bool not = false;
+
+  struct Buffer *name = mutt_buffer_pool_get();
+  struct Buffer *pattern = mutt_buffer_pool_get();
+  struct Buffer *fmtstring = mutt_buffer_pool_get();
+
+  if (!IdxFmtHooks)
+  {
+    IdxFmtHooks = mutt_hash_new(30, MUTT_HASH_STRDUP_KEYS);
+    mutt_hash_set_destructor(IdxFmtHooks, delete_idxfmt_hooklist, 0);
+  }
+
+  if (!MoreArgs(s))
+  {
+    mutt_str_strfcpy(err->data, _("too few arguments"), err->dsize);
+    goto out;
+  }
+  mutt_extract_token(name, s, MUTT_TOKEN_NO_FLAGS);
+  struct HookList *hl = mutt_hash_find(IdxFmtHooks, mutt_b2s(name));
+
+  if (*s->dptr == '!')
+  {
+    s->dptr++;
+    SKIPWS(s->dptr);
+    not = true;
+  }
+  mutt_extract_token(pattern, s, MUTT_TOKEN_NO_FLAGS);
+
+  if (!MoreArgs(s))
+  {
+    mutt_str_strfcpy(err->data, _("too few arguments"), err->dsize);
+    goto out;
+  }
+  mutt_extract_token(fmtstring, s, MUTT_TOKEN_NO_FLAGS);
+
+  if (MoreArgs(s))
+  {
+    mutt_str_strfcpy(err->data, _("too many arguments"), err->dsize);
+    goto out;
+  }
+
+  if (C_DefaultHook)
+  {
+    mutt_buffer_increase_size(pattern, 8192);
+    mutt_check_simple(pattern->data, pattern->dsize, C_DefaultHook);
+    mutt_buffer_fix_dptr(pattern); /* not necessary, but to be safe */
+  }
+
+  /* check to make sure that a matching hook doesn't already exist */
+  struct Hook *hook = NULL;
+  if (hl)
+  {
+    TAILQ_FOREACH(hook, hl, entries)
+    {
+      if ((hook->regex.not == not) &&
+          (mutt_str_strcmp(mutt_b2s(pattern), hook->regex.pattern) == 0))
+      {
+        mutt_str_replace(&hook->command, mutt_b2s(fmtstring));
+        rc = MUTT_CMD_SUCCESS;
+        goto out;
+      }
+    }
+  }
+
+  struct PatternHead *pat = mutt_pattern_comp(pattern->data, MUTT_FULL_MSG, err);
+  if (!pat)
+    goto out;
+
+  hook = mutt_mem_calloc(1, sizeof(struct Hook));
+  hook->type = data;
+  hook->command = mutt_str_strdup(mutt_b2s(fmtstring));
+  hook->pattern = pat;
+  hook->regex.pattern = mutt_str_strdup(mutt_b2s(pattern));
+  hook->regex.regex = NULL;
+  hook->regex.not = not;
+
+  if (!hl)
+  {
+    hl = mutt_mem_calloc(1, sizeof(*hl));
+    TAILQ_INIT(hl);
+    mutt_hash_insert(IdxFmtHooks, mutt_b2s(name), hl);
+  }
+
+  TAILQ_INSERT_TAIL(hl, hook, entries);
+  rc = MUTT_CMD_SUCCESS;
+
+out:
+  mutt_buffer_pool_release(&name);
+  mutt_buffer_pool_release(&pattern);
+  mutt_buffer_pool_release(&fmtstring);
+
+  return rc;
+}
+
 /**
  * mutt_parse_unhook - Parse the 'unhook' command - Implements ::command_t
  */
@@ -339,6 +472,7 @@ enum CommandResult mutt_parse_unhook(struct Buffer *buf, struct Buffer *s,
         return MUTT_CMD_WARNING;
       }
       mutt_delete_hooks(MUTT_HOOK_NO_FLAGS);
+      delete_idxfmt_hooks();
       mutt_ch_lookup_remove();
     }
     else
@@ -361,7 +495,10 @@ enum CommandResult mutt_parse_unhook(struct Buffer *buf, struct Buffer *s,
                            buf->data, buf->data);
         return MUTT_CMD_WARNING;
       }
-      mutt_delete_hooks(type);
+      if (type == MUTT_IDXFMTHOOK)
+        delete_idxfmt_hooks();
+      else
+        mutt_delete_hooks(type);
     }
   }
   return MUTT_CMD_SUCCESS;
@@ -722,3 +859,41 @@ void mutt_startup_shutdown_hook(HookFlags type)
   }
   FREE(&token.data);
 }
+
+/**
+ * mutt_idxfmt_hook - Get index-format-hook format string
+ * @param name Hook name
+ * @param m    Mailbox
+ * @param e    Email
+ * @retval ptr  printf(3)-like format string
+ * @retval NULL No matching hook
+ */
+const char *mutt_idxfmt_hook(const char *name, struct Mailbox *m, struct Email *e)
+{
+  if (!IdxFmtHooks)
+    return NULL;
+
+  struct HookList *hl = mutt_hash_find(IdxFmtHooks, name);
+  if (!hl)
+    return NULL;
+
+  current_hook_type = MUTT_IDXFMTHOOK;
+
+  struct PatternCache cache = { 0 };
+  const char *fmtstring = NULL;
+  struct Hook *hook = NULL;
+
+  TAILQ_FOREACH(hook, hl, entries)
+  {
+    struct Pattern *pat = SLIST_FIRST(hook->pattern);
+    if ((mutt_pattern_exec(pat, 0, m, e, &cache) > 0) ^ hook->regex.not)
+    {
+      fmtstring = hook->command;
+      break;
+    }
+  }
+
+  current_hook_type = MUTT_HOOK_NO_FLAGS;
+
+  return fmtstring;
+}
diff --git a/hook.h b/hook.h
index a2cc0d017333fab064832b530038cadb1f9af289..1043e4bfd0687dcdd41e2054d84b87d2677b73e3 100644 (file)
--- a/hook.h
+++ b/hook.h
@@ -58,10 +58,11 @@ typedef uint32_t HookFlags;          ///< Flags for mutt_parse_hook(), e.g. #MUT
 #define MUTT_APPEND_HOOK   (1 << 13) ///< append-hook: append to a compressed mailbox
 #define MUTT_CLOSE_HOOK    (1 << 14) ///< close-hook: write to a compressed mailbox
 #endif
-#define MUTT_TIMEOUT_HOOK  (1 << 15) ///< timeout-hook: run a command periodically
-#define MUTT_STARTUP_HOOK  (1 << 16) ///< startup-hook: run when starting NeoMutt
-#define MUTT_SHUTDOWN_HOOK (1 << 17) ///< shutdown-hook: run when leaving NeoMutt
-#define MUTT_GLOBAL_HOOK   (1 << 18) ///< Hooks which don't take a regex
+#define MUTT_IDXFMTHOOK    (1 << 15) ///< index-format-hook: customise the format of the index
+#define MUTT_TIMEOUT_HOOK  (1 << 16) ///< timeout-hook: run a command periodically
+#define MUTT_STARTUP_HOOK  (1 << 17) ///< startup-hook: run when starting NeoMutt
+#define MUTT_SHUTDOWN_HOOK (1 << 18) ///< shutdown-hook: run when leaving NeoMutt
+#define MUTT_GLOBAL_HOOK   (1 << 19) ///< Hooks which don't take a regex
 
 void  mutt_account_hook(const char *url);
 void  mutt_crypt_hook(struct ListHead *list, struct Address *addr);
@@ -69,7 +70,9 @@ void  mutt_default_save(char *path, size_t pathlen, struct Email *e);
 void  mutt_delete_hooks(HookFlags type);
 char *mutt_find_hook(HookFlags type, const char *pat);
 void  mutt_folder_hook(const char *path, const char *desc);
+const char *mutt_idxfmt_hook(const char *name, struct Mailbox *m, struct Email *e);
 void  mutt_message_hook(struct Mailbox *m, struct Email *e, HookFlags type);
+enum CommandResult mutt_parse_idxfmt_hook(struct Buffer *buf, struct Buffer *s, unsigned long data, struct Buffer *err);
 enum CommandResult mutt_parse_hook(struct Buffer *buf, struct Buffer *s, unsigned long data, struct Buffer *err);
 enum CommandResult mutt_parse_unhook(struct Buffer *buf, struct Buffer *s, unsigned long data, struct Buffer *err);
 void  mutt_select_fcc(char *path, size_t pathlen, struct Email *e);
diff --git a/init.c b/init.c
index dc28ded02ca48873b3fe91885c607e911c62fbf7..8d87246b226c779a59bcdede2f5cd33fc67b4fbb 100644 (file)
--- a/init.c
+++ b/init.c
@@ -2975,8 +2975,13 @@ void mutt_free_opts(void)
 int mutt_get_hook_type(const char *name)
 {
   for (const struct Command *c = Commands; c->name; c++)
-    if ((c->func == mutt_parse_hook) && (mutt_str_strcasecmp(c->name, name) == 0))
+  {
+    if (((c->func == mutt_parse_hook) || (c->func == mutt_parse_idxfmt_hook)) &&
+        (mutt_str_strcasecmp(c->name, name) == 0))
+    {
       return c->data;
+    }
+  }
   return MUTT_CMD_SUCCESS;
 }
 
diff --git a/init.h b/init.h
index 1fd2851f1fc7f1b73fafacb67aa417ecc25dc29d..0ed39682cb154cec85bc17682b5b7416a7bcabb3 100644 (file)
--- a/init.h
+++ b/init.h
@@ -1819,6 +1819,8 @@ struct ConfigDef MuttVars[] = {
   ** .dt %zc .dd Message crypto flags
   ** .dt %zs .dd Message status flags
   ** .dt %zt .dd Message tag flags
+  ** .dt %@name@ .dd insert and evaluate format-string from the matching
+  **                 ``$index-format-hook'' command
   ** .dt %{fmt} .dd the date and time of the message is converted to sender's
   **                time zone, and "fmt" is expanded by the library function
   **                \fCstrftime(3)\fP; a leading bang disables locales
@@ -4854,6 +4856,7 @@ const struct Command Commands[] = {
   { "ifdef",               parse_ifdef,            0 },
   { "ifndef",              parse_ifdef,            1 },
   { "ignore",              parse_ignore,           0 },
+  { "index-format-hook",   mutt_parse_idxfmt_hook, MUTT_IDXFMTHOOK },
   { "lists",               parse_lists,            0 },
 #ifdef USE_LUA
   { "lua",                 mutt_lua_parse,         0 },