From: Jameson Graef Rollins Date: Sat, 5 Dec 2009 06:19:53 +0000 (-0500) Subject: merge changes from upstream X-Git-Tag: debian-0.1-1~19^2~19 X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=commitdiff_plain;h=e72a6176e3fc3fcf4b1696e3f0ee9cf66509fb4d;hp=4edf37a1d5067197741b8dcb0bdb72ce4c299c05 merge changes from upstream --- diff --git a/.gitignore b/.gitignore index 8794354e..efa98fbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +Makefile.config TAGS tags *cscope* diff --git a/Makefile b/Makefile index 2cd1b1ba..021fdb82 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ -# Default FLAGS, (can be overridden by user such as "make CFLAGS=-O2") -WARN_FLAGS=-Wall -Wextra -Wmissing-declarations -Wwrite-strings -Wswitch-enum -CFLAGS=-O2 +WARN_CXXFLAGS=-Wall -Wextra -Wwrite-strings -Wswitch-enum +WARN_CFLAGS=$(WARN_CXXFLAGS) -Wmissing-declarations # Additional programs that are used during the compilation process. EMACS ?= emacs @@ -8,66 +7,69 @@ EMACS ?= emacs # arguments to gzip. gzip = gzip -# Additional flags that we will append to whatever the user set. -# These aren't intended for the user to manipulate. -extra_cflags := $(shell pkg-config --cflags glib-2.0 gmime-2.4 talloc) -extra_cxxflags := $(shell xapian-config --cxxflags) - -emacs_lispdir := $(shell pkg-config emacs --variable sitepkglispdir) -# Hard-code if this system doesn't have an emacs.pc file -ifeq ($(emacs_lispdir),) - emacs_lispdir = $(prefix)/share/emacs/site-lisp -endif +bash_completion_dir = /etc/bash_completion.d all_deps = Makefile Makefile.local Makefile.config \ lib/Makefile lib/Makefile.local +extra_cflags := +extra_cxxflags := + # Now smash together user's values with our extra values -override CFLAGS += $(WARN_FLAGS) $(extra_cflags) -override CXXFLAGS += $(WARN_FLAGS) $(extra_cflags) $(extra_cxxflags) +FINAL_CFLAGS = $(CFLAGS) $(WARN_CFLAGS) $(CONFIGURE_CFLAGS) $(extra_cflags) +FINAL_CXXFLAGS = $(CXXFLAGS) $(WARN_CXXFLAGS) $(CONFIGURE_CXXFLAGS) $(extra_cflags) $(extra_cxxflags) +FINAL_LDFLAGS = $(LDFLAGS) $(CONFIGURE_LDFLAGS) + +all: notmuch notmuch.1.gz + +# Before including any other Makefile fragments, get settings from the +# output of configure +Makefile.config: configure + @echo "" + @echo "Note: Calling ./configure with no command-line arguments. This is often fine," + @echo " but if you want to specify any arguments (such as an alternate prefix" + @echo " into which to install), call ./configure explicitly and then make again." + @echo " See \"./configure --help\" for more details." + @echo "" + ./configure -override LDFLAGS += \ - $(shell pkg-config --libs glib-2.0 gmime-2.4 talloc) \ - $(shell xapian-config --libs) +include Makefile.config -# Include our local Makefile.local first so that its first target is default -include Makefile.local include lib/Makefile.local - -# And get user settings from the output of configure -include Makefile.config +include compat/Makefile.local +include Makefile.local # The user has not set any verbosity, default to quiet mode and inform the # user how to enable verbose compiles. ifeq ($(V),) quiet_DOC := "Use \"$(MAKE) V=1\" to see the verbose compile lines.\n" -quiet = @echo $(quiet_DOC)$(eval quiet_DOC:=)" $1 $@"; $($1) +quiet = @printf $(quiet_DOC)$(eval quiet_DOC:=)" $1 $2 $@\n"; $($1) endif # The user has explicitly enabled quiet compilation. ifeq ($(V),0) -quiet = @echo " $1 $@"; $($1) +quiet = @printf " $1 $@\n"; $($1) endif # Otherwise, print the full command line. quiet ?= $($1) %.o: %.cc $(all_deps) - $(call quiet,CXX) -c $(CXXFLAGS) $< -o $@ + $(call quiet,CXX,$(CXXFLAGS)) -c $(FINAL_CXXFLAGS) $< -o $@ %.o: %.c $(all_deps) - $(call quiet,CC) -c $(CFLAGS) $< -o $@ + $(call quiet,CC,$(CFLAGS)) -c $(FINAL_CFLAGS) $< -o $@ %.elc: %.el $(call quiet,EMACS) -batch -f batch-byte-compile $< .deps/%.d: %.c $(all_deps) @set -e; rm -f $@; mkdir -p $$(dirname $@) ; \ - $(CC) -M $(CPPFLAGS) $(CFLAGS) $< > $@.$$$$; \ + $(CC) -M $(CPPFLAGS) $(FINAL_CFLAGS) $< > $@.$$$$ 2>/dev/null ; \ sed 's,'$$(basename $*)'\.o[ :]*,$*.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$ .deps/%.d: %.cc $(all_deps) @set -e; rm -f $@; mkdir -p $$(dirname $@) ; \ - $(CXX) -M $(CPPFLAGS) $(CXXFLAGS) $< > $@.$$$$; \ + $(CXX) -M $(CPPFLAGS) $(FINAL_CXXFLAGS) $< > $@.$$$$ 2>/dev/null ; \ sed 's,'$$(basename $*)'\.o[ :]*,$*.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$ diff --git a/Makefile.local b/Makefile.local index b8186277..933ff4c7 100644 --- a/Makefile.local +++ b/Makefile.local @@ -1,8 +1,7 @@ -all: notmuch notmuch.1.gz - emacs: notmuch.elc notmuch_client_srcs = \ + $(notmuch_compat_srcs) \ debugger.c \ gmime-filter-reply.c \ notmuch.c \ @@ -23,21 +22,18 @@ notmuch_client_srcs = \ notmuch_client_modules = $(notmuch_client_srcs:.c=.o) notmuch: $(notmuch_client_modules) lib/notmuch.a - $(call quiet,CXX) $^ $(LDFLAGS) -o $@ + $(call quiet,CXX,$(LDFLAGS)) $^ $(FINAL_LDFLAGS) -o $@ notmuch.1.gz: notmuch.1 $(call quiet,gzip) --stdout $^ > $@ install: all notmuch.1.gz - for d in $(DESTDIR)$(prefix)/bin/ $(DESTDIR)$(prefix)/share/man/man1 \ - $(DESTDIR)$(bash_completion_dir) ; \ + for d in $(DESTDIR)$(prefix)/bin/ $(DESTDIR)$(prefix)/share/man/man1 ; \ do \ install -d $$d ; \ done ; install notmuch $(DESTDIR)$(prefix)/bin/ install -m0644 notmuch.1.gz $(DESTDIR)$(prefix)/share/man/man1/ - install -m0644 contrib/notmuch-completion.bash \ - $(DESTDIR)$(bash_completion_dir)/notmuch install-emacs: install emacs for d in $(DESTDIR)/$(emacs_lispdir) ; \ @@ -47,5 +43,10 @@ install-emacs: install emacs install -m0644 notmuch.el $(DESTDIR)$(emacs_lispdir) install -m0644 notmuch.elc $(DESTDIR)$(emacs_lispdir) +install-bash: + install -d $(DESTDIR)$(bash_completion_dir) + install -m0644 contrib/notmuch-completion.bash \ + $(DESTDIR)$(bash_completion_dir)/notmuch + SRCS := $(SRCS) $(notmuch_client_srcs) CLEAN := $(CLEAN) notmuch $(notmuch_client_modules) notmuch.elc notmuch.1.gz diff --git a/TODO b/TODO index 1b8fb42a..d6c303ee 100644 --- a/TODO +++ b/TODO @@ -8,9 +8,6 @@ Fix the things that are causing the most pain to new users Emacs interface (notmuch.el) ---------------------------- -Make the keybindings help ('?') display the summary of each command's -documentation, not the function name. - Add a global keybinding table for notmuch, and then view-specific tables that add to it. @@ -18,8 +15,6 @@ Add a command to archive all threads in a search view. Add a '|' binding from the search view. -Add a binding to run a search from notmuch-show-mode. - When a thread has been entirely read, start out by closing all messages except those that matched the search terms. @@ -41,10 +36,6 @@ Portability ----------- Fix configure script to test each compiler warning we want to use. -Implement strndup locally (or call talloc_strndup instead). - -Implement getline locally, (look at gnulib). - Completion ---------- Fix bash completion to complete multiple search options (both --first @@ -53,6 +44,11 @@ and *then* --max-threads), and also complete value for --sort= notmuch command-line tool ------------------------- +Fix "notmuch show" so that the UI doesn't fail to show a thread that +is visible in a search buffer, but happens to no longer match the +current search. (Perhaps add a --matching= +option (or similar) to "notmuch show".) + Teach "notmuch search" to return many different kinds of results. Some ideas: @@ -72,6 +68,10 @@ Give "notmuch restore" some progress indicator. Until we get the Xapian bugs fixed that are making this operation slow, we really need to let the user know that things are still moving. +Fix "notmuch restore" to operate in a single pass much like "notmuch +dump" does, rather than doing N searches into the database, each +matching 1/N messages. + Add a "-f " option to select an alternate configuration file. @@ -80,10 +80,6 @@ relative to the database path. (Otherwise, moving the database to a new directory will result in notmuch creating new timestamp documents and leaving stale ones behind.) -Ensure that "notmuch new" is sane if its first, giant indexing session -gets interrupted, (that is, ensure that any results indexed so far are -flushed). - Fix notmuch.c to use a DIR prefix for directory timestamps, (the idea being that it can then add other non-directory timestamps such as for noting how far back in the past mail has been indexed, and whether it @@ -104,10 +100,12 @@ indexing. notmuch library --------------- +Index content from citations, please. + Provide a sane syntax for date ranges. First, we don't want to require both endpoints to be specified. For example it would be nice to be able to say things like "since:2009-01-1" or "until:2009-01-1" and -have the other enpoint be implicit. Second we'de like to support +have the other endpoint be implicit. Second we'd like to support relative specifications of time such as "since:'2 months ago'". To do any of this we're probably going to need to break down an write our own parser for the query string rather than using Xapian's QueryParser @@ -132,11 +130,6 @@ Add support for configuring "virtual tags" which are a tuple of (tag-name, search-specification). The database is responsible for ensuring that the virtual tag is always consistent. -Think about optimizing chunked searches (max-threads > 0) to avoid -repeating work. That would be saving state from the previous chunk and -reusing it if the next search is the next chunk with the same search -string. - General ------- Audit everything for dealing with out-of-memory (and drop xutil.c). diff --git a/compat/Makefile b/compat/Makefile new file mode 100644 index 00000000..9a29ffcf --- /dev/null +++ b/compat/Makefile @@ -0,0 +1,5 @@ +all: + $(MAKE) -C .. all + +clean: + $(MAKE) -C .. clean diff --git a/compat/Makefile.local b/compat/Makefile.local new file mode 100644 index 00000000..ccc59aef --- /dev/null +++ b/compat/Makefile.local @@ -0,0 +1,8 @@ +dir=compat +extra_cflags += -I$(dir) + +notmuch_compat_srcs = + +ifneq ($(HAVE_GETLINE),1) +notmuch_compat_srcs += $(dir)/getline.c $(dir)/getdelim.c +endif diff --git a/compat/compat.h b/compat/compat.h new file mode 100644 index 00000000..d639e0f9 --- /dev/null +++ b/compat/compat.h @@ -0,0 +1,41 @@ +/* notmuch - Not much of an email library, (just index and search) + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +/* This header file defines functions that will only be conditionally + * compiled for compatibility on systems that don't provide their own + * implementations of the functions. + */ + +#ifndef NOTMUCH_COMPAT_H +#define NOTMUCH_COMPAT_H + +#if !HAVE_GETLINE +#include +#include + +ssize_t +getline (char **lineptr, size_t *n, FILE *stream); + +ssize_t +getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp); + +#endif /* !HAVE_GETLINE */ + +#endif /* NOTMUCH_COMPAT_H */ diff --git a/compat/getdelim.c b/compat/getdelim.c new file mode 100644 index 00000000..407f3d07 --- /dev/null +++ b/compat/getdelim.c @@ -0,0 +1,133 @@ +/* getdelim.c --- Implementation of replacement getdelim function. + Copyright (C) 1994, 1996, 1997, 1998, 2001, 2003, 2005, 2006, 2007, + 2008, 2009 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or (at + your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. */ + +/* Ported from glibc by Simon Josefsson. */ + +#include "compat.h" + +#include + +#include +#include +#include +#include + +#ifndef SSIZE_MAX +# define SSIZE_MAX ((ssize_t) (SIZE_MAX / 2)) +#endif + +#if USE_UNLOCKED_IO +# include "unlocked-io.h" +# define getc_maybe_unlocked(fp) getc(fp) +#elif !HAVE_FLOCKFILE || !HAVE_FUNLOCKFILE || !HAVE_DECL_GETC_UNLOCKED +# undef flockfile +# undef funlockfile +# define flockfile(x) ((void) 0) +# define funlockfile(x) ((void) 0) +# define getc_maybe_unlocked(fp) getc(fp) +#else +# define getc_maybe_unlocked(fp) getc_unlocked(fp) +#endif + +/* Read up to (and including) a DELIMITER from FP into *LINEPTR (and + NUL-terminate it). *LINEPTR is a pointer returned from malloc (or + NULL), pointing to *N characters of space. It is realloc'ed as + necessary. Returns the number of characters read (not including + the null terminator), or -1 on error or EOF. */ + +ssize_t +getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp) +{ + ssize_t result = -1; + size_t cur_len = 0; + + if (lineptr == NULL || n == NULL || fp == NULL) + { + errno = EINVAL; + return -1; + } + + flockfile (fp); + + if (*lineptr == NULL || *n == 0) + { + char *new_lineptr; + *n = 120; + new_lineptr = (char *) realloc (*lineptr, *n); + if (new_lineptr == NULL) + { + result = -1; + goto unlock_return; + } + *lineptr = new_lineptr; + } + + for (;;) + { + int i; + + i = getc_maybe_unlocked (fp); + if (i == EOF) + { + result = -1; + break; + } + + /* Make enough space for len+1 (for final NUL) bytes. */ + if (cur_len + 1 >= *n) + { + size_t needed_max = + SSIZE_MAX < SIZE_MAX ? (size_t) SSIZE_MAX + 1 : SIZE_MAX; + size_t needed = 2 * *n + 1; /* Be generous. */ + char *new_lineptr; + + if (needed_max < needed) + needed = needed_max; + if (cur_len + 1 >= needed) + { + result = -1; + errno = EOVERFLOW; + goto unlock_return; + } + + new_lineptr = (char *) realloc (*lineptr, needed); + if (new_lineptr == NULL) + { + result = -1; + goto unlock_return; + } + + *lineptr = new_lineptr; + *n = needed; + } + + (*lineptr)[cur_len] = i; + cur_len++; + + if (i == delimiter) + break; + } + (*lineptr)[cur_len] = '\0'; + result = cur_len ? (ssize_t) cur_len : result; + + unlock_return: + funlockfile (fp); /* doesn't set errno */ + + return result; +} diff --git a/compat/getline.c b/compat/getline.c new file mode 100644 index 00000000..222e0f6c --- /dev/null +++ b/compat/getline.c @@ -0,0 +1,29 @@ +/* getline.c --- Implementation of replacement getline function. + Copyright (C) 2005, 2006, 2007 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or (at + your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. */ + +/* Written by Simon Josefsson. */ + +#include "compat.h" + +#include + +ssize_t +getline (char **lineptr, size_t *n, FILE *stream) +{ + return getdelim (lineptr, n, '\n', stream); +} diff --git a/config/README b/config/README new file mode 100644 index 00000000..eabfe285 --- /dev/null +++ b/config/README @@ -0,0 +1,5 @@ +notmuch/config + +This directory consists of small programs used by the notmuch +configure script to test for the availability of certain system +features, (library functions, etc.). diff --git a/config/have_getline.c b/config/have_getline.c new file mode 100644 index 00000000..a8bcd17e --- /dev/null +++ b/config/have_getline.c @@ -0,0 +1,13 @@ +#define _GNU_SOURCE +#include +#include + +int main() +{ + ssize_t count = 0; + size_t n = 0; + char **lineptr = NULL; + FILE *stream = NULL; + + count = getline(lineptr, &n, stream); +} diff --git a/configure b/configure index 1010799d..fa8e142b 100755 --- a/configure +++ b/configure @@ -1,12 +1,70 @@ #! /bin/sh -# defaults +# Set several defaults (optionally specified by the user in +# environemnt variables) +CC=${CC:-gcc} +CXX=${CXX:-g++} +CFLAGS=${CFLAGS:--O2} +CXXFLAGS=${CXXFLAGS:-\$(CFLAGS)} + +# Set the defaults for values the user can specify with command-line +# options. PREFIX=/usr/local -# option parsing +usage () +{ + cat < /dev/null 2>&1; then - echo "Checking for Xapian development files... Yes." + printf "Yes.\n" have_xapian=1 + xapian_cxxflags=$(xapian-config --cxxflags) + xapian_ldflags=$(xapian-config --libs) else - echo "Checking for Xapian development files... No." + printf "No.\n" have_xapian=0 errors=$((errors + 1)) fi +printf "Checking for GMime 2.4 development files... " if pkg-config --modversion gmime-2.4 > /dev/null 2>&1; then - echo "Checking for GMime 2.4 development files... Yes." + printf "Yes.\n" have_gmime=1 + gmime_cflags=$(pkg-config --cflags gmime-2.4) + gmime_ldflags=$(pkg-config --libs gmime-2.4) else - echo "Checking for GMime 2.4 development files... No." + printf "No.\n" have_gmime=0 errors=$((errors + 1)) fi +printf "Checking for talloc development files... " if pkg-config --modversion talloc > /dev/null 2>&1; then - echo "Checking for talloc development files... Yes." + printf "Yes.\n" have_talloc=1 + talloc_cflags=$(pkg-config --cflags talloc) + talloc_ldflags=$(pkg-config --libs talloc) else - echo "Checking for talloc development files... No." + printf "No.\n" have_talloc=0 + talloc_cflags= errors=$((errors + 1)) fi -if printf 'int main(){return 0;}' | gcc -x c -lz -o /dev/null - > /dev/null 2>&1; then - echo "Checking for zlib development files... Yes." - have_zlib=1 +printf "Checking for valgrind development files... " +if pkg-config --modversion valgrind > /dev/null 2>&1; then + printf "Yes.\n" + have_valgrind=1 + valgrind_cflags=$(pkg-config --cflags valgrind) else - echo "Checking for zlib development files... No." - have_zlib=0 - errors=$((errors + 1)) + printf "No (but that's fine).\n" + have_valgrind=0 fi -if pkg-config --modversion valgrind > /dev/null 2>&1; then - echo "Checking for valgrind development files... Yes." - have_valgrind=-DHAVE_VALGRIND +if pkg-config --modversion emacs > /dev/null 2>&1; then + emacs_lispdir=$(pkg-config emacs --variable sitepkglispdir) else - echo "Checking for valgrind development files... No." - have_valgrind= + emacs_lispdir='$(prefix)/share/emacs/site-lisp' fi if [ $errors -gt 0 ]; then @@ -100,34 +169,39 @@ EOF echo " The talloc library (including development files such as headers)" echo " http://talloc.samba.org/" fi - if [ $have_zlib -eq 0 ]; then - echo " The zlib library (including development files such as headers)" - fi cat < /dev/null 2>&1 +then + printf "Yes.\n" + have_getline=1 +else + printf "No (will use our own instead).\n" + have_getline=0 +fi +rm -f config/have_getline + cat < Makefile.config < +#include "compat.h" + #include "notmuch.h" NOTMUCH_BEGIN_DECLS @@ -330,18 +332,6 @@ void _notmuch_message_add_reply (notmuch_message_t *message, notmuch_message_node_t *reply); -/* date.c */ - -/* Parse an RFC 8222 date string to a time_t value. - * - * The tz_offset argument can be used to also obtain the time-zone - * offset, (but can be NULL if the call is not interested in that). - * - * Returns 0 on error. - */ -time_t -notmuch_parse_date (const char *str, int *tz_offset); - /* sha1.c */ char * diff --git a/lib/xutil.c b/lib/xutil.c index 6fa5eb0d..268225b8 100644 --- a/lib/xutil.c +++ b/lib/xutil.c @@ -18,7 +18,6 @@ * Author: Carl Worth */ -#define _GNU_SOURCE /* For strndup */ #include "notmuch-private.h" #include @@ -84,11 +83,16 @@ xstrndup (const char *s, size_t n) { char *ret; - ret = strndup (s, n); + if (strlen (s) <= n) + n = strlen (s); + + ret = malloc (n + 1); if (ret == NULL) { fprintf (stderr, "Out of memory.\n"); exit (1); } + memcpy (ret, s, n); + ret[n] = '\0'; return ret; } diff --git a/notmuch-client.h b/notmuch-client.h index 2888a6c8..50a30fed 100644 --- a/notmuch-client.h +++ b/notmuch-client.h @@ -21,12 +21,13 @@ #ifndef NOTMUCH_CLIENT_H #define NOTMUCH_CLIENT_H - #ifndef _GNU_SOURCE #define _GNU_SOURCE /* for getline */ #endif #include +#include "compat.h" + #include #include "notmuch.h" diff --git a/notmuch-config.c b/notmuch-config.c index fc65d6b0..95430db1 100644 --- a/notmuch-config.c +++ b/notmuch-config.c @@ -317,9 +317,11 @@ notmuch_config_save (notmuch_config_t *config) fprintf (stderr, "Error saving configuration to %s: %s\n", config->filename, error->message); g_error_free (error); + g_free (data); return 1; } + g_free (data); return 0; } diff --git a/notmuch-new.c b/notmuch-new.c index f58a384b..9d206167 100644 --- a/notmuch-new.c +++ b/notmuch-new.c @@ -35,8 +35,10 @@ static volatile sig_atomic_t interrupted; static void handle_sigint (unused (int sig)) { + ssize_t ignored; static char msg[] = "Stopping... \n"; - write(2, msg, sizeof(msg)-1); + + ignored = write(2, msg, sizeof(msg)-1); interrupted = 1; } diff --git a/notmuch-reply.c b/notmuch-reply.c index 9ca1236b..0cda72dc 100644 --- a/notmuch-reply.c +++ b/notmuch-reply.c @@ -39,11 +39,17 @@ reply_part_content (GMimeObject *part) { GMimeStream *stream_stdout = NULL, *stream_filter = NULL; GMimeDataWrapper *wrapper; + const char *charset; + charset = g_mime_object_get_content_type_parameter (part, "charset"); stream_stdout = g_mime_stream_file_new (stdout); if (stream_stdout) { g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE); stream_filter = g_mime_stream_filter_new(stream_stdout); + if (charset) { + g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter), + g_mime_filter_charset_new(charset, "UTF-8")); + } } g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter), g_mime_filter_reply_new(TRUE)); diff --git a/notmuch-setup.c b/notmuch-setup.c index 5ec176d3..622bbaa6 100644 --- a/notmuch-setup.c +++ b/notmuch-setup.c @@ -100,12 +100,15 @@ notmuch_setup_command (unused (void *ctx), unsigned int i; int is_new; -#define prompt(format, ...) \ - do { \ - printf (format, ##__VA_ARGS__); \ - fflush (stdout); \ - getline (&response, &response_size, stdin); \ - chomp_newline (response); \ +#define prompt(format, ...) \ + do { \ + printf (format, ##__VA_ARGS__); \ + fflush (stdout); \ + if (getline (&response, &response_size, stdin) < 0) { \ + printf ("Exiting.\n"); \ + exit (1); \ + } \ + chomp_newline (response); \ } while (0) config = notmuch_config_open (ctx, NULL, &is_new); diff --git a/notmuch-show.c b/notmuch-show.c index 13c91e47..376aacd7 100644 --- a/notmuch-show.c +++ b/notmuch-show.c @@ -184,9 +184,12 @@ show_message (void *ctx, notmuch_message_t *message, int indent) static void -show_messages (void *ctx, notmuch_messages_t *messages, int indent) +show_messages (void *ctx, notmuch_messages_t *messages, int indent, + notmuch_bool_t entire_thread) { notmuch_message_t *message; + notmuch_bool_t match; + int next_indent; for (; notmuch_messages_has_more (messages); @@ -194,9 +197,17 @@ show_messages (void *ctx, notmuch_messages_t *messages, int indent) { message = notmuch_messages_get (messages); - show_message (ctx, message, indent); + match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH); - show_messages (ctx, notmuch_message_get_replies (message), indent + 1); + next_indent = indent; + + if (match || entire_thread) { + show_message (ctx, message, indent); + next_indent = indent + 1; + } + + show_messages (ctx, notmuch_message_get_replies (message), + next_indent, entire_thread); notmuch_message_destroy (message); } @@ -212,6 +223,24 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[])) notmuch_thread_t *thread; notmuch_messages_t *messages; char *query_string; + int entire_thread = 0; + int i; + + for (i = 0; i < argc && argv[i][0] == '-'; i++) { + if (strcmp (argv[i], "--") == 0) { + i++; + break; + } + if (strcmp(argv[i], "--entire-thread") == 0) { + entire_thread = 1; + } else { + fprintf (stderr, "Unrecognized option: %s\n", argv[i]); + return 1; + } + } + + argc -= i; + argv += i; config = notmuch_config_open (ctx, NULL, NULL); if (config == NULL) @@ -251,7 +280,7 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[])) INTERNAL_ERROR ("Thread %s has no toplevel messages.\n", notmuch_thread_get_thread_id (thread)); - show_messages (ctx, messages, 0); + show_messages (ctx, messages, 0, entire_thread); notmuch_thread_destroy (thread); } diff --git a/notmuch-tag.c b/notmuch-tag.c index 07cb8c5f..00588a11 100644 --- a/notmuch-tag.c +++ b/notmuch-tag.c @@ -25,8 +25,10 @@ static volatile sig_atomic_t interrupted; static void handle_sigint (unused (int sig)) { + ssize_t ignored; + static char msg[] = "Stopping... \n"; - write(2, msg, sizeof(msg)-1); + ignored = write(2, msg, sizeof(msg)-1); interrupted = 1; } diff --git a/notmuch.1 b/notmuch.1 index 04bd0cf1..369ecba1 100644 --- a/notmuch.1 +++ b/notmuch.1 @@ -169,6 +169,8 @@ when sorting by .B newest\-first the threads will be sorted by the newest message in each thread. +.RE +.RS 4 By default, results will be displayed in reverse chronological order, (that is, the newest results will be displayed first). @@ -177,7 +179,7 @@ See the section below for details of the supported syntax for . .RE .TP -.BR show " ..." +.BR show " [options...] ..." Shows all messages matching the search terms. @@ -187,6 +189,19 @@ message in date order). The output is not indented by default, but depth tags are printed so that proper indentation can be performed by a post-processor (such as the emacs interface to notmuch). +Supported options for +.B show +include +.RS 4 +.TP 4 +.B \-\-entire\-thread + +By default only those messages that match the search terms will be +displayed. With this option, all messages in the same thread as any +matched message will be displayed. +.RE + +.RS 4 The output format is plain-text, with all text-content MIME parts decoded. Various components in the output, .RB ( message ", " header ", " body ", " attachment ", and MIME " part ), @@ -207,6 +222,7 @@ See the .B "SEARCH SYNTAX" section below for details of the supported syntax for . .RE +.RE The .B reply diff --git a/notmuch.c b/notmuch.c index d9846ce7..2ac8a592 100644 --- a/notmuch.c +++ b/notmuch.c @@ -177,6 +177,15 @@ command_t commands[] = { "\t\t(all replies to a particular message appear immediately\n" "\t\tafter that message in date order).\n" "\n" + "\t\tSupported options for show include:\n" + "\n" + "\t\t--entire-thread\n" + "\n" + "\t\t\tBy default only those messages that match the\n" + "\t\t\tsearch terms will be displayed. With this option,\n" + "\t\t\tall messages in the same thread as any matched\n" + "\t\t\tmessage will be displayed.\n" + "\n" "\t\tThe output format is plain-text, with all text-content\n" "\t\tMIME parts decoded. Various components in the output,\n" "\t\t('message', 'header', 'body', 'attachment', and MIME 'part')\n" diff --git a/notmuch.el b/notmuch.el index 65473ba7..c504f46d 100644 --- a/notmuch.el +++ b/notmuch.el @@ -53,37 +53,31 @@ (defvar notmuch-show-mode-map (let ((map (make-sparse-keymap))) - ; I don't actually want all of these toggle commands occupying - ; keybindings. They steal valuable key-binding space, are hard - ; to remember, and act globally rather than locally. - ; - ; Will be much preferable to switch to direct manipulation for - ; toggling visibility of these components. Probably using - ; overlays-at to query and manipulate the current overlay. - (define-key map "a" 'notmuch-show-archive-thread) - (define-key map "A" 'notmuch-show-mark-read-then-archive-thread) - (define-key map "f" 'notmuch-show-forward-current) - (define-key map "m" 'message-mail) - (define-key map "n" 'notmuch-show-next-message) - (define-key map "N" 'notmuch-show-mark-read-then-next-open-message) - (define-key map "p" 'notmuch-show-previous-message) - (define-key map (kbd "C-n") 'notmuch-show-next-line) - (define-key map (kbd "C-p") 'notmuch-show-previous-line) + (define-key map "?" 'notmuch-help) (define-key map "q" 'kill-this-buffer) - (define-key map "r" 'notmuch-show-reply) + (define-key map (kbd "C-p") 'notmuch-show-previous-line) + (define-key map (kbd "C-n") 'notmuch-show-next-line) + (define-key map (kbd "M-TAB") 'notmuch-show-previous-button) + (define-key map (kbd "TAB") 'notmuch-show-next-button) (define-key map "s" 'notmuch-search) - (define-key map "v" 'notmuch-show-view-all-mime-parts) - (define-key map "V" 'notmuch-show-view-raw-message) + (define-key map "m" 'message-mail) + (define-key map "f" 'notmuch-show-forward-current) + (define-key map "r" 'notmuch-show-reply) + (define-key map "|" 'notmuch-show-pipe-message) (define-key map "w" 'notmuch-show-save-attachments) - (define-key map "x" 'kill-this-buffer) - (define-key map "+" 'notmuch-show-add-tag) + (define-key map "V" 'notmuch-show-view-raw-message) + (define-key map "v" 'notmuch-show-view-all-mime-parts) (define-key map "-" 'notmuch-show-remove-tag) + (define-key map "+" 'notmuch-show-add-tag) + (define-key map "X" 'notmuch-show-mark-read-then-archive-then-exit) + (define-key map "x" 'notmuch-show-archive-thread-then-exit) + (define-key map "A" 'notmuch-show-mark-read-then-archive-thread) + (define-key map "a" 'notmuch-show-archive-thread) + (define-key map "p" 'notmuch-show-previous-message) + (define-key map "N" 'notmuch-show-mark-read-then-next-open-message) + (define-key map "n" 'notmuch-show-next-message) (define-key map (kbd "DEL") 'notmuch-show-rewind) (define-key map " " 'notmuch-show-advance-marking-read-and-archiving) - (define-key map "|" 'notmuch-show-pipe-message) - (define-key map "?" 'describe-mode) - (define-key map (kbd "TAB") 'notmuch-show-next-button) - (define-key map (kbd "M-TAB") 'notmuch-show-previous-button) map) "Keymap for \"notmuch show\" buffers.") (fset 'notmuch-show-mode-map notmuch-show-mode-map) @@ -117,7 +111,7 @@ pattern can still test against the entire line).") (defvar notmuch-show-marker-regexp "\f\\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$") (defvar notmuch-show-id-regexp "\\(id:[^ ]*\\)") -(defvar notmuch-show-depth-regexp " depth:\\([0-9]*\\) ") +(defvar notmuch-show-depth-match-regexp " depth:\\([0-9]*\\).*match:\\([01]\\) ") (defvar notmuch-show-filename-regexp "filename:\\(.*\\)$") (defvar notmuch-show-tags-regexp "(\\([^)]*\\))$") @@ -165,7 +159,7 @@ Unlike builtin `next-line' this version accepts no arguments." By advancing forward until reaching a visible character. -Unlike builtin `next-line' this version accepts no arguments." +Unlike builtin `previous-line' this version accepts no arguments." (interactive) (set 'this-command 'previous-line) (call-interactively 'previous-line) @@ -252,7 +246,7 @@ Unlike builtin `next-line' this version accepts no arguments." (notmuch-search-show-thread))))) (defun notmuch-show-mark-read-then-archive-thread () - "Remove \"unread\" tag from each message, then archive and show next thread. + "Remove unread tags from thread, then archive and show next thread. Archive each message currently shown by removing the \"unread\" and \"inbox\" tag from each. Then kill this buffer and show the @@ -267,7 +261,7 @@ buffer." (notmuch-show-archive-thread-maybe-mark-read t)) (defun notmuch-show-archive-thread () - "Archive each message in thread, and show next thread from search. + "Archive each message in thread, then show next thread from search. Archive each message currently shown by removing the \"inbox\" tag from each. Then kill this buffer and show the next thread @@ -280,6 +274,18 @@ buffer." (interactive) (notmuch-show-archive-thread-maybe-mark-read nil)) +(defun notmuch-show-archive-thread-then-exit () + "Archive each message in thread, then exit back to search results." + (interactive) + (notmuch-show-archive-thread) + (kill-this-buffer)) + +(defun notmuch-show-mark-read-then-archive-then-exit () + "Remove unread tags from thread, then archive and exit to search results." + (interactive) + (notmuch-show-mark-read-then-archive-thread) + (kill-this-buffer)) + (defun notmuch-show-view-raw-message () "View the raw email of the current message." (interactive) @@ -296,7 +302,7 @@ buffer." (kill-buffer buf))))) (defun notmuch-show-view-all-mime-parts () - "Use external viewers (according to mailcap) to view all MIME-encoded parts." + "Use external viewers to view all attachments from the current message." (interactive) (with-current-notmuch-show-message (mm-display-parts (mm-dissect-buffer)))) @@ -334,7 +340,7 @@ buffer." mm-handle)) (defun notmuch-show-save-attachments () - "Save the attachments to a message" + "Save all attachments from the current message." (interactive) (with-current-notmuch-show-message (let ((mm-handle (mm-dissect-buffer))) @@ -360,7 +366,7 @@ buffer." (notmuch-reply message-id))) (defun notmuch-show-forward-current () - "Forward a the current message." + "Forward the current message." (interactive) (with-current-notmuch-show-message (message-forward))) @@ -385,7 +391,7 @@ point either forward or backward to the next visible character when a command ends with point on an invisible character). Emits an error if point is not within a valid message, (that is -not pattern of `notmuch-show-message-begin-regexp' could be found +no pattern of `notmuch-show-message-begin-regexp' could be found by searching backward)." (beginning-of-line) (if (not (looking-at notmuch-show-message-begin-regexp)) @@ -402,22 +408,35 @@ by searching backward)." (not (re-search-forward notmuch-show-message-begin-regexp nil t))))) (defun notmuch-show-message-unread-p () - "Preficate testing whether current message is unread." + "Predicate testing whether current message is unread." (member "unread" (notmuch-show-get-tags))) +(defun notmuch-show-message-open-p () + "Predicate testing whether current message is open (body is visible)." + (let ((btn (previous-button (point) t))) + (while (not (button-has-type-p btn 'notmuch-button-body-toggle-type)) + (setq btn (previous-button (button-start btn)))) + (not (invisible-p (button-get btn 'invisibility-spec))))) + (defun notmuch-show-next-message () "Advance to the beginning of the next message in the buffer. Moves to the last visible character of the current message if -already on the last message in the buffer." +already on the last message in the buffer. + +Returns nil if already on the last message in the buffer." (interactive) (notmuch-show-move-to-current-message-summary-line) (if (re-search-forward notmuch-show-message-begin-regexp nil t) - (notmuch-show-move-to-current-message-summary-line) + (progn + (notmuch-show-move-to-current-message-summary-line) + (recenter 0) + t) (goto-char (- (point-max) 1)) (while (point-invisible-p) - (backward-char))) - (recenter 0)) + (backward-char)) + (recenter 0) + nil)) (defun notmuch-show-find-next-message () "Returns the position of the next message in the buffer. @@ -445,14 +464,9 @@ there are no more unread messages past the current point." (notmuch-show-next-message))) (defun notmuch-show-next-open-message () - "Advance to the next message which is not hidden. - -If read messages are currently hidden, advance to the next unread -message. Otherwise, advance to the next message." - (if (or (memq 'notmuch-show-body-read buffer-invisibility-spec) - (assq 'notmuch-show-body-read buffer-invisibility-spec)) - (notmuch-show-next-unread-message) - (notmuch-show-next-message))) + "Advance to the next open message (that is, body is not invisible)." + (while (and (notmuch-show-next-message) + (not (notmuch-show-message-open-p))))) (defun notmuch-show-previous-message () "Backup to the beginning of the previous message in the buffer. @@ -486,13 +500,13 @@ it." (point)))) (defun notmuch-show-mark-read-then-next-open-message () - "Remove unread tag from current message, then advance to next unread message." + "Remove unread tag from this message, then advance to next open message." (interactive) (notmuch-show-remove-tag "unread") (notmuch-show-next-open-message)) (defun notmuch-show-rewind () - "Do reverse scrolling compared to `notmuch-show-advance-marking-read-and-archiving' + "Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-marking-read-and-archiving]). Specifically, if the beginning of the previous email is fewer than `window-height' lines from the current point, move to it @@ -514,7 +528,7 @@ any effects from previous calls to (notmuch-show-previous-message)))) (defun notmuch-show-advance-marking-read-and-archiving () - "Advance through buffer, marking read and archiving. + "Advance through thread, marking read and archiving. This command is intended to be one of the simplest ways to process a thread of email. It does the following: @@ -552,7 +566,7 @@ which this thread was originally shown." (goto-char (button-start (previous-button (point))))) (defun notmuch-toggle-invisible-action (cite-button) - (let ((invis-spec (button-get button 'invisibility-spec))) + (let ((invis-spec (button-get cite-button 'invisibility-spec))) (if (invisible-p invis-spec) (remove-from-invisibility-spec invis-spec) (add-to-invisibility-spec invis-spec) @@ -560,14 +574,19 @@ which this thread was originally shown." (force-window-update) (redisplay t)) -(define-button-type 'notmuch-button-invisibility-toggle-type 'action 'notmuch-toggle-invisible-action 'follow-link t) +(define-button-type 'notmuch-button-invisibility-toggle-type + 'action 'notmuch-toggle-invisible-action + 'follow-link t + 'face "default") (define-button-type 'notmuch-button-citation-toggle-type 'help-echo "mouse-1, RET: Show citation" :supertype 'notmuch-button-invisibility-toggle-type) (define-button-type 'notmuch-button-signature-toggle-type 'help-echo "mouse-1, RET: Show signature" :supertype 'notmuch-button-invisibility-toggle-type) (define-button-type 'notmuch-button-headers-toggle-type 'help-echo "mouse-1, RET: Show headers" :supertype 'notmuch-button-invisibility-toggle-type) -(define-button-type 'notmuch-button-body-toggle-type 'help-echo "mouse-1, RET: Show message" +(define-button-type 'notmuch-button-body-toggle-type + 'help-echo "mouse-1, RET: Show message" + 'face 'notmuch-message-summary-face :supertype 'notmuch-button-invisibility-toggle-type) (defun notmuch-show-markup-citations-region (beg end depth) @@ -665,7 +684,20 @@ which this thread was originally shown." (notmuch-show-markup-part beg end depth mime-message)))))) -(defun notmuch-show-markup-body (depth btn) +(defun notmuch-show-markup-body (depth match btn) + "Markup a message body, (indenting, buttonizing citations, +etc.), and conditionally hiding the body itself if the message +has been read and does not match the current search. + +DEPTH specifies the depth at which this message appears in the +tree of the current thread, (the top-level messages have depth 0 +and each reply increases depth by 1). MATCH indicates whether +this message is regarded as matching the current search. BTN is +the button which is used to toggle the visibility of this +message. + +When this function is called, point must be within the message, but +before the delimiter marking the beginning of the body." (re-search-forward notmuch-show-body-begin-regexp) (forward-line) (let ((beg (point-marker))) @@ -676,86 +708,95 @@ which this thread was originally shown." (overlay-put (make-overlay beg end) 'invisible invis-spec) (button-put btn 'invisibility-spec invis-spec) - (if (not (notmuch-show-message-unread-p)) + (if (not (or (notmuch-show-message-unread-p) match)) (add-to-invisibility-spec invis-spec))) (set-marker beg nil) (set-marker end nil) ))) + (defun notmuch-fontify-headers () - (progn - (if (looking-at "[Tt]o:") - (progn - (overlay-put (make-overlay (point) (re-search-forward ":")) - 'face 'message-header-name) - (overlay-put (make-overlay (point) (re-search-forward ".*$")) - 'face 'message-header-to)) + (while (looking-at "[[:space:]]") + (forward-char)) + (if (looking-at "[Tt]o:") + (progn + (overlay-put (make-overlay (point) (re-search-forward ":")) + 'face 'message-header-name) + (overlay-put (make-overlay (point) (re-search-forward ".*$")) + 'face 'message-header-to)) (if (looking-at "[B]?[Cc][Cc]:") (progn (overlay-put (make-overlay (point) (re-search-forward ":")) - 'face 'message-header-name) - (overlay-put (make-overlay (point) (re-search-forward ".*$")) - 'face 'message-header-cc)) - (if (looking-at "[Ss]ubject:") - (progn - (overlay-put (make-overlay (point) (re-search-forward ":")) - 'face 'message-header-name) - (overlay-put (make-overlay (point) (re-search-forward ".*$")) - 'face 'message-header-subject)) - (if (looking-at "[Ff]rom:") - (progn - (overlay-put (make-overlay (point) (re-search-forward ":")) - 'face 'message-header-name) - (overlay-put (make-overlay (point) (re-search-forward ".*$")) - 'face 'message-header-other)))))))) - -(defun notmuch-show-markup-header (depth) + 'face 'message-header-name) + (overlay-put (make-overlay (point) (re-search-forward ".*$")) + 'face 'message-header-cc)) + (if (looking-at "[Ss]ubject:") + (progn + (overlay-put (make-overlay (point) (re-search-forward ":")) + 'face 'message-header-name) + (overlay-put (make-overlay (point) (re-search-forward ".*$")) + 'face 'message-header-subject)) + (if (looking-at "[Ff]rom:") + (progn + (overlay-put (make-overlay (point) (re-search-forward ":")) + 'face 'message-header-name) + (overlay-put (make-overlay (point) (re-search-forward ".*$")) + 'face 'message-header-other))))))) + +(defun notmuch-show-markup-header (message-begin depth) + "Buttonize and decorate faces in a message header. + +MESSAGE-BEGIN is the position of the absolute first character in +the message (including all delimiters that will end up being +invisible etc.). This is to allow a button to reliably extend to +the beginning of the message even if point is positioned at an +invisible character (such as the beginning of the buffer). + +DEPTH specifies the depth at which this message appears in the +tree of the current thread, (the top-level messages have depth 0 +and each reply increases depth by 1)." (re-search-forward notmuch-show-header-begin-regexp) (forward-line) (let ((beg (point-marker)) + (summary-end (copy-marker (line-beginning-position 2))) + (subject-end (copy-marker (line-end-position 2))) + (invis-spec (make-symbol "notmuch-show-header")) (btn nil)) - (end-of-line) - ; Inverse video for subject - (overlay-put (make-overlay beg (point)) 'face '(:inverse-video t)) - (setq btn (make-button beg (point) :type 'notmuch-button-body-toggle-type)) - (forward-line 1) - (end-of-line) - (let ((beg-hidden (point-marker))) - (re-search-forward notmuch-show-header-end-regexp) - (beginning-of-line) - (let ((end (point-marker))) - (goto-char beg) - (forward-line) - (while (looking-at "[A-Za-z][-A-Za-z0-9]*:") - (beginning-of-line) - (notmuch-fontify-headers) - (forward-line) - ) - (indent-rigidly beg end depth) - (let ((invis-spec (make-symbol "notmuch-show-header"))) - (add-to-invisibility-spec (cons invis-spec t)) - (overlay-put (make-overlay beg-hidden end) - 'invisible invis-spec) - (goto-char beg) - (forward-line) - (make-button (line-beginning-position) (line-end-position) - 'invisibility-spec (cons invis-spec t) - :type 'notmuch-button-headers-toggle-type)) - (goto-char end) - (insert "\n") - (set-marker beg nil) - (set-marker beg-hidden nil) - (set-marker end nil) - )) - btn)) + (re-search-forward notmuch-show-header-end-regexp) + (beginning-of-line) + (let ((end (point-marker))) + (indent-rigidly beg end depth) + (goto-char beg) + (setq btn (make-button message-begin summary-end :type 'notmuch-button-body-toggle-type)) + (forward-line) + (add-to-invisibility-spec invis-spec) + (overlay-put (make-overlay subject-end end) + 'invisible invis-spec) + (make-button (line-beginning-position) subject-end + 'invisibility-spec invis-spec + :type 'notmuch-button-headers-toggle-type) + (while (looking-at "[[:space:]]*[A-Za-z][-A-Za-z0-9]*:") + (beginning-of-line) + (notmuch-fontify-headers) + (forward-line) + ) + (goto-char end) + (insert "\n") + (set-marker beg nil) + (set-marker summary-end nil) + (set-marker subject-end nil) + (set-marker end nil) + ) + btn)) (defun notmuch-show-markup-message () (if (re-search-forward notmuch-show-message-begin-regexp nil t) - (progn - (re-search-forward notmuch-show-depth-regexp) + (let ((message-begin (match-beginning 0))) + (re-search-forward notmuch-show-depth-match-regexp) (let ((depth (string-to-number (buffer-substring (match-beginning 1) (match-end 1)))) + (match (string= "1" (buffer-substring (match-beginning 2) (match-end 2)))) (btn nil)) - (setq btn (notmuch-show-markup-header depth)) - (notmuch-show-markup-body depth btn))) + (setq btn (notmuch-show-markup-header message-begin depth)) + (notmuch-show-markup-body depth match btn))) (goto-char (point-max)))) (defun notmuch-show-hide-markers () @@ -775,6 +816,72 @@ which this thread was originally shown." (notmuch-show-markup-message))) (notmuch-show-hide-markers)) +(defun notmuch-documentation-first-line (symbol) + "Return the first line of the documentation string for SYMBOL." + (let ((doc (documentation symbol))) + (if doc + (with-temp-buffer + (insert (documentation symbol t)) + (goto-char (point-min)) + (let ((beg (point))) + (end-of-line) + (buffer-substring beg (point)))) + ""))) + +(defun notmuch-prefix-key-description (key) + "Given a prefix key code, return a human-readable string representation. + +This is basically just `format-kbd-macro' but we also convert ESC to M-." + (let ((desc (format-kbd-macro (vector key)))) + (if (string= desc "ESC") + "M-" + (concat desc " ")))) + +; I would think that emacs would have code handy for walking a keymap +; and generating strings for each key, and I would prefer to just call +; that. But I couldn't find any (could be all implemented in C I +; suppose), so I wrote my own here. +(defun notmuch-substitute-one-command-key-with-prefix (prefix binding) + "For a key binding, return a string showing a human-readable +representation of the prefixed key as well as the first line of +documentation from the bound function. + +For a mouse binding, return nil." + (let ((key (car binding)) + (action (cdr binding))) + (if (mouse-event-p key) + nil + (if (keymapp action) + (let ((substitute (apply-partially 'notmuch-substitute-one-command-key-with-prefix (notmuch-prefix-key-description key)))) + (mapconcat substitute (cdr action) "\n")) + (concat prefix (format-kbd-macro (vector key)) + "\t" + (notmuch-documentation-first-line action)))))) + +(defalias 'notmuch-substitute-one-command-key + (apply-partially 'notmuch-substitute-one-command-key-with-prefix nil)) + +(defun notmuch-substitute-command-keys (doc) + "Like `substitute-command-keys' but with documentation, not function names." + (let ((beg 0)) + (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg) + (let ((map (substring doc (match-beginning 1) (match-end 1)))) + (setq doc (replace-match (mapconcat 'notmuch-substitute-one-command-key + (cdr (symbol-value (intern map))) "\n") 1 1 doc))) + (setq beg (match-end 0))) + doc)) + +(defun notmuch-help () + "Display help for the current notmuch mode." + (interactive) + (let* ((mode major-mode) + (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t))))) + (with-current-buffer (generate-new-buffer "*notmuch-help*") + (insert doc) + (goto-char (point-min)) + (set-buffer-modified-p nil) + (view-buffer (current-buffer) 'kill-buffer-if-not-modified)))) + ;;;###autoload (defun notmuch-show-mode () "Major mode for viewing a thread with notmuch. @@ -783,22 +890,28 @@ This buffer contains the results of the \"notmuch show\" command for displaying a single thread of email from your email archives. By default, various components of email messages, (citations, -signatures, already-read messages), are invisible to help you -focus on the most important things, (new text from unread -messages). See the various commands below for toggling the -visibility of hidden components. - -The `notmuch-show-next-message' and -`notmuch-show-previous-message' commands, (bound to 'n' and 'p by -default), allow you to navigate to the next and previous -messages. Each time you navigate away from a message with -`notmuch-show-next-message' the current message will have its -\"unread\" tag removed. - -You can add or remove tags from the current message with '+' and -'-'. You can also archive all messages in the current -view, (remove the \"inbox\" tag from each), with -`notmuch-show-archive-thread' (bound to 'a' by default). +signatures, already-read messages), are hidden. You can make +these parts visible by clicking with the mouse button or by +pressing RET after positioning the cursor on a hidden part, (for +which \\[notmuch-show-next-button] and \\[notmuch-show-previous-button] are helpful). + +Reading the thread sequentially is well-supported by pressing +\\[notmuch-show-advance-marking-read-and-archiving]. This will scroll the current message (if necessary), +advance to the next message, or advance to the next thread (if +already on the last message of a thread). As each message is +scrolled away its \"unread\" tag will be removed, and as each +thread is scrolled away the \"inbox\" tag will be removed from +each message in the thread. + +Other commands are available to read or manipulate the thread more +selectively, (such as '\\[notmuch-show-next-message]' and '\\[notmuch-show-previous-message]' to advance to messages without +removing any tags, and '\\[notmuch-show-archive-thread]' to archive an entire thread without +scrolling through with \\[notmuch-show-advance-marking-read-and-archiving]). + +You can add or remove arbitary tags from the current message with +'\\[notmuch-show-add-tag]' or '\\[notmuch-show-remove-tag]'. + +All currently available key bindings: \\{notmuch-show-mode-map}" (interactive) @@ -843,7 +956,8 @@ The optional PARENT-BUFFER is the notmuch-search buffer from which this notmuch-show command was executed, (so that the next thread from that buffer can be show when done with this one)." (interactive "sNotmuch show: ") - (let ((buffer (get-buffer-create (concat "*notmuch-show-" thread-id "*")))) + (let ((query notmuch-search-query-string) + (buffer (get-buffer-create (concat "*notmuch-show-" thread-id "*")))) (switch-to-buffer buffer) (notmuch-show-mode) (set (make-local-variable 'notmuch-show-parent-buffer) parent-buffer) @@ -855,30 +969,13 @@ thread from that buffer can be show when done with this one)." (erase-buffer) (goto-char (point-min)) (save-excursion - (call-process notmuch-command nil t nil "show" thread-id) + (call-process notmuch-command nil t nil "show" "--entire-thread" thread-id "and (" query ")") (notmuch-show-markup-messages) ) (run-hooks 'notmuch-show-hook) - ; Move straight to the first unread message - (if (not (notmuch-show-message-unread-p)) - (progn - (notmuch-show-next-unread-message) - ; But if there are no unread messages, go back to the - ; beginning of the buffer, and open up the bodies of all - ; read message. - (if (not (notmuch-show-message-unread-p)) - (progn - (goto-char (point-min)) - (let ((btn (forward-button 1))) - (while btn - (if (button-has-type-p btn 'notmuch-button-body-toggle-type) - (push-button)) - (condition-case err - (setq btn (forward-button 1)) - (error (setq btn nil))) - )) - (beginning-of-buffer) - )))) + ; Move straight to the first open message + (if (not (notmuch-show-message-open-p)) + (notmuch-show-next-open-message)) ))) (defvar notmuch-search-authors-width 40 @@ -886,30 +983,29 @@ thread from that buffer can be show when done with this one)." (defvar notmuch-search-mode-map (let ((map (make-sparse-keymap))) - (define-key map "a" 'notmuch-search-archive-thread) - (define-key map "b" 'notmuch-search-scroll-down) - (define-key map "f" 'notmuch-search-filter) - (define-key map "m" 'message-mail) - (define-key map "n" 'next-line) - (define-key map "o" 'notmuch-search-toggle-order) - (define-key map "p" 'previous-line) + (define-key map "?" 'notmuch-help) (define-key map "q" 'kill-this-buffer) + (define-key map "x" 'kill-this-buffer) + (define-key map (kbd "") 'notmuch-search-scroll-down) + (define-key map "b" 'notmuch-search-scroll-down) + (define-key map " " 'notmuch-search-scroll-up) + (define-key map "<" 'notmuch-search-first-thread) + (define-key map ">" 'notmuch-search-last-thread) + (define-key map "p" 'notmuch-search-previous-thread) + (define-key map "n" 'notmuch-search-next-thread) (define-key map "r" 'notmuch-search-reply-to-thread) + (define-key map "m" 'message-mail) (define-key map "s" 'notmuch-search) + (define-key map "o" 'notmuch-search-toggle-order) + (define-key map "=" 'notmuch-search-refresh-view) (define-key map "t" 'notmuch-search-filter-by-tag) - (define-key map "x" 'kill-this-buffer) - (define-key map (kbd "RET") 'notmuch-search-show-thread) + (define-key map "f" 'notmuch-search-filter) (define-key map [mouse-1] 'notmuch-search-show-thread) - (define-key map "+" 'notmuch-search-add-tag) - (define-key map "-" 'notmuch-search-remove-tag) (define-key map "*" 'notmuch-search-operate-all) - (define-key map "<" 'beginning-of-buffer) - (define-key map ">" 'notmuch-search-goto-last-thread) - (define-key map "=" 'notmuch-search-refresh-view) - (define-key map "\M->" 'notmuch-search-goto-last-thread) - (define-key map " " 'notmuch-search-scroll-up) - (define-key map (kbd "") 'notmuch-search-scroll-down) - (define-key map "?" 'describe-mode) + (define-key map "a" 'notmuch-search-archive-thread) + (define-key map "-" 'notmuch-search-remove-tag) + (define-key map "+" 'notmuch-search-add-tag) + (define-key map (kbd "RET") 'notmuch-search-show-thread) map) "Keymap for \"notmuch search\" buffers.") (fset 'notmuch-search-mode-map notmuch-search-mode-map) @@ -918,16 +1014,17 @@ thread from that buffer can be show when done with this one)." (defvar notmuch-search-oldest-first t "Show the oldest mail first in the search-mode") +(defvar notmuch-search-disjunctive-regexp "\\<[oO][rR]\\>") (defun notmuch-search-scroll-up () - "Scroll up, moving point to last message in thread if at end." + "Move forward through search results by one window's worth." (interactive) (condition-case nil (scroll-up nil) - ((end-of-buffer) (notmuch-search-goto-last-thread)))) + ((end-of-buffer) (notmuch-search-last-thread)))) (defun notmuch-search-scroll-down () - "Scroll down, moving point to first message in thread if at beginning." + "Move backward through the search results by one window's worth." (interactive) ; I don't know why scroll-down doesn't signal beginning-of-buffer ; the way that scroll-up signals end-of-buffer, but c'est la vie. @@ -937,16 +1034,37 @@ thread from that buffer can be show when done with this one)." ; directly to that position. (We have to count lines since the ; window-start position is not the same as point-min due to the ; invisible thread-ID characters on the first line. - (if (equal (count-lines (point-min) (window-start)) 1) - (goto-char (window-start)) + (if (equal (count-lines (point-min) (window-start)) 0) + (goto-char (point-min)) (scroll-down nil))) -(defun notmuch-search-goto-last-thread () - "Move point to the last thread in the buffer." +(defun notmuch-search-next-thread () + "Select the next thread in the search results." + (interactive) + (forward-line 1)) + +(defun notmuch-search-previous-thread () + "Select the previous thread in the search results." (interactive) - (goto-char (point-max)) (forward-line -1)) +(defun notmuch-search-last-thread () + "Select the last thread in the search results." + (interactive) + (goto-char (point-max)) + (forward-line -2)) + +(defun notmuch-search-first-thread () + "Select the first thread in the search results." + (interactive) + (goto-char (point-min))) + +(defface notmuch-message-summary-face + '((((class color) (background light)) (:background "#f0f0f0")) + (((class color) (background dark)) (:background "#303030"))) + "Face for the single-line message summary in notmuch-show-mode." + :group 'notmuch) + (defface notmuch-tag-face '((((class color) (background dark)) @@ -966,22 +1084,27 @@ thread from that buffer can be show when done with this one)." ;;;###autoload (defun notmuch-search-mode () - "Major mode for searching mail with notmuch. + "Major mode displaying results of a notmuch search. This buffer contains the results of a \"notmuch search\" of your email archives. Each line in the buffer represents a single -thread giving a relative date for the thread and a subject. +thread giving a summary of the thread (a relative date, the +number of matched messages and total messages in the thread, +participants in the thread, a representative subject line, and +any tags). -Pressing RET on any line displays that thread. The '+' and '-' -keys can be used to add or remove tags from a thread. The 'a' key -is a convenience key for archiving a thread (removing the -\"inbox\" tag). +Pressing \\[notmuch-search-show-thread] on any line displays that thread. The '\\[notmuch-search-add-tag]' and '\\[notmuch-search-remove-tag]' +keys can be used to add or remove tags from a thread. The '\\[notmuch-search-archive-thread]' key +is a convenience for archiving a thread (removing the \"inbox\" +tag). The '\\[notmuch-search-operate-all]' key can be used to add or remove a tag from all +threads in the current buffer. -Other useful commands are `notmuch-search-filter' for filtering -the current search based on an additional query string, -`notmuch-search-filter-by-tag' for filtering to include only -messages with a given tag, and `notmuch-search' to execute a new, -global search. +Other useful commands are '\\[notmuch-search-filter]' for filtering the current search +based on an additional query string, '\\[notmuch-search-filter-by-tag]' for filtering to include +only messages with a given tag, and '\\[notmuch-search]' to execute a new, global +search. + +Complete list of currently available key bindings: \\{notmuch-search-mode-map}" (interactive) @@ -998,12 +1121,11 @@ global search. (if (not notmuch-tag-face-alist) (add-to-list 'notmuch-search-font-lock-keywords (list "(\\([^)]*\\))$" '(1 'notmuch-tag-face))) - (progn - (setq notmuch-search-tags (mapcar 'car notmuch-tag-face-alist)) - (loop for notmuch-search-tag in notmuch-search-tags - do (add-to-list 'notmuch-search-font-lock-keywords (list - (concat "([^)]*\\(" notmuch-search-tag "\\)[^)]*)$") - `(1 ,(cdr (assoc notmuch-search-tag notmuch-tag-face-alist)))))))) + (let ((notmuch-search-tags (mapcar 'car notmuch-tag-face-alist))) + (loop for notmuch-search-tag in notmuch-search-tags + do (add-to-list 'notmuch-search-font-lock-keywords (list + (concat "([^)]*\\(" notmuch-search-tag "\\)[^)]*)$") + `(1 ,(cdr (assoc notmuch-search-tag notmuch-tag-face-alist)))))))) (set (make-local-variable 'font-lock-defaults) '(notmuch-search-font-lock-keywords t))) @@ -1012,6 +1134,7 @@ global search. (get-text-property (point) 'notmuch-search-thread-id)) (defun notmuch-search-show-thread () + "Display the currently selected thread." (interactive) (let ((thread-id (notmuch-search-find-thread-id))) (if (> (length thread-id) 0) @@ -1064,25 +1187,29 @@ and will also appear in a buffer named \"*Notmuch errors*\"." (split-string (buffer-substring beg end)))))) (defun notmuch-search-add-tag (tag) - "Add a tag to messages in the current thread matching the -active query." + "Add a tag to the currently selected thread. + +The tag is added to messages in the currently selected thread +which match the current search terms." (interactive (list (notmuch-select-tag-with-completion "Tag to add: "))) (notmuch-call-notmuch-process "tag" (concat "+" tag) (notmuch-search-find-thread-id) " and " notmuch-search-query-string) (notmuch-search-set-tags (delete-dups (sort (cons tag (notmuch-search-get-tags)) 'string<)))) (defun notmuch-search-remove-tag (tag) - "Remove a tag from messages in the current thread matching the -active query." + "Remove a tag from the currently selected thread. + +The tag is removed from messages in the currently selected thread +which match the current search terms." (interactive (list (notmuch-select-tag-with-completion "Tag to remove: " (notmuch-search-find-thread-id)))) (notmuch-call-notmuch-process "tag" (concat "-" tag) (notmuch-search-find-thread-id) " and " notmuch-search-query-string) (notmuch-search-set-tags (delete tag (notmuch-search-get-tags)))) (defun notmuch-search-archive-thread () - "Archive the current thread (remove its \"inbox\" tag). + "Archive the currently selected thread (remove its \"inbox\" tag). -This function advances point to the next line when finished." +This function advances the next thread when finished." (interactive) (notmuch-search-remove-tag "inbox") (forward-line)) @@ -1136,12 +1263,12 @@ This function advances point to the next line when finished." (delete-process proc)))) (defun notmuch-search-operate-all (action) - "Operate on all messages matching the current query. Any -number of whitespace separated actions can be given. Each action -must have one of the two forms + "Add/remove tags from all matching messages. - +tagname Add the tag `tagname' - -tagname Remove the tag `tagname' +Tis command adds or removes tags from all messages matching the +current search terms. When called interactively, this command +will prompt for tags to be added or removed. Tags prefixed with +'+' will be added and tags prefixed with '-' will be removed. Each character of the tag name may consist of alphanumeric characters as well as `_.+-'. @@ -1227,7 +1354,8 @@ search." Runs a new search matching only messages that match both the current search results AND the additional query string provided." (interactive "sFilter search: ") - (notmuch-search (concat notmuch-search-query-string " and " query) notmuch-search-oldest-first)) + (let ((grouped-query (if (string-match-p notmuch-search-disjunctive-regexp query) (concat "( " query " )") query))) + (notmuch-search (concat notmuch-search-query-string " and " grouped-query) notmuch-search-oldest-first))) (defun notmuch-search-filter-by-tag (tag) "Filter the current search results based on a single tag. @@ -1249,16 +1377,17 @@ current search results AND that are tagged with the given tag." (defvar notmuch-folder-mode-map (let ((map (make-sparse-keymap))) - (define-key map "n" 'next-line) - (define-key map "p" 'previous-line) + (define-key map "?" 'notmuch-help) (define-key map "x" 'kill-this-buffer) (define-key map "q" 'kill-this-buffer) - (define-key map "s" 'notmuch-search) - (define-key map (kbd "RET") 'notmuch-folder-show-search) - (define-key map "<" 'beginning-of-buffer) + (define-key map ">" 'notmuch-folder-last) + (define-key map "<" 'notmuch-folder-first) (define-key map "=" 'notmuch-folder) - (define-key map "?" 'describe-mode) + (define-key map "s" 'notmuch-search) (define-key map [mouse-1] 'notmuch-folder-show-search) + (define-key map (kbd "RET") 'notmuch-folder-show-search) + (define-key map "p" 'notmuch-folder-previous) + (define-key map "n" 'notmuch-folder-next) map) "Keymap for \"notmuch folder\" buffers.") @@ -1272,12 +1401,26 @@ current search results AND that are tagged with the given tag." (defun notmuch-folder-mode () "Major mode for showing notmuch 'folders'. -This buffer contains a list of messages counts returned by a -customizable set of searches of your email archives. Each line -in the buffer shows the search terms and the resulting message count. +This buffer contains a list of message counts returned by a +customizable set of searches of your email archives. Each line in +the buffer shows the name of a saved search and the resulting +message count. Pressing RET on any line opens a search window containing the -results for the search terms in that line. +results for the saved search on that line. + +Here is an example of how the search list could be +customized, (the following text would be placed in your ~/.emacs +file): + +(setq notmuch-folders '((\"inbox\" . \"tag:inbox\") + (\"unread\" . \"tag:inbox AND tag:unread\") + (\"notmuch\" . \"tag:inbox AND to:notmuchmail.org\"))) + +Of course, you can have any number of folders, each configured +with any supported search terms (see \"notmuch help search-terms\"). + +Currently available key bindings: \\{notmuch-folder-mode-map}" (interactive) @@ -1289,6 +1432,29 @@ results for the search terms in that line. mode-name "notmuch-folder") (setq buffer-read-only t)) +(defun notmuch-folder-next () + "Select the next folder in the list." + (interactive) + (forward-line 1) + (if (eobp) + (forward-line -1))) + +(defun notmuch-folder-previous () + "Select the previous folder in the list." + (interactive) + (forward-line -1)) + +(defun notmuch-folder-first () + "Select the first folder in the list." + (interactive) + (goto-char (point-min))) + +(defun notmuch-folder-last () + "Select the last folder in the list." + (interactive) + (goto-char (point-max)) + (forward-line -1)) + (defun notmuch-folder-add (folders) (if folders (let ((name (car (car folders))) diff --git a/vim/plugin/notmuch.vim b/vim/plugin/notmuch.vim index b415f500..a226f203 100644 --- a/vim/plugin/notmuch.vim +++ b/vim/plugin/notmuch.vim @@ -275,6 +275,7 @@ function! s:NM_search_show_thread(everything) call add(words, ')') endif call NM_cmd_show(words) + let b:nm_show_everything = a:everything endfunction function! s:NM_search_prompt() @@ -408,7 +409,7 @@ endfunction function! s:NM_cmd_show(words) let prev_bufnr = bufnr('%') - let data = s:NM_run(['show'] + a:words) + let data = s:NM_run(['show', '--entire-thread'] + a:words) let lines = split(data, "\n") let info = s:NM_cmd_show_parse(lines) @@ -430,6 +431,7 @@ function! s:NM_cmd_show(words) endfunction function! s:NM_show_previous(can_change_thread, find_matching) + let everything = exists('b:nm_show_everything') ? b:nm_show_everything : 0 let info = b:nm_raw_info let lnum = line('.') for msg in reverse(copy(info['msgs'])) @@ -450,7 +452,7 @@ function! s:NM_show_previous(can_change_thread, find_matching) call NM_kill_this_buffer() if line('.') > 1 norm k - call NM_search_show_thread() + call NM_search_show_thread(everything) norm G call NM_show_previous(0, a:find_matching) else @@ -479,10 +481,11 @@ function! s:NM_show_next(can_change_thread, find_matching) endfunction function! s:NM_show_next_thread() + let everything = exists('b:nm_show_everything') ? b:nm_show_everything : 0 call NM_kill_this_buffer() if line('.') != line('$') norm j - call NM_search_show_thread() + call NM_search_show_thread(everything) else echo 'No more messages.' endif