]> granicus.if.org Git - neomutt/commitdiff
help: First implementation of a help mailbox
authorFloyd Anderson <f.a@31c0.net>
Mon, 25 Jun 2018 21:22:42 +0000 (23:22 +0200)
committerRichard Russon <rich@flatcap.org>
Mon, 21 Oct 2019 18:54:46 +0000 (19:54 +0100)
Help system means to parse Markdown files in a directory (help mailbox),
show valid results threaded by chapter/section in the index view as help
documents and view those "messages" in pager by pressing <Enter> on it.

The help mailbox (helpbox) works local, e.g. with a clone of [1][2], for
testing, is read-only and all files are handled accordingly.

This commit implements:
  - a new DT_PATH option $help_doc_dir, specifies a folder that contains
    the Markdown help files to process, defaults to (PKGDOCDIR "/help")
  - a new function <help-box> (OP_HELP_BOX), bound to "<Esc>H" or may be
    invoked directly by ":exec help-box" or by "<change-folder> help://"
  - a rudimentary "jump to chapter/section/file" functionality from the
    given (and first matching) help URI, e.g. "help://sidebar/intro"
  - hard-coded option for caching a list of valid help documents between
    help invocation; whether or not to link chapter threads upwards; how
    many lines of a YAML file header to read

References:
  - [1] <https://github.com/neomutt/test-doc>
  - [2] <https://github.com/neomutt/neomutt.github.io>

help/help.c
help/help.h

index d1a3dd9f2c2fd786a01238463d8bc4fa10bbb1e5..e9b8cf19857bf1f21fef0d80152abc752b9fc0c6 100644 (file)
@@ -4,6 +4,7 @@
  *
  * @authors
  * Copyright (C) 2018-2019 Richard Russon <rich@flatcap.org>
+ * Copyright (C) 2018 Floyd Anderson <f.a@31c0.net>
  *
  * @copyright
  * This program is free software: you can redistribute it and/or modify it under
 
 #include "config.h"
 #include <stddef.h>
+#include <dirent.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdarg.h>
 #include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
 #include "mutt/mutt.h"
+#include "address/lib.h"
 #include "config/lib.h"
 #include "core/lib.h"
+#include "email/lib.h"
+#include "help.h"
+#include "context.h"
+#include "globals.h"
+#include "mutt_header.h"
+#include "mutt_options.h"
+#include "mutt_thread.h"
+#include "muttlib.h"
 #include "mx.h"
+#include "protos.h"
 
-struct Context;
-struct Header;
-struct Message;
+#define HELP_CACHE_DOCLIST                                                     \
+  1 ///< whether to cache the DocList between help_mbox_open calls
+#define HELP_FHDR_MAXLINES                                                     \
+  -1 ///< max help file header lines to read (N < 0 means all)
+#define HELP_LINK_CHAPTERS                                                     \
+  0 ///< whether to link all help chapter upwards to root box
+
+static bool __Backup_HTS; ///< used to restore $hide_thread_subject on help_mbox_close()
+static char DocDirID[33]; ///< MD5 checksum of current $help_doc_dir DT_PATH option
+static struct HelpList *DocList; ///< all valid help documents within $help_doc_dir folder
+static size_t UpLink = 0; ///< DocList index, used to uplink a parent thread target
 
 /**
- * help_mbox_open - Open a Mailbox -- Implements MxOps::mbox_open
+ * help_list_free - Free a list of Help documents
+ * @param list      List to free
+ * @param item_free Function to free the list contents
  */
