[PATCH v2] cli/show: add --format=pretty

Subject: [PATCH v2] cli/show: add --format=pretty

Date: Fri, 2 Jul 2021 23:31:53 +0300

To: notmuch@notmuchmail.org

Cc: Hannu Hartikainen

From: Hannu Hartikainen


The rationale for this feature is twofold:

1. It is useful to be able to view messages in as human-friendly format
   as possible.
2. The same format should still be machine-readable, too.

The email format is mostly human-readable as is. The things difficult
for a human eye are the huge amount of headers that are common these
days and telling different messages apart when there are many.

This commit adds the display format `pretty`. It recognizes if the
output is a TTY and if so, applies coloring to headers. This turns out
to be a good visual message separator. Additionally it only shows the
most important headers, similarly to `--format=text`, and only displays
plaintext parts (ie. text/* but not text/html).

While human readability is the main goal, another design goal was that
piping the output to `git am` works, at least for individual messages
sent with `git send-email`.
---

I wrote a v2 of this patch. I've been dogfooding for a while now and
wanted a couple of enhancements, and also had learned about the notmuch
test harness. The differences to the first version are:

- add a unit test
- show Message-ID, making replying etc. much easier
- print a newline after each part, which helps a lot with messages that
  do not end in a newline

I'm using this as a daily driver and am happy with it.

 NEWS                               |  7 ++++
 completion/notmuch-completion.bash |  2 +-
 completion/zsh/_notmuch            |  2 +-
 doc/man1/notmuch-show.rst          |  8 +++-
 notmuch-show.c                     | 67 ++++++++++++++++++++++++++++++
 test/T520-show.sh                  | 32 ++++++++++++++
 6 files changed, 115 insertions(+), 3 deletions(-)

diff --git a/NEWS b/NEWS
index 3e776009..f5142ff1 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,13 @@
 Notmuch 0.33 (UNRELEASED)
 =========================
 
+CLI
+---
+
+`notmuch show` now has `--format=pretty`, optimized for reading plain
+text emails on the command line. It only shows the most important
+headers and plain text parts and uses colors for headers.
+
 Emacs
 -----
 
diff --git a/completion/notmuch-completion.bash b/completion/notmuch-completion.bash
index 15425697..86cbbcdc 100644
--- a/completion/notmuch-completion.bash
+++ b/completion/notmuch-completion.bash
@@ -514,7 +514,7 @@ _notmuch_show()
 	    return
 	    ;;
 	--format)
-	    COMPREPLY=( $( compgen -W "text json sexp mbox raw" -- "${cur}" ) )
+	    COMPREPLY=( $( compgen -W "text pretty json sexp mbox raw" -- "${cur}" ) )
 	    return
 	    ;;
 	--exclude|--body)
diff --git a/completion/zsh/_notmuch b/completion/zsh/_notmuch
index e920f10b..5cc386e2 100644
--- a/completion/zsh/_notmuch
+++ b/completion/zsh/_notmuch
@@ -237,7 +237,7 @@ _notmuch_search() {
 _notmuch_show() {
   _arguments -S \
     '--entire-thread=[output entire threads]:show thread:(true false)' \
-    '--format=[set output format]:output format:(text json sexp mbox raw)' \
+    '--format=[set output format]:output format:(text pretty json sexp mbox raw)' \
     '--format-version=[set output format version]:format version: ' \
     '--part=[output a single decoded mime part]:part number: ' \
     '--verify[verify signed MIME parts]' \
diff --git a/doc/man1/notmuch-show.rst b/doc/man1/notmuch-show.rst
index fc6bec62..1fe4dcc7 100644
--- a/doc/man1/notmuch-show.rst
+++ b/doc/man1/notmuch-show.rst
@@ -34,7 +34,7 @@ Supported options for **show** include
    the matching messages. For ``--format=json`` and ``--format=sexp``
    this defaults to true. For other formats, this defaults to false.
 
-.. option:: --format=(text|json|sexp|mbox|raw)
+.. option:: --format=(text|pretty|json|sexp|mbox|raw)
 
    **text** (default for messages)
      The default plain-text format has all text-content MIME parts
@@ -46,6 +46,12 @@ Supported options for **show** include
      '}'), to either open or close the component. For a multipart
      MIME message, these parts will be nested.
 
+   **pretty**
+     The plain-text parts of all matching messages are printed in a
+     format optimized for readability. Only the most important
+     headers are displayed. If the output is to a TTY, the headers
+     are colored.
+
    **json**
      The output is formatted with Javascript Object Notation
      (JSON). This format is more robust than the text format for
diff --git a/notmuch-show.c b/notmuch-show.c
index 232557d5..c417ec00 100644
--- a/notmuch-show.c
+++ b/notmuch-show.c
@@ -606,6 +606,65 @@ format_part_text (const void *ctx, sprinter_t *sp, mime_node_t *node,
     return NOTMUCH_STATUS_SUCCESS;
 }
 
+static notmuch_status_t
+format_part_pretty (const void *ctx, sprinter_t *sp, mime_node_t *node,
+		    int indent, const notmuch_show_params_t *params)
+{
+    /* The disposition and content-type metadata are associated with
+     * the envelope for message parts */
+    GMimeObject *meta = node->envelope_part ? (
+	GMIME_OBJECT (node->envelope_part) ) : node->part;
+    GMimeContentType *content_type = g_mime_object_get_content_type (meta);
+    GMimeStream *stream = params->out_stream;
+    int i;
+    bool color = isatty (fileno (stdout));
+
+    if (GMIME_IS_MESSAGE (node->part)) {
+	GMimeMessage *message = GMIME_MESSAGE (node->part);
+	char *recipients_string;
+	char *date_string;
+
+	if (color)
+	    g_mime_stream_printf (stream, "\e[36m");
+	g_mime_stream_printf (stream, "Subject: %s\n", g_mime_message_get_subject (message));
+	if (color)
+	    g_mime_stream_printf (stream, "\e[33m");
+	g_mime_stream_printf (stream, "From: %s\n", g_mime_message_get_from_string (message));
+	if (color)
+	    g_mime_stream_printf (stream, "\e[31m");
+	recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_TO);
+	if (recipients_string)
+	    g_mime_stream_printf (stream, "To: %s\n", recipients_string);
+	g_free (recipients_string);
+	recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_CC);
+	if (recipients_string)
+	    g_mime_stream_printf (stream, "Cc: %s\n", recipients_string);
+	g_free (recipients_string);
+	date_string = g_mime_message_get_date_string (node, message);
+	if (color)
+	    g_mime_stream_printf (stream, "\e[35m");
+	g_mime_stream_printf (stream, "Date: %s\n", date_string);
+	if (color)
+	    g_mime_stream_printf (stream, "\e[32m");
+	g_mime_stream_printf (stream, "Message-ID: <%s>\n\n", g_mime_message_get_message_id (
+				  message));
+	if (color)
+	    g_mime_stream_printf (stream, "\e[0m");
+    }
+
+    if (GMIME_IS_PART (node->part) &&
+	g_mime_content_type_is_type (content_type, "text", "*") &&
+	! g_mime_content_type_is_type (content_type, "text", "html")) {
+	show_text_part_content (node->part, stream, 0);
+	g_mime_stream_printf (stream, "\n");
+    }
+
+    for (i = 0; i < node->nchildren; i++)
+	format_part_pretty (ctx, sp, mime_node_child (node, i), indent, params);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
 static void
 format_omitted_part_meta_sprinter (sprinter_t *sp, GMimeObject *meta, GMimePart *part)
 {
@@ -1192,6 +1251,7 @@ enum {
     NOTMUCH_FORMAT_JSON,
     NOTMUCH_FORMAT_SEXP,
     NOTMUCH_FORMAT_TEXT,
+    NOTMUCH_FORMAT_PRETTY,
     NOTMUCH_FORMAT_MBOX,
     NOTMUCH_FORMAT_RAW
 };
@@ -1211,6 +1271,11 @@ static const notmuch_show_format_t format_text = {
     .part = format_part_text,
 };
 
+static const notmuch_show_format_t format_pretty = {
+    .new_sprinter = sprinter_text_create,
+    .part = format_part_pretty,
+};
+
 static const notmuch_show_format_t format_mbox = {
     .new_sprinter = sprinter_text_create,
     .part = format_part_mbox,
@@ -1225,6 +1290,7 @@ static const notmuch_show_format_t *formatters[] = {
     [NOTMUCH_FORMAT_JSON] = &format_json,
     [NOTMUCH_FORMAT_SEXP] = &format_sexp,
     [NOTMUCH_FORMAT_TEXT] = &format_text,
+    [NOTMUCH_FORMAT_PRETTY] = &format_pretty,
     [NOTMUCH_FORMAT_MBOX] = &format_mbox,
     [NOTMUCH_FORMAT_RAW] = &format_raw,
 };
@@ -1254,6 +1320,7 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[])
 	{ .opt_keyword = &format, .name = "format", .keywords =
 	      (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
 				      { "text", NOTMUCH_FORMAT_TEXT },
+				      { "pretty", NOTMUCH_FORMAT_PRETTY },
 				      { "sexp", NOTMUCH_FORMAT_SEXP },
 				      { "mbox", NOTMUCH_FORMAT_MBOX },
 				      { "raw", NOTMUCH_FORMAT_RAW },
diff --git a/test/T520-show.sh b/test/T520-show.sh
index 16222650..e555b284 100755
--- a/test/T520-show.sh
+++ b/test/T520-show.sh
@@ -10,4 +10,36 @@ notmuch show foo..
 exit_code=$?
 test_expect_equal 1 $exit_code
 
+test_begin_subtest "--format=pretty"
+output=$(notmuch show --format=pretty id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com 2>&1 && echo OK)
+test_expect_equal "$output" "Subject: [notmuch] preliminary FreeBSD support
+From: Alex Botero-Lowry <alex.boterolowry@gmail.com>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 11:36:14 -0800
+Message-ID: <cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com>
+
+I saw the announcement this morning, and was very excited, as I had been
+hoping sup would be turned into a library,
+since I like the concept more than the UI (I'd rather an emacs interface).
+
+I did a preliminary compile which worked out fine, but
+sysconf(_SC_SC_GETPW_R_SIZE_MAX) returns -1 on
+FreeBSD, so notmuch_config_open segfaulted.
+
+Attached is a patch that supplies a default buffer size of 64 in cases where
+-1 is returned.
+
+http://www.opengroup.org/austin/docs/austin_328.txt - seems to indicate this
+is acceptable behavior,
+and http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg016808.htmlspecifically
+uses 64 as the
+buffer size.
+
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+
+OK"
+
 test_done
-- 
2.32.0
_______________________________________________
notmuch mailing list -- notmuch@notmuchmail.org
To unsubscribe send an email to notmuch-leave@notmuchmail.org

Thread: