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'm a new Notmuch user and have been trying out different MUAs. And reading email directly with Notmuch feels easier to me than adding to my cognitive load with something like Mutt. At least when I want to read something specific that Notmuch queries are well suited for. This is my first (but possibly not last) patch aimed at enhancing the usability of the Notmuch CLI. Any and all feedback is welcome! NEWS | 7 ++++ completion/notmuch-completion.bash | 2 +- completion/zsh/_notmuch | 2 +- doc/man1/notmuch-show.rst | 8 +++- notmuch-show.c | 65 ++++++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index 538ec168..d099bd69 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 bdb87321..fe3b753e 100644 --- a/notmuch-show.c +++ b/notmuch-show.c @@ -601,6 +601,63 @@ 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)); + recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_TO); + if (recipients_string) { + if (color) + g_mime_stream_printf (stream, "\e[31m"); + 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) { + if (color) + g_mime_stream_printf (stream, "\e[31m"); + 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\n", date_string); + 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); + + 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) { @@ -1187,6 +1244,7 @@ enum { NOTMUCH_FORMAT_JSON, NOTMUCH_FORMAT_SEXP, NOTMUCH_FORMAT_TEXT, + NOTMUCH_FORMAT_PRETTY, NOTMUCH_FORMAT_MBOX, NOTMUCH_FORMAT_RAW }; @@ -1206,6 +1264,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, @@ -1220,6 +1283,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, }; @@ -1249,6 +1313,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 }, -- 2.32.0 _______________________________________________ notmuch mailing list -- notmuch@notmuchmail.org To unsubscribe send an email to notmuch-leave@notmuchmail.org