-static int help_mbox_open(struct Mailbox *m)
+static void help_list_free(struct HelpList **list, void (*item_free)(void **))
 {
-  mutt_debug(1, "entering help_mbox_open\n");
-  return -1;
+  if (!list || !*list)
+    return;
+
+  for (size_t i = 0; i < (*list)->size; i++)
+  {
+    item_free(&((*list)->data[i]));
+  }
+  FREE(&(*list)->data);
+  FREE(list);
+}
+
+/**
+ * help_list_shrink - Resize a List of Help documents to save space
+ * @param list List to resize
+ */
+static void help_list_shrink(struct HelpList *list)
+{
+  if (!list)
+    return;
+
+  mutt_mem_realloc(list->data, list->size * list->item_size);
+  list->capa = list->size;
+}
+
+/**
+ * help_list_new - Create a new list of Help documents
+ * @param item_size Size of items in list
+ * @retval ptr New Help list
+ */
+static struct HelpList *help_list_new(size_t item_size)
+{
+  struct HelpList *list = NULL;
+  if (item_size == 0)
+    return NULL;
+
+  list = mutt_mem_malloc(sizeof(struct HelpList));
+  list->item_size = item_size;
+  list->size = 0;
+  list->capa = HELPLIST_INIT_CAPACITY;
+  list->data = mutt_mem_calloc(list->capa, sizeof(void *) * list->item_size);
+
+  return list;
+}
+
+/**
+ * help_list_append - Add an item to the Help document list
+ * @param list List to add to
+ * @param item Item to add
+ */
+static void help_list_append(struct HelpList *list, void *item)
+{
+  if (!list || !item)
+    return;
+
+  if (list->size >= list->capa)
+  {
+    list->capa = (list->capa == 0) ? HELPLIST_INIT_CAPACITY : (list->capa * 2);
+    mutt_mem_realloc(list->data, list->capa * list->item_size);
+  }
+
+  list->data[list->size] = mutt_mem_calloc(1, list->item_size);
+  list->data[list->size] = item;
+  list->size++;
+}
+
+/**
+ * help_list_new_append - Append a new item to a Help document list
+ * @param list      List to append to
+ * @param item_size Size of item to add
+ * @param item      Item to add to list
+ */
+static void help_list_new_append(struct HelpList **list, size_t item_size, void *item)
+{
+  if ((item_size == 0) || !item)
+    return;
+
+  if (!list || !*list)
+    *list = help_list_new(item_size);
+
+  help_list_append(*list, item);
+}
+
+/**
+ * help_list_get - Get an item from a Help document list
+ * @param list  List to use
+ * @param index Index in list
+ * @param copy  Function to copy item (may be NULL)
+ * @retval ptr  Item selected
+ * @retval NULL Invalid index
+ */
+static void *help_list_get(struct HelpList *list, size_t index, void *(*copy)(const void *) )
+{
+  if (!list || (index >= list->size))
+    return NULL;
+
+  return ((copy) ? copy(list->data[index]) : list->data[index]);
+}
+
+/**
+ * help_list_clone - Copy a list of Help documents
+ * @param list   List to copy
+ * @param shrink true if the list should be minimised
+ * @param copy   Function to copy a list item
+ * @retval ptr Duplicated list of Help documents
+ */
+static struct HelpList *help_list_clone(struct HelpList *list, bool shrink,
+                                        void *(*copy)(const void *) )
+{
+  if (!list)
+    return NULL;
+
+  struct HelpList *clone = help_list_new(list->item_size);
+  for (size_t i = 0; i < list->size; i++)
+    help_list_append(clone, help_list_get(list, i, copy));
+
+  if (shrink)
+    help_list_shrink(clone);
+
+  return clone;
+}
+
+/**
+ * help_list_sort - Sort a list of Help documents
+ * @param list    List to sort
+ * @param compare Function to compare two items
+ */
+static void help_list_sort(struct HelpList *list, int (*compare)(const void *, const void *))
+{
+  if (!list)
+    return;
+
+  qsort(list->data, list->size, sizeof(void *), compare);
+}
+
+/**
+ * help_doc_type_cmp - Compare two help documents by their type - Implements ::sort_t
+ */
+static int help_doc_type_cmp(const void *a, const void *b)
+{
+  const struct Email *e1 = *(const struct Email **) a;
+  const struct Email *e2 = *(const struct Email **) b;
+  const HelpDocFlags t1 = ((const struct HelpDocMeta *) e1->edata)->type;
+  const HelpDocFlags t2 = ((const struct HelpDocMeta *) e2->edata)->type;
+
+  return ((t1 < t2) - (t1 > t2));
+}
+
+/**
+ * help_file_hdr_free - Free a file header
+ * @param item File header
+ */
+static void help_file_hdr_free(void **item)
+{
+  struct HelpFileHeader *fhdr = ((struct HelpFileHeader *) *item);
+
+  FREE(&fhdr->key);
+  FREE(&fhdr->val);
+  FREE(&fhdr);
+}
+
+/**
+ * help_doc_meta_free - Free help doc metadata
+ * @param data Metadata
+ *
+ * @note Called by mutt_header_free() to free custom metadata in Context::data
+ */
+static void help_doc_meta_free(void **data)
+{
+  if (!data || !*data)
+    return;
+
+  struct HelpDocMeta *meta = *data;
+
+  FREE(&meta->name);
+  help_list_free(&meta->fhdr, help_file_hdr_free);
+  *data = NULL;
+}
+
+/**
+ * help_doc_free - Free a DocList element
+ * @param item DocList element
+ */
+static void help_doc_free(void **item)
+{
+  struct Email *hdoc = ((struct Email *) *item);
+
+  mutt_email_free(&hdoc);
+  FREE(hdoc);
+}
+
+/**
+ * help_doclist_free - Free the global DocList
+ */
+void help_doclist_free(void)
+{
+  help_list_free(&DocList, help_doc_free);
+  mutt_str_strfcpy(DocDirID, "", sizeof(DocDirID));
+  UpLink = 0;
+}
+
+/**
+ * help_checksum_md5 - Calculate a string MD5 checksum and store it in a buffer
+ * @param string String to hash
+ * @param digest Buffer for storing the calculated hash
+ *
+ * @note The digest buffer _must_ be at least 33 bytes long
+ */
+static void help_checksum_md5(const char *string, char *digest)
+{
+  unsigned char md5[16];
+
+  mutt_md5(NONULL(string), md5);
+  mutt_md5_toascii(md5, digest);
+}
+
+/**
+ * help_docdir_id - Get current DocDirID
+ * @param docdir Path to set the DocDirID from (optional)
+ * @retval ptr Current DocDirID MD5 checksum string
+ */
+static char *help_docdir_id(const char *docdir)
+{
+  if (docdir && DocList) /* only set ID if DocList != NULL */
+    help_checksum_md5(docdir, DocDirID);
+
+  return DocDirID;
+}
+
+/**
+ * help_docdir_changed - Determine if $help_doc_dir differs from previous run
+ * @retval true  `$help_doc_dir` path differs (DocList rebuilt recommended)
+ * @retval false same $help_doc_dir path where DocList was built from
+ */
+static bool help_docdir_changed(void)
+{
+  char digest[33];
+  help_checksum_md5(C_HelpDocDir, digest);
+
+  return (mutt_str_strcmp(DocDirID, digest) != 0);
+}
+
+/**
+ * help_dirent_type - Get the type of a given dirent entry or its file path
+ * @param item    dirent struct that probably holds the wanted entry type
+ * @param path    alternatively used with stat() to determine its type
+ * @param as_flag return type as d_type value or its representing flag
+ * @retval type obtained type or DT_UNKNOWN (0) otherwise
+ *
+ * @note On systems that define macro _DIRENT_HAVE_D_TYPE and supports a d_type
+ *       field, this function may be less costly than an extra call of stat().
+ */
+static DEType help_dirent_type(const struct dirent *item, const char *path, bool as_flag)
+{
+  unsigned char type = 0;
+
+#ifdef _DIRENT_HAVE_D_TYPE
+  type = item->d_type;
+#else
+  struct stat sb;
+
+  if (stat(path, &sb) == 0)
+    type = ((sb.st_mode & 0170000) >> 12);
+#endif
+
+  return (as_flag ? DT2DET(type) : type);
+}
+
+/**
+ * help_file_type - Determine the type of a help file (relative to #C_HelpDocDir)
+ * @param  file as full qualified file path of the document to test
+ * @retval num Type of the file/document, see #HelpDocFlags
+ *
+ * @note The type of a file is determined only from its path string, so it does
+ *       not need to exist. That means also, a file can have a proper type, but
+ *       the document itself may be invalid (and discarded later by a filter).
+ */
+static HelpDocFlags help_file_type(const char *file)
+{
+  HelpDocFlags type = HELP_DOC_UNKNOWN;
+  const size_t l = mutt_str_strlen(file);
+  const size_t m = mutt_str_strlen(C_HelpDocDir);
+
+  if ((l < 5) || (m == 0) || (l <= m))
+    return type; /* relative subpath requirements doesn't match the minimum */
+
+  const char *p = file + m;
+  const char *q = file + l - 3;
+
+  if ((mutt_str_strncasecmp(q, ".md", 3) != 0) ||
+      (mutt_str_strncmp(file, C_HelpDocDir, m) != 0))
+  {
+    return type; /* path below C_HelpDocDir and ".md" extension are mandatory */
+  }
+
+  if (mutt_str_strcasecmp(q = strrchr(p, '/'), "/index.md") == 0)
+    type = HELP_DOC_INDEX; /* help document is a special named ("index.md") file */
+  else
+    type = 0;
+
+  if (p == q)
+    type |= HELP_DOC_ROOTDOC; /* help document lives directly in C_HelpDocDir root */
+  else if ((p = strchr(p + 1, '/')) == q)
+    type |= HELP_DOC_CHAPTER;
+  else /* handle all remaining (deeper nested) help documents as a section */
+    type |= HELP_DOC_SECTION;
+
+  return type;
+}
+
+/**
+ * help_file_header - Process and extract a YAML header of a potential help file
+ * @param fhdr list where to store the final header information
+ * @param file path to the (potential help document) file to parse
+ * @param max  how many header lines to read (N < 0 means all)
+ * @retval (N>=0) success, N valid header lines read from file
+ * @retval -1     file isn't a helpdoc: extension doesn't match ".md"
+ * @retval -2     file header not read: file cannot be open for read
+ * @retval -3     found invalid header: no triple-dashed start mark
+ * @retval -4     found invalid header: no triple-dashed end mark
+ */
+static int help_file_header(struct HelpList **fhdr, const char *file, int max)
+{
+  const char *bfn = mutt_path_basename(NONULL(file));
+  const char *ext = strrchr(bfn, '.');
+  if (!bfn || (ext == bfn) || (mutt_str_strncasecmp(ext, ".md", 3) != 0))
+    return -1;
+
+  FILE *fp = mutt_file_fopen(file, "r");
+  if (!fp)
+    return -2;
+
+  int lineno = 0;
+  size_t linelen;
+  const char *mark = "---";
+
+  char *p = mutt_file_read_line(NULL, &linelen, fp, &lineno, 0);
+  if (mutt_str_strcmp(p, mark) != 0)
+  {
+    mutt_file_fclose(&fp);
+    return -3;
+  }
+
+  struct HelpList *list = NULL;
+  char *q = NULL;
+  bool endmark = false;
+  int count = 0;
+  int limit = (max < 0) ? -1 : max;
+
+  while ((mutt_file_read_line(p, &linelen, fp, &lineno, 0) != NULL) &&
+         !(endmark = (mutt_str_strcmp(p, mark) == 0)) && ((q = strpbrk(p, ": \t")) != NULL))
+  {
+    if (limit == 0)
+      continue; /* to find the end mark that qualify the header as valid */
+    else if ((p == q) || (*q != ':'))
+      continue; /* to skip wrong keyworded lines, XXX: or should we abort? */
+
+    mutt_str_remove_trailing_ws(p);
+    struct HelpFileHeader *item = mutt_mem_calloc(1, sizeof(struct HelpFileHeader));
+    item->key = mutt_str_substr_dup(p, q);
+    item->val = mutt_str_strdup(mutt_str_skip_whitespace(NONULL(++q)));
+    help_list_new_append(&list, sizeof(struct HelpFileHeader), item);
+
+    count++;
+    limit--;
+  }
+  mutt_file_fclose(&fp);
+  FREE(&p);
+
+  if (!endmark)
+  {
+    help_list_free(&list, help_file_hdr_free);
+    count = -4;
+  }
+  else
+  {
+    help_list_shrink(list);
+    *fhdr = list;
+  }
+
+  return count;
+}
+
+/**
+ * help_file_hdr_find - Find a help document header line by its key(word)
+ * @param key  string to search for in fhdr list (case-sensitive)
+ * @param fhdr list of struct HelpFileHeader elements to search for key
+ * @retval ptr  Success, struct containing the found key
+ * @retval NULL Failure, or when key could not be found
+ */
+static struct HelpFileHeader *help_file_hdr_find(const char *key, const struct HelpList *fhdr)
+{
+  if (!fhdr || !key || !*key)
+    return NULL;
+
+  struct HelpFileHeader *hdr = NULL;
+  for (size_t i = 0; i < fhdr->size; i++)
+  {
+    if (mutt_str_strcmp(((struct HelpFileHeader *) fhdr->data[i])->key, key) != 0)
+      continue;
+
+    hdr = fhdr->data[i];
+    break;
+  }
+
+  return hdr;
+}
+
+/**
+ * help_doc_msg_id - Return a simple message ID
+ * @param tm Timestamp used for date part in the message ID
+ * @retval ptr Generated message ID string
+ */
+static char *help_doc_msg_id(const struct tm *tm)
+{
+  char buf[128];
+  unsigned char rndid[MUTT_RANDTAG_LEN + 1];
+  mutt_rand_base32(rndid, sizeof(rndid) - 1);
+  rndid[MUTT_RANDTAG_LEN] = 0;
+
+  snprintf(buf, sizeof(buf), "<%d%02d%02d%02d%02d%02d.%s>", tm->tm_year + 1900,
+           tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, rndid);
+
+  return mutt_str_strdup(buf);
+}
+
+/**
+ * help_doc_subject - Build a message subject, based on a string format and
+ *                    from the keyword value(s) of help file header line(s)
+ * @param fhdr    list of struct HelpFileHeader elements to search for key
+ * @param defsubj this given default subject will be used on failures
+ * @param strfmt  the string format to use for the subject line (only
+ *                string conversion placeholder ("%s") are supported)
+ * @param key     a header line keyword whose value will be used at the
+ *                related strfmt placeholder position
+ * @param ...     additional keywords to find (not limited in count)
+ * @retval ptr Success, holds the built subject or defsubj otherwise
+ *
+ * @note  Whether length, flag nor precision specifier currently supported, only
+ *        poor "%s" placeholder will be recognised.
+ * XXX: - Should the length of the subject line be limited by an additional
+ *        parameter? Currently an internal limit of STRING (256) is used.
+ *      - This function use variable argument list (va_list), that may provide
+ *        more than the real specified arguments and there seems to be no way to
+ *        stop processing after the last real parameter. Therefore, the last
+ *        parameter MUST be NULL currently or weird things happens when count of
+ *        placeholder in strfmt and specified keys differs.
+ */
+static char *help_doc_subject(struct HelpList *fhdr, const char *defsubj,
+                              const char *strfmt, const char *key, ...)
+{
+  va_list ap;
+  char subject[256]; /* XXX: Should trailing white space (from strfmt) be removed? */
+
+  struct HelpFileHeader *hdr = NULL;
+  const char *f = NULL;
+  const char *p = strfmt;
+  const char *q = NULL;
+  size_t l = 0;
+  int size = 0;
+
+  va_start(ap, key);
+  while ((q = strstr(p, "%s")) && key)
+  {
+    hdr = help_file_hdr_find(key, fhdr);
+    if (!hdr)
+    {
+      mutt_str_strfcpy(subject, defsubj, sizeof(subject));
+      break;
+    }
+
+    f = mutt_str_substr_dup(p, strstr(q + 2, "%s"));
+    size = snprintf(subject + l, sizeof(subject) - l, f, hdr->val);
+
+    if (size < 0)
+    {
+      mutt_str_strfcpy(subject, defsubj, sizeof(subject));
+      break;
+    }
+
+    p += mutt_str_strlen(f);
+    l += size;
+    key = va_arg(ap, const char *);
+  }
+  va_end(ap);
+
+  return mutt_str_strdup(subject);
+}
+
+/**
+ * help_path_transpose - Convert (vice versa) a scheme prefixed into a file path
+ * @param path     to transpose (starts with "help[://]" or $help_doc_dir)
+ * @param validate specifies the strictness, if true, file path must exist
+ * @retval ptr  Success, contains the transposed contrary path
+ * @retval NULL Failure, path may not exist or was invalid
+ *
+ * @note The resulting path is sanitised, which means any trailing slash(es) of
+ *       the input path are stripped.
+ */
+static char *help_path_transpose(const char *path, bool validate)
+{
+  if (!path || !*path)
+    return NULL;
+
+  char fqp[PATH_MAX];
+  char url[PATH_MAX];
+
+  const char *docdir = C_HelpDocDir;
+  const char *scheme = "help"; /* TODO: don't hardcode, use sth. like: UrlMap[U_HELP]->name */
+  const char *result = NULL;
+  size_t i = 0;
+  size_t j = 0;
+
+  if (mutt_str_strncasecmp(path, scheme, (j = mutt_str_strlen(scheme))) == 0)
+  { /* unlike url_check_scheme(), allow scheme alone (without a separator) */
+    if ((path[j] != ':') && (path[j] != '\0'))
+      return NULL;
+    if (path[j] == ':')
+      j += 1;
+
+    result = fqp;
+    i = mutt_str_strlen(docdir);
+  }
+  else if (mutt_str_strncmp(path, docdir, (j = mutt_str_strlen(docdir))) == 0)
+  {
+    if ((path[j] != '/') && (path[j] != '\0'))
+      return NULL;
+
+    result = url;
+    i = mutt_str_strlen(scheme) + 3;
+  }
+  else
+  {
+    return NULL;
+  }
+
+  j += strspn(path + j, "/");
+  snprintf(fqp, sizeof(fqp), "%s/%s", docdir, path + j);
+  snprintf(url, sizeof(url), "%s://%s", scheme, path + j);
+  j = mutt_str_strlen(result);
+
+  while ((i < j) && (result[j - 1] == '/'))
+    j--;
+
+  return (validate && !realpath(fqp, NULL)) ? NULL : strndup(result, j);
+}
+
+/**
+ * help_dir_scan - Traverse a directory for specific entry types, filter/gather
+ *                 the found result(s)
+ * @param path      directory in which to start the investigation
+ * @param recursive Whether or not to iterate the given path recursively
+ * @param mask      a bit mask that defines which entry type(s) to filter
+ * @param filter    (optional) preselection filter callback function
+ * @param gather    handler callback function for a single result that prior
+ *                  passed the bit mask and preselection filter
+ * @param items     list that can be used to interchange all results between
+ *                  caller and handler function
+ * @retval  0 Success, execution ended normally
+ * @retval -1 Failure, when path cannot be opened for reading
+ *
+ * @note Function only aborts an iteration when the end of directory stream has
+ *       been reached or the callback filter function returns with N < 0, but
+ *       not on failures, to grab as much as possible entries from the stream.
+ *       An entry named "", "." or "..", will always be skipped (and thus never
+ *       delegated to callback functions), but will be solved/expanded for the
+ *       initial path parameter.
+ */
+static int help_dir_scan(const char *path, bool recursive, const DETMask mask,
+                         int (*filter)(const struct dirent *, const char *, DEType),
+                         int (*gather)(struct HelpList **, const char *),
+                         struct HelpList **items)
+{
+  char curpath[PATH_MAX];
+
+  errno = 0;
+  DIR *dp = opendir(NONULL(realpath(path, curpath)));
+
+  if (!dp)
+  {
+    //mutt_debug(1, "unable to open dir '%s': %s (errno %d).\n", path, strerror(errno), errno);
+    /* XXX: Fake a localised error message by extending an existing one */
+    mutt_error("%s '%s': %s (errno %d).", _("Error opening mailbox"), path,
+               strerror(errno), errno);
+    return -1;
+  }
+
+  while (1)
+  {
+    errno = 0; /* reset errno to distinguish between end-of-(dir)stream and an error */
+    const struct dirent *ep = readdir(dp);
+
+    if (!ep)
+    {
+      if (!errno)
+        break; /* we reached the end-of-stream */
+
+      mutt_debug(1, "unable to read dir: %s (errno %d).\n", strerror(errno), errno);
+      continue; /* this isn't the end-of-stream */
+    }
+
+    const char *np = ep->d_name;
+    if (!np[0] || ((np[0] == '.') && (!np[1] || ((np[1] == '.') && !np[2]))))
+    {
+      continue; /* to skip "", ".", ".." entries */
+    }
+
+    char abspath[mutt_str_strlen(curpath) + mutt_str_strlen(np) + 2];
+    mutt_path_concat(abspath, curpath, np, sizeof(abspath));
+
+    const DEType flag = help_dirent_type(ep, abspath, true);
+    if (mask & flag)
+    { /* delegate preselection processing */
+      int rc = filter ? filter(ep, abspath, flag) : 0;
+      if (rc < 0)
+        break; /* handler wants to abort */
+      else if (0 < rc)
+        continue; /* but skip a recursion */
+      else
+        gather(items, abspath);
+    }
+
+    if ((flag == DET_DIR) && recursive)
+    { /* descend this directory recursive */
+      help_dir_scan(abspath, recursive, mask, filter, gather, items);
+    }
+  }
+  closedir(dp);
+
+  return 0;
+}
+
+/**
+ * help_file_hdr_clone - Callback to clone a file header object (struct HelpFileHeader)
+ * @param item list element pointer to the object to copy
+ * @retval ptr  Success, the duplicated object
+ * @retval NULL Failure, otherwise
+ */
+static void *help_file_hdr_clone(const void *item)
+{
+  if (!item)
+    return NULL;
+
+  struct HelpFileHeader *src = (struct HelpFileHeader *) item;
+  struct HelpFileHeader *dup = mutt_mem_calloc(1, sizeof(struct HelpFileHeader));
+
+  dup->key = mutt_str_strdup(src->key);
+  dup->val = mutt_str_strdup(src->val);
+
+  return dup;
+}
+
+/**
+ * help_doc_meta_clone - Callback to clone a help metadata object (struct HelpDocMeta)
+ * @param item list element pointer to the object to copy
+ * @retval ptr  Success, the duplicated object
+ * @retval NULL Failure, otherwise
+ */
+static void *help_doc_meta_clone(const void *item)
+{
+  if (!item)
+    return NULL;
+
+  struct HelpDocMeta *src = (struct HelpDocMeta *) item;
+  struct HelpDocMeta *dup = mutt_mem_calloc(1, sizeof(struct HelpDocMeta));
+
+  dup->fhdr = help_list_clone(src->fhdr, true, help_file_hdr_clone);
+  dup->name = mutt_str_strdup(src->name);
+  dup->type = src->type;
+
+  return dup;
+}
+
+/**
+ * help_doc_clone - Callback to clone a help document object (Email)
+ * @param item list element pointer to the object to copy
+ * @retval ptr  Success, the duplicated object
+ * @retval NULL Failure, otherwise
+ *
+ * @note This function should only duplicate statically defined attributes from
+ *       an Email object that help_doc_from() build and return.
+ */
+static void *help_doc_clone(const void *item)
+{
+  if (!item)
+    return NULL;
+
+  struct Email *src = (struct Email *) item;
+  struct Email *dup = mutt_email_new();
+  /* struct Email */
+  dup->date_sent = src->date_sent;
+  dup->display_subject = src->display_subject;
+  dup->index = src->index;
+  dup->path = mutt_str_strdup(src->path);
+  dup->read = src->read;
+  dup->received = src->received;
+  /* struct Email::data (custom metadata) */
+  dup->edata = help_doc_meta_clone(src->edata);
+  dup->free_edata = help_doc_meta_free;
+  /* struct Body */
+  dup->content = mutt_body_new();
+  dup->content->disposition = src->content->disposition;
+  dup->content->encoding = src->content->encoding;
+  dup->content->length = src->content->length;
+  dup->content->subtype = mutt_str_strdup(src->content->subtype);
+  dup->content->type = src->content->type;
+  /* struct Envelope */
+  dup->env = mutt_env_new();
+  mutt_addrlist_copy(&dup->env->from, &src->env->from, false);
+  dup->env->message_id = mutt_str_strdup(src->env->message_id);
+  dup->env->organization = mutt_str_strdup(src->env->organization);
+  dup->env->subject = mutt_str_strdup(src->env->subject);
+  /* struct Envelope::references */
+  struct ListNode *src_np = NULL, *dup_np = NULL;
+  STAILQ_FOREACH(src_np, &src->env->references, entries)
+  {
+    dup_np = mutt_mem_calloc(1, sizeof(struct ListNode));
+    dup_np->data = mutt_str_strdup(src_np->data);
+    STAILQ_INSERT_TAIL(&dup->env->references, dup_np, entries);
+  }
+
+  return dup;
+}
+
+/**
+ * help_doc_from - Provides a validated/newly created help document (Email) from
+ *                 a full qualified file path
+ * @param file that is related to be a help document
+ * @retval ptr  Success, an Email
+ * @retval NULL Failure, otherwise
+ *
+ * @note This function only statically set specific member of an Email structure
+ *       and some attributes, like Email::index, should be reset/updated.
+ *       It also use Email::data to store additional help document information.
+ */
+static struct Email *help_doc_from(const char *file)
+{
+  HelpDocFlags type = HELP_DOC_UNKNOWN;
+
+  type = help_file_type(file);
+  if (type == HELP_DOC_UNKNOWN)
+    return NULL; /* file is not a valid help doc */
+
+  struct HelpList *fhdr = NULL;
+  int len = help_file_header(&fhdr, file, HELP_FHDR_MAXLINES);
+  if (!fhdr || (len < 1))
+    return NULL; /* invalid or empty file header */
+
+  /* from here, it should be safe to treat file as a valid help document */
+  const char *bfn = mutt_path_basename(file);
+  const char *pdn = mutt_path_basename(mutt_path_dirname(file));
+  const char *rfp = (file + mutt_str_strlen(C_HelpDocDir) + 1);
+  /* default timestamp, based on PACKAGE_VERSION */
+  struct tm *tm = mutt_mem_calloc(1, sizeof(struct tm));
+  strptime(PACKAGE_VERSION, "%Y%m%d", tm);
+  time_t epoch = mutt_date_make_time(tm, 0);
+  /* default subject, final may come from file header, e.g. "[title]: description" */
+  char sbj[256];
+  snprintf(sbj, sizeof(sbj), "[%s]: %s", pdn, bfn);
+  /* bundle metadata */
+  struct HelpDocMeta *meta = mutt_mem_calloc(1, sizeof(struct HelpDocMeta));
+  meta->fhdr = fhdr;
+  meta->name = mutt_str_strdup(bfn);
+  meta->type = type;
+
+  struct Email *hdoc = mutt_email_new();
+  /* struct Email */
+  hdoc->date_sent = epoch;
+  hdoc->display_subject = true;
+  hdoc->index = 0;
+  hdoc->path = mutt_str_strdup(rfp);
+  hdoc->read = true;
+  hdoc->received = epoch;
+  /* struct Email::data (custom metadata) */
+  hdoc->edata = meta;
+  hdoc->free_edata = help_doc_meta_free;
+  /* struct Body */
+  hdoc->content = mutt_body_new();
+  hdoc->content->disposition = DISP_INLINE;
+  hdoc->content->encoding = ENC_8BIT;
+  hdoc->content->length = -1;
+  hdoc->content->subtype = mutt_str_strdup("plain");
+  hdoc->content->type = TYPE_TEXT;
+  /* struct Envelope */
+  hdoc->env = mutt_env_new();
+  mutt_addrlist_parse(&hdoc->env->from, "Richard Russon <rich@flatcap.org>");
+  hdoc->env->message_id = help_doc_msg_id(tm);
+  FREE(tm);
+  hdoc->env->organization = mutt_str_strdup("NeoMutt");
+  hdoc->env->subject =
+      help_doc_subject(fhdr, sbj, "[%s]: %s", "title", "description", NULL);
+
+  return hdoc;
+}
+
+/**
+ * help_doc_gather - Handler callback function for help_dir_scan()
+ *                   Builds a list of help document objects
+ * @param list generic list, for successfully processed item paths
+ * @param path absolute path of a dir entry that pass preselection
+ * @retval      0  Success,
+ * @retval  (N!=0) Failure, (not used currently, failures are externally
+ *                 checked and silently suppressed herein)
+ */
+static int help_doc_gather(struct HelpList **list, const char *path)
+{
+  help_list_new_append(list, sizeof(struct Email *), help_doc_from(path));
+
+  return 0;
+}
+
+/**
+ * help_doc_uplink - Set a reference (threading) of one help document to an other
+ * @param target Document to refer to (via Email::message_id)
+ * @param source Document to link
+ */
+static void help_doc_uplink(const struct Email *target, const struct Email *source)
+{
+  if (!target || !source)
+    return;
+
+  char *tgt_msgid = target->env->message_id;
+  if (!tgt_msgid || !*tgt_msgid)
+    return;
+
+  mutt_list_insert_tail(&source->env->references, mutt_str_strdup(tgt_msgid));
+}
+
+/**
+ * help_read_dir - Read a directory and process its entries (not recursively) to
+ *                 find and link all help documents
+ * @param path absolute path of a directory
+ *
+ * @note All sections are linked to their parent chapter regardless how deeply
+ *       they're nested on the filesystem. Empty directories are ignored.
+ */
+static void help_read_dir(const char *path)
+{
+  struct HelpList *list = NULL;
+
+  if ((help_dir_scan(path, false, DET_REG, NULL, help_doc_gather, &list) != 0) ||
+      (list == NULL))
+    return; /* skip errors and empty folder */
+
+  /* sort any 'index.md' in list to the top */
+  help_list_sort(list, help_doc_type_cmp);
+
+  struct Email *help_msg_top = help_list_get(list, 0, NULL), *help_msg_cur = NULL;
+  HelpDocFlags help_msg_top_type = ((struct HelpDocMeta *) help_msg_top->edata)->type;
+
+  /* uplink a help chapter/section top node */
+  if (help_msg_top_type & HELP_DOC_CHAPTER)
+  {
+    if (HELP_LINK_CHAPTERS != 0)
+      help_doc_uplink(help_list_get(DocList, 0, NULL), help_msg_top);
+
+    UpLink = DocList->size;
+  }
+  else if (help_msg_top_type & HELP_DOC_SECTION)
+    help_doc_uplink(help_list_get(DocList, UpLink, NULL), help_msg_top);
+  else
+    UpLink = 0;
+
+  help_msg_top->index = DocList->size;
+  help_list_append(DocList, help_msg_top);
+
+  /* link remaining docs to first list item */
+  for (size_t i = 1; i < list->size; i++)
+  {
+    help_msg_cur = help_list_get(list, i, NULL);
+    help_doc_uplink(help_msg_top, help_msg_cur);
+
+    help_msg_cur->index = DocList->size;
+    help_list_append(DocList, help_msg_cur);
+  }
+}
+
+/**
+ * help_dir_gather - Handler callback function for help_dir_scan()
+ *                   Simple invoke help_read_dir() to search for help documents
+ * @param list generic list, for successfully processed item paths (not used)
+ * @param path absolute path of a dir entry that pass preselection
+ * @retval     0   Success,
+ * @retval (N!=0)  Failure, (not used currently, failures are externally
+ *                 checked and silently suppressed herein)
+ *
+ * @note The list parameter isn't used herein, because every single result from
+ *       help_scan_dir() will be processed directly.
+ */
+static int help_dir_gather(struct HelpList **list, const char *path)
+{
+  help_read_dir(path);
+
+  return 0;
+}
+
+/**
+ * help_doclist_init - Initialise the DocList at $help_doc_dir
+ * @retval  0 Success,
+ * @retval -1 Failure, when help_dir_scan() of $help_doc_dir fails
+ *
+ * @note Initialisation depends on several things, like $help_doc_dir changed,
+ *       DocList isn't (and should not) be cached, DocList is empty.
+ */
+int help_doclist_init(void)
+{
+  if ((HELP_CACHE_DOCLIST != 0) && DocList && !help_docdir_changed())
+    return 0;
+
+  help_doclist_free();
+  DocList = help_list_new(sizeof(struct Email));
+  help_read_dir(C_HelpDocDir);
+  help_docdir_id(C_HelpDocDir);
+
+  return help_dir_scan(C_HelpDocDir, true, DET_DIR, NULL, help_dir_gather, NULL);
+}
+
+/**
+ * help_doclist_parse - Evaluate and copy the DocList items to Context struct
+ * @param m Mailbox
+ * @retval  0 Success,
+ * @retval -1 Failure, e.g. DocList initialisation failed
+ *
+ * @note XXX This function also sets the status of a help document to unread,
+ *       when its path match the user input, so the index line will mark it.
+ *       This is just a test, has room for improvements and is less-than-ideal,
+ *       because the user needs some knowledge about helpbox folder structure.
+ */
+static int help_doclist_parse(struct Mailbox *m)
+{
+  if ((help_doclist_init() != 0) || (DocList->size == 0))
+    return -1;
+
+  m->emails = (struct Email **) (help_list_clone(DocList, true, help_doc_clone))->data;
+  m->msg_count = m->email_max = DocList->size;
+  mutt_mem_realloc(&m->v2r, sizeof(int) * m->email_max);
+
+  mutt_make_label_hash(m);
+
+  m->readonly = true;
+  /* all document paths are relative to C_HelpDocDir, so no transpose of ctx->path */
+  mutt_str_replace(&m->realpath, C_HelpDocDir);
+
+  /* check (none strict) what the user wants to see */
+  const char *request = help_path_transpose(mutt_b2s(m->pathbuf), false);
+  m->emails[0]->read = false;
+  if (request)
+  {
+    mutt_buffer_increase_size(m->pathbuf, PATH_MAX);
+    mutt_str_strfcpy(m->pathbuf->data, help_path_transpose(request, false),
+                     m->pathbuf->dsize); /* just sanitise */
+    request += mutt_str_strlen(C_HelpDocDir) + 1;
+    for (size_t i = 0; i < m->msg_count; i++)
+    { /* TODO: prioritise folder (chapter/section) over root file names */
+      if (mutt_str_strncmp(m->emails[i]->path, request, mutt_str_strlen(request)) == 0)
+      {
+        m->emails[0]->read = true;
+        m->emails[i]->read = false;
+        break;
+      }
+    }
+  }
+
+  return 0;
 }
 
 /**
  * help_ac_find - Find an Account that matches a Mailbox path -- Implements MxOps::ac_find
  */
-static struct Account *help_ac_find(struct Account *a, const char *path)
+struct Account *help_ac_find(struct Account *a, const char *path)
 {
   if (!a || !path)
     return NULL;
@@ -55,7 +1046,7 @@ static struct Account *help_ac_find(struct Account *a, const char *path)
 /**
  * help_ac_add - Add a Mailbox to an Account -- Implements MxOps::ac_add
  */
-static int help_ac_add(struct Account *a, struct Mailbox *m)
+int help_ac_add(struct Account *a, struct Mailbox *m)
 {
   if (!a || !m)
     return -1;
@@ -71,6 +1062,39 @@ static int help_ac_add(struct Account *a, struct Mailbox *m)
   return 0;
 }
 
+/**
+ * help_mbox_open - Open a Mailbox -- Implements MxOps::mbox_open
+ */
+static int help_mbox_open(struct Mailbox *m)
+{
+  mutt_debug(1, "entering help_mbox_open\n");
+
+  if (m->magic != MUTT_HELP)
+    return -1;
+
+  /* TODO: ensure either mutt_option_set()/mutt_expand_path() sanitise a DT_PATH
+   * option or let help_docdir_changed() treat "/path" and "/path///" as equally
+   * to avoid a useless re-caching of the same directory */
+  if (help_docdir_changed())
+  {
+    if (access(C_HelpDocDir, F_OK) == 0)
+    { /* ensure a proper path, especially without any trailing slashes */
+      mutt_str_replace(&C_HelpDocDir, NONULL(realpath(C_HelpDocDir, NULL)));
+    }
+    else
+    {
+      mutt_debug(1, "unable to access help mailbox '%s': %s (errno %d).\n",
+                 C_HelpDocDir, strerror(errno), errno);
+      return -1;
+    }
+  }
+
+  __Backup_HTS = C_HideThreadSubject; /* backup the current global setting */
+  C_HideThreadSubject = false; /* temporarily ensure subject is shown in thread view */
+
+  return help_doclist_parse(m);
+}
+
 /**
  * help_mbox_open_append - Open a Mailbox for appending -- Implements MxOps::mbox_open_append
  */
@@ -86,7 +1110,7 @@ static int help_mbox_open_append(struct Mailbox *m, OpenMailboxFlags flags)
 static int help_mbox_check(struct Mailbox *m, int *index_hint)
 {
   mutt_debug(1, "entering help_mbox_check\n");
-  return -1;
+  return 0;
 }
 
 /**
@@ -95,7 +1119,7 @@ static int help_mbox_check(struct Mailbox *m, int *index_hint)
 static int help_mbox_sync(struct Mailbox *m, int *index_hint)
 {
   mutt_debug(1, "entering help_mbox_sync\n");
-  return -1;
+  return 0;
 }
 
 /**
@@ -104,7 +1128,10 @@ static int help_mbox_sync(struct Mailbox *m, int *index_hint)
 static int help_mbox_close(struct Mailbox *m)
 {
   mutt_debug(1, "entering help_mbox_close\n");
-  return -1;
+
+  C_HideThreadSubject = __Backup_HTS; /* restore the previous global setting */
+
+  return 0;
 }
 
 /**
@@ -112,8 +1139,22 @@ static int help_mbox_close(struct Mailbox *m)
  */
 static int help_msg_open(struct Mailbox *m, struct Message *msg, int msgno)
 {
-  mutt_debug(1, "entering help_msg_open\n");
-  return -1;
+  mutt_debug(1, "entering help_msg_open: %d, %s\n", msgno, m->emails[msgno]->env->subject);
+
+  char path[PATH_MAX];
+  snprintf(path, sizeof(path), "%s/%s", m->realpath, m->emails[msgno]->path);
+
+  m->emails[msgno]->read = true; /* reset a probably previously set unread status */
+
+  msg->fp = fopen(path, "r");
+  if (!msg->fp)
+  {
+    mutt_perror(path);
+    mutt_debug(1, "fopen: %s: %s (errno %d).\n", path, strerror(errno), errno);
+    return -1;
+  }
+
+  return 0;
 }
 
 /**
@@ -140,7 +1181,8 @@ static int help_msg_commit(struct Mailbox *m, struct Message *msg)
 static int help_msg_close(struct Mailbox *m, struct Message *msg)
 {
   mutt_debug(1, "entering help_msg_close\n");
-  return -1;
+  mutt_file_fclose(&msg->fp);
+  return 0;
 }
 
 /**
index 48849a5c22f5c00d938206c46eea22fb6e0b6937..f594f43edda854a11cfe05f0d349b0e8df33b06c 100644 (file)
@@ -4,6 +4,7 @@
  *
  * @authors
  * Copyright (C) 2018-2019 Richard Russon <rich@flatcap.org>
+ * Copyright (C) 2018 Floyd Anderson <f.a@31c0.net>
  *
  * @copyright
  * This program is free software: you can redistribute it and/or modify it under
 
 extern struct MxOps MxHelpOps;
 
+/**
+ * enum dirent_type - Constants for d_type field values of the dirent structure
+ *                    and used for bitwise filter mask matching, even the macro
+ *                    _DIRENT_HAVE_D_TYPE for the d_type field is not defined
+ */
+typedef enum dirent_type
+{
+  DET_UNKNOWN = (1 << 0), /* flag for DT_UNKNOWN field value (0) */
+  DET_FIFO    = (1 << 1), /* flag for DT_FIFO field value (1) */
+  DET_CHR     = (1 << 2), /* flag for DT_CHR field value (2) */
+  DET_DIR     = (1 << 3), /* flag for DT_DIR field value (4) */
+  DET_BLK     = (1 << 4), /* flag for DT_BLK field value (6) */
+  DET_REG     = (1 << 5), /* flag for DT_REG field value (8) */
+  DET_LNK     = (1 << 6), /* flag for DT_LNK field value (10) */
+  DET_SOCK    = (1 << 7), /* flag for DT_SOCK field value (12) */
+  DET_WHT     = (1 << 8)  /* flag for DT_WHT (dummy, whiteout inode) field value (14) */
+} DEType;
+#define DT2DET(type) (((type) ? 2 : 1) << ((type) >> 1))
+typedef unsigned int DETMask;
+
+typedef uint8_t HelpDocFlags;     ///< Types of Help Documents, e.g. #HELP_DOC_INDEX
+#define HELP_DOC_NO_FLAGS      0  ///< No flags are set
+#define HELP_DOC_UNKNOWN (1 << 0) ///< File isn't a help document
+#define HELP_DOC_INDEX   (1 << 1) ///< Document is treated as help index (index.md)
+#define HELP_DOC_ROOTDOC (1 << 2) ///< Document lives directly in root of #C_HelpDocDir
+#define HELP_DOC_CHAPTER (1 << 3) ///< Document is treated as help chapter
+#define HELP_DOC_SECTION (1 << 4) ///< Document is treated as help section
+
+/**
+ * struct HelpList - Generic list to hold several help elements
+ */
+struct HelpList
+{
+  size_t item_size; ///< Size of a single element
+  size_t size;      ///< List length
+  size_t capa;      ///< List capacity
+  void **data;      ///< Internal list data pointers
+};
+#define HELPLIST_INIT_CAPACITY 10
+
+/**
+ * struct helpfile_header - Describes the header of a help file
+ */
+struct HelpFileHeader
+{
+  char *key;
+  char *val;
+};
+
+/**
+ * struct helpdoc_meta - Bundle additional information to a help document
+ */
+struct HelpDocMeta
+{
+  struct HelpList *fhdr; ///< File header lines (list of key/value pairs)
+  char *name;            ///< Base file name
+  HelpDocFlags type;     ///< Type of the help document
+};
+
+void help_doclist_free(void);
+int help_doclist_init(void);
+
 #endif /* MUTT_HELP_HELP_H */